Files
model_team_tools/main.py
T
feifei.xu 1b6796ec43 docs: 完善README文档与架构图,修复全量注释与日志问题
- README: 补充项目概述、技术栈、架构图(Mermaid)、插件生命周期时序图
- build.py: 补充模块和函数 docstring
- core/__init__.py: 修正模块说明(工具箱→插件系统)
- log.py: 修复过时注释,清理死代码,抑制第三方库日志刷屏
- plugin_registry.py: 修正矛盾注释,补充缺失日志,隐藏远程路径
- plugins.py: 修正过时注释,清理死代码和无用导入
- plugins_card.py: 修正错误类名注释,补充全量 docstring
- main.py: 修正错误注释,补充启动/退出/插件加载日志
2026-07-21 17:04:34 +08:00

203 lines
7.5 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.
"""
Model Team Tools - 主程序入口
应用程序主窗口的初始化和配置模块
使用 PySide6 构建的桌面应用程序框架
主要功能:
- 初始化 Qt 主窗口
- 加载 Qt Material 样式表
- 配置菜单栏和状态栏
- 提供统一的日志记录接口
"""
import os
import sys
import logging
import subprocess
import runtime_hook # noqa: F401 启动时把 plugins/*/ 加进 sys.path,必须在 core 之前
import core
import traceback
from typing import Optional
from PySide6.QtCore import QTimer
from PySide6.QtGui import QIcon,QAction
from PySide6.QtWidgets import QApplication, QMainWindow, QMenuBar, QStatusBar
from qt_material import apply_stylesheet
from main_ui import Ui_MainWindow
# 获取模块日志记录器
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}"
class MainWindow(QMainWindow, Ui_MainWindow):
"""
主窗口类
继承自 QMainWindowQt 主窗口基类)和 Ui_MainWindowQt Designer 生成的 UI 界面)
负责应用程序主窗口的初始化、样式配置和界面布局
"""
def __init__(self) -> None:
super().__init__()
self.setupUi(self)
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 = {}
self.initUI()
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:
"""打开插件管理窗口。
包含完整的错误处理机制,确保任何异常都不会导致主程序崩溃。
"""
try:
self.plugins.show()
except Exception as e:
logger.error(f"无法打开工具箱窗口: {e}")
logger.error(traceback.format_exc())
def on_plugins_loader(self, plugins: dict) -> None:
"""加载已安装插件到主窗口工具栏和堆栈页面。
为每个插件创建 QAction 并添加到工具栏,同时将插件 QWidget
添加到 stackedWidget 中,点击 action 时切换对应页面。
"""
if not plugins:
return
for name in plugins.keys():
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())}")
def on_action_triggered(self, action: QAction) -> None:
"""工具栏按钮点击:切换到对应插件的 stack 页面。"""
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}")
if __name__ == '__main__':
print(f"{APP_NAME} v{APP_VERSION} 启动中...")
app = QApplication(sys.argv)
main: MainWindow = MainWindow()
app.aboutToQuit.connect(lambda: logger.info(f"{APP_NAME} 退出"))
app.aboutToQuit.connect(main.plugin_registry.shutdown)
main.show()
sys.exit(app.exec())