diff --git a/app/views/modern_styles.py b/app/views/modern_styles.py index 1eab196..0b578ba 100644 --- a/app/views/modern_styles.py +++ b/app/views/modern_styles.py @@ -45,10 +45,10 @@ def apply_modern_styles() -> None: fg = theme.fg # 主前景 primary = theme.primary secondary = theme.secondary + info = theme.info + dark = theme.dark border = theme.border inputbg = theme.inputbg - selectbg = theme.selectbg - selectfg = theme.selectfg dark_theme = _is_dark(bg) @@ -61,6 +61,11 @@ def apply_modern_styles() -> None: header_hover_bg = _mix(secondary, "#ffffff", 0.08) if _is_dark(secondary) else _mix(secondary, "#000000", 0.08) preview_fg = _mix(header_fg, header_bg, 0.35) + sidebar_bg = _mix(dark, bg, 0.18) if dark_theme else _mix(primary, "#000000", 0.10) + sidebar_hover = _mix(sidebar_bg, "#ffffff", 0.07) if dark_theme else _mix(sidebar_bg, "#000000", 0.06) + sidebar_selected = _mix(sidebar_bg, "#ffffff", 0.14) if dark_theme else _mix(sidebar_bg, "#000000", 0.10) + sidebar_fg = _mix(fg, bg, 0.05) + sidebar_muted = _mix(fg, sidebar_bg, 0.45) # ---------------- 卡片 ---------------- style.configure( @@ -73,20 +78,20 @@ def apply_modern_styles() -> None: style.configure( "CardTitle.TLabel", background=card_bg, - foreground=primary, - font=("微软雅黑", 10, "bold"), + foreground=fg, + font=("Segoe UI", 10, "bold"), ) style.configure( "CardBody.TLabel", background=card_bg, foreground=fg, - font=("微软雅黑", 9), + font=("Segoe UI", 9), ) style.configure( "CardIcon.TLabel", background=card_bg, - foreground=primary, - font=("Segoe UI Emoji", 14), + foreground=info if dark_theme else primary, + font=("Segoe UI", 9, "bold"), ) # 内嵌于 Card 的容器(与 Card.TFrame 同背景,无边框) @@ -107,13 +112,13 @@ def apply_modern_styles() -> None: "ConfigHeader.TLabel", background=header_bg, foreground=header_fg, - font=("微软雅黑", 10, "bold"), + font=("Segoe UI", 10, "bold"), ) style.configure( "ConfigHeaderHover.TLabel", background=header_hover_bg, foreground=header_fg, - font=("微软雅黑", 10, "bold"), + font=("Segoe UI", 10, "bold"), ) style.configure( "ConfigChevron.TLabel", @@ -125,7 +130,7 @@ def apply_modern_styles() -> None: "ConfigPreview.TLabel", background=header_bg, foreground=preview_fg, - font=("微软雅黑", 9), + font=("Segoe UI", 9), ) # ---------------- 顶部工具条 ---------------- @@ -133,7 +138,7 @@ def apply_modern_styles() -> None: # 工具条上的次要按钮(清理配置等) style.configure( "ToolbarMuted.TButton", - font=("微软雅黑", 9), + font=("Segoe UI", 9), padding=(10, 5), ) @@ -142,14 +147,28 @@ def apply_modern_styles() -> None: "SectionTitle.TLabel", background=bg, foreground=_mix(fg, bg, 0.45), - font=("微软雅黑", 8, "bold"), + font=("Segoe UI", 8, "bold"), ) + style.configure("Sidebar.TFrame", background=sidebar_bg, borderwidth=0) # 侧栏内的小区段标题(侧栏背景是 primary) style.configure( "SidebarSection.TLabel", - background=primary, - foreground=_mix("#ffffff", primary, 0.35), - font=("微软雅黑", 8, "bold"), + background=sidebar_bg, + foreground=sidebar_muted, + font=("Segoe UI", 8, "bold"), + ) + # 侧栏顶部品牌区 + brand_bg = _mix(sidebar_bg, "#ffffff", 0.05) if dark_theme else _mix(sidebar_bg, "#000000", 0.05) + style.configure( + "SidebarBrand.TFrame", + background=brand_bg, + borderwidth=0, + ) + style.configure( + "SidebarBrand.TLabel", + background=brand_bg, + foreground="#ffffff", + font=("Segoe UI Semibold", 12), ) # ---------------- 结果区无边框标题行 ---------------- @@ -158,7 +177,7 @@ def apply_modern_styles() -> None: "ResultHeader.TLabel", background=bg, foreground=fg, - font=("微软雅黑", 11, "bold"), + font=("Segoe UI", 11, "bold"), ) # ---------------- 状态栏 ---------------- @@ -173,47 +192,48 @@ def apply_modern_styles() -> None: "StatusBar.TLabel", background=statusbar_bg, foreground=statusbar_fg, - font=("微软雅黑", 9), + font=("Segoe UI", 9), padding=(10, 4), ) style.configure( "StatusBarAccent.TLabel", background=statusbar_bg, - foreground=primary, - font=("微软雅黑", 9, "bold"), + foreground=info if dark_theme else primary, + font=("Segoe UI", 9, "bold"), padding=(10, 4), ) # ---------------- Sidebar 按钮(保留兼容名) ---------------- style.configure( "Sidebar.TButton", - background=primary, - foreground="#ffffff" if _is_dark(primary) else "#1a1a1a", - font=("微软雅黑", 10), - padding=(12, 10), + background=sidebar_bg, + foreground=sidebar_fg, + font=("Segoe UI", 10), + padding=(18, 9), borderwidth=0, anchor="w", ) style.map( "Sidebar.TButton", background=[ - ("active", _mix(primary, "#ffffff", 0.10)), - ("pressed", _mix(primary, "#000000", 0.10)), + ("active", sidebar_hover), + ("pressed", sidebar_selected), ], + foreground=[("active", "#ffffff" if dark_theme else sidebar_fg)], ) style.configure( "SidebarSelected.TButton", - background=_mix(primary, "#000000", 0.18), + background=sidebar_selected, foreground="#ffffff", - font=("微软雅黑", 10, "bold"), - padding=(12, 10), + font=("Segoe UI Semibold", 10), + padding=(18, 9), borderwidth=0, anchor="w", ) style.map( "SidebarSelected.TButton", background=[ - ("active", _mix(primary, "#000000", 0.10)), - ("pressed", _mix(primary, "#000000", 0.25)), + ("active", _mix(sidebar_selected, "#ffffff", 0.06) if dark_theme else _mix(sidebar_selected, "#000000", 0.06)), + ("pressed", _mix(sidebar_selected, "#000000", 0.08)), ], ) diff --git a/app/views/panels/main_layout.py b/app/views/panels/main_layout.py index 10621b8..edb9825 100644 --- a/app/views/panels/main_layout.py +++ b/app/views/panels/main_layout.py @@ -76,17 +76,17 @@ def create_floating_config_panel(self: "PQAutomationApp"): config_row_frame.pack(fill=tk.X, expand=False) # 设备连接 卡片 - connection_card = _make_card(config_row_frame, icon="\U0001F4E1", title="设备连接") # 📡 + connection_card = _make_card(config_row_frame, icon="01", title="设备连接") connection_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6)) self.connection_frame = connection_card._body # type: ignore[attr-defined] # 测试项目 卡片 - test_items_card = _make_card(config_row_frame, icon="\u2714", title="测试项目") # ✔ + test_items_card = _make_card(config_row_frame, icon="02", title="测试项目") test_items_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=6) self.test_items_frame = test_items_card._body # type: ignore[attr-defined] # 信号格式 卡片 - signal_card = _make_card(config_row_frame, icon="\u2699", title="信号格式") # ⚙ + signal_card = _make_card(config_row_frame, icon="03", title="信号格式") signal_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(6, 0)) self.signal_format_frame = signal_card._body # type: ignore[attr-defined] @@ -590,15 +590,37 @@ def create_connection_content(self: "PQAutomationApp"): def create_test_type_frame(self: "PQAutomationApp"): - """创建测试类型选择区域(侧边栏形式)""" + """创建测试类型选择区域(侧边栏形式)。 + + 新版(v3)改进: + - 深灰分层背景,接近 Calman 的侧栏密度; + - 纯文字按钮,不使用 emoji; + - 用更克制的字号 / 间距做层级区分; + - 不再使用 padding=10 硬覆盖(交给 Sidebar.TButton 样式统一管理)。 + """ # 设置测试类型变量 self.test_type_var = tk.StringVar(value="screen_module") - # 创建测试类型按钮并放置在侧边栏 + # ---------- 顶部品牌区 ---------- + brand = ttk.Frame(self.sidebar_frame, style="SidebarBrand.TFrame") + brand.pack(fill=tk.X, pady=(2, 10)) + ttk.Label( + brand, + text="PQ AUTOMATION", + style="SidebarBrand.TLabel", + ).pack(side=tk.LEFT, padx=16, pady=11) + + # ---------- 分组:测试类型 ---------- + ttk.Label( + self.sidebar_frame, + text="测试类型", + style="SidebarSection.TLabel", + ).pack(fill=tk.X, padx=16, pady=(2, 6), anchor="w") + test_types = [ ("屏模组性能测试", "screen_module"), - ("SDR Movie测试", "sdr_movie"), - ("HDR Movie测试", "hdr_movie"), + ("SDR Movie", "sdr_movie"), + ("HDR Movie", "hdr_movie"), ] for text, type_value in test_types: @@ -606,87 +628,37 @@ def create_test_type_frame(self: "PQAutomationApp"): master=self.sidebar_frame, text=text, style="Sidebar.TButton", - padding=10, command=lambda v=type_value: self.change_test_type(v), takefocus=False, ) btn.pack(fill=tk.X, padx=0, pady=1) - - # 保存按钮引用以便后续更新样式 setattr(self, f"{type_value}_btn", btn) - # 添加分隔线 - ttk.Separator(self.sidebar_frame, orient="horizontal").pack( - fill=tk.X, padx=10, pady=10 - ) - - # 只保留日志按钮 - self.log_btn = ttk.Button( + # ---------- 分组:工具面板 ---------- + ttk.Label( self.sidebar_frame, - text="测试日志", - style="Sidebar.TButton", - command=self.toggle_log_panel, - takefocus=False, - ) - self.log_btn.pack(fill=tk.X, padx=0, pady=1) + text="工具面板", + style="SidebarSection.TLabel", + ).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w") - # Local Dimming 测试按钮 - self.local_dimming_btn = ttk.Button( - self.sidebar_frame, - text="Local Dimming", - style="Sidebar.TButton", - command=self.toggle_local_dimming_panel, - takefocus=False, - ) - self.local_dimming_btn.pack(fill=tk.X, padx=0, pady=1) - - # AI 图片对话按钮 - self.ai_image_btn = ttk.Button( - self.sidebar_frame, - text="AI 图片", - style="Sidebar.TButton", - command=self.toggle_ai_image_panel, - takefocus=False, - ) - self.ai_image_btn.pack(fill=tk.X, padx=0, pady=1) - - # self.single_step_btn = ttk.Button( - # self.sidebar_frame, - # text="单步调试", - # style="Sidebar.TButton", - # command=self.toggle_single_step_panel, - # takefocus=False, - # ) - # self.single_step_btn.pack(fill=tk.X, padx=0, pady=1) - - self.pantone_baseline_btn = ttk.Button( - self.sidebar_frame, - text="Pantone认证摸底测试", - style="Sidebar.TButton", - command=self.toggle_pantone_baseline_panel, - takefocus=False, - ) - self.pantone_baseline_btn.pack(fill=tk.X, padx=0, pady=1) - - # Gamma 测试图案配置按钮 - self.gamma_pattern_btn = ttk.Button( - self.sidebar_frame, - text="Gamma 图案配置", - style="Sidebar.TButton", - command=self.toggle_gamma_pattern_panel, - takefocus=False, - ) - self.gamma_pattern_btn.pack(fill=tk.X, padx=0, pady=1) - - # CALMAN 风格灰阶测试按钮 - self.calman_btn = ttk.Button( - self.sidebar_frame, - text="CALMAN 灰阶", - style="Sidebar.TButton", - command=self.toggle_calman_panel, - takefocus=False, - ) - self.calman_btn.pack(fill=tk.X, padx=0, pady=1) + panel_buttons = [ + ("log_btn", "测试日志", self.toggle_log_panel), + ("local_dimming_btn", "Local Dimming", self.toggle_local_dimming_panel), + ("ai_image_btn", "AI 图片", self.toggle_ai_image_panel), + ("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel), + ("gamma_pattern_btn", "Gamma 图案", self.toggle_gamma_pattern_panel), + ("calman_btn", "CALMAN 灰阶", self.toggle_calman_panel), + ] + for attr, text, cmd in panel_buttons: + btn = ttk.Button( + self.sidebar_frame, + text=text, + style="Sidebar.TButton", + command=cmd, + takefocus=False, + ) + btn.pack(fill=tk.X, padx=0, pady=1) + setattr(self, attr, btn) # 测试版水印标签(版本 x.x.0.0 时显示) from app_version import is_beta_version, APP_VERSION @@ -701,6 +673,17 @@ def create_test_type_frame(self: "PQAutomationApp"): ) beta_lbl.pack(fill=tk.X, side=tk.BOTTOM, padx=4, pady=(6, 4)) + # ---------- 主题切换(底部固定) ---------- + self.theme_toggle_btn = ttk.Button( + self.sidebar_frame, + text="切换深色模式", + style="Sidebar.TButton", + command=self._on_toggle_theme, + takefocus=False, + ) + self.theme_toggle_btn.pack(fill=tk.X, padx=0, pady=(0, 2), side=tk.BOTTOM) + _refresh_theme_toggle_label(self) + # 注册面板按钮 if hasattr(self, "panels"): if "log" in self.panels: @@ -710,7 +693,7 @@ def create_test_type_frame(self: "PQAutomationApp"): if "ai_image" in self.panels: self.panels["ai_image"]["button"] = self.ai_image_btn if "single_step" in self.panels: - self.panels["single_step"]["button"] = self.single_step_btn + self.panels["single_step"]["button"] = getattr(self, "single_step_btn", None) if "pantone_baseline" in self.panels: self.panels["pantone_baseline"]["button"] = self.pantone_baseline_btn if "gamma_pattern" in self.panels: @@ -719,6 +702,30 @@ def create_test_type_frame(self: "PQAutomationApp"): self.panels["calman"]["button"] = self.calman_btn +def _refresh_theme_toggle_label(self: "PQAutomationApp") -> None: + """根据当前主题刷新切换按钮的文字。""" + from app.views.theme_manager import is_dark + if not hasattr(self, "theme_toggle_btn"): + return + if is_dark(): + self.theme_toggle_btn.configure(text="切换浅色模式") + else: + self.theme_toggle_btn.configure(text="切换深色模式") + + +def _on_toggle_theme(self: "PQAutomationApp") -> None: + """切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。""" + from app.views.theme_manager import toggle_theme + toggle_theme() + _refresh_theme_toggle_label(self) + # 同步刷新侧栏选中态(高亮样式跟随新色板) + if hasattr(self, "update_sidebar_selection"): + try: + self.update_sidebar_selection() + except Exception: + pass + + def update_config_info_display(self: "PQAutomationApp"): """更新配置信息显示""" if hasattr(self, "config") and hasattr(self.config, "get_current_config"): @@ -1046,6 +1053,7 @@ class MainLayoutMixin: create_test_type_frame = create_test_type_frame update_config_info_display = update_config_info_display create_operation_frame = create_operation_frame + _on_toggle_theme = _on_toggle_theme on_screen_module_timing_changed = on_screen_module_timing_changed on_screen_module_signal_format_changed = on_screen_module_signal_format_changed on_sdr_timing_changed = on_sdr_timing_changed diff --git a/app/views/theme_manager.py b/app/views/theme_manager.py new file mode 100644 index 0000000..dec70f5 --- /dev/null +++ b/app/views/theme_manager.py @@ -0,0 +1,134 @@ +"""主题管理:注册 Calman 风格深色主题 + 提供运行时切换。 + +主题在启动时通过 ``apply_initial_theme(root_style)`` 注入到 ttkbootstrap, +当前选择持久化到 ``settings/ui_preferences.json``。运行时调用 +``toggle_theme(root_style)`` / ``set_theme(root_style, name)`` 可即时切换, +并自动重新调用 ``apply_modern_styles()`` 让自定义样式跟上新色板。 +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Optional + +from ttkbootstrap.style import Style, ThemeDefinition + +from app.views.modern_styles import apply_modern_styles + + +_PREFS_PATH = Path("settings/ui_preferences.json") + +# 浅色主题:沿用旧的 yeti(首发布兼容) +LIGHT_THEME = "yeti" +# 深色主题:自定义 Calman 风格 +DARK_THEME = "calman_dark" + + +# ---------------------------------------------------------------------- +# Calman 风格深色主题色板(参考实测截图取色) +# ---------------------------------------------------------------------- +_CALMAN_DARK_COLORS = { + "primary": "#343A41", # 主色改为炭灰,避免大面积亮蓝 + "secondary": "#444A51", # 中性深灰(用于 header / 分组背景) + "success": "#4FB960", + "info": "#6FAFCC", # 降低饱和度,只做少量点缀 + "warning": "#F2A93B", + "danger": "#E0524A", + "light": "#BFC6CE", # 高亮文本 + "dark": "#0D1014", # 最深背景(侧栏底色) + "bg": "#1B1F24", # 主窗口背景 + "fg": "#E4E8EE", # 主文本颜色 + "selectbg": "#5A6169", + "selectfg": "#E4E8EE", + "border": "#2A2F36", + "inputfg": "#E4E8EE", + "inputbg": "#24292F", + "active": "#2A2F36", +} + + +def register_themes() -> None: + """把自定义深色主题注册到 ttkbootstrap(可重复调用,幂等)。""" + style = Style() + if DARK_THEME in style.theme_names(): + return + theme_def = ThemeDefinition( + name=DARK_THEME, + themetype="dark", + colors=_CALMAN_DARK_COLORS, + ) + style.register_theme(theme_def) + + +# ---------------------------------------------------------------------- +# 偏好持久化 +# ---------------------------------------------------------------------- +def _read_prefs() -> dict: + try: + return json.loads(_PREFS_PATH.read_text(encoding="utf-8")) + except (FileNotFoundError, json.JSONDecodeError): + return {} + + +def _write_prefs(data: dict) -> None: + try: + _PREFS_PATH.parent.mkdir(parents=True, exist_ok=True) + _PREFS_PATH.write_text( + json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8" + ) + except OSError: + # 写入失败不应影响 UI + pass + + +def get_saved_theme() -> Optional[str]: + return _read_prefs().get("theme") + + +def save_theme(name: str) -> None: + prefs = _read_prefs() + prefs["theme"] = name + _write_prefs(prefs) + + +# ---------------------------------------------------------------------- +# 主题应用 / 切换 +# ---------------------------------------------------------------------- +def apply_initial_theme() -> str: + """启动时调用:注册主题 + 加载偏好 + 切到对应主题。 + + 返回最终生效的主题名。 + """ + register_themes() + name = get_saved_theme() or LIGHT_THEME + style = Style() + if name not in style.theme_names(): + name = LIGHT_THEME + style.theme_use(name) + apply_modern_styles() + return name + + +def set_theme(name: str) -> str: + """切换到指定主题,持久化偏好,并刷新自定义样式。""" + register_themes() + style = Style() + if name not in style.theme_names(): + name = LIGHT_THEME + style.theme_use(name) + apply_modern_styles() + save_theme(name) + return name + + +def toggle_theme() -> str: + """在浅 / 深之间切换。返回新主题名。""" + style = Style() + current = style.theme.name + target = DARK_THEME if current != DARK_THEME else LIGHT_THEME + return set_theme(target) + + +def is_dark() -> bool: + return Style().theme.name == DARK_THEME diff --git a/pqAutomationApp.py b/pqAutomationApp.py index 4df6777..7b20ac7 100644 --- a/pqAutomationApp.py +++ b/pqAutomationApp.py @@ -149,12 +149,12 @@ class PQAutomationApp( self.log_visible = False # 创建左侧面板 - self.left_frame = ttk.Frame(self.main_frame, width=180) + self.left_frame = ttk.Frame(self.main_frame, width=208) self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=0, pady=5) self.left_frame.pack_propagate(False) # 创建左侧导航栏 - self.sidebar_frame = ttk.Frame(self.left_frame, bootstyle="primary") + self.sidebar_frame = ttk.Frame(self.left_frame, style="Sidebar.TFrame") self.sidebar_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=5) # self.sidebar_frame.pack_propagate(False) @@ -824,8 +824,10 @@ class PQAutomationApp( def main(): try: setup_logging() - # root = tk.Tk() + # 先以浅色主题启动 Window,再根据用户偏好(含自定义 Calman 深色主题)切换 root = ttk.Window(themename="yeti") + from app.views.theme_manager import apply_initial_theme + apply_initial_theme() app = PQAutomationApp(root) # GUI 创建完成后,把 logging 记录同步到日志面板 if hasattr(app, "log_gui"):