Files
model_team_tools/main.py
T

203 lines
7.5 KiB
Python
Raw Normal View History

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 subprocess
import runtime_hook # noqa: F401 启动时把 plugins/*/ 加进 sys.path,必须在 core 之前
2026-06-12 16:01:53 +08:00
import core
import traceback
from typing import Optional
from PySide6.QtCore import 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-06-12 16:01:53 +08:00
class MainWindow(QMainWindow, Ui_MainWindow):
"""
主窗口类
继承自 QMainWindowQt 主窗口基类)和 Ui_MainWindowQt 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)
self.plugin_registry.uninstall_completed_signal.connect(self.on_plugin_uninstalled)
# 工具栏:插件名称 → QAction 映射
self.tool_actions: dict = {}
# 内容区:插件名称 → QWidget 映射
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:
"""打开插件管理窗口。
包含完整的错误处理机制,确保任何异常都不会导致主程序崩溃。
2026-06-12 16:01:53 +08:00
"""
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
def on_plugins_loader(self, plugins: dict) -> None:
"""加载已安装插件到主窗口工具栏和堆栈页面。
为每个插件创建 QAction 并添加到工具栏,同时将插件 QWidget
添加到 stackedWidget 中,点击 action 时切换对应页面。
"""
if not plugins:
return
2026-07-17 15:27:33 +08:00
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))
widget = plugins[name]['obj']
self.stackedWidget.addWidget(widget)
self.tool_actions[name] = action
self.plugin_widgets[name] = widget
logger.info(f"已加载 {len(plugins)} 个插件到工具栏: {list(plugins.keys())}")
2026-07-17 15:27:33 +08:00
def on_action_triggered(self, action: QAction) -> None:
"""工具栏按钮点击:切换到对应插件的 stack 页面。"""
2026-07-20 10:31:10 +08:00
self.stackedWidget.setCurrentWidget(self.plugin_registry.plugins[action.text()]['obj'])
def on_plugin_uninstalled(self, name: str) -> None:
"""卸载完成后清理主窗口工具栏与堆栈窗口中的对应项,并自动重启。
.pyd 已被进程加载,运行时无法删除(Windows 文件锁),因此重启进程:
新进程启动时会先执行待删清单,在 import 之前删除 .pyd 文件。
"""
logger.info(f"从主窗口移除插件: {name}")
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:
"""启动新进程并退出当前进程。
开发模式:python.exe main.py
打包模式:main.exeNuitka standalone
用 subprocess.Popen + DETACHED_PROCESS 直接调 Win32 CreateProcess。
Nuitka standalone 下 sys.executable 指向不存在的原始 python.exe
此时改用 sys.argv[0](即 main.exe 自身)。
"""
work_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
args = sys.argv[:]
if '--restarting' not in args:
args.append('--restarting')
# Nuitka standalone 下 sys.executable 指向编译机器的 python.exe(分发环境不存在),
# 直接用 os.path.isfile 检测,不存在时回退到 sys.argv[0]main.exe 自身)。
if os.path.isfile(sys.executable):
cmd = [sys.executable] + args
else:
cmd = [os.path.abspath(sys.argv[0])] + args[1:]
try:
proc = subprocess.Popen(
cmd,
cwd=work_dir,
creationflags=subprocess.DETACHED_PROCESS,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
logger.info(f"新进程已启动 (PID: {proc.pid}),当前进程即将退出")
QTimer.singleShot(100, QApplication.quit)
except Exception as e:
logger.error(f"启动新进程失败: {e}")
2026-05-13 14:44:05 +08:00
if __name__ == '__main__':
print(f"{APP_NAME} v{APP_VERSION} 启动中...")
2026-06-12 16:01:53 +08:00
app = QApplication(sys.argv)
main: MainWindow = MainWindow()
app.aboutToQuit.connect(lambda: logger.info(f"{APP_NAME} 退出"))
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())