From 05b93799d408b797b5db423f728c1c4f708cca6b Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sat, 30 May 2026 17:56:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(gui):=20=E5=BC=95=E5=85=A5=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E8=AE=BE=E7=BD=AE=E7=AA=97=E5=8F=A3=20ALSettingsWidge?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ALSettingsWidget,左侧导航+右侧内容的设置面板 - 合并外观主题、界面风格、自定义QSS为单页布局 - 深浅色主题通过 Qt.ColorScheme 官方 API 实现 - 界面风格变更检测基于当前运行的 QStyle 比对 - 使用 qtawesome 提供矢量导航图标 - 风格变更时弹出重启确认对话框 - ALAutoScriptEditDialog 中重置/复制按钮改用 qtawesome 图标 - 外观初始化迁移至 boot.AppInitializer - 菜单栏新增工具→全局设置入口 - GLOBAL 配置扩展 appearance 段(theme/style/custom_qss) Co-Authored-By: Claude Opus 4.8 --- requirements.txt | Bin 1490 -> 1588 bytes src/Main.py | 1 - src/boot/AppInitializer.py | 20 +- src/gui/ALAutoScriptEditDialog.py | 19 +- src/gui/ALMainWindow.py | 32 +- src/gui/ALSettingsWidget.py | 340 ++++++++++++++++ src/gui/resources/ui/ALMainWindow.ui | 12 + src/gui/resources/ui/ALSettingsWidget.ui | 480 +++++++++++++++++++++++ src/interfaces/ConfigProvider.py | 6 + src/managers/config/ConfigManager.py | 5 + 10 files changed, 906 insertions(+), 9 deletions(-) create mode 100644 src/gui/ALSettingsWidget.py create mode 100644 src/gui/resources/ui/ALSettingsWidget.ui diff --git a/requirements.txt b/requirements.txt index b85c98d87b0d8fdbee7985539adb91ceeaf2801b..95204cc9a1eb0e474262b145072cb7b46c881a72 100644 GIT binary patch delta 106 zcmcb_y@f~Z|Gz|r9EK8xbcP~^M1}%}37s0xJZm31FxM%NT)VHlJr^W&!}UqZ7{n delta 16 YcmdnObBUYj|G$lD*O)h3vHW5L06@hDIRF3v diff --git a/src/Main.py b/src/Main.py index c9e50ca..cf35f22 100644 --- a/src/Main.py +++ b/src/Main.py @@ -24,7 +24,6 @@ def main(): translator = QTranslator() if translator.load(":/res/translators/qtbase_zh_CN.ts"): app.installTranslator(translator) - app.setStyle("Fusion") app.setApplicationName("AutoLibrary") if not initializeApp(): sys.exit(-1) diff --git a/src/boot/AppInitializer.py b/src/boot/AppInitializer.py index 4512368..4c0440e 100644 --- a/src/boot/AppInitializer.py +++ b/src/boot/AppInitializer.py @@ -10,10 +10,13 @@ See the LICENSE file for details. import os from PySide6.QtCore import QStandardPaths, QDir +from PySide6.QtWidgets import QApplication -from managers.log.LogManager import instance as logInstance +from gui.ALSettingsWidget import _applyTheme +from interfaces.ConfigProvider import CfgKey from managers.config.ConfigManager import instance as configInstance from managers.driver.WebDriverManager import instance as webdriverInstance +from managers.log.LogManager import instance as logInstance def _initializeLogManager( @@ -64,13 +67,25 @@ def _initializeWebDriverManager( webdriverInstance(driver_dir) return True +def _initializeAppearance( +): + + app = QApplication.instance() + if not app: + return + cfg = configInstance() + saved_style = cfg.get(CfgKey.GLOBAL.APPEARANCE.STYLE, "Fusion") + saved_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.THEME, "system") + app.setStyle(saved_style) + _applyTheme(saved_theme) + def initializeApp( ) -> bool: """ Initialize the application components Order: - LogManager -> ConfigManager -> WebDriverManager + LogManager -> ConfigManager -> WebDriverManager -> Appearance """ if not _initializeLogManager(): @@ -79,4 +94,5 @@ def initializeApp( return False if not _initializeWebDriverManager(): return False + _initializeAppearance() return True diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py index 750686b..a501de0 100644 --- a/src/gui/ALAutoScriptEditDialog.py +++ b/src/gui/ALAutoScriptEditDialog.py @@ -9,6 +9,8 @@ See the LICENSE file for details. """ from copy import deepcopy +import qtawesome as qta + from PySide6.QtCore import ( QDate, QSize, @@ -20,7 +22,6 @@ from PySide6.QtCore import ( from PySide6.QtGui import ( QColor, QFont, - QIcon, QSyntaxHighlighter, QTextCharFormat, ) @@ -215,8 +216,8 @@ class ALAutoScriptEditDialog(QDialog): self.ZoomOutBtn = QPushButton("-") self.ZoomOutBtn.setFixedSize(25, 25) self.ZoomResetBtn = QPushButton("") - self.ZoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg")) - self.ZoomResetBtn.setIconSize(QSize(20, 20)) + self.ZoomResetBtn.setIcon(qta.icon("fa5s.undo", color=self._iconColor())) + self.ZoomResetBtn.setIconSize(QSize(14, 14)) self.ZoomResetBtn.setFixedSize(25, 25) self.ZoomResetBtn.setToolTip("重置缩放") self.ZoomLabel = QLabel(f"{self._fontSize}px") @@ -240,8 +241,8 @@ class ALAutoScriptEditDialog(QDialog): ToolbarLayout.addWidget(self.ZoomLabel) ToolbarLayout.addStretch() self.CopyBtn = QPushButton("") - self.CopyBtn.setIcon(QIcon(":/res/icons/Copy.svg")) - self.CopyBtn.setIconSize(QSize(20, 20)) + self.CopyBtn.setIcon(qta.icon("fa5s.copy", color=self._iconColor())) + self.CopyBtn.setIconSize(QSize(14, 14)) self.CopyBtn.setFixedSize(25, 25) self.CopyBtn.setToolTip("复制脚本") ToolbarLayout.addWidget(self.CopyBtn) @@ -537,6 +538,14 @@ class ALAutoScriptEditDialog(QDialog): else: widget.setText(str(value)) + def _iconColor( + self + ) -> str: + + return QApplication.instance().palette().color( + QApplication.instance().palette().ColorRole.WindowText + ).name() + def connectSignals( self ): diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index 82a944f..5d805c6 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -33,6 +33,7 @@ from PySide6.QtWidgets import ( from base.MsgBase import MsgBase from gui.ALAboutDialog import ALAboutDialog from gui.ALConfigWidget import ALConfigWidget +from gui.ALSettingsWidget import ALSettingsWidget from gui.ALMainWorkers import ( AutoLibWorker, TimerTaskWorker @@ -60,6 +61,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__config_paths = ConfigUtils.getAutomationConfigPaths() self.__alTimerTaskManageWidget = None self.__alConfigWidget = None + self.__alSettingsWidget = None self.__auto_lib_thread = None self.__current_timer_task_thread = None self.__is_running_timer_task = False @@ -81,6 +83,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.MessageIOTextEdit.setFont(QFont("Courier New", 10)) self.ManualAction.triggered.connect(self.onManualActionTriggered) self.AboutAction.triggered.connect(self.onAboutActionTriggered) + self.SettingsAction.triggered.connect(self.onSettingsActionTriggered) # initialize timer task widget, but not show it try: @@ -125,7 +128,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): return self.TrayIcon = QSystemTrayIcon(self.Icon, self) self.TrayIcon.setToolTip("AutoLibrary") - self.TrayMenu = QMenu() self.TrayMenu.addAction("显示主窗口", self.showNormal) self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked) @@ -190,6 +192,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): if self.__alConfigWidget: self.__alConfigWidget.close() # the config widget is already deleted in the 'self.onConfigWidgetClosed' + if self.__alSettingsWidget: + self.__alSettingsWidget.close() + # the settings widget is already deleted in the 'self.onSettingsWidgetClosed' self._showLog("主窗口关闭") QMainWindow.closeEvent(self, event) @@ -302,6 +307,31 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.setControlButtons(True, None, None) self._showLog("配置窗口已关闭,配置文件路径已更新") + @Slot() + def onSettingsWidgetClosed( + self + ): + + if self.__alSettingsWidget: + self.__alSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed) + self.__alSettingsWidget.deleteLater() + self.__alSettingsWidget = None + self.SettingsAction.setEnabled(True) + + @Slot() + def onSettingsActionTriggered( + self + ): + + if self.__alSettingsWidget is None: + self.__alSettingsWidget = ALSettingsWidget(self) + self.__alSettingsWidget.settingsWidgetIsClosed.connect(self.onSettingsWidgetClosed) + self.__alSettingsWidget.show() + self.__alSettingsWidget.raise_() + self.__alSettingsWidget.activateWindow() + self.SettingsAction.setEnabled(False) + self._showLog("打开全局设置窗口") + @Slot(dict) def onTimerTaskIsReady( self, diff --git a/src/gui/ALSettingsWidget.py b/src/gui/ALSettingsWidget.py new file mode 100644 index 0000000..1fe44fb --- /dev/null +++ b/src/gui/ALSettingsWidget.py @@ -0,0 +1,340 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2026 KenanZhu. +All rights reserved. + +This software is provided "as is", without any warranty of any kind. +You may use, modify, and distribute this file under the terms of the MIT License. +See the LICENSE file for details. +""" +import os +import sys + +import qtawesome as qta + +from PySide6.QtCore import ( + QProcess, + Qt, + Signal, + Slot +) +from PySide6.QtGui import ( + QCloseEvent, + QShowEvent +) +from PySide6.QtWidgets import ( + QApplication, + QFileDialog, + QMessageBox, + QStyle, + QStyleFactory, + QWidget +) + +import managers.config.ConfigManager as ConfigManager + +from gui.resources.ui.Ui_ALSettingsWidget import Ui_ALSettingsWidget +from interfaces.ConfigProvider import ( + CfgKey, + ConfigProvider +) + + +def _clearQss( +): + + app : QApplication | None = QApplication.instance() + if app: + app.setStyleSheet("") + +def _loadQss( + file_path: str +) -> str: + + if not file_path or not os.path.isfile(file_path): + return "" + try: + with open(file_path, "r", encoding="utf-8") as fh: + return fh.read() + except Exception: + return "" + +def _applyQss( + file_path: str +): + + app : QApplication | None = QApplication.instance() + if not app: + return + qss = _loadQss(file_path) + if qss: + app.setStyleSheet(qss) + else: + _clearQss() + +def _applyTheme( + theme: str +): + + app : QApplication | None = QApplication.instance() + if not app: + return + if theme == "dark": + app.styleHints().setColorScheme(Qt.ColorScheme.Dark) + elif theme == "light": + app.styleHints().setColorScheme(Qt.ColorScheme.Light) + else: + app.styleHints().setColorScheme(Qt.ColorScheme.Unknown) + app.setStyle(QStyleFactory.create(app.style().objectName())) + +def _restartApp( +): + + QApplication.instance().quit() + QProcess.startDetached(sys.executable, sys.argv) + + +class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): + + settingsWidgetIsClosed = Signal() + + def __init__( + self, + parent=None + ): + super().__init__(parent) + self.__cfg_mgr: ConfigProvider = ConfigManager.instance() + self.__original_style: QStyle | None = None + + self.setupUi(self) + self.modifyUi() + self.connectSignals() + self.loadSettings() + + def modifyUi( + self + ): + + self.setWindowFlags(Qt.WindowType.Window) + self.NavigationList.setCurrentRow(0) + self.populateStyles() + self.setNavigationIcons() + + def setNavigationIcons( + self + ): + + app : QApplication | None = QApplication.instance() + color = app.palette().color(app.palette().ColorRole.WindowText).name() + item = self.NavigationList.item(0) + if item: + item.setIcon(qta.icon("fa5s.palette", color=color)) + + def populateStyles( + self + ): + + self.StyleComboBox.clear() + self.StyleComboBox.addItems(QStyleFactory.keys()) + + def connectSignals( + self + ): + + self.BrowseQssButton.clicked.connect(self.onBrowseQssButtonClicked) + self.ApplyQssButton.clicked.connect(self.onApplyQssButtonClicked) + self.ResetQssButton.clicked.connect(self.onResetQssButtonClicked) + self.CancelButton.clicked.connect(self.onCancelButtonClicked) + self.ApplyButton.clicked.connect(self.onApplyButtonClicked) + self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) + + def showEvent( + self, + event: QShowEvent + ): + + result = super().showEvent(event) + screen_rect = self.screen().geometry() + target_pos = self.parent().geometry().center() + target_pos.setX(target_pos.x() - self.width()//2) + target_pos.setY(target_pos.y() - self.height()//2) + if target_pos.x() < 0: + target_pos.setX(0) + if target_pos.x() + self.width() > screen_rect.width(): + target_pos.setX(screen_rect.width() - self.width()) + if target_pos.y() < 0: + target_pos.setY(0) + if target_pos.y() + self.height() > screen_rect.height(): + target_pos.setY(screen_rect.height() - self.height()) + self.move(target_pos) + return result + + def closeEvent( + self, + event: QCloseEvent + ): + + self.settingsWidgetIsClosed.emit() + super().closeEvent(event) + + def loadSettings( + self + ): + + theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.THEME, "system") + style = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.STYLE, "Fusion") + custom_qss = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_QSS, "") + self.__original_style = QApplication.instance().style() + if theme == "light": + self.LightThemeRadio.setChecked(True) + elif theme == "dark": + self.DarkThemeRadio.setChecked(True) + else: + self.SystemThemeRadio.setChecked(True) + index = self.StyleComboBox.findText(style) + if index >= 0: + self.StyleComboBox.setCurrentIndex(index) + else: + self.StyleComboBox.setCurrentIndex(0) + self.QssPathEdit.setText(custom_qss) + self.updateQssStatus(custom_qss) + + def updateQssStatus( + self, + qss_path: str + ): + + if qss_path and os.path.isfile(qss_path): + self.QssStatusLabel.setText(f"已加载自定义样式文件:{qss_path}") + else: + self.QssStatusLabel.setText("当前使用程序默认外观。") + + def collectSettings( + self + ): + + if self.LightThemeRadio.isChecked(): + theme = "light" + elif self.DarkThemeRadio.isChecked(): + theme = "dark" + else: + theme = "system" + style = QStyleFactory.create(self.StyleComboBox.currentText()) + custom_qss = self.QssPathEdit.text().strip() + return theme, style, custom_qss + + def saveAndApply( + self + ): + + theme, style, custom_qss = self.collectSettings() + self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme) + self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.STYLE, style.name()) + self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_QSS, custom_qss) + if custom_qss and os.path.isfile(custom_qss): + _applyQss(custom_qss) + else: + _clearQss() + _applyTheme(theme) + self.setNavigationIcons() + self.updateQssStatus(custom_qss) + self.__original_style = QApplication.instance().style() + + def maybeRestart( + self + ) -> bool: + + reply = QMessageBox.question( + self, + "提示 - AutoLibrary", + "界面风格已修改,需要重启程序才能生效。是否立即重启?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + if reply == QMessageBox.Yes: + _restartApp() + return True + return False + + @Slot() + def onBrowseQssButtonClicked( + self + ): + + file_path, _ = QFileDialog.getOpenFileName( + self, + "选择 QSS 样式文件 - AutoLibrary", + self.QssPathEdit.text(), + "QSS 样式表文件 (*.qss);;所有文件 (*)" + ) + if file_path: + self.QssPathEdit.setText(file_path) + + @Slot() + def onApplyQssButtonClicked( + self + ): + + qss_path = self.QssPathEdit.text().strip() + if not qss_path: + QMessageBox.warning( + self, + "提示 - AutoLibrary", + "请先选择或输入 QSS 样式表文件路径。" + ) + return + if not os.path.isfile(qss_path): + QMessageBox.warning( + self, + "警告 - AutoLibrary", + f"未找到指定的样式文件:\n{qss_path}" + ) + return + _applyQss(qss_path) + self.updateQssStatus(qss_path) + + @Slot() + def onResetQssButtonClicked( + self + ): + + self.QssPathEdit.clear() + _clearQss() + if self.LightThemeRadio.isChecked(): + _applyTheme("light") + elif self.DarkThemeRadio.isChecked(): + _applyTheme("dark") + else: + _applyTheme("system") + self.setNavigationIcons() + self.updateQssStatus("") + + @Slot() + def onCancelButtonClicked( + self + ): + + self.close() + + @Slot() + def onApplyButtonClicked( + self + ): + + _, style, _ = self.collectSettings() + style_changed = self.__original_style.name() != style.name() + self.saveAndApply() + if style_changed: + self.maybeRestart() + + @Slot() + def onConfirmButtonClicked( + self + ): + + _, style, _ = self.collectSettings() + style_changed = self.__original_style.name() != style.name() + self.saveAndApply() + if style_changed: + self.maybeRestart() + self.close() diff --git a/src/gui/resources/ui/ALMainWindow.ui b/src/gui/resources/ui/ALMainWindow.ui index c210a8b..84aa38c 100644 --- a/src/gui/resources/ui/ALMainWindow.ui +++ b/src/gui/resources/ui/ALMainWindow.ui @@ -281,6 +281,12 @@ font: 700 9pt; true + + + 工具 + + + true @@ -291,6 +297,7 @@ font: 700 9pt; + @@ -308,6 +315,11 @@ font: 700 9pt; 关于 + + + 全局设置 + + diff --git a/src/gui/resources/ui/ALSettingsWidget.ui b/src/gui/resources/ui/ALSettingsWidget.ui new file mode 100644 index 0000000..7db0631 --- /dev/null +++ b/src/gui/resources/ui/ALSettingsWidget.ui @@ -0,0 +1,480 @@ + + + ALSettingsWidget + + + + 0 + 0 + 400 + 420 + + + + + 400 + 420 + + + + + 500 + 420 + + + + 全局设置 - AutoLibrary + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + Qt::FocusPolicy::StrongFocus + + + QFrame::Shape::NoFrame + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + QAbstractItemView::SelectionMode::SingleSelection + + + + 20 + 20 + + + + 0 + + + + 外观 + + + + + + + + + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 主题模式 + + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 浅色 + + + + + + + 深色 + + + + + + + 跟随系统 + + + true + + + + + + + + + + 界面风格 + + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 5 + + + + + + 100 + 25 + + + + + 100 + 25 + + + + 应用程序样式: + + + + + + + + 160 + 25 + + + + + + + + + + 更改样式将在下次启动应用程序时生效。 + + + + + + + + + + 自定义外观 + + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 自定义程序外观,文件加载后将立即生效。 + + + true + + + + + + + 5 + + + + + + 0 + 25 + + + + 选择或输入 QSS 样式表文件路径... + + + + + + + + 25 + 25 + + + + + 25 + 25 + + + + ... + + + + + + + + + 5 + + + + + + 80 + 25 + + + + 应用样式 + + + + + + + + 80 + 25 + + + + 重置外观 + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + 当前使用程序默认外观。 + + + true + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + 80 + 25 + + + + + 80 + 25 + + + + 取消 + + + + + + + + 80 + 25 + + + + + 80 + 25 + + + + 应用 + + + + + + + + 80 + 25 + + + + + 80 + 25 + + + + 确认 + + + true + + + + + + + + + + diff --git a/src/interfaces/ConfigProvider.py b/src/interfaces/ConfigProvider.py index 9ee5bb4..51e0d2a 100644 --- a/src/interfaces/ConfigProvider.py +++ b/src/interfaces/ConfigProvider.py @@ -66,6 +66,12 @@ class CfgKey: CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.user_path.current") PATHS = ConfigPath(ConfigType.GLOBAL, "automation.user_path.paths") + class APPEARANCE: + ROOT = ConfigPath(ConfigType.GLOBAL, "appearance") + THEME = ConfigPath(ConfigType.GLOBAL, "appearance.theme") + STYLE = ConfigPath(ConfigType.GLOBAL, "appearance.style") + CUSTOM_QSS = ConfigPath(ConfigType.GLOBAL, "appearance.custom_qss") + class TIMERTASK: ROOT = ConfigPath(ConfigType.TIMERTASK, "") TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks") diff --git a/src/managers/config/ConfigManager.py b/src/managers/config/ConfigManager.py index 4f89eb4..58f0787 100644 --- a/src/managers/config/ConfigManager.py +++ b/src/managers/config/ConfigManager.py @@ -54,6 +54,11 @@ class ConfigTemplate: "current": 0, "paths": [] } + }, + "appearance": { + "theme": "system", + "style": "Fusion", + "custom_qss": "" } } case ConfigType.BULLETIN: