Files
pqAutomationApp/app/views/panels/calman_panel.py

1323 lines
49 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.
"""CALMAN 风格灰阶测试面板(持续演进版)。
布局尽量贴近 Calman Grayscale - Multi
- 顶部暗色四图DeltaE、RGB Balance 线图、RGB Balance 条图、Gamma Log/Log
- 中部双行灰阶条Actual实测亮度映射+ Target目标灰阶可点击发送图案
- 底部左Current Reading + CIE 1931 xy 散点;
- 底部右按灰阶展开的矩阵表x/y/Y/Gamma/CCT/DeltaE 等)。
"""
from __future__ import annotations
import datetime
import math
import threading
import time
import tkinter as tk
from tkinter import messagebox
from typing import TYPE_CHECKING
import ttkbootstrap as ttk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from app.tests.color_accuracy import calculate_delta_e_2000
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
# 默认灰阶档位(百分比)
DEFAULT_LEVELS_PCT: list[int] = list(range(0, 101, 5))
# 目标白点 D65CIE 1931
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"]
def _pct_to_gray_rgb(pct: int) -> tuple[int, int, int]:
value = int(round(pct * 255 / 100))
return value, value, value
def _rgb_to_hex(rgb: tuple[int, int, int]) -> str:
r, g, b = rgb
return f"#{r:02x}{g:02x}{b:02x}"
def _contrast_fg(gray_value: int) -> str:
return "#ffffff" if gray_value < 128 else "#000000"
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.itemconfigure("patch_bg", fill=color, outline=color)
canvas.itemconfigure("patch_text", text=text, fill=_contrast_fg(gray))
def _xy_to_cct_mccamy(x: float, y: float) -> float:
"""McCamy 近似公式计算 CCT。对极暗灰阶 (xy 噪声大) 仅做参考。"""
denom = 0.1858 - y
if denom == 0:
return float("nan")
n = (x - 0.3320) / denom
return 437 * n ** 3 + 3601 * n ** 2 + 6861 * n + 5517
def _safe_float(value, fmt="{:.4f}", placeholder="-"):
try:
if value is None or value != value: # NaN
return placeholder
return fmt.format(value)
except Exception:
return placeholder
def _xy_to_upvp(x: float, y: float) -> tuple[float, float]:
denom = (-2.0 * x) + (12.0 * y) + 3.0
if denom == 0:
return float("nan"), float("nan")
up = (4.0 * x) / denom
vp = (9.0 * y) / denom
return up, vp
def _hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
c = hex_color.lstrip("#")
return int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16)
def _mix(hex_a: str, hex_b: str, ratio: float) -> str:
r1, g1, b1 = _hex_to_rgb(hex_a)
r2, g2, b2 = _hex_to_rgb(hex_b)
r = int(r1 * (1 - ratio) + r2 * ratio)
g = int(g1 * (1 - ratio) + g2 * ratio)
b = int(b1 * (1 - ratio) + b2 * ratio)
return f"#{r:02x}{g:02x}{b:02x}"
def _is_dark_hex(hex_color: str) -> bool:
r, g, b = _hex_to_rgb(hex_color)
return (r * 299 + g * 587 + b * 114) / 1000 < 128
def _get_calman_palette() -> dict[str, str]:
"""根据当前主题生成 Calman 调试面板色板。"""
style = ttk.Style()
colors = style.colors
bg = colors.bg
fg = colors.fg
dark_mode = _is_dark_hex(bg)
if dark_mode:
figure_bg = _mix(bg, "#000000", 0.18)
axes_bg = _mix(bg, "#000000", 0.25)
grid = _mix(fg, axes_bg, 0.55)
tree_bg = _mix(bg, "#000000", 0.10)
tree_even = _mix(bg, "#ffffff", 0.03)
tree_odd = _mix(bg, "#ffffff", 0.07)
heading_bg = _mix(bg, "#ffffff", 0.12)
reading_bg = _mix(bg, "#ffffff", 0.06)
reading_fg = _mix(fg, "#ffffff", 0.06)
status_fg = _mix(fg, bg, 0.35)
reading_accent = colors.info
xy_series = "#d7dce4"
d65_mark = "#ffffff"
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_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"
return {
"figure_bg": figure_bg,
"axes_bg": axes_bg,
"fg": fg,
"grid": grid,
"metric_tile_bg": _mix(figure_bg, "#ffffff", 0.08 if dark_mode else 0.25),
"metric_tile_fg": reading_fg,
"status_fg": status_fg,
"reading_accent": reading_accent,
"reading_bg": reading_bg,
"reading_fg": reading_fg,
"tree_bg": tree_bg,
"tree_fg": reading_fg,
"tree_even": tree_even,
"tree_odd": tree_odd,
"tree_heading_bg": heading_bg,
"tree_heading_fg": reading_fg,
"tree_select": _mix(colors.info, figure_bg, 0.35),
"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。"""
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
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)
r = max(r, 0.0)
g = max(g, 0.0)
b = max(b, 0.0)
avg = (r + g + b) / 3.0
if avg <= 0:
return float("nan"), float("nan"), float("nan")
return (r / avg) * 100.0, (g / avg) * 100.0, (b / avg) * 100.0
def _style_axes(self: "PQAutomationApp", ax, title: str) -> None:
palette = _get_calman_palette()
ax.set_title(title, color=palette["fg"], fontsize=9, pad=4)
ax.set_facecolor(palette["axes_bg"])
ax.grid(True, color=palette["grid"], alpha=0.35, linewidth=0.6)
ax.tick_params(colors=palette["fg"], labelsize=8)
for spine in ax.spines.values():
spine.set_color(_mix(palette["fg"], palette["axes_bg"], 0.55))
def _apply_calman_tree_style(self: "PQAutomationApp") -> None:
"""应用矩阵 Treeview 的深浅色样式。"""
palette = _get_calman_palette()
style = ttk.Style()
style.configure(
"Calman.Treeview",
rowheight=22,
font=("Consolas", 9),
background=palette["tree_bg"],
fieldbackground=palette["tree_bg"],
foreground=palette["tree_fg"],
)
style.map(
"Calman.Treeview",
background=[("selected", palette["tree_select"])],
foreground=[("selected", palette["tree_fg"])],
)
style.configure(
"Calman.Treeview.Heading",
font=("微软雅黑", 9, "bold"),
background=palette["tree_heading_bg"],
foreground=palette["tree_heading_fg"],
)
if hasattr(self, "calman_metric_tree"):
self.calman_metric_tree.configure(style="Calman.Treeview")
if hasattr(self, "calman_data_tree"):
self.calman_data_tree.configure(style="Calman.Treeview")
def _calman_log(self: "PQAutomationApp", message: str, level: str = "info") -> None:
"""统一输出 Calman 面板日志。"""
logger = getattr(self, "log_gui", None)
if logger is None:
return
self._dispatch_ui(self.log_gui.log, f"CALMAN: {message}", level)
def _build_calman_config_summary(self: "PQAutomationApp") -> str:
"""生成顶部配置摘要,跟随当前测试类型展示 UCD 参数。"""
cfg = getattr(self, "config", None)
test_type = getattr(cfg, "current_test_type", "screen_module")
test_cfg = {}
if cfg is not None:
test_cfg = getattr(cfg, "current_test_types", {}).get(test_type, {})
if test_type == "screen_module":
color_space = getattr(getattr(self, "screen_module_color_space_var", None), "get", lambda: test_cfg.get("colorimetry", "-"))()
output_format = getattr(getattr(self, "screen_module_output_format_var", None), "get", lambda: test_cfg.get("color_format", "-"))()
bit_depth = getattr(getattr(self, "screen_module_bit_depth_var", None), "get", lambda: f"{int(test_cfg.get('bpc', 8))}bit")()
data_range = getattr(getattr(self, "screen_module_data_range_var", None), "get", lambda: test_cfg.get("data_range", "Full"))()
timing = test_cfg.get("timing", "-")
profile_name = "Screen"
elif test_type == "sdr_movie":
color_space = getattr(getattr(self, "sdr_color_space_var", None), "get", lambda: test_cfg.get("colorimetry", "-"))()
output_format = getattr(getattr(self, "sdr_output_format_var", None), "get", lambda: test_cfg.get("color_format", "-"))()
bit_depth = getattr(getattr(self, "sdr_bit_depth_var", None), "get", lambda: f"{int(test_cfg.get('bpc', 8))}bit")()
data_range = getattr(getattr(self, "sdr_data_range_var", None), "get", lambda: test_cfg.get("data_range", "Full"))()
timing = test_cfg.get("timing", "-")
profile_name = "SDR"
elif test_type == "hdr_movie":
color_space = getattr(getattr(self, "hdr_color_space_var", None), "get", lambda: test_cfg.get("colorimetry", "-"))()
output_format = getattr(getattr(self, "hdr_output_format_var", None), "get", lambda: test_cfg.get("color_format", "-"))()
bit_depth = getattr(getattr(self, "hdr_bit_depth_var", None), "get", lambda: f"{int(test_cfg.get('bpc', 10))}bit")()
data_range = getattr(getattr(self, "hdr_data_range_var", None), "get", lambda: test_cfg.get("data_range", "Limited"))()
timing = test_cfg.get("timing", "-")
profile_name = "HDR"
else:
color_space = test_cfg.get("colorimetry", "-")
output_format = test_cfg.get("color_format", "-")
bit_depth = test_cfg.get("bpc", "-")
data_range = test_cfg.get("data_range", "-")
timing = test_cfg.get("timing", "-")
profile_name = test_type
return (
f"Profile: {profile_name} | Timing: {timing} | CS: {color_space} | "
f"Fmt: {output_format} | Depth: {bit_depth} | Range: {data_range}"
)
def _refresh_calman_config_summary(self: "PQAutomationApp") -> None:
if hasattr(self, "calman_config_summary_var"):
self.calman_config_summary_var.set(_build_calman_config_summary(self))
def create_calman_panel(self: "PQAutomationApp") -> None:
"""创建 CALMAN 风格灰阶测试面板,注册到 panel_manager。"""
palette = _get_calman_palette()
self.calman_frame = ttk.Frame(self.content_frame)
self.calman_visible = False
self.calman_levels = list(DEFAULT_LEVELS_PCT)
# level_pct -> dict(pct, x, y, Y, X, Z, cct, gamma, de2000, rgb_r, rgb_g, rgb_b, time)
self.calman_results = {}
self.calman_stop_event = threading.Event()
self.calman_running = False
self.calman_patch_send_busy = False
self.calman_current_level = None
self.calman_last_record = None
self.calman_last_step_seconds = None
root = ttk.Frame(self.calman_frame, padding=8)
root.pack(fill=tk.BOTH, expand=True)
root.rowconfigure(0, weight=4)
root.rowconfigure(1, weight=0)
root.rowconfigure(2, weight=3)
root.columnconfigure(0, weight=1)
root.columnconfigure(1, weight=0)
# ---------------------------- 顶部:图表区(暗色) ----------------------------
chart_frame = ttk.LabelFrame(root, text="Grayscale - Multi", padding=4)
chart_frame.grid(row=0, column=0, sticky=tk.NSEW)
chart_frame.rowconfigure(0, weight=4)
chart_frame.rowconfigure(1, weight=0)
chart_frame.rowconfigure(2, weight=0)
chart_frame.columnconfigure(0, weight=1)
fig = Figure(figsize=(10.5, 3.4), dpi=90, facecolor=palette["figure_bg"])
self.calman_fig = fig
self.calman_ax_de = fig.add_subplot(141)
self.calman_ax_rgb_line = fig.add_subplot(142)
self.calman_ax_rgb_bar = fig.add_subplot(143)
self.calman_ax_gamma = fig.add_subplot(144)
fig.subplots_adjust(
left=0.045, right=0.985, top=0.90, bottom=0.18, wspace=0.30
)
canvas = FigureCanvasTkAgg(fig, master=chart_frame)
canvas_widget = canvas.get_tk_widget()
canvas_widget.configure(bg=palette["figure_bg"], highlightthickness=0)
canvas_widget.grid(row=0, column=0, sticky=tk.NSEW)
self.calman_canvas = canvas
control_row = ttk.Frame(chart_frame)
control_row.grid(row=1, column=0, sticky=tk.EW, pady=(2, 2))
ttk.Label(control_row, text="dE Formula:").pack(side=tk.LEFT)
self.calman_de_formula_var = tk.StringVar(value="2000")
de_combo = ttk.Combobox(
control_row,
values=DE_FORMULAS,
textvariable=self.calman_de_formula_var,
width=8,
state="readonly",
)
de_combo.pack(side=tk.LEFT, padx=(4, 10))
self.calman_elapsed_var = tk.StringVar(value="Step: -- s | Total: -- s")
self.calman_elapsed_label = ttk.Label(
control_row,
textvariable=self.calman_elapsed_var,
foreground=palette["status_fg"],
)
self.calman_elapsed_label.pack(side=tk.LEFT)
self.calman_config_summary_var = tk.StringVar(value="")
self.calman_config_summary_label = ttk.Label(
control_row,
textvariable=self.calman_config_summary_var,
foreground=palette["status_fg"],
anchor=tk.W,
)
self.calman_config_summary_label.pack(side=tk.LEFT, padx=(12, 0), fill=tk.X, expand=True)
metrics_row = ttk.Frame(chart_frame)
metrics_row.grid(row=2, column=0, sticky=tk.EW, pady=(2, 0))
metrics_row.columnconfigure((0, 1, 2, 3), weight=1)
self.calman_avg_de_var = tk.StringVar(value="Avg dE2000: --")
self.calman_avg_cct_var = tk.StringVar(value="Avg CCT: --")
self.calman_contrast_var = tk.StringVar(value="Contrast Ratio: --")
self.calman_avg_gamma_var = tk.StringVar(value="Average Gamma: --")
self.calman_metric_labels = []
for idx, v in enumerate(
(
self.calman_avg_de_var,
self.calman_avg_cct_var,
self.calman_contrast_var,
self.calman_avg_gamma_var,
)
):
lbl = tk.Label(
metrics_row,
textvariable=v,
anchor=tk.CENTER,
fg=palette["metric_tile_fg"],
bg=palette["metric_tile_bg"],
font=("微软雅黑", 10, "bold"),
)
lbl.grid(row=0, column=idx, sticky=tk.EW, padx=2)
self.calman_metric_labels.append(lbl)
# ---------------------------- 顶部右:按钮列 ----------------------------
btn_col = ttk.LabelFrame(root, text="操作", padding=6)
btn_col.grid(row=0, column=1, sticky=tk.NS, padx=(8, 0))
ttk.Button(
btn_col,
text="停止",
bootstyle="danger",
width=18,
command=lambda: stop_sequence_test(self),
).pack(fill=tk.X, pady=2)
ttk.Button(
btn_col,
text="测试该色块",
bootstyle="primary",
width=18,
command=lambda: measure_current_patch(self),
).pack(fill=tk.X, pady=2)
ttk.Button(
btn_col,
text="连续测试列表",
bootstyle="success",
width=18,
command=lambda: start_sequence_test(self),
).pack(fill=tk.X, pady=2)
ttk.Separator(btn_col, orient="horizontal").pack(fill=tk.X, pady=6)
ttk.Button(
btn_col,
text="清空结果",
bootstyle="warning-outline",
width=18,
command=lambda: clear_results(self),
).pack(fill=tk.X, pady=2)
self.calman_status_var = tk.StringVar(value="待机")
self.calman_status_label = ttk.Label(
btn_col,
textvariable=self.calman_status_var,
foreground=palette["status_fg"],
wraplength=150,
justify=tk.LEFT,
)
self.calman_status_label.pack(fill=tk.X, pady=(8, 0))
self.calman_progress_var = tk.StringVar(value="0 / 0")
self.calman_progress = ttk.Progressbar(
btn_col,
orient="horizontal",
mode="determinate",
maximum=100,
value=0,
length=160,
)
self.calman_progress.pack(fill=tk.X, pady=(8, 2))
ttk.Label(btn_col, textvariable=self.calman_progress_var).pack(anchor=tk.W)
self.calman_reading_var = tk.StringVar(
value="x: -- y: -- Y: --\nCCT: -- ΔE: --"
)
self.calman_reading_summary_label = ttk.Label(
btn_col,
textvariable=self.calman_reading_var,
font=("Consolas", 9),
foreground=palette["reading_accent"],
wraplength=160,
justify=tk.LEFT,
)
self.calman_reading_summary_label.pack(fill=tk.X, pady=(8, 0))
# ---------------------------- 中部灰阶色块Target / Actual ----------------------------
patch_outer = ttk.LabelFrame(root, text="灰阶色块(点击可直接输出 Pattern", padding=6)
patch_outer.grid(row=1, column=0, columnspan=2, sticky=tk.EW, pady=(8, 4))
patch_outer.columnconfigure(0, weight=0)
patch_outer.columnconfigure(1, weight=1)
lbl_col = ttk.Frame(patch_outer)
lbl_col.grid(row=0, column=0, sticky=tk.NS, padx=(0, 6))
ttk.Label(lbl_col, text="Actual", width=7).pack(fill=tk.X, pady=(1, 2))
ttk.Label(lbl_col, text="Target", width=7).pack(fill=tk.X, pady=(1, 2))
patch_holder = ttk.Frame(patch_outer)
patch_holder.grid(row=0, column=1, sticky=tk.EW)
patch_holder.columnconfigure(tuple(range(len(self.calman_levels))), weight=1)
self.calman_patch_cells = []
self.calman_actual_cells = []
self.calman_actual_patch_cells = []
self.calman_target_patch_canvases = []
self.calman_target_hexes = []
for idx, pct in enumerate(self.calman_levels):
rgb = _pct_to_gray_rgb(pct)
color = _rgb_to_hex(rgb)
rgb_val = rgb[0]
text_color = _contrast_fg(rgb_val)
self.calman_target_hexes.append(color)
patch_holder.columnconfigure(idx, weight=1, uniform="patch")
actual_cell = tk.Frame(
patch_holder,
bd=1,
relief="solid",
highlightthickness=1,
highlightbackground="#808080",
cursor="hand2",
)
actual_cell.grid(row=0, column=idx, padx=1, pady=1, sticky=tk.NSEW)
actual_canvas = tk.Canvas(
actual_cell,
bg=color,
highlightthickness=0,
width=3,
height=16,
)
actual_canvas.pack(fill=tk.BOTH, expand=True)
actual_canvas.create_rectangle(0, 0, 400, 40, fill=color, outline=color, tags="patch_bg")
actual_canvas.create_text(
18,
8,
text=f"{pct}",
fill=text_color,
font=("Consolas", 6, "bold"),
tags="patch_text",
)
cell = tk.Frame(
patch_holder,
bd=1,
relief="solid",
highlightthickness=1,
highlightbackground="#9c9c9c",
cursor="hand2",
)
cell.grid(row=1, column=idx, padx=1, pady=1, sticky=tk.NSEW)
target_canvas = tk.Canvas(
cell,
bg=color,
highlightthickness=0,
width=3,
height=30,
)
target_canvas.pack(fill=tk.BOTH, expand=True)
target_canvas.create_rectangle(0, 0, 400, 40, fill=color, outline=color, tags="patch_bg")
target_canvas.create_text(
18,
8,
text=f"{pct}",
fill=text_color,
font=("Consolas", 7, "bold"),
tags="patch_text",
)
def _bind_click(widget, p=pct):
def _on_click(_e, pp=p):
send_patch(self, pp)
# Prevent event bubbling from canvas -> parent cell, which would
# otherwise trigger duplicated sends for a single click.
return "break"
widget.bind("<Button-1>", _on_click)
for w in (cell, target_canvas):
_bind_click(w)
for w in (actual_cell, actual_canvas):
_bind_click(w)
self.calman_patch_cells.append(cell)
self.calman_actual_cells.append(actual_cell)
self.calman_actual_patch_cells.append(actual_canvas)
self.calman_target_patch_canvases.append(target_canvas)
# ---------------------------- 底部Current Reading + xy + 数据矩阵 ----------------------------
bottom = ttk.Frame(root)
bottom.grid(row=2, column=0, columnspan=2, sticky=tk.NSEW, pady=(4, 0))
bottom.columnconfigure(0, weight=0)
bottom.columnconfigure(1, weight=1)
bottom.rowconfigure(0, weight=1)
left = ttk.LabelFrame(bottom, text="Current Reading", padding=6)
left.grid(row=0, column=0, sticky=tk.NS, padx=(0, 6))
self.calman_reading_var.set(
"x: -- y: --\n"
"u': -- v': --\n"
"cd/m²: --\n"
"ΔE2000: --"
)
self.calman_reading_detail_label = tk.Label(
left,
textvariable=self.calman_reading_var,
justify=tk.LEFT,
font=("Consolas", 10),
fg=palette["reading_fg"],
bg=palette["reading_bg"],
width=22,
padx=4,
pady=4,
)
self.calman_reading_detail_label.pack(fill=tk.X)
xy_fig = Figure(figsize=(2.6, 2.2), dpi=90, facecolor=palette["figure_bg"])
self.calman_xy_fig = xy_fig
self.calman_xy_ax = xy_fig.add_subplot(111)
xy_fig.subplots_adjust(left=0.20, right=0.96, top=0.90, bottom=0.18)
xy_canvas = FigureCanvasTkAgg(xy_fig, master=left)
xy_widget = xy_canvas.get_tk_widget()
xy_widget.configure(bg=palette["figure_bg"], highlightthickness=0)
xy_widget.pack(fill=tk.BOTH, expand=True, pady=(6, 0))
self.calman_xy_canvas = xy_canvas
right = ttk.LabelFrame(bottom, text="测量矩阵", padding=4)
right.grid(row=0, column=1, sticky=tk.NSEW)
right.rowconfigure(0, weight=1)
right.rowconfigure(1, weight=0)
right.columnconfigure(0, weight=0)
right.columnconfigure(1, weight=1)
right.columnconfigure(2, weight=0)
metric_tree = ttk.Treeview(
right,
columns=("metric",),
show="headings",
height=9,
selectmode="none",
)
metric_tree.heading("metric", text="Metric")
metric_tree.column("metric", width=118, anchor=tk.W, stretch=False)
metric_tree.grid(row=0, column=0, sticky=tk.NS)
data_columns = [str(p) for p in self.calman_levels]
data_tree = ttk.Treeview(
right,
columns=data_columns,
show="headings",
height=9,
selectmode="none",
)
for p in self.calman_levels:
cid = str(p)
data_tree.heading(cid, text=cid)
data_tree.column(cid, width=50, anchor=tk.CENTER, stretch=False)
data_tree.grid(row=0, column=1, sticky=tk.NSEW)
ysb = ttk.Scrollbar(right, orient="vertical", command=lambda *a: _matrix_yview(self, *a))
ysb.grid(row=0, column=2, sticky=tk.NS)
xsb = ttk.Scrollbar(right, orient="horizontal", command=data_tree.xview)
xsb.grid(row=1, column=1, sticky=tk.EW)
data_tree.configure(xscrollcommand=xsb.set)
self.calman_metric_tree = metric_tree
self.calman_data_tree = data_tree
self.calman_table_ysb = ysb
self.calman_tree = data_tree
for widget in (metric_tree, data_tree):
widget.bind("<MouseWheel>", lambda e: _matrix_mousewheel(self, e))
_apply_calman_tree_style(self)
right.bind("<Configure>", lambda _e: _adaptive_matrix_columns(self))
_refresh_metric_table(self)
_refresh_calman_config_summary(self)
_update_target_strip(self)
_update_actual_strip(self)
_redraw_calman_charts(self)
# 注册到统一面板管理(按钮稍后由 main_layout 注入)
self.register_panel("calman", self.calman_frame, None, "calman_visible")
def toggle_calman_panel(self: "PQAutomationApp") -> None:
"""切换 CALMAN 灰阶面板显示。"""
self.show_panel("calman")
_refresh_calman_config_summary(self)
# ---------------------------------------------------------------------------
# 发送 / 测量
# ---------------------------------------------------------------------------
def send_patch(self: "PQAutomationApp", pct: int) -> None:
"""点击色块时,发送对应灰阶图案到信号发生器。"""
if not self.signal_service.is_connected:
messagebox.showwarning("提示", "请先连接 UCD323 设备")
return
if getattr(self, "calman_patch_send_busy", False):
_calman_log(self, f"send busy, ignore click pct={pct}", "warning")
self.calman_status_var.set("发送进行中,请稍候...")
return
rgb_val = int(round(pct * 255 / 100))
self.calman_current_level = pct
self.calman_status_var.set(f"发送 {pct}%RGB={rgb_val}...")
_highlight_patch(self, pct)
_refresh_calman_config_summary(self)
_calman_log(self, f"click patch pct={pct}, rgb=({rgb_val}, {rgb_val}, {rgb_val})")
self.calman_patch_send_busy = True
def worker():
try:
_calman_log(self, f"send_solid_rgb start pct={pct}")
test_type = getattr(self.config, "current_test_type", "screen_module")
if hasattr(self, "pattern_service") and self.pattern_service is not None:
self.pattern_service.send_rgb(
(rgb_val, rgb_val, rgb_val),
test_type=test_type,
)
else:
self.signal_service.send_solid_rgb((rgb_val, rgb_val, rgb_val))
_calman_log(self, f"send_solid_rgb success pct={pct}")
_calman_log(self, f"ucd profile applied test_type={test_type}")
self._dispatch_ui(
self.log_gui.log, f"CALMAN: 已发送 {pct}% 灰阶 (RGB={rgb_val})",
"info",
)
self._dispatch_ui(self.calman_status_var.set, f"{pct}% 已发送")
except Exception as exc:
_calman_log(self, f"send_solid_rgb failed pct={pct}: {exc}", "error")
self._dispatch_ui(self.log_gui.log, f"发送失败: {exc}", "error")
self._dispatch_ui(self.calman_status_var.set, f"发送失败: {exc}")
finally:
self.calman_patch_send_busy = False
threading.Thread(target=worker, daemon=True).start()
def _measure_once(self: "PQAutomationApp", pct: int) -> dict | None:
"""采集一次 CA410并组装一条记录含 CCT/Gamma/ΔE2000"""
try:
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
except Exception as exc:
self._dispatch_ui(self.log_gui.log, f"CA410 采集异常: {exc}", "error")
return None
if lv is None:
return None
# CCT对很暗的色块意义不大按阈值过滤
cct = _xy_to_cct_mccamy(x, y) if lv >= 0.5 else float("nan")
# Gamma 需要 100% 作为参考亮度
ref = self.calman_results.get(100, {}).get("Y")
gamma = float("nan")
if ref and ref > 0 and 0 < pct < 100 and lv > 0:
nv = pct / 100.0
ny = lv / ref
if ny > 0:
try:
gamma = math.log(ny) / math.log(nv)
except (ValueError, ZeroDivisionError):
gamma = float("nan")
# ΔE根据下拉框切换公式当前 94/76 先复用 2000保留接口
formula = getattr(self, "calman_de_formula_var", None)
formula_value = formula.get() if formula is not None else "2000"
try:
de = calculate_delta_e_2000(x, y, lv, D65_X, D65_Y)
except Exception:
de = float("nan")
# 未来接入 76/94 时可在此切换实现。
if formula_value in ("94", "76"):
pass
rr, gg, bb = _xyY_to_rgb_balance(x, y, lv)
return {
"pct": pct,
"x": x,
"y": y,
"Y": lv,
"X": X,
"Z": Z,
"cct": cct,
"gamma": gamma,
"de2000": de,
"rgb_r": rr,
"rgb_g": gg,
"rgb_b": bb,
"time": datetime.datetime.now().strftime("%H:%M:%S"),
}
def measure_current_patch(self: "PQAutomationApp") -> None:
"""采集当前已发送色块对应的 CA410 数据。"""
if not getattr(self, "ca", None):
messagebox.showwarning("提示", "请先连接 CA410 色度计")
return
pct = self.calman_current_level
if pct is None:
messagebox.showinfo("提示", "请先点击一个灰阶色块发送")
return
def worker():
t0 = time.perf_counter()
_calman_log(self, f"measure start pct={pct}")
self._dispatch_ui(self.calman_status_var.set, f"采集 {pct}% 中...")
rec = _measure_once(self, pct)
if rec is None:
_calman_log(self, f"measure failed pct={pct}", "error")
self._dispatch_ui(self.calman_status_var.set, "采集失败")
return
step_s = time.perf_counter() - t0
self.calman_last_step_seconds = step_s
self.calman_results[pct] = rec
_calman_log(
self,
(
"measure success pct={pct}, x={x:.4f}, y={y:.4f}, Y={Y:.3f}, "
"cct={cct:.0f}, gamma={gamma:.3f}, de2000={de:.3f}, step={step:.2f}s"
).format(
pct=pct,
x=rec["x"],
y=rec["y"],
Y=rec["Y"],
cct=rec["cct"] if rec["cct"] == rec["cct"] else float("nan"),
gamma=rec["gamma"] if rec["gamma"] == rec["gamma"] else float("nan"),
de=rec["de2000"] if rec["de2000"] == rec["de2000"] else float("nan"),
step=step_s,
),
)
self._dispatch_ui(_apply_record_to_ui, self, rec)
self._dispatch_ui(
self.calman_status_var.set, f"{pct}% 采集完成 ({step_s:.2f}s)"
)
self._dispatch_ui(
self.calman_elapsed_var.set,
f"Step: {step_s:.2f} s | Total: -- s",
)
threading.Thread(target=worker, daemon=True).start()
def start_sequence_test(self: "PQAutomationApp") -> None:
"""从 100% 到 0% 连续发送并采集(先测 100% 以确定 gamma 参考)。"""
if not getattr(self, "ca", None) or not self.signal_service.is_connected:
messagebox.showerror("错误", "请先连接 CA410 和 UCD323")
return
if self.calman_running:
return
self.calman_running = True
self.calman_stop_event.clear()
settle = float(getattr(self, "pattern_settle_time", 0.4))
self.calman_progress["value"] = 0
self.calman_progress_var.set("0 / 0")
_refresh_calman_config_summary(self)
_calman_log(self, f"sequence start levels={len(self.calman_levels)}, settle={settle:.2f}s")
def worker():
seq_t0 = time.perf_counter()
try:
test_type = getattr(self.config, "current_test_type", "screen_module")
rgb_session = None
if hasattr(self, "pattern_service") and self.pattern_service is not None:
rgb_session = self.pattern_service.prepare_session(
"rgb",
test_type=test_type,
log_details=False,
)
_calman_log(self, f"sequence ucd profile applied test_type={test_type}")
order = sorted(self.calman_levels, reverse=True)
total = len(order)
self._dispatch_ui(self.calman_progress_var.set, f"0 / {total}")
for i, pct in enumerate(order, 1):
if self.calman_stop_event.is_set():
_calman_log(self, f"sequence stop requested at step={i-1}/{total}", "warning")
self._dispatch_ui(self.log_gui.log, "已停止连续测试", "warning")
break
step_t0 = time.perf_counter()
rgb_val = int(round(pct * 255 / 100))
self._dispatch_ui(
self.calman_status_var.set, f"[{i}/{total}] 发送 {pct}%"
)
_calman_log(self, f"sequence send step={i}/{total}, pct={pct}, rgb={rgb_val}")
self._dispatch_ui(_highlight_patch, self, pct)
try:
if rgb_session is not None:
self.pattern_service.send_rgb(
(rgb_val, rgb_val, rgb_val),
session=rgb_session,
)
else:
self.signal_service.send_solid_rgb(
(rgb_val, rgb_val, rgb_val)
)
except Exception as exc:
_calman_log(self, f"sequence send failed step={i}/{total}, pct={pct}: {exc}", "error")
self._dispatch_ui(
self.log_gui.log, f"发送 {pct}% 失败: {exc}", "error"
)
continue
self.calman_current_level = pct
# 等待稳定,停止事件触发时尽快退出
if self.calman_stop_event.wait(settle):
_calman_log(self, f"sequence interrupted during settle step={i}/{total}, pct={pct}", "warning")
break
rec = _measure_once(self, pct)
if rec is None:
_calman_log(self, f"sequence measure failed step={i}/{total}, pct={pct}", "error")
continue
self.calman_results[pct] = rec
_calman_log(
self,
(
"sequence measure step={i}/{total}, pct={pct}, x={x:.4f}, y={y:.4f}, Y={Y:.3f}, "
"cct={cct:.0f}, gamma={gamma:.3f}, de2000={de:.3f}"
).format(
i=i,
total=total,
pct=pct,
x=rec["x"],
y=rec["y"],
Y=rec["Y"],
cct=rec["cct"] if rec["cct"] == rec["cct"] else float("nan"),
gamma=rec["gamma"] if rec["gamma"] == rec["gamma"] else float("nan"),
de=rec["de2000"] if rec["de2000"] == rec["de2000"] else float("nan"),
),
)
self._dispatch_ui(_apply_record_to_ui, self, rec)
step_s = time.perf_counter() - step_t0
total_s = time.perf_counter() - seq_t0
self._dispatch_ui(
_set_sequence_progress,
self,
i,
total,
step_s,
total_s,
)
else:
_calman_log(self, f"sequence complete total={total}")
self._dispatch_ui(self.calman_status_var.set, "连续测试完成")
self._dispatch_ui(self.log_gui.log, "CALMAN: 连续测试完成", "success")
return
_calman_log(self, "sequence stopped", "warning")
self._dispatch_ui(self.calman_status_var.set, "已停止")
finally:
self.calman_running = False
threading.Thread(target=worker, daemon=True).start()
def stop_sequence_test(self: "PQAutomationApp") -> None:
"""请求停止连续测试。"""
if self.calman_running:
_calman_log(self, "stop requested", "warning")
self.calman_stop_event.set()
self.calman_status_var.set("正在停止...")
else:
_calman_log(self, "stop requested but no sequence is running", "warning")
self.calman_status_var.set("当前没有运行中的连续测试")
def clear_results(self: "PQAutomationApp") -> None:
"""清空结果表和图表。"""
_calman_log(self, "clear results")
self.calman_results.clear()
self.calman_last_record = None
self.calman_reading_var.set(
"x: -- y: --\n"
"u': -- v': --\n"
"cd/m²: --\n"
"ΔE2000: --"
)
_refresh_metric_table(self)
_update_actual_strip(self)
_redraw_calman_charts(self)
self.calman_progress["value"] = 0
self.calman_progress_var.set("0 / 0")
self.calman_elapsed_var.set("Step: -- s | Total: -- s")
self.calman_status_var.set("已清空")
# ---------------------------------------------------------------------------
# UI 更新辅助
# ---------------------------------------------------------------------------
def _highlight_patch(self: "PQAutomationApp", pct: int) -> None:
"""高亮当前选中色块。"""
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)
else:
cell.configure(highlightbackground="#9c9c9c", highlightthickness=1)
for i, cell in enumerate(self.calman_actual_cells):
if i == idx:
cell.configure(highlightbackground="#1f6fb2", highlightthickness=2)
else:
cell.configure(highlightbackground="#808080", highlightthickness=1)
total_cols = len(self.calman_levels) + 1 # 含 metric 列
col_index = idx + 1
left_fraction = max(0.0, min(1.0, (col_index - 2) / max(1, total_cols - 1)))
try:
self.calman_data_tree.xview_moveto(left_fraction)
except Exception:
pass
def _apply_record_to_ui(self: "PQAutomationApp", rec: dict) -> None:
"""把一条测量结果写入 Treeview并刷新图表与 Current Reading。"""
self.calman_last_record = rec
_refresh_metric_table(self)
_update_actual_strip(self)
up, vp = _xy_to_upvp(rec["x"], rec["y"])
self.calman_reading_var.set(
f"x: {_safe_float(rec['x'])} y: {_safe_float(rec['y'])}\n"
f"u': {_safe_float(up)} v': {_safe_float(vp)}\n"
f"cd/m²: {_safe_float(rec['Y'], '{:.3f}')}\n"
f"ΔE2000: {_safe_float(rec['de2000'], '{:.3f}')}"
)
_redraw_calman_charts(self)
def _set_sequence_progress(
self: "PQAutomationApp",
finished: int,
total: int,
step_seconds: float,
total_seconds: float,
) -> None:
percent = (finished / total) * 100 if total > 0 else 0
self.calman_progress["value"] = percent
self.calman_progress_var.set(f"{finished} / {total}")
self.calman_elapsed_var.set(
f"Step: {step_seconds:.2f} s | Total: {total_seconds:.1f} s"
)
def _matrix_yview(self: "PQAutomationApp", *args) -> None:
self.calman_metric_tree.yview(*args)
self.calman_data_tree.yview(*args)
first, last = self.calman_data_tree.yview()
self.calman_table_ysb.set(first, last)
def _matrix_mousewheel(self: "PQAutomationApp", event) -> str:
delta = -1 if event.delta > 0 else 1
self.calman_metric_tree.yview_scroll(delta, "units")
self.calman_data_tree.yview_scroll(delta, "units")
first, last = self.calman_data_tree.yview()
self.calman_table_ysb.set(first, last)
return "break"
def _adaptive_matrix_columns(self: "PQAutomationApp") -> None:
"""按可用宽度自适应数据列宽;空间不足时保留横向滚动。"""
try:
available = self.calman_data_tree.winfo_width()
except Exception:
return
if available <= 40:
return
col_count = max(1, len(self.calman_levels))
min_w = 44
ideal = int(available / col_count)
width = max(min_w, ideal)
for p in self.calman_levels:
self.calman_data_tree.column(str(p), width=width, minwidth=min_w, stretch=False)
def _redraw_calman_charts(self: "PQAutomationApp") -> None:
"""根据 calman_results 重绘四张图和 xy 散点。"""
palette = _get_calman_palette()
if hasattr(self, "calman_fig"):
self.calman_fig.patch.set_facecolor(palette["figure_bg"])
if hasattr(self, "calman_canvas"):
try:
self.calman_canvas.get_tk_widget().configure(
bg=palette["figure_bg"], highlightthickness=0
)
except Exception:
pass
recs = sorted(self.calman_results.values(), key=lambda r: r["pct"])
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"]]
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"]]
if de_vals:
avg_de = sum(de_vals) / len(de_vals)
self.calman_avg_de_var.set(f"Avg dE2000: {avg_de:.2f}")
else:
self.calman_avg_de_var.set("Avg dE2000: --")
if cct_vals:
avg_cct = sum(cct_vals) / len(cct_vals)
self.calman_avg_cct_var.set(f"Avg CCT: {avg_cct:.0f}")
else:
self.calman_avg_cct_var.set("Avg CCT: --")
if gamma_vals:
avg_gamma = sum(gamma_vals) / len(gamma_vals)
self.calman_avg_gamma_var.set(f"Average Gamma: {avg_gamma:.2f}")
else:
self.calman_avg_gamma_var.set("Average Gamma: --")
if len(lum_vals) >= 2 and min(v for v in lum_vals if v > 0) > 0:
max_lum = max(lum_vals)
min_lum = min(v for v in lum_vals if v > 0)
contrast = max_lum / min_lum
self.calman_contrast_var.set(f"Contrast Ratio: {contrast:.0f}")
else:
self.calman_contrast_var.set("Contrast Ratio: --")
# ΔE2000
a1 = self.calman_ax_de
a1.clear()
_style_axes(self, a1, "DeltaE 2000")
if pcts:
a1.bar(pcts, de_vals, color="#ffcf57", width=3.5)
a1.set_xlim(-2, 102)
a1.set_ylim(bottom=0)
a1.set_xlabel("", fontsize=8)
# RGB Balance 线图
a2 = self.calman_ax_rgb_line
a2.clear()
_style_axes(self, a2, "RGB Balance")
if rgb_pcts:
a2.plot(rgb_pcts, rgb_r, "-", color="#ff4d4d", linewidth=1.2)
a2.plot(rgb_pcts, rgb_g, "-", color="#4caf50", linewidth=1.2)
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_xlabel("", fontsize=8)
# RGB Balance 条图(用最后一个点)
a3 = self.calman_ax_rgb_bar
a3.clear()
_style_axes(self, a3, "RGB Balance")
if recs:
last = 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,
]
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_xlabel("", fontsize=8)
# Gamma
a4 = self.calman_ax_gamma
a4.clear()
_style_axes(self, a4, "Gamma Log/Log")
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.set_xlim(-2, 102)
a4.set_ylim(1.6, 2.8)
a4.set_xlabel("", fontsize=8)
self.calman_canvas.draw_idle()
_redraw_xy_chart(self)
def _redraw_xy_chart(self: "PQAutomationApp") -> None:
palette = _get_calman_palette()
if hasattr(self, "calman_xy_fig"):
self.calman_xy_fig.patch.set_facecolor(palette["figure_bg"])
if hasattr(self, "calman_xy_canvas"):
try:
self.calman_xy_canvas.get_tk_widget().configure(
bg=palette["figure_bg"], highlightthickness=0
)
except Exception:
pass
ax = self.calman_xy_ax
ax.clear()
_style_axes(self, ax, "CIE 1931 xy")
ax.set_xlim(0.29, 0.34)
ax.set_ylim(0.31, 0.35)
ax.plot([D65_X], [D65_Y], marker="x", color=palette["d65_mark"], markersize=7)
recs = sorted(self.calman_results.values(), key=lambda r: r["pct"])
if recs:
xs = [r["x"] for r in recs]
ys = [r["y"] for r in recs]
ax.plot(xs, ys, "o-", color=palette["xy_series"], linewidth=1.0, markersize=3)
last = recs[-1]
ax.plot([last["x"]], [last["y"]], marker="o", color="#ffcc00", markersize=5)
ax.plot([last["x"], D65_X], [last["y"], D65_Y], color=_mix(palette["fg"], palette["axes_bg"], 0.4), linewidth=0.8)
self.calman_xy_canvas.draw_idle()
def _update_actual_strip(self: "PQAutomationApp") -> None:
"""把实测亮度归一后映射到 Actual 色条。"""
y_map = {pct: rec["Y"] for pct, rec in self.calman_results.items() if rec.get("Y") is not None}
if not y_map:
for idx, w in enumerate(self.calman_actual_patch_cells):
base = self.calman_target_hexes[idx]
_set_canvas_patch(w, base, f"{self.calman_levels[idx]}")
return
max_y = max(y_map.values())
if max_y <= 0:
max_y = 1.0
for idx, pct in enumerate(self.calman_levels):
yy = y_map.get(pct)
if yy is None:
base = self.calman_target_hexes[idx]
_set_canvas_patch(self.calman_actual_patch_cells[idx], base, f"{pct}")
continue
norm = max(0.0, min(1.0, yy / max_y))
g = int(round(norm * 255))
_set_canvas_patch(self.calman_actual_patch_cells[idx], f"#{g:02x}{g:02x}{g:02x}", f"{pct}")
def _update_target_strip(self: "PQAutomationApp") -> None:
for idx, canvas in enumerate(self.calman_target_patch_canvases):
_set_canvas_patch(canvas, self.calman_target_hexes[idx], f"{self.calman_levels[idx]}")
def _refresh_metric_table(self: "PQAutomationApp") -> None:
"""重绘下方矩阵表。"""
_apply_calman_tree_style(self)
palette = _get_calman_palette()
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 "-"),
),
("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 "-"),
("RGB R", lambda r: _safe_float(r.get("rgb_r"), "{:.2f}") if r else "-"),
("RGB G", lambda r: _safe_float(r.get("rgb_g"), "{:.2f}") if r else "-"),
("RGB B", lambda r: _safe_float(r.get("rgb_b"), "{:.2f}") if r else "-"),
]
for iid in self.calman_metric_tree.get_children():
self.calman_metric_tree.delete(iid)
for iid in self.calman_data_tree.get_children():
self.calman_data_tree.delete(iid)
for row_idx, (name, func) in enumerate(metrics):
values = []
for pct in self.calman_levels:
rec = self.calman_results.get(pct)
if name == "Target Y":
values.append(func(rec, pctx=pct))
else:
values.append(func(rec))
iid = f"row_{row_idx}"
tags = ("odd",) if row_idx % 2 else ("even",)
self.calman_metric_tree.insert("", tk.END, iid=iid, values=(name,), tags=tags)
self.calman_data_tree.insert("", tk.END, iid=iid, values=values, tags=tags)
self.calman_metric_tree.tag_configure(
"odd", background=palette["tree_odd"], foreground=palette["tree_fg"]
)
self.calman_metric_tree.tag_configure(
"even", background=palette["tree_even"], foreground=palette["tree_fg"]
)
self.calman_data_tree.tag_configure(
"odd", background=palette["tree_odd"], foreground=palette["tree_fg"]
)
self.calman_data_tree.tag_configure(
"even", background=palette["tree_even"], foreground=palette["tree_fg"]
)
first, last = self.calman_data_tree.yview()
self.calman_table_ysb.set(first, last)
def refresh_calman_theme(self: "PQAutomationApp") -> None:
"""主题切换后刷新 Calman 灰阶调试面板的表格与图表颜色。"""
if not hasattr(self, "calman_frame"):
return
palette = _get_calman_palette()
if hasattr(self, "calman_elapsed_label"):
self.calman_elapsed_label.configure(foreground=palette["status_fg"])
if hasattr(self, "calman_config_summary_label"):
self.calman_config_summary_label.configure(foreground=palette["status_fg"])
if hasattr(self, "calman_status_label"):
self.calman_status_label.configure(foreground=palette["status_fg"])
if hasattr(self, "calman_reading_summary_label"):
self.calman_reading_summary_label.configure(foreground=palette["reading_accent"])
if hasattr(self, "calman_reading_detail_label"):
self.calman_reading_detail_label.configure(
fg=palette["reading_fg"],
bg=palette["reading_bg"],
)
for lbl in getattr(self, "calman_metric_labels", []):
lbl.configure(
fg=palette["metric_tile_fg"],
bg=palette["metric_tile_bg"],
)
_refresh_metric_table(self)
_refresh_calman_config_summary(self)
_redraw_calman_charts(self)
class CalmanPanelMixin:
"""挂载本模块的自由函数到 PQAutomationApp。"""
create_calman_panel = create_calman_panel
toggle_calman_panel = toggle_calman_panel
refresh_calman_theme = refresh_calman_theme