"""Theme persistence and Fluent Widgets integration. 与 DLPilot/SerialMate 的 theme.py 同源(三件套: setTheme + QPalette/QSS + DWM 标题栏), 仅改应用名。""" import ctypes import os from PySide6.QtCore import QObject, QSettings, Qt from PySide6.QtGui import QColor, QPalette from qfluentwidgets import Theme, setTheme, setThemeColor # Fluent widgets restyle themselves on setTheme(); QMainWindow, the native # QTabWidget/QStatusBar and the Windows title bar keep the OS palette unless # the palette, an app stylesheet and the DWM title bar attributes follow suit # (same approach as DLPilot app_fluent.py). _SURFACES = { False: {"window": "#f4f6f8", "base": "#ffffff", "text": "#1f2328", "dim": "#57606a", "border": "#d8dee4", "selected_text": "#ffffff"}, True: {"window": "#202020", "base": "#2b2b2b", "text": "#f5f5f5", "dim": "#9a9a9a", "border": "#3a3a3a", "selected_text": "#1f2328"}, } class ThemeManager(QObject): """Keep the appearance choice persistent and synchronized with Qt.""" MODES = {"system", "light", "dark", "dark1", "dark2"} _THEMES = { "system": Theme.AUTO, "light": Theme.LIGHT, "dark": Theme.DARK, "dark1": Theme.DARK, "dark2": Theme.DARK, } _ACCENTS = { "light": "#0f6cbd", "dark": "#f2c94c", "dark1": "#5aa9e6", "dark2": "#35c6c8", } def __init__(self, app): super().__init__(app) self.app = app self.window = None self.settings = QSettings("LT8619C-Tools", "I2C-Lab") mode = self.settings.value("appearance/theme", "system") self.mode = mode if mode in self.MODES else "system" self.apply() app.styleHints().colorSchemeChanged.connect(self.apply) def set_window(self, window): """Register the main window so the native title bar follows the theme.""" self.window = window self._apply_titlebar() def set_mode(self, mode): if mode not in self.MODES: raise ValueError(f"Unknown theme: {mode}") self.mode = mode self.settings.setValue("appearance/theme", mode) self.apply() def is_dark(self): if self.mode in {"dark", "dark1", "dark2"}: return True return self.mode == "system" and self.app.styleHints().colorScheme() == Qt.ColorScheme.Dark def apply(self, *_): is_dark = self.is_dark() setTheme(self._THEMES[self.mode]) setThemeColor(self._ACCENTS.get(self.mode, self._ACCENTS["light"])) self._apply_palette(is_dark) self._apply_titlebar() def _apply_palette(self, is_dark): """Give native Qt surfaces the same colors as the Fluent widgets.""" surface = _SURFACES[is_dark] accent = QColor(self._ACCENTS.get(self.mode, self._ACCENTS["light"])) palette = QPalette() for role, color in ( (QPalette.Window, surface["window"]), (QPalette.Base, surface["base"]), (QPalette.AlternateBase, surface["base"]), (QPalette.Text, surface["text"]), (QPalette.WindowText, surface["text"]), (QPalette.Button, surface["base"]), (QPalette.ButtonText, surface["text"]), (QPalette.PlaceholderText, surface["dim"]), (QPalette.Highlight, accent), (QPalette.HighlightedText, surface["selected_text"]), (QPalette.ToolTipBase, surface["base"]), (QPalette.ToolTipText, surface["text"]), ): palette.setColor(role, QColor(color)) self.app.setPalette(palette) self.app.setStyleSheet(f""" QMainWindow, QWidget#root {{ background-color: {surface['window']}; color: {surface['text']}; }} QTabWidget::pane {{ background: {surface['base']}; border: 1px solid {surface['border']}; border-radius: 6px; }} QTabBar {{ qproperty-drawBase: 0; }} QTabBar::tab {{ background: transparent; color: {surface['dim']}; padding: 6px 16px; border: 1px solid transparent; border-top-left-radius: 6px; border-top-right-radius: 6px; }} QTabBar::tab:selected {{ background: {surface['base']}; color: {surface['text']}; border: 1px solid {surface['border']}; }} QTabBar::tab:hover:!selected {{ background: {surface['base']}; }} QSplitter::handle {{ background: {surface['window']}; }} QStatusBar {{ background: {surface['window']}; color: {surface['dim']}; border-top: 1px solid {surface['border']}; }} QToolTip {{ background-color: {surface['base']}; color: {surface['text']}; border: 1px solid {surface['border']}; }} """) def _apply_titlebar(self): """Ask DWM for a title bar in the current surface color (Win10/11).""" if self.window is None or os.name != "nt" or not hasattr(ctypes, "windll"): return try: hwnd = int(self.window.winId()) dwm = ctypes.windll.dwmapi dark = ctypes.c_int(1 if self.is_dark() else 0) dwm.DwmSetWindowAttribute(hwnd, 20, ctypes.byref(dark), ctypes.sizeof(dark)) surface = _SURFACES[self.is_dark()] dwm.DwmSetWindowAttribute(hwnd, 35, ctypes.byref(self._colorref(QColor(surface["window"]))), 4) dwm.DwmSetWindowAttribute(hwnd, 36, ctypes.byref(self._colorref(QColor(surface["text"]))), 4) except Exception: pass @staticmethod def _colorref(color): return ctypes.c_int(color.red() | (color.green() << 8) | (color.blue() << 16))