GUI_Utils/comfyui_gui.py

572 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sys
import os
import json
import time
import requests
import copy
import uuid
import configparser
from pathlib import Path
from urllib.parse import quote
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLineEdit, QPushButton, QComboBox, QPlainTextEdit, QGroupBox,
QFormLayout, QFileDialog, QMessageBox, QStatusBar, QProgressBar,
QDialog, QLabel, QDialogButtonBox, QStyle, QListWidget, QListWidgetItem
)
from PySide6.QtCore import Qt, QThread, Signal, QObject, QSettings
from PySide6.QtGui import QIcon, QPixmap
# --- 配置管理类 ---
class ConfigManager:
def __init__(self, filename='config.ini'):
self.filename, self.config = filename, configparser.ConfigParser()
self.config.read(self.filename)
def get(self, section, key, fallback=None): return self.config.get(section, key, fallback=fallback)
def set(self, section, key, value):
if not self.config.has_section(section): self.config.add_section(section)
self.config.set(section, key, str(value))
def save(self):
with open(self.filename, 'w') as configfile: self.config.write(configfile)
# --- 辅助函数将工作流图谱格式转换为API提示格式 ---
def convert_graph_to_prompt(workflow_graph):
if 'nodes' not in workflow_graph or 'links' not in workflow_graph: return None
prompt, nodes_by_id = {}, {str(node['id']): node for node in workflow_graph['nodes']}
for node_id, node_data in nodes_by_id.items():
inputs, widget_values, widget_index = {}, node_data.get('widgets_values', []), 0
if 'inputs' in node_data:
for an_input in node_data['inputs']:
if an_input.get('widget') and widget_index < len(widget_values):
inputs[an_input['name']] = widget_values[widget_index]
widget_index += 1
for link_info in workflow_graph['links']:
target_node_id_str, source_node_id_str = str(link_info[3]), str(link_info[1])
if target_node_id_str == node_id:
source_slot_idx, target_slot_idx = link_info[2], link_info[4]
if 'inputs' in node_data and target_slot_idx < len(node_data['inputs']):
inputs[node_data['inputs'][target_slot_idx]['name']] = [source_node_id_str, source_slot_idx]
prompt[node_id] = {"class_type": node_data['type'], "inputs": inputs}
return prompt
# --- API通信类 ---
class ComfyAPI:
def __init__(self, host, port):
self.base_url, self.client_id = f"http://{host}:{port}", str(uuid.uuid4())
def _request(self, method, endpoint, **kwargs):
try:
response = requests.request(method, f"{self.base_url}{endpoint}", timeout=10, **kwargs)
response.raise_for_status()
return response.json(), None
except requests.exceptions.RequestException as e:
return None, str(e)
def get_workflows(self):
return self._request("get", "/api/userdata?dir=workflows&recurse=true&split=false&full_info=true")
def get_workflow_details(self, workflow_name):
return self._request("get", f"/api/userdata/workflows%2F{quote(workflow_name)}")
def get_queue(self):
return self._request("get", "/api/queue")
def queue_prompt(self, prompt, workflow_graph):
payload = {"prompt": prompt, "extra_data": {"extra_pnginfo": {"workflow": workflow_graph}},
"client_id": self.client_id}
return self._request("post", "/api/prompt", json=payload)
# --- 后台执行线程 ---
class ExecutionWorker(QObject):
log, progress, finished = Signal(str), Signal(int, int), Signal(bool, str)
status_update = Signal(str) # FIX 2: New signal for status bar
def __init__(self, api, workflow_prompt, workflow_graph, model_image_path, model_node_id,
clothing_dir, clothing_node_id, output_dir, output_node_id):
super().__init__()
self.api, self.workflow_prompt, self.workflow_graph = api, workflow_prompt, workflow_graph
self.model_image_path, self.model_node_id = Path(model_image_path), model_node_id
self.clothing_dir, self.clothing_node_id = Path(clothing_dir), clothing_node_id
self.output_dir, self.output_node_id = Path(output_dir), output_node_id
self._is_running = True
def stop(self):
self._is_running = False; self.log.emit("执行停止请求已发送...")
def run(self):
image_extensions = ['.png', '.jpg', '.jpeg', '.webp', '.bmp']
try:
clothing_files = [f for f in self.clothing_dir.iterdir() if
f.is_file() and f.suffix.lower() in image_extensions]
except FileNotFoundError:
return self.finished.emit(False, f"错误:找不到衣物文件夹 {self.clothing_dir}")
if not clothing_files: return self.finished.emit(False, f"{self.clothing_dir} 中没有找到有效的图片文件。")
total_files = len(clothing_files);
self.log.emit(f"找到 {total_files} 件衣物图片待处理。");
self.progress.emit(0, total_files)
for i, clothing_path in enumerate(clothing_files):
if not self._is_running: break
self.log.emit(
f"--- 开始处理: {self.model_image_path.name} + {clothing_path.name} ({i + 1}/{total_files}) ---")
prompt = copy.deepcopy(self.workflow_prompt)
if self.model_node_id in prompt:
prompt[self.model_node_id]["inputs"]["image"] = str(self.model_image_path)
else:
return self.finished.emit(False, f"错误模特节点ID '{self.model_node_id}' 在工作流中不存在!")
if self.clothing_node_id in prompt:
prompt[self.clothing_node_id]["inputs"]["image"] = str(clothing_path)
else:
return self.finished.emit(False, f"错误衣物节点ID '{self.clothing_node_id}' 在工作流中不存在!")
if self.output_node_id in prompt:
prompt[self.output_node_id]["inputs"]["output_dir"] = str(self.output_dir)
prompt[self.output_node_id]["inputs"][
"filename_prefix"] = f"{self.model_image_path.stem}_{clothing_path.stem}"
else:
return self.finished.emit(False, f"错误输出节点ID '{self.output_node_id}' 在工作流中不存在!")
response, error = self.api.queue_prompt(prompt, self.workflow_graph)
if error: self.log.emit(f"错误: 任务排队失败 - {error}"); continue
prompt_id = response['prompt_id'];
self.log.emit(f"任务已入队ID: {prompt_id}")
# FIX 2: New wait logic
logged_wait_for_task = False
while self._is_running:
queue_info, error = self.api.get_queue()
if error: self.status_update.emit(f"无法获取队列状态: {error}"); time.sleep(2); continue
queue_running = queue_info.get('queue_running', [])
queue_pending = queue_info.get('queue_pending', [])
is_running = any(item[1] == prompt_id for item in queue_running)
is_pending = any(item[1] == prompt_id for item in queue_pending)
if not is_running and not is_pending:
self.status_update.emit(f"任务 {prompt_id} 已完成")
self.log.emit(f"任务 {prompt_id} 处理完成。")
break
if not logged_wait_for_task:
self.log.emit(f"等待任务 {prompt_id} 完成...")
logged_wait_for_task = True
if is_running:
self.status_update.emit(f"正在执行任务: {prompt_id}")
elif is_pending:
try:
# Find position in the pending queue
pending_ids = [item[1] for item in queue_pending]
position = pending_ids.index(prompt_id) + 1
self.status_update.emit(f"队列等候中 (位置 {position}/{len(pending_ids)})")
except ValueError:
self.status_update.emit("正在更新队列状态...")
time.sleep(1)
self.progress.emit(i + 1, total_files)
if self._is_running:
self.finished.emit(True, "全部任务执行完毕。")
else:
self.finished.emit(False, "执行被用户中止。")
# --- UI 组件类 ---
class ImagePreviewDialog(QDialog):
def __init__(self, image_path, parent=None):
super().__init__(parent);
self.setWindowTitle("模特预览")
self.image_label = QLabel(self);
self.image_label.setAlignment(Qt.AlignCenter)
pixmap = QPixmap(image_path);
scaled_pixmap = pixmap.scaled(800, 800, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.image_label.setPixmap(scaled_pixmap);
layout = QVBoxLayout(self);
layout.addWidget(self.image_label)
button_box = QDialogButtonBox(QDialogButtonBox.Ok);
button_box.accepted.connect(self.accept)
layout.addWidget(button_box);
self.setLayout(layout);
self.adjustSize()
class SettingsDialog(QDialog):
def __init__(self, workflow_prompt, current_settings, parent=None):
super().__init__(parent);
self.setWindowTitle("工作流输入输出设置");
self.setMinimumWidth(450)
self.workflow_prompt = workflow_prompt;
layout = QFormLayout(self)
self.model_node_combo = QComboBox();
self.clothing_node_combo = QComboBox();
self.output_node_combo = QComboBox()
for node_id, node_info in self.workflow_prompt.items():
text = f"ID: {node_id} (类型: {node_info['class_type']})"
if "LoadImage" in node_info['class_type']: self.model_node_combo.addItem(text,
userData=node_id); self.clothing_node_combo.addItem(
text, userData=node_id)
if "Save" in node_info['class_type']: self.output_node_combo.addItem(text, userData=node_id)
if current_settings.get('model_node_id'): self.model_node_combo.setCurrentIndex(
self.model_node_combo.findData(current_settings['model_node_id']))
if current_settings.get('clothing_node_id'): self.clothing_node_combo.setCurrentIndex(
self.clothing_node_combo.findData(current_settings['clothing_node_id']))
if current_settings.get('output_node_id'): self.output_node_combo.setCurrentIndex(
self.output_node_combo.findData(current_settings['output_node_id']))
layout.addRow("1. 模特加载节点 (LoadImage):", self.model_node_combo);
layout.addRow("2. 衣物加载节点 (LoadImage):", self.clothing_node_combo);
layout.addRow("3. 图像保存节点 (SaveImageAnywhere):", self.output_node_combo)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel);
button_box.accepted.connect(self.accept);
button_box.rejected.connect(self.reject);
layout.addRow(button_box)
def get_settings(self):
return {"model_node_id": self.model_node_combo.currentData(),
"clothing_node_id": self.clothing_node_combo.currentData(),
"output_node_id": self.output_node_combo.currentData()}
# --- 主窗口 ---
class ComfyUIController(QMainWindow):
def __init__(self):
super().__init__();
self.setWindowTitle("ComfyUI 智能试衣控制器");
self.setWindowIcon(QIcon(os.path.join(os.path.dirname(__file__), "comfyui_hz.ico")));
self.setGeometry(100, 100, 1000, 700)
self.config = ConfigManager(os.path.join(os.path.dirname(os.path.dirname(__file__)), "config_hz.ini"));
self.api, self.workflow_settings = None, {}
self.current_workflow_prompt, self.current_workflow_graph = None, None
self.execution_thread, self.execution_worker = None, None
self._create_widgets();
self._create_layouts();
self._connect_signals();
self._load_settings();
self._load_models();
self._update_ui_state()
def _create_widgets(self):
self.model_group = QGroupBox("模特库");
self.model_list_widget = QListWidget();
self.add_model_button = QPushButton("✚ 添加模特");
self.delete_model_button = QPushButton("✖ 删除选中")
self.server_group = QGroupBox("① 服务器连接");
self.host_input = QLineEdit();
self.port_input = QLineEdit();
self.connect_button = QPushButton("连接")
self.workflow_group = QGroupBox("② 工作流选择");
self.workflow_combo = QComboBox();
self.refresh_button = QPushButton("🔄");
self.settings_button = QPushButton("⚙️");
self.workflow_tip_label = QLabel("提示请选择一个包含两个LoadImage节点和1个SaveImageAnywhere节点的换装工作流。");
self.workflow_tip_label.setStyleSheet("color: #e67e22;")
self.exec_group = QGroupBox("③ 执行配置");
self.clothing_dir_input = QLineEdit();
self.clothing_dir_button = QPushButton("浏览...")
self.output_group = QGroupBox("④ 输出设置");
self.output_dir_input = QLineEdit();
self.output_dir_button = QPushButton("浏览...")
self.action_group = QGroupBox("⑤ 开始执行");
self.execute_button = QPushButton("▶️ 开始批量换装");
self.stop_button = QPushButton("⏹️ 停止执行")
self.log_group = QGroupBox("执行日志");
self.log_box = QPlainTextEdit();
self.log_box.setReadOnly(True)
self.status_bar = QStatusBar();
self.setStatusBar(self.status_bar);
self.progress_bar = QProgressBar();
self.status_bar.addPermanentWidget(self.progress_bar)
def _create_layouts(self):
model_layout, model_buttons_layout = QVBoxLayout(), QHBoxLayout();
model_buttons_layout.addWidget(self.add_model_button);
model_buttons_layout.addWidget(self.delete_model_button);
model_layout.addWidget(self.model_list_widget);
model_layout.addLayout(model_buttons_layout);
self.model_group.setLayout(model_layout)
right_v_layout = QVBoxLayout()
server_h_layout = QHBoxLayout();
server_h_layout.addWidget(QLabel("主机:"));
server_h_layout.addWidget(self.host_input, 1);
server_h_layout.addWidget(QLabel("端口:"));
server_h_layout.addWidget(self.port_input);
server_h_layout.addWidget(self.connect_button);
self.server_group.setLayout(server_h_layout)
workflow_layout, workflow_controls_layout = QVBoxLayout(), QHBoxLayout();
workflow_controls_layout.addWidget(self.workflow_combo, 1);
workflow_controls_layout.addWidget(self.refresh_button);
workflow_controls_layout.addWidget(self.settings_button);
workflow_layout.addLayout(workflow_controls_layout);
workflow_layout.addWidget(self.workflow_tip_label);
self.workflow_group.setLayout(workflow_layout)
exec_form_layout = QFormLayout();
clothing_layout = QHBoxLayout();
clothing_layout.addWidget(self.clothing_dir_input, 1);
clothing_layout.addWidget(self.clothing_dir_button);
exec_form_layout.addRow("衣物文件夹:", clothing_layout);
self.exec_group.setLayout(exec_form_layout)
output_form_layout = QFormLayout();
output_layout = QHBoxLayout();
output_layout.addWidget(self.output_dir_input, 1);
output_layout.addWidget(self.output_dir_button);
output_form_layout.addRow("输出文件夹:", output_layout);
self.output_group.setLayout(output_form_layout)
action_layout = QHBoxLayout();
action_layout.addStretch();
action_layout.addWidget(self.execute_button);
action_layout.addWidget(self.stop_button);
self.action_group.setLayout(action_layout)
log_layout = QVBoxLayout();
log_layout.addWidget(self.log_box);
self.log_group.setLayout(log_layout)
right_v_layout.addWidget(self.server_group);
right_v_layout.addWidget(self.workflow_group);
right_v_layout.addWidget(self.exec_group);
right_v_layout.addWidget(self.output_group);
right_v_layout.addWidget(self.action_group);
right_v_layout.addWidget(self.log_group, 1)
main_h_layout = QHBoxLayout();
main_h_layout.setSpacing(15);
main_h_layout.addWidget(self.model_group, 1);
main_h_layout.addLayout(right_v_layout, 3)
central_widget = QWidget();
central_widget.setLayout(main_h_layout);
self.setCentralWidget(central_widget)
def _connect_signals(self):
self.add_model_button.clicked.connect(self._add_model);
self.delete_model_button.clicked.connect(self._delete_model);
self.model_list_widget.itemDoubleClicked.connect(self.show_model_preview)
self.connect_button.clicked.connect(self.connect_and_fetch_workflows);
self.refresh_button.clicked.connect(self.fetch_workflows);
self.workflow_combo.currentTextChanged.connect(self.fetch_workflow_details)
self.settings_button.clicked.connect(self.open_settings_dialog)
self.clothing_dir_button.clicked.connect(
lambda: self._select_directory(self.clothing_dir_input, "选择衣物文件夹"))
self.output_dir_button.clicked.connect(lambda: self._select_directory(self.output_dir_input, "选择输出文件夹"))
self.execute_button.clicked.connect(self.start_execution);
self.stop_button.clicked.connect(self.stop_execution)
def _update_ui_state(self, is_running=False):
# FIX 1: Finer control over UI elements
self.server_group.setEnabled(not is_running)
self.workflow_group.setEnabled(not is_running)
self.exec_group.setEnabled(not is_running)
self.output_group.setEnabled(not is_running)
self.add_model_button.setEnabled(not is_running)
self.delete_model_button.setEnabled(not is_running)
self.model_list_widget.setEnabled(True) # Always enabled for preview
self.execute_button.setEnabled(not is_running and self.api is not None)
self.stop_button.setEnabled(is_running)
def _load_settings(self):
self.host_input.setText(self.config.get("server", "host", "127.0.0.1"));
self.port_input.setText(self.config.get("server", "port", "8188"));
self.clothing_dir_input.setText(self.config.get("paths", "clothing", ""));
self.output_dir_input.setText(self.config.get("paths", "output", ""));
self.log_message("欢迎使用ComfyUI智能试衣控制器配置已从 config.ini 加载。")
def _save_settings(self):
self.config.set("server", "host", self.host_input.text());
self.config.set("server", "port", self.port_input.text());
self.config.set("paths", "clothing", self.clothing_dir_input.text());
self.config.set("paths", "output", self.output_dir_input.text());
self._save_models()
self.config.save()
def closeEvent(self, event):
reply = QMessageBox.question(self, '确认退出', '您确定要退出程序吗?\n所有配置将自动保存。',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
self._save_settings(); self.log_message("配置已保存,程序退出。"); event.accept()
else:
event.ignore()
def log_message(self, message):
self.log_box.appendPlainText(f"[{time.strftime('%H:%M:%S')}] {message}"); self.status_bar.showMessage(
message.split('\n')[-1], 5000)
# FIX 2: Slot for status bar updates
def _update_status_bar(self, message):
self.status_bar.showMessage(message, 0) # 0 means persistent message
def show_model_preview(self, item):
image_path = item.data(Qt.UserRole)
if image_path and os.path.exists(image_path):
ImagePreviewDialog(image_path, self).exec()
else:
self.show_error("文件错误", f"找不到图片文件:\n{image_path}")
def _add_model(self):
file_path, _ = QFileDialog.getOpenFileName(self, "选择模特图片", self.config.get("paths", "last_used", ""),
"图片文件 (*.png *.jpg *.jpeg *.webp)");
if file_path: item = QListWidgetItem(QIcon(file_path), os.path.basename(file_path)); item.setData(Qt.UserRole,
file_path); self.model_list_widget.addItem(
item); self.log_message(f"已添加模特: {os.path.basename(file_path)}")
def _delete_model(self):
selected_items = self.model_list_widget.selectedItems()
if not selected_items: return self.show_error("操作无效", "请先在左侧列表中选择一个要删除的模特。")
for item in selected_items: self.model_list_widget.takeItem(self.model_list_widget.row(item)); self.log_message(
f"已删除模特: {item.text()}")
def _load_models(self):
models_str = self.config.get("models", "list", "");
if models_str:
models = models_str.splitlines()
for path in models:
if path and os.path.exists(path): item = QListWidgetItem(QIcon(path),
os.path.basename(path)); item.setData(
Qt.UserRole, path); self.model_list_widget.addItem(item)
self.log_message(f"已加载 {len(models)} 个模特。")
def _save_models(self):
models = [self.model_list_widget.item(i).data(Qt.UserRole) for i in range(self.model_list_widget.count())]
self.config.set("models", "list", "\n".join(models))
def connect_and_fetch_workflows(self):
host, port = self.host_input.text(), self.port_input.text();
self.log_message(f"正在连接到 http://{host}:{port}...");
self.api = ComfyAPI(host, port);
self.fetch_workflows()
def fetch_workflows(self):
if not self.api: return
self.log_message("正在获取工作流列表...")
workflows_data, error = self.api.get_workflows();
self.workflow_combo.clear()
if error:
self.show_error("连接错误", f"无法获取工作流列表。\n原因: {error}"); self.api = None
elif workflows_data:
workflows = [item['path'] for item in workflows_data if item.get('path', '').endswith('.json')]
if workflows:
self.workflow_combo.addItems(workflows); self.log_message(f"成功找到 {len(workflows)} 个工作流。")
else:
self.log_message("连接成功但在workflows文件夹中未找到.json文件。")
self._update_ui_state()
def fetch_workflow_details(self, workflow_name):
self.current_workflow_prompt, self.current_workflow_graph = None, None
if not workflow_name or not self.api: self._update_ui_state(); return
self.log_message(f"正在加载工作流: {workflow_name}")
workflow_graph, error = self.api.get_workflow_details(workflow_name)
if error: self._update_ui_state(); return self.show_error("加载失败",
f"无法获取工作流 '{workflow_name}' 的详细信息。\n\n原因: {error}")
if workflow_graph:
self.current_workflow_graph = copy.deepcopy(workflow_graph)
original_node_count = len(workflow_graph['nodes'])
workflow_graph['nodes'] = [node for node in workflow_graph['nodes'] if node.get('type') != 'Note']
removed_count = original_node_count - len(workflow_graph['nodes'])
if removed_count > 0: self.log_message(f"已自动移除 {removed_count}'Note' 节点。")
self.log_message("正在转换工作流为可执行格式...")
self.current_workflow_prompt = convert_graph_to_prompt(workflow_graph)
if self.current_workflow_prompt:
self.log_message("转换成功,工作流已就绪。");
settings_key = f"workflow/{workflow_name.replace('/', '_').replace('.', '_')}"
self.workflow_settings = json.loads(self.config.get("workflows", settings_key, "{}"))
else:
self.show_error("转换失败", "无法将工作流转换为API格式。")
self._update_ui_state()
def _select_directory(self, line_edit, title="选择文件夹"):
start_dir = line_edit.text() or self.config.get("paths", "last_used", "")
directory = QFileDialog.getExistingDirectory(self, title, start_dir);
if directory: line_edit.setText(directory); self.config.set("paths", "last_used", directory)
def open_settings_dialog(self):
if not self.current_workflow_prompt: return self.show_error("未加载工作流", "请先选择一个工作流。")
dialog = SettingsDialog(self.current_workflow_prompt, self.workflow_settings, self)
if dialog.exec():
self.workflow_settings = dialog.get_settings();
workflow_name = self.workflow_combo.currentText()
settings_key = f"workflow/{workflow_name.replace('/', '_').replace('.', '_')}";
self.config.set("workflows", settings_key, json.dumps(self.workflow_settings))
self.log_message(f"已为'{workflow_name}'保存节点设置。")
def start_execution(self):
if self.model_list_widget.currentItem() is None: return self.show_error("缺少输入",
"请在左侧模特库中选择一位模特。")
if not self.clothing_dir_input.text(): return self.show_error("缺少输入", "请选择包含衣物图片的文件夹。")
if not self.output_dir_input.text(): return self.show_error("缺少输入", "请选择输出文件夹。")
if not self.current_workflow_prompt: return self.show_error("缺少工作流", "请选择一个有效的工作流。")
ws = self.workflow_settings
if not all(
[ws.get('model_node_id'), ws.get('clothing_node_id'), ws.get('output_node_id')]): return self.show_error(
"配置不完整", "请点击设置按钮(⚙️)来指定模特、衣物和输出节点。")
self._update_ui_state(is_running=True)
self.execution_thread = QThread()
self.execution_worker = ExecutionWorker(api=self.api, workflow_prompt=self.current_workflow_prompt,
workflow_graph=self.current_workflow_graph,
model_image_path=self.model_list_widget.currentItem().data(Qt.UserRole),
model_node_id=ws['model_node_id'],
clothing_dir=self.clothing_dir_input.text(),
clothing_node_id=ws['clothing_node_id'],
output_dir=self.output_dir_input.text(),
output_node_id=ws['output_node_id'])
self.execution_worker.moveToThread(self.execution_thread);
self.execution_worker.log.connect(self.log_message)
self.execution_worker.progress.connect(self.update_progress)
self.execution_worker.finished.connect(self.execution_finished)
self.execution_worker.status_update.connect(self._update_status_bar) # FIX 2
self.execution_thread.started.connect(self.execution_worker.run)
self.execution_thread.start();
self.log_message("=== 开始批量换装任务 ===")
def stop_execution(self):
if self.execution_worker:
self.execution_worker.stop()
else:
self.log_message("没有正在执行的任务可停止。")
def update_progress(self, current, total):
self.progress_bar.setMaximum(total); self.progress_bar.setValue(current)
def execution_finished(self, success, message):
self.log_message(f"=== {message} ===");
if not success and "中止" not in message: self.show_error("执行失败", message)
self.progress_bar.setValue(0);
self.status_bar.clearMessage() # Clear status bar on finish
if self.execution_thread:
if self.execution_thread.isRunning(): self.execution_thread.quit()
self.execution_thread.wait(1000)
self.execution_thread, self.execution_worker = None, None
self._update_ui_state(is_running=False)
def show_error(self, title, message):
self.log_message(f"错误: {title} - {message}");
QMessageBox.critical(self, title, message)
if __name__ == '__main__':
app = QApplication(sys.argv)
LIGHT_STYLE_SHEET = """
QMainWindow, QDialog { background-color: #f4f6f8; color: #34495e; font-family: "Microsoft YaHei", "Segoe UI", "Roboto", sans-serif; }
QGroupBox { background-color: #ffffff; border: 1px solid #dcdfe6; border-radius: 8px; margin-top: 10px; padding: 15px; }
QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 5px 15px; background-color: #5dade2; color: white; border-top-left-radius: 8px; border-bottom-right-radius: 8px; font-weight: bold; }
QLabel { font-size: 13px; font-weight: bold; color: #34495e; padding: 5px 0; }
QLineEdit, QPlainTextEdit, QComboBox { background-color: #ffffff; border: 1px solid #dcdfe6; border-radius: 4px; padding: 6px; color: #34495e; font-size: 13px; }
QLineEdit:focus, QPlainTextEdit:focus, QComboBox:focus { border-color: #5dade2; }
QPushButton { background-color: #5dade2; color: white; border: none; border-radius: 4px; padding: 8px 16px; font-size: 13px; font-weight: bold; }
QPushButton:hover { background-color: #85c1e9; }
QPushButton:pressed { background-color: #2e86c1; }
QPushButton:disabled { background-color: #bdc3c7; color: #7f8c8d; }
QComboBox::drop-down { border: none; }
QComboBox QAbstractItemView { background-color: #ffffff; border: 1px solid #dcdfe6; selection-background-color: #5dade2; selection-color: white; }
QListWidget { background-color: #ffffff; border: 1px solid #dcdfe6; border-radius: 4px; }
QListWidget::item { padding: 8px; border-bottom: 1px solid #f4f6f8; }
QListWidget::item:hover { background-color: #ecf5ff; }
QListWidget::item:selected { background-color: #5dade2; color: white; }
QProgressBar { border: 1px solid #dcdfe6; border-radius: 4px; text-align: center; background-color: #e9ecef; color: #495057; }
QProgressBar::chunk { background-color: #28a745; border-radius: 3px; }
QStatusBar { background-color: #ffffff; border-top: 1px solid #dcdfe6; }
"""
app.setStyleSheet(LIGHT_STYLE_SHEET)
window = ComfyUIController()
window.show()
sys.exit(app.exec())