2026-06-12 16:01:53 +08:00
|
|
|
|
"""
|
|
|
|
|
|
Model Team Tools - 主程序入口
|
|
|
|
|
|
|
|
|
|
|
|
应用程序主窗口的初始化和配置模块
|
|
|
|
|
|
使用 PySide6 构建的桌面应用程序框架
|
|
|
|
|
|
|
|
|
|
|
|
主要功能:
|
|
|
|
|
|
- 初始化 Qt 主窗口
|
|
|
|
|
|
- 加载 Qt Material 样式表
|
|
|
|
|
|
- 配置菜单栏和状态栏
|
|
|
|
|
|
- 提供统一的日志记录接口
|
|
|
|
|
|
"""
|
2026-07-17 15:27:33 +08:00
|
|
|
|
import os
|
2026-05-13 14:44:05 +08:00
|
|
|
|
import sys
|
2026-06-12 16:01:53 +08:00
|
|
|
|
import logging
|
|
|
|
|
|
import core
|
|
|
|
|
|
import traceback
|
2026-07-17 15:27:33 +08:00
|
|
|
|
import importlib
|
2026-05-13 14:44:05 +08:00
|
|
|
|
|
2026-06-12 16:01:53 +08:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Optional
|
2026-07-20 13:15:16 +08:00
|
|
|
|
from PySide6.QtCore import QProcess, QTimer
|
2026-07-20 10:31:10 +08:00
|
|
|
|
from PySide6.QtGui import QIcon,QAction
|
|
|
|
|
|
from PySide6.QtWidgets import QApplication, QMainWindow, QMenuBar, QStatusBar
|
2026-05-13 14:44:05 +08:00
|
|
|
|
from qt_material import apply_stylesheet
|
|
|
|
|
|
from main_ui import Ui_MainWindow
|
2026-06-12 16:01:53 +08:00
|
|
|
|
# 获取模块日志记录器
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
# 版本号常量
|
|
|
|
|
|
MAJOR_VER: int = 0 # 主版本号
|
|
|
|
|
|
MINOR_VER: int = 0 # 次版本号
|
|
|
|
|
|
MICRO_VER: int = 1 # 修订版本号
|
|
|
|
|
|
|
|
|
|
|
|
APP_NAME: str = "Model Team Tools"
|
|
|
|
|
|
APP_VERSION: str = f"{MAJOR_VER}.{MINOR_VER}.{MICRO_VER}"
|
2026-05-13 14:44:05 +08:00
|
|
|
|
|
2026-07-17 15:27:33 +08:00
|
|
|
|
LOCAL_PLUGINS_PATH = Path("./plugins")
|
2026-05-13 14:44:05 +08:00
|
|
|
|
|
2026-06-12 16:01:53 +08:00
|
|
|
|
class MainWindow(QMainWindow, Ui_MainWindow):
|
|
|
|
|
|
"""
|
|
|
|
|
|
主窗口类
|
|
|
|
|
|
|
|
|
|
|
|
继承自 QMainWindow(Qt 主窗口基类)和 Ui_MainWindow(Qt Designer 生成的 UI 界面)
|
|
|
|
|
|
负责应用程序主窗口的初始化、样式配置和界面布局
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
2026-05-13 14:44:05 +08:00
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.setupUi(self)
|
2026-05-13 14:46:10 +08:00
|
|
|
|
|
2026-07-17 15:27:33 +08:00
|
|
|
|
self.plugin_registry = core.PluginRegistry()
|
|
|
|
|
|
self.plugins = core.Plugins(self.plugin_registry)
|
|
|
|
|
|
self.plugin_registry.plugins_loader_signal.connect(self.on_plugins_loader)
|
2026-07-20 13:15:16 +08:00
|
|
|
|
self.plugin_registry.uninstall_completed_signal.connect(self.on_plugin_uninstalled)
|
|
|
|
|
|
|
|
|
|
|
|
self.tool_actions: dict = {}
|
|
|
|
|
|
self.plugin_widgets: dict = {}
|
2026-07-17 15:27:33 +08:00
|
|
|
|
|
|
|
|
|
|
self.initUI()
|
2026-07-14 14:24:54 +08:00
|
|
|
|
|
2026-06-12 16:01:53 +08:00
|
|
|
|
def initUI(self) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
初始化用户界面组件
|
|
|
|
|
|
|
|
|
|
|
|
执行以下初始化操作:
|
|
|
|
|
|
- 加载 Qt 样式表和自定义样式
|
|
|
|
|
|
- 设置窗口标题
|
|
|
|
|
|
- 配置菜单栏
|
|
|
|
|
|
- 配置状态栏
|
|
|
|
|
|
- 记录初始化日志
|
|
|
|
|
|
|
|
|
|
|
|
注意:此方法在主窗口创建后调用
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.load_stylesheet()
|
|
|
|
|
|
self.setWindowTitle(APP_NAME)
|
|
|
|
|
|
self.setWindowIcon(QIcon('resources/main.ico'))
|
|
|
|
|
|
self.menubar: Optional[QMenuBar] = self.menuBar()
|
|
|
|
|
|
self.statusbar: QStatusBar = self.statusBar()
|
|
|
|
|
|
self.statusbar.showMessage(f"Version: {APP_VERSION}")
|
|
|
|
|
|
|
|
|
|
|
|
self.action_tools.triggered.connect(self.on_tools)
|
|
|
|
|
|
|
|
|
|
|
|
def load_stylesheet(self) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
加载 Qt 样式表和自定义样式文件
|
|
|
|
|
|
|
|
|
|
|
|
使用 qt_material 库加载暗色主题,并应用自定义 QSS 样式表
|
|
|
|
|
|
失败时记录错误日志,但不影响应用继续运行(降级策略)
|
|
|
|
|
|
|
|
|
|
|
|
异常处理:
|
|
|
|
|
|
- FileNotFoundError: 样式文件不存在
|
|
|
|
|
|
- RuntimeError: 主题加载失败
|
|
|
|
|
|
- 其他异常: 记录警告但不中断应用
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
apply_stylesheet(self, style=None,theme='dark_teal.xml', css_file='resources/my.qss')
|
|
|
|
|
|
except FileNotFoundError as e:
|
|
|
|
|
|
logger.warning(f"样式文件未找到,使用默认主题: {e}")
|
|
|
|
|
|
except RuntimeError as e:
|
|
|
|
|
|
logger.warning(f"主题加载失败,使用默认主题: {e}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"样式加载失败,使用默认主题: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def on_tools(self) -> None:
|
|
|
|
|
|
"""打开工具箱窗口
|
|
|
|
|
|
|
|
|
|
|
|
创建并显示工具箱窗口实例。包含完整的错误处理机制,
|
|
|
|
|
|
确保任何异常都不会导致主程序崩溃。
|
|
|
|
|
|
|
|
|
|
|
|
异常处理:
|
|
|
|
|
|
- 捕获所有异常并记录到日志
|
|
|
|
|
|
- 显示详细的错误堆栈信息
|
|
|
|
|
|
- 不影响主窗口的正常使用
|
|
|
|
|
|
|
|
|
|
|
|
日志记录:
|
|
|
|
|
|
- 成功打开工具箱时记录 INFO 级别日志
|
|
|
|
|
|
- 打开失败时记录 ERROR 级别日志
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2026-07-17 15:27:33 +08:00
|
|
|
|
self.plugins.show()
|
2026-06-12 16:01:53 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"无法打开工具箱窗口: {e}")
|
|
|
|
|
|
logger.error(traceback.format_exc())
|
2026-05-13 14:44:05 +08:00
|
|
|
|
|
2026-07-17 15:27:33 +08:00
|
|
|
|
def on_plugins_loader(self, plugins:dict):
|
|
|
|
|
|
for name in plugins.keys():
|
2026-07-20 10:31:10 +08:00
|
|
|
|
action = QAction(name,self)
|
|
|
|
|
|
self.toolBar.addAction(action)
|
|
|
|
|
|
action.triggered.connect(lambda checked, action=action: self.on_action_triggered(action))
|
2026-07-20 13:15:16 +08:00
|
|
|
|
|
|
|
|
|
|
widget = plugins[name]['obj']
|
|
|
|
|
|
self.stackedWidget.addWidget(widget)
|
|
|
|
|
|
self.tool_actions[name] = action
|
|
|
|
|
|
self.plugin_widgets[name] = widget
|
2026-07-17 15:27:33 +08:00
|
|
|
|
|
2026-07-20 10:31:10 +08:00
|
|
|
|
def on_action_triggered(self, action:QAction):
|
|
|
|
|
|
print(action.text())
|
|
|
|
|
|
self.stackedWidget.setCurrentWidget(self.plugin_registry.plugins[action.text()]['obj'])
|
|
|
|
|
|
|
2026-07-20 13:15:16 +08:00
|
|
|
|
def on_plugin_uninstalled(self, name: str) -> None:
|
|
|
|
|
|
"""卸载完成后清理主窗口工具栏与堆栈窗口中的对应项,并自动重启。
|
|
|
|
|
|
|
|
|
|
|
|
.pyd 已被进程加载,运行时无法删除(Windows 文件锁),因此重启进程:
|
|
|
|
|
|
新进程启动时会先执行待删清单,在 import 之前删除 .pyd 文件。
|
|
|
|
|
|
"""
|
|
|
|
|
|
action = self.tool_actions.pop(name, None)
|
|
|
|
|
|
if action is not None:
|
|
|
|
|
|
self.toolBar.removeAction(action)
|
|
|
|
|
|
action.deleteLater()
|
|
|
|
|
|
widget = self.plugin_widgets.pop(name, None)
|
|
|
|
|
|
if widget is not None:
|
|
|
|
|
|
self.stackedWidget.removeWidget(widget)
|
|
|
|
|
|
widget.deleteLater()
|
|
|
|
|
|
self.statusbar.showMessage(f"{name} 已卸载,应用即将重启以完成清理...", 1500)
|
|
|
|
|
|
QTimer.singleShot(1000, self._restart_app)
|
|
|
|
|
|
|
|
|
|
|
|
def _restart_app(self) -> None:
|
|
|
|
|
|
"""启动新进程并退出当前进程。
|
|
|
|
|
|
|
|
|
|
|
|
sys.executable + sys.argv 同时兼容开发模式(python.exe main.py)
|
|
|
|
|
|
与打包模式(main.exe)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
work_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
|
|
|
|
|
|
|
|
|
|
# 【重要】添加防递归标记,防止新进程启动后再次触发重启逻辑
|
|
|
|
|
|
args = sys.argv[:]
|
|
|
|
|
|
if '--restarting' not in args:
|
|
|
|
|
|
args.append('--restarting')
|
|
|
|
|
|
|
|
|
|
|
|
# 启动新进程
|
|
|
|
|
|
pid = QProcess.startDetached(sys.executable, args, work_dir)
|
|
|
|
|
|
|
|
|
|
|
|
if pid:
|
|
|
|
|
|
print(f"新进程启动成功 (PID: {pid}),准备退出当前进程...")
|
|
|
|
|
|
# ✅ 关键修复:延迟 300ms 退出,确保新进程彻底“断奶”
|
|
|
|
|
|
QTimer.singleShot(300, QApplication.quit)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("重启失败,请检查路径或权限")
|
|
|
|
|
|
|
2026-05-13 14:44:05 +08:00
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2026-06-12 16:01:53 +08:00
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
|
|
main: MainWindow = MainWindow()
|
2026-07-20 13:15:16 +08:00
|
|
|
|
app.aboutToQuit.connect(main.plugin_registry.shutdown)
|
2026-05-13 14:46:10 +08:00
|
|
|
|
main.show()
|
2026-06-12 16:01:53 +08:00
|
|
|
|
sys.exit(app.exec())
|