From 49d82da8b91b535178d9f086bd1588721c9d4b3d Mon Sep 17 00:00:00 2001 From: "xinzhu.yin" Date: Thu, 4 Jun 2026 10:36:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9Calman=E7=81=B0=E9=98=B6?= =?UTF-8?q?=E4=B8=AD=E7=BB=93=E6=9E=9C=E5=9B=BE=E6=98=BE=E7=A4=BA=E3=80=81?= =?UTF-8?q?=E4=BF=AE=E6=94=B9UI=E4=B8=BB=E9=A2=98=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E5=BA=94=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/device/connection.py | 50 +++++ app/runner/test_runner.py | 10 +- app/tests/local_dimming.py | 4 +- app/views/modern_styles.py | 236 +++++++++++++++++++-- app/views/panels/ai_image_panel.py | 39 ++-- app/views/panels/calman_panel.py | 150 ++++++++----- app/views/panels/custom_template_panel.py | 107 +++++++--- app/views/panels/gamma_pattern_panel.py | 22 +- app/views/panels/main_layout.py | 23 +- app/views/panels/pantone_baseline_panel.py | 4 +- app/views/panels/side_panels.py | 6 +- app/views/panels/single_step_panel.py | 20 +- app/views/pq_debug_panel.py | 2 +- app/views/theme_manager.py | 56 ++++- pqAutomationApp.py | 63 ++---- settings/pq_config.json | 15 +- 16 files changed, 597 insertions(+), 210 deletions(-) diff --git a/app/device/connection.py b/app/device/connection.py index a4e0e70..5f06f07 100644 --- a/app/device/connection.py +++ b/app/device/connection.py @@ -272,6 +272,49 @@ def disconnect_com_connections(self: "PQAutomationApp"): self.connection.disconnect_all() +def _get_ca_measure_lock(self: "PQAutomationApp"): + lock = getattr(self, "_ca_measure_lock", None) + if lock is None: + lock = threading.RLock() + self._ca_measure_lock = lock + return lock + + +def _read_ca_display(self: "PQAutomationApp", mode: int): + """在锁内切换 CA410 Display 模式并立即读取,避免模式串扰。""" + if getattr(self, "ca", None) is None: + raise RuntimeError("请先连接 CA410 色度计") + + with _get_ca_measure_lock(self): + self.ca.set_Display(mode) + return self.ca.readAllDisplay() + + +def read_ca_xyLv(self: "PQAutomationApp"): + """读取 xy/Lv/XYZ(Display 0)。""" + return _read_ca_display(self, 0) + + +def read_ca_tcp_duv(self: "PQAutomationApp"): + """读取 Tcp/duv/Lv/XYZ(Display 1)。""" + return _read_ca_display(self, 1) + + +def read_ca_uvLv(self: "PQAutomationApp"): + """读取 u'/v'/Lv/XYZ(Display 5)。""" + return _read_ca_display(self, 5) + + +def read_ca_xyz(self: "PQAutomationApp"): + """读取 XYZ(Display 7)。""" + return _read_ca_display(self, 7) + + +def read_ca_lambda_pe(self: "PQAutomationApp"): + """读取 λd/Pe/Lv/XYZ(Display 8)。""" + return _read_ca_display(self, 8) + + __all__ = [ "ConnectionController", # 兼容层 @@ -298,3 +341,10 @@ class DeviceConnectionMixin: check_port_connection = check_port_connection enable_com_widgets = enable_com_widgets disconnect_com_connections = disconnect_com_connections + _get_ca_measure_lock = _get_ca_measure_lock + _read_ca_display = _read_ca_display + read_ca_xyLv = read_ca_xyLv + read_ca_tcp_duv = read_ca_tcp_duv + read_ca_uvLv = read_ca_uvLv + read_ca_xyz = read_ca_xyz + read_ca_lambda_pe = read_ca_lambda_pe diff --git a/app/runner/test_runner.py b/app/runner/test_runner.py index d5ae7ef..39dce29 100644 --- a/app/runner/test_runner.py +++ b/app/runner/test_runner.py @@ -394,8 +394,7 @@ def send_fix_pattern(self: "PQAutomationApp", mode): # 测量数据 if mode == "custom": result = [] - self.ca.set_Display(1) - tcp, duv, lv, X, Y, Z = self.ca.readAllDisplay() + tcp, duv, lv, X, Y, Z = self.read_ca_tcp_duv() if should_log_detail: self.log_gui.log( @@ -403,8 +402,7 @@ def send_fix_pattern(self: "PQAutomationApp", mode): f"X={X:.4f}, Y={Y:.4f}, Z={Z:.4f}" , level="success") - self.ca.set_Display(8) - lambda_, Pe, lv, X, Y, Z = self.ca.readAllDisplay() + lambda_, Pe, lv, X, Y, Z = self.read_ca_lambda_pe() if should_log_detail: self.log_gui.log( @@ -449,9 +447,7 @@ def send_fix_pattern(self: "PQAutomationApp", mode): self.log_gui.log(f"第 {i+1} 行实时结果写入失败: {str(e)}", level="error") else: - self.ca.set_xyLv_Display() - - x, y, lv, X, Y, Z = self.ca.readAllDisplay() + x, y, lv, X, Y, Z = self.read_ca_xyLv() results.append([x, y, lv, X, Y, Z]) if should_log_detail: diff --git a/app/tests/local_dimming.py b/app/tests/local_dimming.py index 3ba97d2..eabd072 100644 --- a/app/tests/local_dimming.py +++ b/app/tests/local_dimming.py @@ -171,7 +171,7 @@ def _build_ld_result_row(test_item, pattern_label, value, x="--", y="--"): def _measure_ld_row(self: "PQAutomationApp", test_item, pattern_label): """读取一次 CA410 数据并包装为表格行。""" - x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay() + x, y, lv, _X, _Y, _Z = self.read_ca_xyLv() if lv is None: raise RuntimeError(f"{pattern_label} 采集失败") return _build_ld_result_row(test_item, pattern_label, lv, x, y), lv @@ -549,7 +549,7 @@ def measure_ld_luminance(self: "PQAutomationApp"): def measure(): try: - x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay() + x, y, lv, _X, _Y, _Z = self.read_ca_xyLv() except Exception as e: self._dispatch_ui(self.log_gui.log, f"采集异常: {str(e)}") return diff --git a/app/views/modern_styles.py b/app/views/modern_styles.py index bd6bf1e..1024809 100644 --- a/app/views/modern_styles.py +++ b/app/views/modern_styles.py @@ -36,37 +36,188 @@ def _is_dark(color: str) -> bool: return (r * 299 + g * 587 + b * 114) / 1000 < 128 -def apply_modern_styles() -> None: - """注册或刷新现代化样式集。可在主题切换后再次调用。""" - style = ttk.Style() - theme = style.colors # ttkbootstrap.style.Colors +def _contrast_text(color: str, *, dark_text: str, light_text: str) -> str: + return dark_text if _is_dark(color) else light_text - bg = theme.bg # 主背景 - fg = theme.fg # 主前景 + +def get_theme_palette() -> dict[str, str]: + """返回当前主题的语义色板,供 ttk / tk 自定义控件共用。""" + style = ttk.Style() + theme = style.colors + + bg = theme.bg + fg = theme.fg primary = theme.primary secondary = theme.secondary + success = theme.success info = theme.info + warning = theme.warning + danger = theme.danger dark = theme.dark border = theme.border inputbg = theme.inputbg + inputfg = getattr(theme, "inputfg", fg) dark_theme = _is_dark(bg) + select_bg = getattr(theme, "selectbg", _mix(primary, bg, 0.30 if dark_theme else 0.12)) + select_fg = getattr(theme, "selectfg", "#ffffff" if _is_dark(select_bg) else fg) - # 卡片背景:在主背景上轻微偏移,营造层级感 - card_bg = _mix(bg, "#ffffff", 0.04) if dark_theme else _mix(bg, "#000000", 0.025) - card_border = _mix(bg, fg, 0.18) if dark_theme else _mix(bg, "#000000", 0.10) - # 配置项 header 用 secondary 主题色 - header_bg = secondary - header_fg = "#ffffff" if _is_dark(secondary) else "#1a1a1a" + if dark_theme: + card_bg = _mix(bg, "#ffffff", 0.04) + card_border = _mix(bg, fg, 0.18) + header_fg = _contrast_text( + "#444A51", + dark_text="#ffffff", + light_text="#1a1a1a", + ) + sidebar_bg = _mix(dark, bg, 0.18) + sidebar_hover = _mix(sidebar_bg, "#ffffff", 0.07) + sidebar_selected = _mix(sidebar_bg, "#ffffff", 0.14) + sidebar_fg = _mix(fg, "#ffffff", 0.04) + sidebar_muted = _mix(sidebar_fg, sidebar_bg, 0.45) + muted_fg = _mix(fg, bg, 0.32) + disabled_fg = _mix(fg, bg, 0.42) + disabled_bg = _mix(inputbg, bg, 0.18) + disabled_border = _mix(border, fg, 0.22) + readonly_bg = _mix(inputbg, "#ffffff", 0.06) + success_fg = _mix(success, "#ffffff", 0.08) + warning_fg = _mix(warning, "#ffffff", 0.06) + info_fg = _mix(info, "#ffffff", 0.06) + statusbar_bg = _mix(bg, "#ffffff", 0.06) + tooltip_bg = _mix(inputbg, bg, 0.08) + tooltip_fg = inputfg + tooltip_border = _mix(border, fg, 0.20) + surface_alt_bg = _mix(card_bg, "#ffffff", 0.05) + surface_hover_bg = _mix(card_bg, "#ffffff", 0.09) + badge_bg = _mix(danger, bg, 0.12) + badge_fg = "#ffffff" + focus = _mix(primary, "#ffffff", 0.18) + config_bg = _mix("#444A51", bg, 0.30) + else: + card_bg = inputbg + card_border = border + header_fg = bg + config_bg = _mix(primary, bg, 0.25) + sidebar_bg = _mix(primary, bg, 0.82) + sidebar_hover = _mix(primary, bg, 0.72) + sidebar_selected = primary + sidebar_fg = fg + sidebar_muted = _mix(fg, sidebar_bg, 0.35) + muted_fg = _mix(fg, bg, 0.38) + disabled_fg = _mix(fg, bg, 0.55) + disabled_bg = _mix(bg, border, 0.18) + disabled_border = _mix(border, bg, 0.18) + readonly_bg = _mix(inputbg, primary, 0.04) + success_fg = success + warning_fg = _mix(warning, fg, 0.18) + info_fg = info + statusbar_bg = _mix(bg, dark, 0.04) + tooltip_bg = inputbg + tooltip_fg = inputfg + tooltip_border = border + surface_alt_bg = _mix(bg, dark, 0.03) + surface_hover_bg = _mix(bg, dark, 0.05) + badge_bg = danger + badge_fg = "#ffffff" + focus = _mix(primary, bg, 0.20) + + return { + "bg": bg, + "fg": fg, + "primary": primary, + "secondary": secondary, + "success": success, + "info": info, + "warning": warning, + "danger": danger, + "border": border, + "input_bg": inputbg, + "input_fg": inputfg, + "select_bg": select_bg, + "select_fg": select_fg, + "card_bg": card_bg, + "card_border": card_border, + "header_fg": header_fg, + "sidebar_bg": sidebar_bg, + "sidebar_hover": sidebar_hover, + "sidebar_selected": sidebar_selected, + "sidebar_fg": sidebar_fg, + "sidebar_muted": sidebar_muted, + "muted_fg": muted_fg, + "disabled_fg": disabled_fg, + "disabled_bg": disabled_bg, + "disabled_border": disabled_border, + "readonly_bg": readonly_bg, + "success_fg": success_fg, + "warning_fg": warning_fg, + "info_fg": info_fg, + "statusbar_bg": statusbar_bg, + "tooltip_bg": tooltip_bg, + "tooltip_fg": tooltip_fg, + "tooltip_border": tooltip_border, + "surface_alt_bg": surface_alt_bg, + "surface_hover_bg": surface_hover_bg, + "badge_bg": badge_bg, + "badge_fg": badge_fg, + "focus": focus, + "config_bg": config_bg, + } + + +def apply_listbox_theme(widget) -> None: + """将 tk.Listbox 颜色同步到当前主题。""" + palette = get_theme_palette() + widget.configure( + background=palette["input_bg"], + foreground=palette["input_fg"], + highlightbackground=palette["border"], + highlightcolor=palette["focus"], + selectbackground=palette["select_bg"], + selectforeground=palette["select_fg"], + disabledforeground=palette["disabled_fg"], + ) + + +def apply_tooltip_theme(toplevel, label) -> None: + """将 tooltip 的 tk.Toplevel / Label 同步到当前主题。""" + palette = get_theme_palette() + toplevel.configure(background=palette["tooltip_border"]) + label.configure( + bg=palette["tooltip_bg"], + fg=palette["tooltip_fg"], + highlightbackground=palette["tooltip_border"], + ) + + +def apply_modern_styles() -> None: + """注册或刷新现代化样式集。可在主题切换后再次调用。""" + style = ttk.Style() + palette = get_theme_palette() + + bg = palette["bg"] + fg = palette["fg"] + primary = palette["primary"] + secondary = palette["secondary"] + info = palette["info"] + card_bg = palette["card_bg"] + card_border = palette["card_border"] + header_bg = palette["config_bg"] + header_fg = palette["header_fg"] + dark_theme = _is_dark(bg) 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 = "#F4F8FD" if _is_dark(sidebar_bg) else _mix(fg, bg, 0.05) - sidebar_muted = _mix(sidebar_fg, sidebar_bg, 0.45) + sidebar_bg = palette["sidebar_bg"] + sidebar_hover = palette["sidebar_hover"] + sidebar_selected = palette["sidebar_selected"] + sidebar_fg = palette["sidebar_fg"] + sidebar_muted = palette["sidebar_muted"] + muted_fg = palette["muted_fg"] + disabled_fg = palette["disabled_fg"] + disabled_bg = palette["disabled_bg"] + disabled_border = palette["disabled_border"] + readonly_bg = palette["readonly_bg"] + success_fg = palette["success_fg"] + warning_fg = palette["warning_fg"] # ---------------- 卡片 ---------------- style.configure( @@ -134,6 +285,12 @@ def apply_modern_styles() -> None: font=("Segoe UI", 9), ) + # ---------------- 通用文字语义 ---------------- + style.configure("Muted.TLabel", background=bg, foreground=muted_fg) + style.configure("SuccessState.TLabel", background=bg, foreground=success_fg) + style.configure("WarningState.TLabel", background=bg, foreground=warning_fg) + style.configure("InfoState.TLabel", background=bg, foreground=palette["info_fg"]) + # ---------------- 顶部工具条 ---------------- style.configure("Toolbar.TFrame", background=bg, borderwidth=0) # 工具条上的次要按钮(清理配置等) @@ -168,9 +325,17 @@ def apply_modern_styles() -> None: style.configure( "SidebarBrand.TLabel", background=brand_bg, - foreground="#ffffff", + foreground=palette["badge_fg"], font=("Segoe UI Semibold", 12), ) + style.configure( + "SidebarBadge.TLabel", + background=palette["badge_bg"], + foreground=palette["badge_fg"], + font=("微软雅黑", 8, "bold"), + anchor="center", + padding=(6, 2), + ) # ---------------- 结果区无边框标题行 ---------------- style.configure("ResultHeader.TFrame", background=bg, borderwidth=0) @@ -182,7 +347,7 @@ def apply_modern_styles() -> None: ) # ---------------- 状态栏 ---------------- - statusbar_bg = _mix(bg, "#000000", 0.06) if not dark_theme else _mix(bg, "#ffffff", 0.06) + statusbar_bg = palette["statusbar_bg"] statusbar_fg = _mix(fg, bg, 0.15) style.configure( "StatusBar.TFrame", @@ -204,6 +369,33 @@ def apply_modern_styles() -> None: padding=(10, 4), ) + # ---------------- 深色禁用态 / 只读态增强 ---------------- + style.map( + "TLabel", + foreground=[("disabled", disabled_fg)], + ) + style.map( + "TButton", + foreground=[("disabled", disabled_fg)], + background=[("disabled", disabled_bg)], + bordercolor=[("disabled", disabled_border)], + darkcolor=[("disabled", disabled_bg)], + lightcolor=[("disabled", disabled_bg)], + ) + style.map( + "TEntry", + foreground=[("disabled", disabled_fg)], + fieldbackground=[("disabled", disabled_bg), ("readonly", readonly_bg)], + bordercolor=[("disabled", disabled_border), ("readonly", disabled_border)], + ) + style.map( + "TCombobox", + foreground=[("disabled", disabled_fg), ("readonly", fg)], + fieldbackground=[("disabled", disabled_bg), ("readonly", readonly_bg)], + bordercolor=[("disabled", disabled_border), ("readonly", disabled_border)], + arrowcolor=[("disabled", disabled_fg), ("readonly", muted_fg)], + ) + # ---------------- Sidebar 按钮(保留兼容名) ---------------- style.configure( "Sidebar.TButton", @@ -225,7 +417,7 @@ def apply_modern_styles() -> None: style.configure( "SidebarSelected.TButton", background=sidebar_selected, - foreground="#ffffff", + foreground=_contrast_text(sidebar_selected, dark_text=palette["badge_fg"], light_text=sidebar_fg), font=("Segoe UI Semibold", 10), padding=(18, 9), borderwidth=0, diff --git a/app/views/panels/ai_image_panel.py b/app/views/panels/ai_image_panel.py index bb5f749..c11e6d1 100644 --- a/app/views/panels/ai_image_panel.py +++ b/app/views/panels/ai_image_panel.py @@ -15,6 +15,7 @@ import ttkbootstrap as ttk from PIL import Image, ImageTk from app.services import ai_image as _svc +from app.views.modern_styles import apply_tooltip_theme, get_theme_palette from typing import TYPE_CHECKING @@ -26,17 +27,19 @@ logger = logging.getLogger(__name__) def _theme_colors(): - style = ttk.Style() - colors = style.colors + palette = get_theme_palette() return { - "bg": colors.bg, - "fg": colors.fg, - "muted": colors.secondary, - "input_bg": colors.inputbg, - "input_fg": colors.inputfg, - "select_bg": colors.selectbg, - "select_fg": colors.selectfg, - "border": colors.border, + "bg": palette["bg"], + "fg": palette["fg"], + "muted": palette["muted_fg"], + "input_bg": palette["input_bg"], + "input_fg": palette["input_fg"], + "select_bg": palette["select_bg"], + "select_fg": palette["select_fg"], + "border": palette["border"], + "tooltip_bg": palette["tooltip_bg"], + "tooltip_fg": palette["tooltip_fg"], + "tooltip_border": palette["tooltip_border"], } @@ -95,8 +98,6 @@ def _show_tree_tooltip(self: "PQAutomationApp", text: str, x_root: int, y_root: text="", justify=tk.LEFT, anchor=tk.W, - bg="#ffffff", - fg="#1f2937", relief=tk.SOLID, bd=1, padx=8, @@ -104,6 +105,7 @@ def _show_tree_tooltip(self: "PQAutomationApp", text: str, x_root: int, y_root: font=("微软雅黑", 9), wraplength=520, ) + apply_tooltip_theme(tip, label) label.pack(fill=tk.BOTH, expand=True) self._ai_image_tooltip = tip self._ai_image_tooltip_label = label @@ -114,6 +116,7 @@ def _show_tree_tooltip(self: "PQAutomationApp", text: str, x_root: int, y_root: self._ai_image_tooltip_item = item_id label.configure(text=text) + apply_tooltip_theme(tip, label) tip.geometry(f"+{x_root + 14}+{y_root + 18}") tip.deiconify() tip.lift() @@ -1225,6 +1228,16 @@ def _build_ucd_resized_image(image_path: str, target_w: int, target_h: int) -> s return out_path +def refresh_ai_image_theme(self: "PQAutomationApp"): + """刷新 AI 图片面板中的主题相关控件。""" + if hasattr(self, "_apply_ai_image_list_style"): + self._apply_ai_image_list_style() + tip = getattr(self, "_ai_image_tooltip", None) + label = getattr(self, "_ai_image_tooltip_label", None) + if tip is not None and label is not None: + apply_tooltip_theme(tip, label) + + class AIImagePanelMixin: """由 tools/refactor_to_mixins.py 自动生成。 把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。 @@ -1249,3 +1262,5 @@ class AIImagePanelMixin: _rename_current = _rename_current _show_list_context_menu = _show_list_context_menu _send_to_ucd = _send_to_ucd + _apply_ai_image_list_style = _apply_ai_image_list_style + refresh_ai_image_theme = refresh_ai_image_theme diff --git a/app/views/panels/calman_panel.py b/app/views/panels/calman_panel.py index a75a5b7..27d193d 100644 --- a/app/views/panels/calman_panel.py +++ b/app/views/panels/calman_panel.py @@ -22,6 +22,7 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure from app.tests.color_accuracy import calculate_delta_e_2000 +from app.views.modern_styles import get_theme_palette if TYPE_CHECKING: from pqAutomationApp import PQAutomationApp @@ -35,10 +36,6 @@ D65_X = 0.3127 D65_Y = 0.3290 TARGET_CCT = 6504 TARGET_GAMMA = 2.2 -_DARK_BG = "#2f2f2f" -_AX_BG = "#262626" -_FG = "#d8d8d8" -_GRID = "#5b5b5b" DE_FORMULAS = ["2000", "94", "76"] @@ -60,7 +57,7 @@ def _contrast_fg(gray_value: int) -> str: def _set_canvas_patch(canvas: tk.Canvas, color: str, text: str) -> None: """统一设置色块 Canvas 颜色与文字,避免主题对 Label 背景渲染的干扰。""" gray = int(color[1:3], 16) - canvas.configure(bg=color, highlightbackground="#666666") + canvas.configure(bg=color, highlightbackground=get_theme_palette()["border"]) canvas.itemconfigure("patch_bg", fill=color, outline=color) canvas.itemconfigure("patch_text", text=text, fill=_contrast_fg(gray)) @@ -115,6 +112,7 @@ def _get_calman_palette() -> dict[str, str]: """根据当前主题生成 Calman 调试面板色板。""" style = ttk.Style() colors = style.colors + theme_palette = get_theme_palette() bg = colors.bg fg = colors.fg dark_mode = _is_dark_hex(bg) @@ -131,22 +129,22 @@ def _get_calman_palette() -> dict[str, str]: reading_fg = _mix(fg, "#ffffff", 0.06) status_fg = _mix(fg, bg, 0.35) reading_accent = colors.info - xy_series = "#d7dce4" - d65_mark = "#ffffff" + xy_series = _mix(fg, "#ffffff", 0.10) + d65_mark = _mix(fg, "#ffffff", 0.04) else: figure_bg = _mix(bg, "#dfe7ef", 0.45) axes_bg = _mix(bg, "#eff4f9", 0.72) grid = _mix("#5f6f82", axes_bg, 0.55) - tree_bg = "#ffffff" - tree_even = "#ffffff" + tree_bg = theme_palette["input_bg"] + tree_even = theme_palette["input_bg"] tree_odd = "#f3f7fb" heading_bg = _mix(colors.primary, "#ffffff", 0.82) reading_bg = _mix(bg, "#e7eef5", 0.58) reading_fg = fg status_fg = _mix(fg, bg, 0.25) reading_accent = _mix(colors.info, "#000000", 0.25) - xy_series = "#1f2a36" - d65_mark = "#253142" + xy_series = _mix(fg, bg, 0.18) + d65_mark = _mix(fg, bg, 0.28) return { "figure_bg": figure_bg, @@ -166,31 +164,59 @@ def _get_calman_palette() -> dict[str, str]: "tree_heading_bg": heading_bg, "tree_heading_fg": reading_fg, "tree_select": _mix(colors.info, figure_bg, 0.35), + "patch_border": theme_palette["border"], + "patch_border_alt": _mix(theme_palette["border"], theme_palette["fg"], 0.12), + "patch_focus": theme_palette["focus"], "xy_series": xy_series, "d65_mark": d65_mark, } def _xyY_to_rgb_balance(x: float, y: float, big_y: float) -> tuple[float, float, float]: - """把 xyY 近似映射到 RGB 比例,并归一到平均值 100。""" + """按 D65 同亮度参考计算 RGB Balance(Calman 常见口径)。""" if y <= 0 or big_y <= 0: return float("nan"), float("nan"), float("nan") - big_x = (x * big_y) / y - big_z = ((1.0 - x - y) * big_y) / y + def _xyY_to_xyz(cx: float, cy: float, cy_big: float) -> tuple[float, float, float]: + if cy <= 0: + return float("nan"), float("nan"), float("nan") + cx_big = (cx * cy_big) / cy + cz_big = ((1.0 - cx - cy) * cy_big) / cy + return cx_big, cy_big, cz_big - r = (3.2406 * big_x) + (-1.5372 * big_y) + (-0.4986 * big_z) - g = (-0.9689 * big_x) + (1.8758 * big_y) + (0.0415 * big_z) - b = (0.0557 * big_x) + (-0.2040 * big_y) + (1.0570 * big_z) + def _xyz_to_linear_rgb(cx_big: float, cy_big: float, cz_big: float) -> tuple[float, float, float]: + rr = (3.2406 * cx_big) + (-1.5372 * cy_big) + (-0.4986 * cz_big) + gg = (-0.9689 * cx_big) + (1.8758 * cy_big) + (0.0415 * cz_big) + bb = (0.0557 * cx_big) + (-0.2040 * cy_big) + (1.0570 * cz_big) + return rr, gg, bb - r = max(r, 0.0) - g = max(g, 0.0) - b = max(b, 0.0) + mx, my, mz = _xyY_to_xyz(x, y, big_y) + tx, ty, tz = _xyY_to_xyz(D65_X, D65_Y, big_y) + mr, mg, mb = _xyz_to_linear_rgb(mx, my, mz) + tr, tg, tb = _xyz_to_linear_rgb(tx, ty, tz) - avg = (r + g + b) / 3.0 - if avg <= 0: + eps = 1e-9 + if tr <= eps or tg <= eps or tb <= eps: return float("nan"), float("nan"), float("nan") - return (r / avg) * 100.0, (g / avg) * 100.0, (b / avg) * 100.0 + + rr = (mr / tr) * 100.0 + gg = (mg / tg) * 100.0 + bb = (mb / tb) * 100.0 + + # 明显异常值视为无效,避免图表被离群点拉坏。 + if not (math.isfinite(rr) and math.isfinite(gg) and math.isfinite(bb)): + return float("nan"), float("nan"), float("nan") + if rr < 0 or gg < 0 or bb < 0: + return float("nan"), float("nan"), float("nan") + + return rr, gg, bb + + +def _target_gamma_loglog_curve(pct: int) -> float: + """Calman风格目标曲线:低灰阶从 1.8 过渡并逐步逼近 2.2。""" + if pct <= 0: + return 1.8 + return TARGET_GAMMA - 0.4 * math.exp(-pct / 6.0) def _style_axes(self: "PQAutomationApp", ax, title: str) -> None: @@ -480,6 +506,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None: self.calman_actual_patch_cells = [] self.calman_target_patch_canvases = [] self.calman_target_hexes = [] + patch_palette = _get_calman_palette() for idx, pct in enumerate(self.calman_levels): rgb = _pct_to_gray_rgb(pct) color = _rgb_to_hex(rgb) @@ -493,7 +520,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None: bd=1, relief="solid", highlightthickness=1, - highlightbackground="#808080", + highlightbackground=patch_palette["patch_border_alt"], cursor="hand2", ) actual_cell.grid(row=0, column=idx, padx=1, pady=1, sticky=tk.NSEW) @@ -520,7 +547,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None: bd=1, relief="solid", highlightthickness=1, - highlightbackground="#9c9c9c", + highlightbackground=patch_palette["patch_border"], cursor="hand2", ) cell.grid(row=1, column=idx, padx=1, pady=1, sticky=tk.NSEW) @@ -722,7 +749,7 @@ def send_patch(self: "PQAutomationApp", pct: int) -> None: def _measure_once(self: "PQAutomationApp", pct: int) -> dict | None: """采集一次 CA410,并组装一条记录(含 CCT/Gamma/ΔE2000)。""" try: - x, y, lv, X, Y, Z = self.ca.readAllDisplay() + x, y, lv, X, Y, Z = self.read_ca_xyLv() except Exception as exc: self._dispatch_ui(self.log_gui.log, f"CA410 采集异常: {exc}", "error") return None @@ -974,20 +1001,21 @@ def clear_results(self: "PQAutomationApp") -> None: def _highlight_patch(self: "PQAutomationApp", pct: int) -> None: """高亮当前选中色块。""" + palette = _get_calman_palette() try: idx = self.calman_levels.index(pct) except ValueError: return for i, cell in enumerate(self.calman_patch_cells): if i == idx: - cell.configure(highlightbackground="#1f6fb2", highlightthickness=2) + cell.configure(highlightbackground=palette["patch_focus"], highlightthickness=2) else: - cell.configure(highlightbackground="#9c9c9c", highlightthickness=1) + cell.configure(highlightbackground=palette["patch_border"], highlightthickness=1) for i, cell in enumerate(self.calman_actual_cells): if i == idx: - cell.configure(highlightbackground="#1f6fb2", highlightthickness=2) + cell.configure(highlightbackground=palette["patch_focus"], highlightthickness=2) else: - cell.configure(highlightbackground="#808080", highlightthickness=1) + cell.configure(highlightbackground=palette["patch_border_alt"], highlightthickness=1) total_cols = len(self.calman_levels) + 1 # 含 metric 列 col_index = idx + 1 @@ -1082,10 +1110,18 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None: pcts = [r["pct"] for r in recs] de_vals = [r["de2000"] if r["de2000"] == r["de2000"] else 0 for r in recs] lum_vals = [r["Y"] if r["Y"] == r["Y"] else 0 for r in recs] - rgb_r = [r["rgb_r"] for r in recs if r["rgb_r"] == r["rgb_r"]] - rgb_g = [r["rgb_g"] for r in recs if r["rgb_g"] == r["rgb_g"]] - rgb_b = [r["rgb_b"] for r in recs if r["rgb_b"] == r["rgb_b"]] - rgb_pcts = [r["pct"] for r in recs if r["rgb_r"] == r["rgb_r"]] + rgb_recs = [ + r for r in recs + if ( + r.get("rgb_r") == r.get("rgb_r") + and r.get("rgb_g") == r.get("rgb_g") + and r.get("rgb_b") == r.get("rgb_b") + ) + ] + rgb_pcts = [r["pct"] for r in rgb_recs] + rgb_r = [r["rgb_r"] for r in rgb_recs] + rgb_g = [r["rgb_g"] for r in rgb_recs] + rgb_b = [r["rgb_b"] for r in rgb_recs] gamma_pcts = [r["pct"] for r in recs if r["gamma"] == r["gamma"]] gamma_vals = [r["gamma"] for r in recs if r["gamma"] == r["gamma"]] cct_vals = [r["cct"] for r in recs if r["cct"] == r["cct"]] @@ -1123,6 +1159,16 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None: a1.set_ylim(bottom=0) a1.set_xlabel("", fontsize=8) + rgb_ylim_low = 95.0 + rgb_ylim_high = 105.0 + if rgb_recs: + rgb_values = rgb_r + rgb_g + rgb_b + rgb_min = min(rgb_values + [100.0]) + rgb_max = max(rgb_values + [100.0]) + pad = max(0.8, (rgb_max - rgb_min) * 0.15) + rgb_ylim_low = min(95.0, rgb_min - pad) + rgb_ylim_high = max(105.0, rgb_max + pad) + # RGB Balance 线图 a2 = self.calman_ax_rgb_line a2.clear() @@ -1133,36 +1179,38 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None: a2.plot(rgb_pcts, rgb_b, "-", color="#4a73ff", linewidth=1.2) a2.axhline(100, color="#9e9e9e", lw=0.8, ls="--") a2.set_xlim(-2, 102) - a2.set_ylim(95, 105) + a2.set_ylim(rgb_ylim_low, rgb_ylim_high) a2.set_xlabel("", fontsize=8) # RGB Balance 条图(用最后一个点) a3 = self.calman_ax_rgb_bar a3.clear() _style_axes(self, a3, "RGB Balance") - if recs: - last = recs[-1] + if rgb_recs: + last = rgb_recs[-1] bars = [ - last["rgb_r"] if last["rgb_r"] == last["rgb_r"] else 100, - last["rgb_g"] if last["rgb_g"] == last["rgb_g"] else 100, - last["rgb_b"] if last["rgb_b"] == last["rgb_b"] else 100, + last["rgb_r"], + last["rgb_g"], + last["rgb_b"], ] a3.bar([0, 1, 2], bars, color=["#ff4d4d", "#4caf50", "#4a73ff"], width=0.7) a3.set_xticks([0, 1, 2], ["R", "G", "B"]) else: a3.set_xticks([0, 1, 2], ["R", "G", "B"]) - a3.set_ylim(95, 105) + a3.set_ylim(rgb_ylim_low, rgb_ylim_high) a3.set_xlabel("", fontsize=8) # Gamma a4 = self.calman_ax_gamma a4.clear() _style_axes(self, a4, "Gamma Log/Log") + target_pcts = list(self.calman_levels) + target_vals = [_target_gamma_loglog_curve(p) for p in target_pcts] + a4.plot(target_pcts, target_vals, "-", color="#f4ff00", linewidth=1.8) if gamma_pcts: - a4.plot(gamma_pcts, gamma_vals, "-", color="#ffe24d", linewidth=1.3) - a4.axhline(TARGET_GAMMA, color="#9e9e9e", lw=0.8, ls="--") + a4.plot(gamma_pcts, gamma_vals, "-", color="#8f8f8f", linewidth=2.0) a4.set_xlim(-2, 102) - a4.set_ylim(1.6, 2.8) + a4.set_ylim(1.8, 2.8) a4.set_xlabel("", fontsize=8) self.calman_canvas.draw_idle() @@ -1232,14 +1280,20 @@ def _refresh_metric_table(self: "PQAutomationApp") -> None: """重绘下方矩阵表。""" _apply_calman_tree_style(self) palette = _get_calman_palette() + ref_white_y = self.calman_results.get(100, {}).get("Y") + + def _target_y_abs(pctx): + if pctx is None: + return "-" + if ref_white_y is None or ref_white_y != ref_white_y or ref_white_y <= 0: + return "-" + return _safe_float(ref_white_y * ((pctx / 100.0) ** TARGET_GAMMA), "{:.3f}") + metrics = [ ("x CIE31", lambda r: _safe_float(r.get("x")) if r else "-"), ("y CIE31", lambda r: _safe_float(r.get("y")) if r else "-"), ("Y", lambda r: _safe_float(r.get("Y"), "{:.3f}") if r else "-"), - ( - "Target Y", - lambda _r, pctx=None: _safe_float((pctx / 100.0) ** TARGET_GAMMA, "{:.4f}") if pctx and pctx > 0 else ("0.0000" if pctx == 0 else "-"), - ), + ("Target Y", lambda _r, pctx=None: _target_y_abs(pctx)), ("Gamma Log/Log", lambda r: _safe_float(r.get("gamma"), "{:.3f}") if r else "-"), ("CCT", lambda r: _safe_float(r.get("cct"), "{:.0f}") if r else "-"), ("dE 2000", lambda r: _safe_float(r.get("de2000"), "{:.3f}") if r else "-"), diff --git a/app/views/panels/custom_template_panel.py b/app/views/panels/custom_template_panel.py index b36b5ac..35e2a7f 100644 --- a/app/views/panels/custom_template_panel.py +++ b/app/views/panels/custom_template_panel.py @@ -10,6 +10,7 @@ import colour import numpy as np from app.data_range_converter import convert_pattern_params +from app.views.modern_styles import get_theme_palette from typing import TYPE_CHECKING @@ -28,37 +29,12 @@ def create_custom_template_result_panel(self: "PQAutomationApp"): table_container = tk.Frame( self.custom_result_frame, - bg="#000000", highlightthickness=1, - highlightbackground="#5a5a5a", ) table_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + self.custom_result_table_container = table_container - style = ttk.Style() - style.configure( - "CustomResult.Treeview", - background="#000000", - fieldbackground="#000000", - foreground="#ffffff", - rowheight=28, - borderwidth=0, - ) - style.configure( - "CustomResult.Treeview.Heading", - background="#2f2f2f", - foreground="#f5f5f5", - font=("Microsoft YaHei", 10, "bold"), - relief="flat", - ) - style.map( - "CustomResult.Treeview", - background=[("selected", "#1f4e79")], - foreground=[("selected", "#ffffff")], - ) - style.map( - "CustomResult.Treeview.Heading", - background=[("active", "#3b3b3b")], - ) + _apply_custom_result_theme(self) columns = ( "Pattern", @@ -157,6 +133,70 @@ def create_custom_template_result_panel(self: "PQAutomationApp"): table_container.grid_columnconfigure(0, weight=1) +def _apply_custom_result_theme(self: "PQAutomationApp"): + palette = get_theme_palette() + container = getattr(self, "custom_result_table_container", None) + if container is not None: + container.configure( + bg=palette["input_bg"], + highlightbackground=palette["border"], + highlightcolor=palette["border"], + ) + + style = ttk.Style() + style.configure( + "CustomResult.Treeview", + background=palette["input_bg"], + fieldbackground=palette["input_bg"], + foreground=palette["input_fg"], + rowheight=28, + borderwidth=0, + ) + style.configure( + "CustomResult.Treeview.Heading", + background=palette["surface_alt_bg"], + foreground=palette["muted_fg"], + font=("Microsoft YaHei", 10, "bold"), + relief="flat", + ) + style.map( + "CustomResult.Treeview", + background=[("selected", palette["select_bg"])], + foreground=[("selected", palette["select_fg"])], + ) + style.map( + "CustomResult.Treeview.Heading", + background=[("active", palette["surface_hover_bg"])], + ) + + +def refresh_custom_template_theme(self: "PQAutomationApp"): + """刷新客户模板结果表的主题色。""" + _apply_custom_result_theme(self) + + +def _set_custom_template_tab_visible(self: "PQAutomationApp", visible: bool): + """控制客户模板结果 TAB 的显示与隐藏。""" + if not hasattr(self, "chart_notebook") or not hasattr(self, "custom_template_tab_frame"): + return + + tab_id = str(self.custom_template_tab_frame) + current_tabs = list(self.chart_notebook.tabs()) + + if visible: + if tab_id not in current_tabs: + self.chart_notebook.add(self.custom_template_tab_frame, text="客户模板结果显示") + self.chart_notebook.select(self.custom_template_tab_frame) + return + + if tab_id in current_tabs: + current_selected = self.chart_notebook.select() + self.chart_notebook.forget(self.custom_template_tab_frame) + remaining_tabs = list(self.chart_notebook.tabs()) + if current_selected == tab_id and remaining_tabs: + self.chart_notebook.select(remaining_tabs[0]) + + def show_custom_result_context_menu(self: "PQAutomationApp", event): """显示客户模板结果右键菜单""" if not hasattr(self, "custom_result_tree") or not hasattr( @@ -322,11 +362,9 @@ def _run_custom_row_single_step(self: "PQAutomationApp", item_id, row_no): time.sleep(self.pattern_settle_time) # 测量:显示模式1读取 Tcp/duv/Lv,显示模式8读取 λd/Pe/Lv 与 XYZ。 - self.ca.set_Display(1) - tcp, duv, lv, _, _, _ = self.ca.readAllDisplay() + tcp, duv, lv, _, _, _ = self.read_ca_tcp_duv() - self.ca.set_Display(8) - lambda_d, pe, lv, X, Y, Z = self.ca.readAllDisplay() + lambda_d, pe, lv, X, Y, Z = self.read_ca_lambda_pe() xy = colour.XYZ_to_xy(np.array([X, Y, Z])) u_prime, v_prime, _ = colour.XYZ_to_CIE1976UCS(np.array([X, Y, Z])) @@ -532,9 +570,6 @@ def append_custom_template_result(self: "PQAutomationApp", row_no, result_data): def start_custom_template_test(self: "PQAutomationApp"): """开始客户模板测试(SDR)""" - if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"): - self.chart_notebook.select(self.custom_template_tab_frame) - if self.ca is None or self.ucd is None: messagebox.showerror("错误", "请先连接CA410和信号发生器") return @@ -569,8 +604,10 @@ def start_custom_template_test(self: "PQAutomationApp"): self.custom_btn.config(state=tk.NORMAL) self.status_var.set("测试已取消") self.set_custom_result_table_locked(False) + _set_custom_template_tab_visible(self, False) return + _set_custom_template_tab_visible(self, True) self.set_custom_result_table_locked(True) self.test_thread = threading.Thread(target=self.run_custom_sdr_test, args=([],)) @@ -923,6 +960,7 @@ class CustomTemplatePanelMixin: 把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。 """ create_custom_template_result_panel = create_custom_template_result_panel + _set_custom_template_tab_visible = _set_custom_template_tab_visible show_custom_result_context_menu = show_custom_result_context_menu set_custom_result_table_locked = set_custom_result_table_locked start_custom_row_single_step = start_custom_row_single_step @@ -937,3 +975,4 @@ class CustomTemplatePanelMixin: update_custom_button_visibility = update_custom_button_visibility export_custom_template_excel = export_custom_template_excel export_custom_template_charts = export_custom_template_charts + refresh_custom_template_theme = refresh_custom_template_theme diff --git a/app/views/panels/gamma_pattern_panel.py b/app/views/panels/gamma_pattern_panel.py index c6167f9..5353fe1 100644 --- a/app/views/panels/gamma_pattern_panel.py +++ b/app/views/panels/gamma_pattern_panel.py @@ -171,7 +171,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"): ttk.Label( title_row, text="(Gamma / CCT / 对比度 / EOTF 共用此列表)", - foreground="#888", + style="Muted.TLabel", ).pack(side=tk.LEFT, padx=(8, 0)) # ===== 预设管理行 ===== @@ -207,7 +207,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"): ).pack(side=tk.LEFT, padx=2) self._gamma_active_label = ttk.Label( - preset_row1, text="", foreground="#0a8", font=("微软雅黑", 9, "bold") + preset_row1, text="", style="SuccessState.TLabel", font=("微软雅黑", 9, "bold") ) self._gamma_active_label.pack(side=tk.LEFT, padx=(10, 0)) @@ -230,7 +230,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"): # 描述行 self._gamma_meta_label = ttk.Label( - preset_box, text="", foreground="#666", font=("微软雅黑", 9) + preset_box, text="", style="Muted.TLabel", font=("微软雅黑", 9) ) self._gamma_meta_label.pack(anchor=tk.W, pady=(6, 0)) @@ -350,7 +350,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"): paste_frame.pack(fill=tk.X, pady=(10, 0)) ttk.Label( paste_frame, text="每行:R,G,B 或 R G B\n或:灰度% (如 50%)", - foreground="#888", justify=tk.LEFT, + style="Muted.TLabel", justify=tk.LEFT, ).pack(anchor=tk.W) ttk.Button( paste_frame, text="从剪贴板导入", @@ -363,7 +363,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"): bottom.pack(fill=tk.X, pady=(10, 0)) self._gamma_validate_label = ttk.Label( - bottom, text="", foreground="#666", justify=tk.LEFT + bottom, text="", style="Muted.TLabel", justify=tk.LEFT ) self._gamma_validate_label.pack(anchor=tk.W) @@ -435,16 +435,16 @@ def _update_active_label(self: "PQAutomationApp"): current = self._gamma_current_preset if active and current == active and not self._gamma_dirty: self._gamma_active_label.config( - text=f"✔ 当前激活:{active}", foreground="#0a8" + text=f"✔ 当前激活:{active}", style="SuccessState.TLabel" ) elif active: extra = "(有未保存改动)" if self._gamma_dirty else "" self._gamma_active_label.config( text=f"● 激活:{active} 编辑中:{current or '-'}{extra}", - foreground="#a60" if self._gamma_dirty else "#888", + style="WarningState.TLabel" if self._gamma_dirty else "Muted.TLabel", ) else: - self._gamma_active_label.config(text="● 未激活任何预设", foreground="#888") + self._gamma_active_label.config(text="● 未激活任何预设", style="Muted.TLabel") def _on_preset_selected(self: "PQAutomationApp"): @@ -1023,11 +1023,11 @@ def _run_validation(self: "PQAutomationApp"): if not msgs: text = f"✔ 校验通过(共 {len(params)} 点)" - color = "#0a8" + style_name = "SuccessState.TLabel" else: text = f"共 {len(params)} 点 | " + " ".join(msgs) - color = "#a60" if any(m.startswith("⚠") for m in msgs) else "#666" - self._gamma_validate_label.config(text=text, foreground=color) + style_name = "WarningState.TLabel" if any(m.startswith("⚠") for m in msgs) else "Muted.TLabel" + self._gamma_validate_label.config(text=text, style=style_name) # ============================================================ diff --git a/app/views/panels/main_layout.py b/app/views/panels/main_layout.py index d941437..d515240 100644 --- a/app/views/panels/main_layout.py +++ b/app/views/panels/main_layout.py @@ -757,13 +757,10 @@ def create_test_type_frame(self: "PQAutomationApp"): # 测试版水印标签(版本 x.x.0.0 时显示) from app_version import is_beta_version, APP_VERSION if is_beta_version(): - beta_lbl = tk.Label( + beta_lbl = ttk.Label( self.sidebar_frame, text=f"[测试版] v{APP_VERSION}", - foreground="#ffffff", - background="#cc3300", - font=("微软雅黑", 8, "bold"), - anchor="center", + style="SidebarBadge.TLabel", ) beta_lbl.pack(fill=tk.X, side=tk.BOTTOM, padx=4, pady=(6, 4)) @@ -809,6 +806,7 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None: """切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。""" from app.views.theme_manager import toggle_theme toggle_theme() + # apply_modern_styles() _refresh_theme_toggle_label(self) if hasattr(self, "apply_result_chart_theme"): try: @@ -820,6 +818,21 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None: self.log_gui.refresh_log_theme() except Exception: pass + if hasattr(self, "refresh_ai_image_theme"): + try: + self.refresh_ai_image_theme() + except Exception: + pass + if hasattr(self, "refresh_single_step_theme"): + try: + self.refresh_single_step_theme() + except Exception: + pass + if hasattr(self, "refresh_custom_template_theme"): + try: + self.refresh_custom_template_theme() + except Exception: + pass if hasattr(self, "refresh_calman_theme"): try: self.refresh_calman_theme() diff --git a/app/views/panels/pantone_baseline_panel.py b/app/views/panels/pantone_baseline_panel.py index 5727a67..ae5c8e5 100644 --- a/app/views/panels/pantone_baseline_panel.py +++ b/app/views/panels/pantone_baseline_panel.py @@ -63,7 +63,7 @@ def create_pantone_baseline_panel(self: "PQAutomationApp"): ttk.Label(config_row, textvariable=self.pantone_progress_var).pack( side=tk.RIGHT, padx=(8, 0) ) - ttk.Label(config_row, textvariable=self.pantone_status_var, foreground="#666").pack( + ttk.Label(config_row, textvariable=self.pantone_status_var, style="Muted.TLabel").pack( side=tk.RIGHT ) @@ -336,7 +336,7 @@ def _launch_worker(self: "PQAutomationApp", start_index, settle): end_state = "paused" break - x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay() + x, y, lv, _X, _Y, _Z = self.read_ca_xyLv() if lv is None: raise RuntimeError(f"第 {i + 1} 组 CA410 采集失败") diff --git a/app/views/panels/side_panels.py b/app/views/panels/side_panels.py index 2223e9a..034bb38 100644 --- a/app/views/panels/side_panels.py +++ b/app/views/panels/side_panels.py @@ -57,7 +57,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"): window_frame, text="点击按钮发送对应百分比的白色窗口(黑色背景 + 居中白色矩形)", font=("", 9), - foreground="#28a745", + style="SuccessState.TLabel", ).pack(pady=(0, 8)) # 第一行:1%, 2%, 5%, 10%, 18% @@ -96,7 +96,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"): pattern_frame, text="手动发送棋盘格、瞬时峰值、黑场图案,再点击采集当前亮度", font=("", 9), - foreground="#28a745", + style="SuccessState.TLabel", ).pack(pady=(0, 8)) pattern_row = ttk.Frame(pattern_frame) @@ -155,7 +155,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"): measure_btn_frame, text="亮度: -- cd/m² | x: -- | y: --", font=("Consolas", 10), - foreground="#007bff", + style="InfoState.TLabel", ) self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0)) diff --git a/app/views/panels/single_step_panel.py b/app/views/panels/single_step_panel.py index 31814f9..aa4724b 100644 --- a/app/views/panels/single_step_panel.py +++ b/app/views/panels/single_step_panel.py @@ -13,6 +13,8 @@ from tkinter import filedialog, messagebox import ttkbootstrap as ttk from PIL import Image +from app.views.modern_styles import apply_listbox_theme + from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -59,7 +61,7 @@ def create_single_step_panel(self: "PQAutomationApp"): ttk.Label( title_row, text="录入目标色块,发送纯色色块并采集 xyY / ΔE2000。", - foreground="#666", + style="Muted.TLabel", ).pack(side=tk.LEFT, padx=(12, 0)) left = ttk.LabelFrame(root, text="样本列表", padding=8) @@ -73,11 +75,8 @@ def create_single_step_panel(self: "PQAutomationApp"): activestyle="none", font=("微软雅黑", 9), highlightthickness=1, - highlightbackground="#d8d8d8", - highlightcolor="#4a90e2", - selectbackground="#2b6cb0", - selectforeground="#ffffff", ) + apply_listbox_theme(self.single_step_listbox) self.single_step_listbox.pack(fill=tk.BOTH, expand=True) self.single_step_listbox.bind( "<>", lambda e: _on_sample_select(self) @@ -154,7 +153,7 @@ def create_single_step_panel(self: "PQAutomationApp"): ttk.Label( form_frame, textvariable=self.single_step_status_var, - foreground="#666", + style="Muted.TLabel", ).grid(row=2, column=2, columnspan=4, sticky=tk.W, pady=4) action_row = ttk.Frame(form_frame) @@ -444,7 +443,7 @@ def _measure_current_sample(self: "PQAutomationApp"): def worker(): try: - x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay() + x, y, lv, _X, _Y, _Z = self.read_ca_xyLv() if lv is None: raise RuntimeError("CA410 未返回有效亮度") self._dispatch_ui(self.single_step_measured_x_var.set, f"{x:.4f}") @@ -556,6 +555,12 @@ def _export_results_csv(self: "PQAutomationApp"): messagebox.showerror("导出失败", f"写入 CSV 失败: {exc}") +def refresh_single_step_theme(self: "PQAutomationApp"): + """刷新单步调试中 tk.Listbox 的主题色。""" + if hasattr(self, "single_step_listbox"): + apply_listbox_theme(self.single_step_listbox) + + class SingleStepPanelMixin: """由 tools/refactor_to_mixins.py 自动生成。 把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。 @@ -575,3 +580,4 @@ class SingleStepPanelMixin: _commit_result = _commit_result _clear_results = _clear_results _export_results_csv = _export_results_csv + refresh_single_step_theme = refresh_single_step_theme diff --git a/app/views/pq_debug_panel.py b/app/views/pq_debug_panel.py index 997f33a..5314391 100644 --- a/app/views/pq_debug_panel.py +++ b/app/views/pq_debug_panel.py @@ -802,7 +802,7 @@ class PQDebugPanel: time.sleep(1.5) # 测量数据 - x, y, lv, X, Y, Z = self.app.ca.readAllDisplay() + x, y, lv, X, Y, Z = self.app.read_ca_xyLv() self.app.log_gui.log( f"测量完成: x={x:.4f}, y={y:.4f}, lv={lv:.2f}, " diff --git a/app/views/theme_manager.py b/app/views/theme_manager.py index dec70f5..c8820be 100644 --- a/app/views/theme_manager.py +++ b/app/views/theme_manager.py @@ -19,20 +19,44 @@ from app.views.modern_styles import apply_modern_styles _PREFS_PATH = Path("settings/ui_preferences.json") -# 浅色主题:沿用旧的 yeti(首发布兼容) -LIGHT_THEME = "yeti" +# 浅色主题:自定义轻量蓝灰色板,恢复旧版浅色观感 +LIGHT_THEME = "calman_light" # 深色主题:自定义 Calman 风格 DARK_THEME = "calman_dark" +_LEGACY_LIGHT_THEMES = {"yeti"} + + +_CALMAN_LIGHT_COLORS = { + "primary": "#1755a6", + "secondary": "#2B6CB0", + "success": "#2F9E44", + "info": "#247BA0", + "warning": "#C98700", + "danger": "#CC3300", + "light": "#F7FAFC", + "dark": "#1F2A36", + "bg": "#F5F8FB", + "fg": "#1F2933", + "selectbg": "#2B6CB0", + "selectfg": "#FFFFFF", + "border": "#C8D4E3", + "inputfg": "#243240", + "inputbg": "#FFFFFF", + "active": "#D9E6F2", +} + # ---------------------------------------------------------------------- -# Calman 风格深色主题色板(参考实测截图取色) +# Calman 风格深色主题色板 # ---------------------------------------------------------------------- _CALMAN_DARK_COLORS = { - "primary": "#343A41", # 主色改为炭灰,避免大面积亮蓝 - "secondary": "#444A51", # 中性深灰(用于 header / 分组背景) + # "primary": "#2A2F36", + # "secondary": "#444A51", + "primary": "#6FAFCC", + "secondary": "#AEAEAE", "success": "#4FB960", - "info": "#6FAFCC", # 降低饱和度,只做少量点缀 + "info": "#6FAFCC", "warning": "#F2A93B", "danger": "#E0524A", "light": "#BFC6CE", # 高亮文本 @@ -51,14 +75,27 @@ _CALMAN_DARK_COLORS = { def register_themes() -> None: """把自定义深色主题注册到 ttkbootstrap(可重复调用,幂等)。""" style = Style() + if LIGHT_THEME not in style.theme_names(): + light_def = ThemeDefinition( + name=LIGHT_THEME, + themetype="light", + colors=_CALMAN_LIGHT_COLORS, + ) + style.register_theme(light_def) if DARK_THEME in style.theme_names(): return - theme_def = ThemeDefinition( + dark_def = ThemeDefinition( name=DARK_THEME, themetype="dark", colors=_CALMAN_DARK_COLORS, ) - style.register_theme(theme_def) + style.register_theme(dark_def) + + +def _normalize_theme_name(name: Optional[str]) -> str: + if not name or name in _LEGACY_LIGHT_THEMES: + return LIGHT_THEME + return name # ---------------------------------------------------------------------- @@ -101,7 +138,7 @@ def apply_initial_theme() -> str: 返回最终生效的主题名。 """ register_themes() - name = get_saved_theme() or LIGHT_THEME + name = _normalize_theme_name(get_saved_theme()) style = Style() if name not in style.theme_names(): name = LIGHT_THEME @@ -113,6 +150,7 @@ def apply_initial_theme() -> str: def set_theme(name: str) -> str: """切换到指定主题,持久化偏好,并刷新自定义样式。""" register_themes() + name = _normalize_theme_name(name) style = Style() if name not in style.theme_names(): name = LIGHT_THEME diff --git a/pqAutomationApp.py b/pqAutomationApp.py index 0229ad8..a505e73 100644 --- a/pqAutomationApp.py +++ b/pqAutomationApp.py @@ -388,50 +388,31 @@ class PQAutomationApp( if hasattr(self, "log_gui"): self.log_gui.log(f"切换信号格式失败: {str(e)}", level="error") - def _switch_chart_tabs_by_test_type(self, test_type): - """按测试类型切换 Gamma/EOTF 与客户模板结果 Tab。""" - if not hasattr(self, "chart_notebook"): + def _sync_custom_template_tab_visibility(self, test_type): + """按测试类型与客户模板结果状态同步客户模板 Tab 可见性。""" + if not hasattr(self, "_set_custom_template_tab_visible"): return - try: - def _safe_insert_tab(frame, text, target_pos=1): - """安全插入 Tab:有目标位置则插入,否则追加到末尾。""" - tabs = list(self.chart_notebook.tabs()) - if not tabs or target_pos >= len(tabs): - self.chart_notebook.add(frame, text=text) - return - before_tab_id = tabs[target_pos] - self.chart_notebook.insert(before_tab_id, frame, text=text) + # 客户模板结果 Tab 只属于 SDR Movie。 + if test_type != "sdr_movie": + self._set_custom_template_tab_visible(False) + return - current_tabs = list(self.chart_notebook.tabs()) - gamma_tab_id = str(self.gamma_chart_frame) - eotf_tab_id = str(self.eotf_chart_frame) + has_custom_rows = False + tree = getattr(self, "custom_result_tree", None) + if tree is not None: + try: + has_custom_rows = len(tree.get_children()) > 0 + except Exception: + has_custom_rows = False - if test_type == "hdr_movie": - if gamma_tab_id in current_tabs: - self.chart_notebook.forget(self.gamma_chart_frame) - if eotf_tab_id not in current_tabs: - _safe_insert_tab(self.eotf_chart_frame, "EOTF 曲线", target_pos=1) - else: - if eotf_tab_id in current_tabs: - self.chart_notebook.forget(self.eotf_chart_frame) - if gamma_tab_id not in current_tabs: - _safe_insert_tab(self.gamma_chart_frame, "Gamma 曲线", target_pos=1) - - custom_tab_id = str(self.custom_template_tab_frame) - current_tabs = list(self.chart_notebook.tabs()) - - if test_type == "sdr_movie": - if custom_tab_id not in current_tabs: - self.chart_notebook.add(self.custom_template_tab_frame, text="客户模板结果显示") - else: - if custom_tab_id in current_tabs: - self.chart_notebook.forget(self.custom_template_tab_frame) - - self.chart_notebook.update_idletasks() - except Exception as e: - if hasattr(self, "log_gui"): - self.log_gui.log(f"切换 Gamma/EOTF Tab 失败: {str(e)}", level="error") + # SDR 下仅在客户模板测试进行中,或已有客户模板结果时显示。 + show_tab = has_custom_rows or ( + getattr(self, "testing", False) + and getattr(self, "test_type_var", None) is not None + and self.test_type_var.get() == "sdr_movie" + ) + self._set_custom_template_tab_visible(show_tab) def change_test_type(self, test_type): """切换测试类型""" @@ -453,7 +434,7 @@ class PQAutomationApp( self.update_sidebar_selection() self.on_test_type_change() self._switch_signal_format_tabs(test_type) - self._switch_chart_tabs_by_test_type(test_type) + self._sync_custom_template_tab_visibility(test_type) self.sync_gamut_toolbar() self._restore_charts_for_type(test_type) diff --git a/settings/pq_config.json b/settings/pq_config.json index a92d57f..a7a28e0 100644 --- a/settings/pq_config.json +++ b/settings/pq_config.json @@ -6,11 +6,11 @@ "test_items": [ "gamma" ], - "timing": "OVT 1280x 720 @ 120Hz", + "timing": "DMT 1920x 1080 @ 60Hz", "data_range": "Full", "color_format": "RGB", "bpc": 8, - "colorimetry": "DCI-P3", + "colorimetry": "sRGB", "patterns": { "gamut": "rgb", "gamma": "gray", @@ -22,15 +22,18 @@ "x_tolerance": 0.003, "y_ideal": 0.329, "y_tolerance": 0.003 - }, - "gamut_reference": "DCI-P3" + } }, "sdr_movie": { "name": "SDR Movie测试", "test_items": [ + "gamut", + "gamma", + "cct", + "contrast", "accuracy" ], - "timing": "DMT 1680x 1050 @ 60Hz", + "timing": "DMT 1920x 1080 @ 60Hz", "data_range": "Full", "color_format": "RGB", "bpc": 8, @@ -48,7 +51,7 @@ "y_ideal": 0.329, "y_tolerance": 0.003 }, - "gamut_reference": "DCI-P3" + "gamut_reference": "BT.709" }, "hdr_movie": { "name": "HDR Movie测试",