Добавлен новый интерфейс с навигационной панелью и страницами для управления моделями, настройками и генерацией изображений. Реализованы стили для новых компонентов и добавлены функции для загрузки шрифтов и иконок. Обновлен основной класс окна для поддержки новых страниц.

This commit is contained in:
kromskii2 committed 2025-03-15 09:10:31 +07:00
1 parent df447a1ecf
commit ac32c55ab9
25 files changed
+1496 -476

No files matched your search

+54 -476
View File
@@ -8,9 +8,10 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QComboBox, QSpinBox, QDoubleSpinBox, QProgressBar,
QScrollArea, QTextEdit, QFileDialog, QMenuBar, QMenu)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt6.QtGui import QPixmap, QImage, QAction
from PyQt6.QtGui import QPixmap, QImage, QAction, QFontDatabase, QFont
from PIL import Image
from io import BytesIO
from pathlib import Path
# Импортируем новые модули
from modules.console_window import ConsoleWindow
@@ -20,6 +21,13 @@ from modules.prompt_window import PromptWindow
from modules.negative_prompt_window import NegativePromptWindow
from modules.size_preset_manager import SizePresetManager
from modules.settings_menu import SettingsMenu
from modules.styles.base_window import BaseWindow
from modules.styles.navigation import NavigationPanel
from modules.styles.page import PageContainer
from modules.pages.home_page import HomePage
from modules.pages.models_page import ModelsPage
from modules.pages.settings_page import SettingsPage
from scripts.download_resources import download_font
class GenerationThread(QThread):
progress_updated = pyqtSignal(float, str)
@@ -61,497 +69,67 @@ class GenerationThread(QThread):
def stop(self):
self.is_generating = False
class MainWindow(QMainWindow):
class MainWindow(BaseWindow):
def __init__(self):
super().__init__()
self.api_url = "http://localhost:7860" # URL по умолчанию
self.console_window = None
self.prompt_window = None
self.negative_prompt_window = None
self.progress_timer = None
# Создаем меню бар
self.create_menu_bar()
self.init_ui()
def create_menu_bar(self):
"""Создание меню бара с настройками"""
menubar = QMenuBar(self)
menubar.setStyleSheet("""
QMenuBar {
background-color: #2b2b2b;
color: #ffffff;
border-bottom: 1px solid #3b3b3b;
padding: 2px;
}
QMenuBar::item {
background-color: transparent;
padding: 8px 12px;
margin: 0;
border-radius: 4px;
}
QMenuBar::item:selected {
background-color: #3b3b3b;
}
QMenuBar::item:pressed {
background-color: #404040;
}
""")
# Добавляем меню настроек
settings_menu = SettingsMenu(self)
menubar.addMenu(settings_menu)
self.setMenuBar(menubar)
def init_ui(self):
self.setWindowTitle("Stable Diffusion UI")
self.setStyleSheet("""
QMainWindow {
background-color: #2b2b2b;
}
QWidget {
color: #ffffff;
font-size: 14px;
}
QLineEdit, QTextEdit, QComboBox, QSpinBox, QDoubleSpinBox {
background-color: #3b3b3b;
border: 1px solid #555555;
border-radius: 4px;
padding: 5px;
color: #ffffff;
min-width: 100px;
}
QSpinBox::up-button, QDoubleSpinBox::up-button {
background-color: #4a4a4a;
border: none;
border-left: 1px solid #555555;
border-bottom: 1px solid #555555;
border-top-right-radius: 4px;
width: 20px;
height: 12px;
}
QSpinBox::down-button, QDoubleSpinBox::down-button {
background-color: #4a4a4a;
border: none;
border-left: 1px solid #555555;
border-bottom-right-radius: 4px;
width: 20px;
height: 12px;
}
QSpinBox::up-button:hover, QDoubleSpinBox::up-button:hover,
QSpinBox::down-button:hover, QDoubleSpinBox::down-button:hover {
background-color: #5a5a5a;
}
QSpinBox::up-button:pressed, QDoubleSpinBox::up-button:pressed,
QSpinBox::down-button:pressed, QDoubleSpinBox::down-button:pressed {
background-color: #666666;
}
QSpinBox::up-arrow, QDoubleSpinBox::up-arrow {
image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOCA0TDQgOEgxMkw4IDRaIiBmaWxsPSIjZmZmZmZmIi8+PC9zdmc+);
width: 12px;
height: 6px;
}
QSpinBox::down-arrow, QDoubleSpinBox::down-arrow {
image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOCAxMkwxMiA4SDRMOCAxMloiIGZpbGw9IiNmZmZmZmYiLz48L3N2Zz4=);
width: 12px;
height: 6px;
}
QPushButton {
background-color: #0d6efd;
border: none;
border-radius: 4px;
padding: 8px 16px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #0b5ed7;
}
QPushButton:pressed {
background-color: #0a58ca;
}
QProgressBar {
border: 1px solid #555555;
border-radius: 4px;
text-align: center;
}
QProgressBar::chunk {
background-color: #0d6efd;
}
""")
self.setMinimumSize(1200, 800)
self.setup_ui()
# Создаем центральный виджет
def create_horizontal_layout(self):
"""Создает горизонтальный layout для главного окна"""
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout(central_widget)
layout = QHBoxLayout(central_widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
return layout
# Левая панель с настройками
settings_widget = QWidget()
settings_layout = QVBoxLayout(settings_widget)
settings_layout.setSpacing(10)
# URL API
api_layout = QHBoxLayout()
api_label = QLabel("API URL:")
self.api_url_input = QLineEdit(self.api_url)
self.api_url_input.textChanged.connect(self.update_api_url)
api_layout.addWidget(api_label)
api_layout.addWidget(self.api_url_input)
settings_layout.addLayout(api_layout)
# Промпт
prompt_layout = QVBoxLayout()
prompt_header = QHBoxLayout()
prompt_label = QLabel("Промпт:")
self.prompt_input = QTextEdit()
self.prompt_input.setMinimumHeight(100)
prompt_buttons = QHBoxLayout()
self.prompt_button = QPushButton("Готовые промпты")
self.prompt_button.clicked.connect(self.toggle_prompt_window)
self.negative_prompt_button = QPushButton("Негативные промпты")
self.negative_prompt_button.clicked.connect(self.toggle_negative_prompt_window)
prompt_buttons.addWidget(self.prompt_button)
prompt_buttons.addWidget(self.negative_prompt_button)
prompt_header.addWidget(prompt_label)
prompt_header.addLayout(prompt_buttons)
prompt_layout.addLayout(prompt_header)
prompt_layout.addWidget(self.prompt_input)
settings_layout.addLayout(prompt_layout)
# Негативный промпт
negative_prompt_layout = QVBoxLayout()
negative_prompt_label = QLabel("Негативный промпт:")
self.negative_prompt_input = QTextEdit()
self.negative_prompt_input.setMinimumHeight(100)
negative_prompt_layout.addWidget(negative_prompt_label)
negative_prompt_layout.addWidget(self.negative_prompt_input)
settings_layout.addLayout(negative_prompt_layout)
# Добавляем менеджер моделей
self.model_manager = ModelManager(self.api_url)
self.model_manager.model_changed.connect(self.on_model_changed)
settings_layout.addWidget(self.model_manager)
# Добавляем менеджер сэмплеров
self.sampler_manager = SamplerManager(self.api_url)
settings_layout.addWidget(self.sampler_manager)
# Добавляем менеджер пресетов размеров
self.size_preset_manager = SizePresetManager()
self.size_preset_manager.size_changed.connect(self.on_size_preset_changed)
settings_layout.addWidget(self.size_preset_manager)
# Параметры генерации
params_layout = QVBoxLayout()
def setup_ui(self):
# Загружаем шрифт
font_path = Path("resources/fonts/InterVariable.ttf")
if not font_path.exists():
download_font()
font_id = QFontDatabase.addApplicationFont(str(font_path))
if font_id != -1:
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
app_font = QFont(font_family)
QApplication.setFont(app_font)
# Размеры
size_layout = QHBoxLayout()
width_label = QLabel("Ширина:")
self.width_input = QSpinBox()
self.width_input.setRange(64, 2048)
self.width_input.setValue(512)
height_label = QLabel("Высота:")
self.height_input = QSpinBox()
self.height_input.setRange(64, 2048)
self.height_input.setValue(512)
size_layout.addWidget(width_label)
size_layout.addWidget(self.width_input)
size_layout.addWidget(height_label)
size_layout.addWidget(self.height_input)
params_layout.addLayout(size_layout)
# Сид
seed_layout = QHBoxLayout()
seed_label = QLabel("Сид:")
self.seed_input = QSpinBox()
self.seed_input.setRange(-1, 999999999)
self.seed_input.setValue(-1)
seed_layout.addWidget(seed_label)
seed_layout.addWidget(self.seed_input)
params_layout.addLayout(seed_layout)
# CFG Scale
cfg_layout = QHBoxLayout()
cfg_label = QLabel("CFG Scale:")
self.cfg_input = QDoubleSpinBox()
self.cfg_input.setRange(1.0, 30.0)
self.cfg_input.setValue(7.0)
cfg_layout.addWidget(cfg_label)
cfg_layout.addWidget(self.cfg_input)
params_layout.addLayout(cfg_layout)
# Шаги
steps_layout = QHBoxLayout()
steps_label = QLabel("Шаги:")
self.steps_input = QSpinBox()
self.steps_input.setRange(1, 150)
self.steps_input.setValue(20)
steps_layout.addWidget(steps_label)
steps_layout.addWidget(self.steps_input)
params_layout.addLayout(steps_layout)
settings_layout.addLayout(params_layout)
# Добавляем кнопку консоли
self.console_button = QPushButton("Открыть консоль")
self.console_button.clicked.connect(self.toggle_console)
settings_layout.addWidget(self.console_button)
# Кнопка генерации
self.generate_button = QPushButton("Сгенерировать")
self.generate_button.clicked.connect(self.start_generation)
settings_layout.addWidget(self.generate_button)
# Прогресс бар
self.progress_bar = QProgressBar()
settings_layout.addWidget(self.progress_bar)
# Добавляем левую панель в основной layout
main_layout.addWidget(settings_widget, stretch=1)
# Правая панель для результатов
results_widget = QWidget()
results_layout = QVBoxLayout(results_widget)
# Создаем горизонтальный layout
layout = self.create_horizontal_layout()
# Область для отображения изображений
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setMinimumWidth(512)
self.results_content = QWidget()
self.results_content_layout = QVBoxLayout(self.results_content)
scroll_area.setWidget(self.results_content)
results_layout.addWidget(scroll_area)
# Добавляем правую панель в основной layout
main_layout.addWidget(results_widget, stretch=2)
# Устанавливаем размер окна
self.setMinimumSize(1200, 800)
def toggle_console(self):
"""Открыть/закрыть окно консоли"""
if self.console_window is None:
self.console_window = ConsoleWindow(parent=self)
self.console_window.console_closed.connect(self.on_console_closed)
self.console_window.show()
else:
self.console_window.close()
def on_console_closed(self):
"""Обработчик закрытия окна консоли"""
self.console_window = None
def on_model_changed(self, model_name):
"""Обработчик смены модели"""
print(f"Модель изменена на: {model_name}")
def log_to_console(self, message):
"""Логирование в консоль"""
if self.console_window:
self.console_window.log(message)
print(message)
def toggle_prompt_window(self):
"""Открыть/закрыть окно с промптами"""
if self.prompt_window is None:
self.prompt_window = PromptWindow(parent=self)
self.prompt_window.prompt_selected.connect(self.add_prompt)
self.prompt_window.show()
else:
self.prompt_window.close()
def toggle_negative_prompt_window(self):
"""Открыть/закрыть окно с негативными промптами"""
if self.negative_prompt_window is None:
self.negative_prompt_window = NegativePromptWindow(parent=self)
self.negative_prompt_window.prompt_selected.connect(self.add_negative_prompt)
self.negative_prompt_window.show()
else:
self.negative_prompt_window.close()
def add_prompt(self, text):
"""Добавить промпт в поле ввода"""
current_text = self.prompt_input.toPlainText()
if current_text:
current_text += ", "
self.prompt_input.setText(current_text + text)
def add_negative_prompt(self, text):
"""Добавить негативный промпт в поле ввода"""
current_text = self.negative_prompt_input.toPlainText()
if current_text:
current_text += ", "
self.negative_prompt_input.setText(current_text + text)
def start_generation(self):
self.generate_button.setEnabled(False)
self.progress_bar.setValue(0)
params = {
"prompt": self.prompt_input.toPlainText(),
"negative_prompt": self.negative_prompt_input.toPlainText(),
"width": self.width_input.value(),
"height": self.height_input.value(),
"seed": self.seed_input.value(),
"cfg_scale": self.cfg_input.value(),
"steps": self.steps_input.value(),
"sampler_name": self.sampler_manager.get_current_sampler(),
"batch_size": 1
}
self.log_to_console(f"Начало генерации с параметрами: {json.dumps(params, indent=2, ensure_ascii=False)}")
# Добавляем навигационную панель
self.nav_panel = NavigationPanel()
layout.addWidget(self.nav_panel)
# Запускаем таймер для отслеживания прогресса
self.progress_timer = QTimer()
self.progress_timer.timeout.connect(self.check_progress)
self.progress_timer.start(100) # Проверяем каждые 100 мс
# Создаем контейнер для страниц
self.page_container = PageContainer()
layout.addWidget(self.page_container)
self.generation_thread = GenerationThread(self.api_url_input.text(), params)
self.generation_thread.generation_complete.connect(self.handle_generation_complete)
self.generation_thread.error_occurred.connect(self.handle_error)
self.generation_thread.start()
def check_progress(self):
"""Проверка прогресса генерации"""
try:
response = requests.get(f"{self.api_url_input.text()}/sdapi/v1/progress")
if response.status_code == 200:
data = response.json()
progress = data.get('progress', 0)
current_step = data.get('current_step', 0)
total_steps = data.get('total_steps', 1)
percentage = int(progress * 100)
self.progress_bar.setValue(percentage)
self.log_to_console(f"Прогресс: {percentage}% - Шаг {current_step}/{total_steps}")
# Если генерация завершена, останавливаем таймер
if progress >= 1.0:
self.progress_timer.stop()
self.progress_timer = None
except Exception as e:
print(f"Ошибка при получении прогресса: {str(e)}")
def handle_generation_complete(self, images):
"""Обработка завершения генерации"""
# Останавливаем таймер, если он ещё активен
if self.progress_timer:
self.progress_timer.stop()
self.progress_timer = None
self.generate_button.setEnabled(True)
self.progress_bar.setValue(100)
self.log_to_console("Генерация завершена успешно")
# Добавляем страницы
self.home_page = HomePage()
self.models_page = ModelsPage()
self.settings_page = SettingsPage()
# Очищаем предыдущие результаты
for i in reversed(range(self.results_content_layout.count())):
self.results_content_layout.itemAt(i).widget().setParent(None)
for img_data in images:
try:
# Декодируем base64 в байты
img_bytes = base64.b64decode(img_data)
self.log_to_console(f"Размер полученных данных: {len(img_bytes)} байт")
# Создаем объект изображения
img = Image.open(BytesIO(img_bytes))
self.log_to_console(f"Размер изображения: {img.size}, формат: {img.format}")
# Конвертируем в RGB если нужно
if img.mode != 'RGB':
img = img.convert('RGB')
# Сохраняем во временный буфер как PNG
buffer = BytesIO()
img.save(buffer, format='PNG')
buffer.seek(0)
# Создаем QPixmap
pixmap = QPixmap()
if not pixmap.loadFromData(buffer.getvalue()):
raise Exception("Не удалось создать QPixmap из данных")
# Проверяем размер QPixmap
if pixmap.isNull():
raise Exception("Создан пустой QPixmap")
self.log_to_console(f"Размер QPixmap: {pixmap.width()}x{pixmap.height()}")
# Создаем и добавляем QLabel с изображением
img_label = QLabel()
img_label.setPixmap(pixmap)
img_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Создаем кнопку сохранения
save_button = QPushButton("Сохранить")
save_button.clicked.connect(lambda checked, img=img: self.save_image(img))
# Создаем кнопку копирования в буфер обмена
copy_button = QPushButton("Копировать")
copy_button.clicked.connect(lambda checked, img=img: self.copy_to_clipboard(img))
# Создаем контейнер для изображения и кнопок
container = QWidget()
container_layout = QVBoxLayout(container)
container_layout.addWidget(img_label)
# Создаем горизонтальный layout для кнопок
buttons_layout = QHBoxLayout()
buttons_layout.addWidget(save_button)
buttons_layout.addWidget(copy_button)
container_layout.addLayout(buttons_layout)
self.results_content_layout.addWidget(container)
self.log_to_console("Изображение успешно добавлено в интерфейс")
except Exception as e:
error_msg = f"Ошибка отображения изображения: {str(e)}"
self.log_to_console(error_msg)
error_label = QLabel(error_msg)
self.results_content_layout.addWidget(error_label)
def handle_error(self, error_message):
"""Обработка ошибок"""
self.generate_button.setEnabled(True)
self.log_to_console(f"Ошибка: {error_message}")
def save_image(self, img):
file_path, _ = QFileDialog.getSaveFileName(self, "Сохранить изображение", "", "PNG Files (*.png);;JPEG Files (*.jpg)")
if file_path:
img.save(file_path)
def copy_to_clipboard(self, img):
"""Копирование изображения в буфер обмена"""
# Конвертируем изображение в QPixmap
buffer = BytesIO()
img.save(buffer, format='PNG')
buffer.seek(0)
pixmap = QPixmap()
pixmap.loadFromData(buffer.getvalue())
self.page_container.add_page("home", self.home_page)
self.page_container.add_page("models", self.models_page)
self.page_container.add_page("settings", self.settings_page)
# Копируем в буфер обмена
clipboard = QApplication.clipboard()
clipboard.setPixmap(pixmap)
# Подключаем сигнал смены страницы
self.nav_panel.page_changed.connect(self.switch_page)
# Показываем уведомление
self.log_to_console("Изображение скопировано в буфер обмена")
# Показываем домашнюю страницу по умолчанию
self.page_container.show_page("home")
def update_api_url(self):
"""Обновление URL API во всех компонентах"""
new_url = self.api_url_input.text()
self.model_manager.update_api_url(new_url)
self.sampler_manager.update_api_url(new_url)
self.log_to_console(f"API URL обновлен: {new_url}")
def switch_page(self, page_name):
self.page_container.show_page(page_name)
def on_size_preset_changed(self, width, height):
"""Обработчик изменения размера через пресет"""
self.width_input.setValue(width)
self.height_input.setValue(height)
self.log_to_console(f"Размер изменен на: {width}x{height}")
if __name__ == '__main__':
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
sys.exit(app.exec())
if __name__ == "__main__":
main()