2026-06-17 17:18:15 +08:00
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
|
|
|
|
|
水质参数反演分析系统 - 图形用户界面(重构版:纯壳模式)
|
|
|
|
|
|
|
|
|
|
|
|
WaterQualityGUI 只负责窗口框架(标题栏、菜单栏、状态栏、QTabWidget),
|
|
|
|
|
|
所有业务逻辑委托给独立的 Manager 类。
|
|
|
|
|
|
|
|
|
|
|
|
已实现的 Manager:
|
|
|
|
|
|
- PanelFactory → 面板懒加载与生命周期
|
|
|
|
|
|
- PipelineExecutor → Pipeline 执行/停止/回调(通过 EventBus 发布状态)
|
|
|
|
|
|
- WorkspaceInitializer → 工作目录选择 + 自动回填(通过 EventBus 广播)
|
2026-06-17 17:35:27 +08:00
|
|
|
|
- LogManager → 日志区 + 进度条(内部订阅 LogMessage/ProgressUpdate)
|
|
|
|
|
|
- ConfigManager → 配置读写(new/load/save/get_current_config)
|
|
|
|
|
|
- DialogService → 纯展示类弹窗(Pipeline状态/关于/AI设置)
|
|
|
|
|
|
- TrainingModeManager → 训练模式切换(发布 TrainingModeChanged 事件)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import osgeo # noqa: F401
|
|
|
|
|
|
from osgeo import gdal, ogr # noqa: F401
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import ctypes
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
import multiprocessing
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
from PyQt5.QtWidgets import (
|
|
|
|
|
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
|
|
|
|
QPushButton, QLabel, QTabWidget, QToolBar, QSizePolicy,
|
|
|
|
|
|
QListWidget, QListWidgetItem, QGroupBox,
|
|
|
|
|
|
QTextEdit, QProgressBar, QMessageBox, QFileDialog,
|
2026-06-30 09:38:27 +08:00
|
|
|
|
QSpinBox, QDoubleSpinBox, QComboBox,
|
2026-06-17 17:18:15 +08:00
|
|
|
|
)
|
2026-06-30 09:38:27 +08:00
|
|
|
|
from PyQt5.QtCore import Qt, QTimer, QSize
|
2026-06-17 17:18:15 +08:00
|
|
|
|
from PyQt5.QtGui import QIcon, QFont, QPixmap, QColor, QTextCursor
|
|
|
|
|
|
|
|
|
|
|
|
if multiprocessing.current_process().name == 'MainProcess':
|
|
|
|
|
|
if not QApplication.instance():
|
|
|
|
|
|
try:
|
|
|
|
|
|
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
|
|
|
|
|
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
_global_app = QApplication(sys.argv)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_resource_path(relative_path: str) -> str:
|
|
|
|
|
|
if hasattr(sys, '_MEIPASS'):
|
|
|
|
|
|
return os.path.join(sys._MEIPASS, relative_path)
|
|
|
|
|
|
return os.path.abspath(
|
|
|
|
|
|
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), relative_path)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def global_exception_handler(exc_type, exc_value, exc_traceback):
|
|
|
|
|
|
err_lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
|
|
|
|
|
|
err_msg = "".join(err_lines)
|
|
|
|
|
|
dump_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "crash_dump.txt")
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(dump_path, "a", encoding="utf-8") as f:
|
|
|
|
|
|
f.write(f"\n{'='*60}\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]\n")
|
|
|
|
|
|
f.write(err_msg)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
try:
|
|
|
|
|
|
QMessageBox.critical(None, "程序崩溃",
|
|
|
|
|
|
f"错误类型: {exc_type.__name__}\n错误信息: {exc_value}\n详细信息已写入: {dump_path}")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sys.excepthook = global_exception_handler
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WaterQualityGUI(QMainWindow):
|
|
|
|
|
|
"""水质参数反演分析系统主窗口 —— 纯壳模式。
|
|
|
|
|
|
|
|
|
|
|
|
职责边界:
|
|
|
|
|
|
- 窗口框架(标题栏、菜单栏、状态栏、导航栏)
|
|
|
|
|
|
- QTabWidget 托管(通过 PanelFactory 懒加载)
|
|
|
|
|
|
- 日志区 + 进度条(UI 控件归 shell,内容由 EventBus 驱动)
|
|
|
|
|
|
- 各 Manager 的创建与 EventBus 连线
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
my_appid = u'mycompany.megacube.waterquality.v1'
|
|
|
|
|
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(my_appid)
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
|
|
|
|
icon_path = get_resource_path("data/icons-1/uitubiao.ico")
|
|
|
|
|
|
self.setWindowIcon(QIcon(icon_path))
|
|
|
|
|
|
|
|
|
|
|
|
# 第一步:创建各 Manager(纯连线,不执行业务)
|
|
|
|
|
|
self._init_managers()
|
|
|
|
|
|
|
|
|
|
|
|
# 第二步:构建窗口壳
|
|
|
|
|
|
self._init_shell()
|
|
|
|
|
|
|
|
|
|
|
|
# 第三步:订阅 EventBus 事件 → 驱动 UI 更新
|
|
|
|
|
|
self._wire_event_bus()
|
|
|
|
|
|
|
|
|
|
|
|
# 第四步:应用样式 + 禁用滚轮
|
|
|
|
|
|
self._apply_stylesheet()
|
|
|
|
|
|
self._disable_wheel_for_all_spinboxes()
|
|
|
|
|
|
|
2026-06-30 09:38:27 +08:00
|
|
|
|
# 第五步:默认选中第一个步骤(延迟执行,确保导航列表和 Tab 均已就位)
|
|
|
|
|
|
QTimer.singleShot(120, self._select_first_nav_item)
|
|
|
|
|
|
|
|
|
|
|
|
# 第六步:延迟启动工作目录选择
|
|
|
|
|
|
QTimer.singleShot(200, self._workspace_initializer.run)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
# Manager 初始化
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _init_managers(self):
|
|
|
|
|
|
from src.gui.core.panel_factory import PanelFactory
|
|
|
|
|
|
from src.gui.core.panel_registry import PANEL_REGISTRY
|
|
|
|
|
|
from src.gui.core.workspace_initializer import WorkspaceInitializer
|
|
|
|
|
|
from src.gui.core.pipeline_executor import PipelineExecutor
|
2026-06-17 17:35:27 +08:00
|
|
|
|
from src.gui.core.log_manager import LogManager
|
|
|
|
|
|
from src.gui.core.config_manager import ConfigManager
|
|
|
|
|
|
from src.gui.core.dialog_service import DialogService
|
|
|
|
|
|
from src.gui.core.training_mode_manager import TrainingModeManager
|
2026-06-17 17:18:15 +08:00
|
|
|
|
from src.gui.core.event_bus import global_event_bus
|
|
|
|
|
|
|
|
|
|
|
|
self._panel_factory = PanelFactory(
|
|
|
|
|
|
registry=PANEL_REGISTRY,
|
|
|
|
|
|
main_window=self,
|
|
|
|
|
|
preload_window=1,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._workspace_initializer = WorkspaceInitializer(
|
|
|
|
|
|
panel_factory=self._panel_factory,
|
|
|
|
|
|
parent=self,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._pipeline_executor = PipelineExecutor(
|
|
|
|
|
|
panel_factory=self._panel_factory,
|
|
|
|
|
|
workspace_initializer=self._workspace_initializer,
|
|
|
|
|
|
parent=self,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-17 17:35:27 +08:00
|
|
|
|
self._log_manager = LogManager(parent=self)
|
|
|
|
|
|
|
|
|
|
|
|
self._config_manager = ConfigManager(
|
|
|
|
|
|
panel_factory=self._panel_factory,
|
|
|
|
|
|
parent=self,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._dialog_service = DialogService(parent=self)
|
|
|
|
|
|
|
|
|
|
|
|
self._training_mode_manager = TrainingModeManager(parent=self)
|
|
|
|
|
|
|
2026-06-17 17:18:15 +08:00
|
|
|
|
self._event_bus = global_event_bus
|
|
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
# 窗口壳构建
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _init_shell(self):
|
|
|
|
|
|
self.setWindowTitle("MegaCube-Water Quality V1.2")
|
|
|
|
|
|
|
|
|
|
|
|
screen_geometry = QApplication.primaryScreen().availableGeometry()
|
|
|
|
|
|
screen_width = screen_geometry.width()
|
|
|
|
|
|
screen_height = screen_geometry.height()
|
|
|
|
|
|
self.resize(1200, screen_height)
|
|
|
|
|
|
self.move((screen_width - 1200) // 2, 0)
|
|
|
|
|
|
self.setMinimumSize(600, 400)
|
|
|
|
|
|
|
|
|
|
|
|
self._create_title_bar()
|
|
|
|
|
|
self._create_banner()
|
|
|
|
|
|
self._create_central_layout()
|
|
|
|
|
|
self.statusBar().showMessage("就绪")
|
|
|
|
|
|
|
|
|
|
|
|
def _create_title_bar(self):
|
|
|
|
|
|
title_widget = QWidget()
|
|
|
|
|
|
title_layout = QHBoxLayout()
|
|
|
|
|
|
title_layout.setContentsMargins(8, 4, 8, 4)
|
|
|
|
|
|
title_layout.setSpacing(0)
|
|
|
|
|
|
|
|
|
|
|
|
logo_label = QLabel()
|
|
|
|
|
|
logo_label.setFixedSize(180, 48)
|
|
|
|
|
|
logo_label.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
logo_label.setStyleSheet(
|
|
|
|
|
|
"background-color: #f8f9fa;"
|
|
|
|
|
|
"border-top-left-radius: 4px; border-bottom-left-radius: 4px;"
|
|
|
|
|
|
)
|
|
|
|
|
|
logo_path = get_resource_path("data/icons/logo.png")
|
|
|
|
|
|
logo_pixmap = QPixmap(logo_path)
|
|
|
|
|
|
if not logo_pixmap.isNull():
|
|
|
|
|
|
logo_label.setPixmap(logo_pixmap.scaledToHeight(38, Qt.SmoothTransformation))
|
|
|
|
|
|
else:
|
|
|
|
|
|
logo_label.setText("Logo")
|
|
|
|
|
|
title_layout.addWidget(logo_label)
|
|
|
|
|
|
|
|
|
|
|
|
menubar = self.menuBar()
|
|
|
|
|
|
menubar.setStyleSheet("""
|
|
|
|
|
|
QMenuBar { background-color: #f8f9fa; border: none; padding: 4px 8px; }
|
|
|
|
|
|
QMenuBar::item { padding: 6px 12px; font-size: 13px; }
|
|
|
|
|
|
QMenuBar::item:selected { background-color: #e6f0ff; border-radius: 3px; }
|
|
|
|
|
|
""")
|
|
|
|
|
|
self._build_menus(menubar)
|
|
|
|
|
|
title_layout.addWidget(menubar)
|
|
|
|
|
|
|
|
|
|
|
|
title_widget.setLayout(title_layout)
|
|
|
|
|
|
title_widget.setStyleSheet(
|
|
|
|
|
|
"background-color: #f8f9fa; border-bottom: 1px solid #d0d0d0;"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.setMenuWidget(title_widget)
|
|
|
|
|
|
|
|
|
|
|
|
def _build_menus(self, menubar):
|
|
|
|
|
|
file_menu = menubar.addMenu("文件")
|
|
|
|
|
|
file_menu.addAction("新建配置").triggered.connect(self._on_new_config)
|
|
|
|
|
|
file_menu.addAction("打开配置").triggered.connect(self._on_load_config)
|
|
|
|
|
|
file_menu.addAction("保存配置").triggered.connect(self._on_save_config)
|
|
|
|
|
|
file_menu.addSeparator()
|
|
|
|
|
|
file_menu.addAction("退出").triggered.connect(self.close)
|
|
|
|
|
|
|
|
|
|
|
|
tools_menu = menubar.addMenu("工具")
|
|
|
|
|
|
tools_menu.addAction("设置工作目录").triggered.connect(
|
|
|
|
|
|
self._workspace_initializer.set_work_directory
|
|
|
|
|
|
)
|
|
|
|
|
|
tools_menu.addAction("打开工作目录").triggered.connect(
|
|
|
|
|
|
self._workspace_initializer.open_work_directory
|
|
|
|
|
|
)
|
|
|
|
|
|
tools_menu.addSeparator()
|
|
|
|
|
|
tools_menu.addAction("AI 引擎配置...").triggered.connect(self._on_ai_settings)
|
|
|
|
|
|
tools_menu.addSeparator()
|
|
|
|
|
|
tools_menu.addAction("自动填充所有输入路径").triggered.connect(
|
|
|
|
|
|
self._workspace_initializer.auto_populate_all
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._training_mode_action = tools_menu.addAction("有训练数据模式")
|
|
|
|
|
|
self._training_mode_action.setCheckable(True)
|
|
|
|
|
|
self._training_mode_action.setChecked(True)
|
|
|
|
|
|
self._training_mode_action.triggered.connect(self._on_toggle_training_mode)
|
|
|
|
|
|
|
|
|
|
|
|
help_menu = menubar.addMenu("帮助")
|
|
|
|
|
|
help_menu.addAction("检查Pipeline状态").triggered.connect(self._on_show_pipeline_status)
|
|
|
|
|
|
help_menu.addSeparator()
|
|
|
|
|
|
help_menu.addAction("关于").triggered.connect(self._on_show_about)
|
|
|
|
|
|
|
|
|
|
|
|
def _create_banner(self):
|
|
|
|
|
|
banner_widget = QWidget()
|
|
|
|
|
|
banner_layout = QHBoxLayout()
|
|
|
|
|
|
banner_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
banner_layout.setSpacing(0)
|
|
|
|
|
|
|
|
|
|
|
|
self._banner_label = QLabel()
|
|
|
|
|
|
self._banner_label.setMinimumHeight(140)
|
|
|
|
|
|
self._banner_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
|
|
|
|
self._banner_label.setScaledContents(False)
|
|
|
|
|
|
|
|
|
|
|
|
banner_path = get_resource_path("data/icons/Mega Water 1.0.jpg")
|
|
|
|
|
|
self._banner_pixmap = QPixmap(banner_path)
|
|
|
|
|
|
if not self._banner_pixmap.isNull():
|
|
|
|
|
|
QTimer.singleShot(50, self._update_banner_image)
|
|
|
|
|
|
|
|
|
|
|
|
banner_layout.addWidget(self._banner_label)
|
|
|
|
|
|
|
|
|
|
|
|
self._banner_title_label = QLabel("MegaCube-Water Quality V1.2", self._banner_label)
|
2026-06-26 09:45:11 +08:00
|
|
|
|
# 现代化字体设计:去掉老旧的衬线体,换用带字间距的粗体无衬线字体,增加高级感
|
2026-06-17 17:18:15 +08:00
|
|
|
|
self._banner_title_label.setStyleSheet("""
|
|
|
|
|
|
QLabel {
|
2026-06-26 09:45:11 +08:00
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: #FFFFFF;
|
|
|
|
|
|
font-size: 42px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-family: "Microsoft YaHei UI", "Segoe UI", sans-serif;
|
|
|
|
|
|
letter-spacing: 3px;
|
2026-06-17 17:18:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
self._banner_title_label.setAttribute(Qt.WA_TransparentForMouseEvents)
|
|
|
|
|
|
self._banner_title_label.show()
|
|
|
|
|
|
self._banner_title_label.raise_()
|
|
|
|
|
|
|
|
|
|
|
|
banner_widget.setLayout(banner_layout)
|
|
|
|
|
|
|
|
|
|
|
|
banner_toolbar = QToolBar()
|
|
|
|
|
|
banner_toolbar.setMovable(False)
|
|
|
|
|
|
banner_toolbar.setFloatable(False)
|
|
|
|
|
|
banner_toolbar.addWidget(banner_widget)
|
|
|
|
|
|
banner_toolbar.setStyleSheet(
|
|
|
|
|
|
"QToolBar { background: white; border: none; padding: 0px; margin: 0px; }"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.addToolBar(Qt.TopToolBarArea, banner_toolbar)
|
|
|
|
|
|
|
|
|
|
|
|
def _create_central_layout(self):
|
2026-06-18 11:18:37 +08:00
|
|
|
|
from src.gui.styles import ModernStylesheet
|
2026-06-17 17:18:15 +08:00
|
|
|
|
central_widget = QWidget()
|
|
|
|
|
|
main_layout = QHBoxLayout()
|
|
|
|
|
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
main_layout.setSpacing(0)
|
|
|
|
|
|
|
|
|
|
|
|
main_layout.addWidget(self._create_navigation(), 1)
|
|
|
|
|
|
|
|
|
|
|
|
right_widget = QWidget()
|
|
|
|
|
|
right_layout = QVBoxLayout()
|
|
|
|
|
|
right_layout.setContentsMargins(15, 15, 15, 15)
|
|
|
|
|
|
right_layout.setSpacing(10)
|
|
|
|
|
|
|
|
|
|
|
|
self._tab_widget = self._panel_factory.create_tab_widget(icons_dir="data/icons")
|
2026-06-18 11:18:37 +08:00
|
|
|
|
self._tab_widget.tabBar().setVisible(False)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
right_layout.addWidget(self._tab_widget, 3)
|
|
|
|
|
|
|
2026-06-18 11:18:37 +08:00
|
|
|
|
# 上一步 / 下一步 导航按钮
|
|
|
|
|
|
nav_btn_layout = QHBoxLayout()
|
2026-06-26 09:45:11 +08:00
|
|
|
|
nav_btn_layout.setSpacing(12)
|
|
|
|
|
|
|
|
|
|
|
|
self._prev_btn = QPushButton(" 上一步 ")
|
|
|
|
|
|
self._prev_btn.setMinimumHeight(36)
|
|
|
|
|
|
self._prev_btn.setMinimumWidth(100)
|
|
|
|
|
|
# 上一步是次要操作,用 normal 样式
|
|
|
|
|
|
self._prev_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('normal'))
|
2026-06-18 11:18:37 +08:00
|
|
|
|
self._prev_btn.clicked.connect(self._on_prev_clicked)
|
|
|
|
|
|
nav_btn_layout.addWidget(self._prev_btn)
|
|
|
|
|
|
|
2026-06-26 09:45:11 +08:00
|
|
|
|
self._next_btn = QPushButton(" 下一步 ")
|
|
|
|
|
|
self._next_btn.setMinimumHeight(36)
|
|
|
|
|
|
self._next_btn.setMinimumWidth(100)
|
|
|
|
|
|
# 下一步是主要操作,用 primary (主题蓝) 样式
|
|
|
|
|
|
self._next_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('primary'))
|
2026-06-18 11:18:37 +08:00
|
|
|
|
self._next_btn.clicked.connect(self._on_next_clicked)
|
|
|
|
|
|
nav_btn_layout.addWidget(self._next_btn)
|
|
|
|
|
|
|
|
|
|
|
|
nav_btn_layout.addStretch()
|
|
|
|
|
|
right_layout.addLayout(nav_btn_layout)
|
|
|
|
|
|
|
2026-06-17 17:35:27 +08:00
|
|
|
|
right_layout.addWidget(self._log_manager.create_log_panel(), 1)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
right_widget.setLayout(right_layout)
|
|
|
|
|
|
main_layout.addWidget(right_widget, 4)
|
|
|
|
|
|
|
|
|
|
|
|
central_widget.setLayout(main_layout)
|
|
|
|
|
|
self.setCentralWidget(central_widget)
|
|
|
|
|
|
|
|
|
|
|
|
def _create_navigation(self):
|
|
|
|
|
|
from src.gui.core.panel_registry import build_stage_groups
|
|
|
|
|
|
from src.gui.styles import ModernStylesheet
|
|
|
|
|
|
|
|
|
|
|
|
nav_widget = QWidget()
|
|
|
|
|
|
nav_layout = QVBoxLayout()
|
2026-06-26 09:45:11 +08:00
|
|
|
|
nav_layout.setContentsMargins(0, 15, 0, 15) # 去掉左右边距,让列表通栏
|
2026-06-17 17:18:15 +08:00
|
|
|
|
nav_layout.setSpacing(10)
|
|
|
|
|
|
|
2026-06-26 09:45:11 +08:00
|
|
|
|
title = QLabel("流程导航")
|
|
|
|
|
|
title.setFont(QFont("Microsoft YaHei", 14, QFont.Bold))
|
2026-06-17 17:18:15 +08:00
|
|
|
|
title.setAlignment(Qt.AlignCenter)
|
2026-06-26 09:45:11 +08:00
|
|
|
|
title.setStyleSheet(f"color: {ModernStylesheet.COLORS['primary']}; padding: 10px;")
|
2026-06-17 17:18:15 +08:00
|
|
|
|
nav_layout.addWidget(title)
|
|
|
|
|
|
|
|
|
|
|
|
self._step_list = QListWidget()
|
2026-06-26 09:45:11 +08:00
|
|
|
|
# 注入纯净的现代侧边栏 QSS
|
|
|
|
|
|
self._step_list.setStyleSheet(f"""
|
|
|
|
|
|
QListWidget {{
|
|
|
|
|
|
background-color: {ModernStylesheet.COLORS['panel_bg']};
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
outline: 0; /* 去掉点击时的虚线框 */
|
|
|
|
|
|
}}
|
|
|
|
|
|
QListWidget::item {{
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
border-left: 4px solid transparent;
|
|
|
|
|
|
color: {ModernStylesheet.COLORS['text_primary']};
|
|
|
|
|
|
}}
|
|
|
|
|
|
QListWidget::item:hover {{
|
|
|
|
|
|
background-color: {ModernStylesheet.COLORS['hover']};
|
|
|
|
|
|
}}
|
|
|
|
|
|
QListWidget::item:selected {{
|
|
|
|
|
|
background-color: {ModernStylesheet.COLORS['selected']};
|
|
|
|
|
|
color: {ModernStylesheet.COLORS['primary']};
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
border-left: 4px solid {ModernStylesheet.COLORS['primary']};
|
|
|
|
|
|
}}
|
|
|
|
|
|
""")
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
process_stages = build_stage_groups()
|
|
|
|
|
|
stage_names = list(process_stages.keys())
|
|
|
|
|
|
|
|
|
|
|
|
for stage_idx, (stage_name, steps) in enumerate(process_stages.items()):
|
2026-06-26 09:45:11 +08:00
|
|
|
|
# 分类头
|
2026-06-17 17:18:15 +08:00
|
|
|
|
stage_item = QListWidgetItem(stage_name)
|
2026-06-26 09:45:11 +08:00
|
|
|
|
stage_font = QFont("Microsoft YaHei", 11, QFont.Bold)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
stage_item.setFont(stage_font)
|
2026-06-26 09:45:11 +08:00
|
|
|
|
stage_item.setForeground(QColor(ModernStylesheet.COLORS['primary']))
|
|
|
|
|
|
stage_item.setBackground(QColor(ModernStylesheet.COLORS['main_bg'])) # 给头加个浅灰底色区隔
|
|
|
|
|
|
stage_item.setFlags(stage_item.flags() & ~Qt.ItemIsSelectable & ~Qt.ItemIsEnabled)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
stage_item.setData(Qt.UserRole, "stage_header")
|
|
|
|
|
|
self._step_list.addItem(stage_item)
|
|
|
|
|
|
|
2026-06-26 09:45:11 +08:00
|
|
|
|
# 具体步骤
|
2026-06-17 17:18:15 +08:00
|
|
|
|
for step_id, step_display in steps:
|
2026-06-26 09:45:11 +08:00
|
|
|
|
item = QListWidgetItem(f" {step_display}") # 增加缩进
|
2026-06-17 17:18:15 +08:00
|
|
|
|
item.setData(Qt.UserRole, step_id)
|
2026-06-26 09:45:11 +08:00
|
|
|
|
item.setFont(QFont("Microsoft YaHei", 10))
|
2026-06-17 17:18:15 +08:00
|
|
|
|
self._step_list.addItem(item)
|
|
|
|
|
|
|
2026-06-26 09:45:11 +08:00
|
|
|
|
# 分隔符 (不需要实体占用高度,缩小即可)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
if stage_idx < len(stage_names) - 1:
|
|
|
|
|
|
sep = QListWidgetItem("")
|
2026-06-30 09:38:27 +08:00
|
|
|
|
sep.setSizeHint(QSize(0, 10))
|
2026-06-26 09:45:11 +08:00
|
|
|
|
sep.setFlags(sep.flags() & ~Qt.ItemIsSelectable & ~Qt.ItemIsEnabled)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
self._step_list.addItem(sep)
|
|
|
|
|
|
|
|
|
|
|
|
self._step_list.currentRowChanged.connect(self._on_step_list_changed)
|
|
|
|
|
|
nav_layout.addWidget(self._step_list)
|
|
|
|
|
|
|
2026-06-26 09:45:11 +08:00
|
|
|
|
# 底部按钮区 (仅保留强制停止)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
btn_layout = QVBoxLayout()
|
2026-06-26 09:45:11 +08:00
|
|
|
|
btn_layout.setContentsMargins(15, 0, 15, 0)
|
|
|
|
|
|
|
|
|
|
|
|
self._stop_btn = QPushButton("⏹ 强制停止当前任务")
|
2026-06-17 17:18:15 +08:00
|
|
|
|
self._stop_btn.setEnabled(False)
|
2026-06-26 09:45:11 +08:00
|
|
|
|
self._stop_btn.setMinimumHeight(38)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
self._stop_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('danger'))
|
|
|
|
|
|
self._stop_btn.clicked.connect(self._pipeline_executor.stop_pipeline)
|
|
|
|
|
|
btn_layout.addWidget(self._stop_btn)
|
|
|
|
|
|
|
|
|
|
|
|
nav_layout.addLayout(btn_layout)
|
|
|
|
|
|
nav_widget.setLayout(nav_layout)
|
2026-06-26 09:45:11 +08:00
|
|
|
|
nav_widget.setMaximumWidth(260)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
nav_widget.setStyleSheet(
|
|
|
|
|
|
f"background-color: {ModernStylesheet.COLORS['panel_bg']};"
|
|
|
|
|
|
f"border-right: 1px solid {ModernStylesheet.COLORS['border_light']};"
|
|
|
|
|
|
)
|
|
|
|
|
|
return nav_widget
|
|
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
# EventBus 连线(UI 状态由事件驱动)
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _wire_event_bus(self):
|
|
|
|
|
|
self._event_bus.subscribe('PipelineStarted', self._on_pipeline_started)
|
|
|
|
|
|
self._event_bus.subscribe('PipelineFinished', self._on_pipeline_finished)
|
|
|
|
|
|
self._event_bus.subscribe('PipelineStopped', self._on_pipeline_stopped)
|
|
|
|
|
|
self._event_bus.subscribe('NavigateToTab', self._on_navigate_to_tab)
|
|
|
|
|
|
self._event_bus.subscribe('WorkspaceChanged', self._on_workspace_changed)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_pipeline_started(self, data):
|
|
|
|
|
|
self._stop_btn.setEnabled(True)
|
2026-06-17 17:35:27 +08:00
|
|
|
|
self._log_manager.progress_bar.setValue(0)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
def _on_pipeline_finished(self, data):
|
|
|
|
|
|
self._stop_btn.setEnabled(False)
|
|
|
|
|
|
success = data.get('success', False)
|
|
|
|
|
|
message = data.get('message', '')
|
|
|
|
|
|
if success:
|
2026-06-17 17:35:27 +08:00
|
|
|
|
self._log_manager.progress_bar.setValue(100)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
QMessageBox.information(self, "完成", "流程执行成功!\n\n请查看工作目录中的结果文件。")
|
|
|
|
|
|
else:
|
|
|
|
|
|
QMessageBox.critical(self, "失败", f"流程执行失败:\n\n{message[:200]}")
|
|
|
|
|
|
|
|
|
|
|
|
def _on_pipeline_stopped(self, data):
|
|
|
|
|
|
self._stop_btn.setEnabled(False)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_navigate_to_tab(self, data):
|
|
|
|
|
|
tab_index = data.get('tab_index', 0)
|
|
|
|
|
|
if 0 <= tab_index < self._tab_widget.count():
|
|
|
|
|
|
self._tab_widget.setCurrentIndex(tab_index)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_workspace_changed(self, data):
|
|
|
|
|
|
work_dir = data.get('work_dir', '')
|
|
|
|
|
|
self.statusBar().showMessage(f"工作目录: {work_dir}")
|
|
|
|
|
|
|
2026-06-18 11:18:37 +08:00
|
|
|
|
# 遍历已加载面板,调用 update_from_config 重建内存级参数和默认输出路径
|
|
|
|
|
|
for step_id, panel in self._panel_factory.get_loaded_panels().items():
|
|
|
|
|
|
if not hasattr(panel, 'update_from_config'):
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
panel.update_from_config(work_dir=work_dir, pipeline=None)
|
2026-06-30 09:38:27 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
# 2026-06-30:不再静默吞异常,至少通过 EventBus 输出日志
|
|
|
|
|
|
self._event_bus.publish('LogMessage', {
|
|
|
|
|
|
'message': f'[警告] 面板 {step_id} 的 update_from_config 失败: {e}',
|
|
|
|
|
|
'level': 'warning',
|
|
|
|
|
|
})
|
2026-06-18 11:18:37 +08:00
|
|
|
|
|
2026-06-17 17:18:15 +08:00
|
|
|
|
# ================================================================
|
2026-06-18 11:18:37 +08:00
|
|
|
|
# 导航 → Tab 单向路由(左侧 List 驱动右侧 Tab,Tab 头部已隐藏)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
2026-06-30 09:38:27 +08:00
|
|
|
|
def _select_first_nav_item(self):
|
|
|
|
|
|
"""默认选中左侧导航栏的第一个可导航步骤项(跳过阶段标题/分隔符)。"""
|
|
|
|
|
|
for i in range(self._step_list.count()):
|
|
|
|
|
|
item = self._step_list.item(i)
|
|
|
|
|
|
if item and item.data(Qt.UserRole) not in (None, "stage_header"):
|
|
|
|
|
|
self._step_list.setCurrentRow(i)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-06-17 17:18:15 +08:00
|
|
|
|
def _on_step_list_changed(self, index):
|
2026-06-23 14:39:54 +08:00
|
|
|
|
if index < 0: return
|
2026-06-17 17:18:15 +08:00
|
|
|
|
item = self._step_list.item(index)
|
2026-06-23 14:39:54 +08:00
|
|
|
|
if not item: return
|
2026-06-17 17:18:15 +08:00
|
|
|
|
item_data = item.data(Qt.UserRole)
|
2026-06-23 14:39:54 +08:00
|
|
|
|
|
|
|
|
|
|
if item_data in (None, "stage_header"): return # 跳过阶段标题
|
|
|
|
|
|
|
|
|
|
|
|
from src.gui.core.panel_registry import get_tab_index, PANEL_REGISTRY
|
2026-06-17 17:18:15 +08:00
|
|
|
|
tab_index = get_tab_index(item_data)
|
2026-06-23 14:39:54 +08:00
|
|
|
|
if tab_index < 0: return
|
|
|
|
|
|
|
2026-06-18 13:59:30 +08:00
|
|
|
|
try:
|
2026-06-23 14:39:54 +08:00
|
|
|
|
# 1. 触发懒加载生成面板
|
2026-06-18 13:59:30 +08:00
|
|
|
|
self._panel_factory.get_panel(item_data)
|
2026-06-23 14:39:54 +08:00
|
|
|
|
|
|
|
|
|
|
# 🚨 核心防卡死补丁:如果目标 Tab 被后台任务异常永久锁定,强制撬开!
|
|
|
|
|
|
if not self._tab_widget.isTabEnabled(tab_index):
|
|
|
|
|
|
self._log_manager.info(f"检测到 {item_data} 处于异常锁定状态,已执行强制解锁。")
|
|
|
|
|
|
self._tab_widget.setTabEnabled(tab_index, True)
|
|
|
|
|
|
|
|
|
|
|
|
# 【新增修复】:在跳之前,再核实一遍要去的 tab_index 和 item_data 的 step_id 是否吻合!
|
|
|
|
|
|
# 如果因为删除了死文件导致索引错位,这里强行用真实 step_id 再找一遍!
|
2026-06-18 13:59:30 +08:00
|
|
|
|
try:
|
2026-06-23 14:39:54 +08:00
|
|
|
|
self._panel_factory.get_panel(item_data)
|
|
|
|
|
|
# 遍历右侧所有已加载的 Tab,找到属于当前 step_id 的那个正确的 Tab 索引
|
|
|
|
|
|
for i in range(self._tab_widget.count()):
|
|
|
|
|
|
scroll_area = self._tab_widget.widget(i)
|
|
|
|
|
|
if scroll_area and hasattr(scroll_area, 'widget'):
|
|
|
|
|
|
real_panel = scroll_area.widget()
|
|
|
|
|
|
# 如果这个 panel 就是我们要找的 panel,强制更新 tab_index
|
|
|
|
|
|
if real_panel == self._panel_factory.get_panel(item_data):
|
|
|
|
|
|
tab_index = i
|
2026-06-18 13:59:30 +08:00
|
|
|
|
break
|
2026-06-23 14:39:54 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self._log_manager.error(f"页面实例化失败: {str(e)}")
|
|
|
|
|
|
return
|
2026-06-18 13:59:30 +08:00
|
|
|
|
|
2026-06-23 14:39:54 +08:00
|
|
|
|
# 2. 强制使用注册表的固定索引进行跳转
|
|
|
|
|
|
self._tab_widget.setCurrentIndex(tab_index)
|
2026-06-18 11:18:37 +08:00
|
|
|
|
|
2026-06-23 14:39:54 +08:00
|
|
|
|
# 【新增修复】:每次切页时,强制重播所有面板的输入值!
|
|
|
|
|
|
# 原理:打破 preload_window 造成的时序差,确保目标页面能拿到源页面最新填写的值
|
|
|
|
|
|
if hasattr(self, '_panel_factory'):
|
|
|
|
|
|
self._panel_factory._replay_live_panel_inputs()
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 防撕裂回弹机制:如果由于某种原因没跳过去,把左边的蓝条强行拽回当前真实页面
|
|
|
|
|
|
if self._tab_widget.currentIndex() != tab_index:
|
|
|
|
|
|
self._step_list.blockSignals(True)
|
|
|
|
|
|
current_step_id = PANEL_REGISTRY[self._tab_widget.currentIndex()]['step_id']
|
|
|
|
|
|
for i in range(self._step_list.count()):
|
|
|
|
|
|
list_item = self._step_list.item(i)
|
|
|
|
|
|
if list_item and list_item.data(Qt.UserRole) == current_step_id:
|
|
|
|
|
|
self._step_list.setCurrentRow(i)
|
|
|
|
|
|
break
|
|
|
|
|
|
self._step_list.blockSignals(False)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self._log_manager.error(f"页面跳转失败: {str(e)}")
|
2026-06-18 11:18:37 +08:00
|
|
|
|
|
|
|
|
|
|
def _find_prev_step_row(self, current_row):
|
|
|
|
|
|
"""从 current_row 向上遍历,跳过 stage_header 和空分隔符,返回上一个有效 step 的行号。"""
|
|
|
|
|
|
for i in range(current_row - 1, -1, -1):
|
|
|
|
|
|
item = self._step_list.item(i)
|
|
|
|
|
|
if not item:
|
|
|
|
|
|
continue
|
|
|
|
|
|
data = item.data(Qt.UserRole)
|
|
|
|
|
|
if data and data != "stage_header":
|
|
|
|
|
|
return i
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _find_next_step_row(self, current_row):
|
|
|
|
|
|
"""从 current_row 向下遍历,跳过 stage_header 和空分隔符,返回下一个有效 step 的行号。"""
|
|
|
|
|
|
for i in range(current_row + 1, self._step_list.count()):
|
|
|
|
|
|
item = self._step_list.item(i)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
if not item:
|
|
|
|
|
|
continue
|
2026-06-18 11:18:37 +08:00
|
|
|
|
data = item.data(Qt.UserRole)
|
|
|
|
|
|
if data and data != "stage_header":
|
|
|
|
|
|
return i
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _on_prev_clicked(self):
|
|
|
|
|
|
"""上一步按钮:跳转到上一个有效步骤。"""
|
|
|
|
|
|
row = self._find_prev_step_row(self._step_list.currentRow())
|
|
|
|
|
|
if row is not None:
|
|
|
|
|
|
self._step_list.setCurrentRow(row)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_next_clicked(self):
|
|
|
|
|
|
"""下一步按钮:跳转到下一个有效步骤。"""
|
|
|
|
|
|
row = self._find_next_step_row(self._step_list.currentRow())
|
|
|
|
|
|
if row is not None:
|
|
|
|
|
|
self._step_list.setCurrentRow(row)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
# 横幅自适应
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _update_banner_image(self):
|
|
|
|
|
|
if not hasattr(self, '_banner_pixmap') or self._banner_pixmap.isNull():
|
|
|
|
|
|
return
|
|
|
|
|
|
TARGET_HEIGHT = 140
|
|
|
|
|
|
target_width = self.width()
|
|
|
|
|
|
orig_w = self._banner_pixmap.width()
|
|
|
|
|
|
orig_h = self._banner_pixmap.height()
|
|
|
|
|
|
scale_factor = max(target_width / orig_w, TARGET_HEIGHT / orig_h)
|
|
|
|
|
|
new_w = int(orig_w * scale_factor)
|
|
|
|
|
|
new_h = int(orig_h * scale_factor)
|
|
|
|
|
|
scaled = self._banner_pixmap.scaled(new_w, new_h, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
|
|
|
|
|
crop_x = (new_w - target_width) // 2
|
|
|
|
|
|
crop_y = (new_h - TARGET_HEIGHT) // 2
|
|
|
|
|
|
final = scaled.copy(crop_x, crop_y, target_width, TARGET_HEIGHT)
|
|
|
|
|
|
self._banner_label.setFixedHeight(TARGET_HEIGHT)
|
|
|
|
|
|
self._banner_label.setFixedWidth(target_width)
|
|
|
|
|
|
self._banner_label.setPixmap(final)
|
|
|
|
|
|
if hasattr(self, '_banner_title_label'):
|
|
|
|
|
|
title_x = 160
|
|
|
|
|
|
title_y = max(0, (TARGET_HEIGHT - 60) // 2)
|
|
|
|
|
|
self._banner_title_label.move(title_x, title_y)
|
|
|
|
|
|
self._banner_title_label.resize(target_width - title_x - 20, 60)
|
|
|
|
|
|
|
|
|
|
|
|
def resizeEvent(self, event):
|
|
|
|
|
|
super().resizeEvent(event)
|
|
|
|
|
|
self._update_banner_image()
|
|
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
# 样式与 UX
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_stylesheet(self):
|
|
|
|
|
|
from src.gui.styles import ModernStylesheet
|
|
|
|
|
|
self.setStyleSheet(ModernStylesheet.get_main_stylesheet())
|
|
|
|
|
|
|
|
|
|
|
|
def _disable_wheel_for_all_spinboxes(self):
|
|
|
|
|
|
for sb in self.findChildren(QSpinBox):
|
|
|
|
|
|
sb.setFocusPolicy(Qt.StrongFocus)
|
|
|
|
|
|
sb.wheelEvent = lambda e, s=sb: None
|
|
|
|
|
|
for sb in self.findChildren(QDoubleSpinBox):
|
|
|
|
|
|
sb.setFocusPolicy(Qt.StrongFocus)
|
|
|
|
|
|
sb.wheelEvent = lambda e, s=sb: None
|
|
|
|
|
|
for cb in self.findChildren(QComboBox):
|
|
|
|
|
|
cb.setFocusPolicy(Qt.StrongFocus)
|
|
|
|
|
|
cb.wheelEvent = lambda e, c=cb: None
|
|
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
2026-06-17 17:35:27 +08:00
|
|
|
|
# 菜单回调(全部委托给对应 Manager)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _on_new_config(self):
|
2026-06-17 17:35:27 +08:00
|
|
|
|
self._config_manager.new_config()
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
def _on_load_config(self):
|
2026-06-17 17:35:27 +08:00
|
|
|
|
self._config_manager.load_config()
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
def _on_save_config(self):
|
2026-06-17 17:35:27 +08:00
|
|
|
|
self._config_manager.save_config()
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
def _on_ai_settings(self):
|
2026-06-17 17:35:27 +08:00
|
|
|
|
self._dialog_service.show_ai_settings()
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
def _on_toggle_training_mode(self, checked):
|
2026-06-17 17:35:27 +08:00
|
|
|
|
self._training_mode_manager.toggle(checked)
|
|
|
|
|
|
self._training_mode_action.setText(
|
|
|
|
|
|
self._training_mode_manager.get_action_text(checked)
|
|
|
|
|
|
)
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
def _on_show_pipeline_status(self):
|
2026-06-17 17:35:27 +08:00
|
|
|
|
self._dialog_service.show_pipeline_status()
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
def _on_show_about(self):
|
2026-06-17 17:35:27 +08:00
|
|
|
|
self._dialog_service.show_about()
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
# 向后兼容属性
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def panels(self):
|
|
|
|
|
|
return self._panel_factory.get_loaded_panels()
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def work_dir(self):
|
|
|
|
|
|
return self._workspace_initializer.work_dir
|
|
|
|
|
|
|
|
|
|
|
|
@work_dir.setter
|
|
|
|
|
|
def work_dir(self, value):
|
|
|
|
|
|
self._workspace_initializer.work_dir = value
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def pipeline(self):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def worker(self):
|
|
|
|
|
|
return self._pipeline_executor.worker
|
|
|
|
|
|
|
|
|
|
|
|
def log_message(self, message, level='info'):
|
|
|
|
|
|
self._event_bus.publish('LogMessage', {'message': message, 'level': level})
|
|
|
|
|
|
|
|
|
|
|
|
def update_progress(self, percentage, message):
|
|
|
|
|
|
self._event_bus.publish('ProgressUpdate', {'percentage': percentage, 'message': message})
|
|
|
|
|
|
|
|
|
|
|
|
def get_current_config(self):
|
2026-06-17 17:35:27 +08:00
|
|
|
|
return self._config_manager.get_current_config()
|
2026-06-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
def main():
|
|
|
|
|
|
if multiprocessing.current_process().name != 'MainProcess':
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
app = QApplication.instance()
|
|
|
|
|
|
if not app:
|
|
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
|
|
app.setApplicationName("Mega Water")
|
|
|
|
|
|
app.setOrganizationName("WaterQuality")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
from src.auth.license_manager import verify_license
|
|
|
|
|
|
from src.auth.license_dialog import LicenseDialog
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
_current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
_project_root = os.path.abspath(os.path.join(_current_dir, '..', '..'))
|
|
|
|
|
|
if _project_root not in sys.path:
|
|
|
|
|
|
sys.path.insert(0, _project_root)
|
|
|
|
|
|
from src.auth.license_manager import verify_license
|
|
|
|
|
|
from src.auth.license_dialog import LicenseDialog
|
|
|
|
|
|
|
|
|
|
|
|
_is_license_valid, _license_msg = verify_license()
|
|
|
|
|
|
if not _is_license_valid:
|
|
|
|
|
|
LicenseDialog().exec_()
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
window = WaterQualityGUI()
|
|
|
|
|
|
window.show()
|
|
|
|
|
|
sys.exit(app.exec_())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
if multiprocessing.current_process().name != 'MainProcess':
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
try:
|
|
|
|
|
|
multiprocessing.freeze_support()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
main()
|