修改引用逻辑、新增Pattern更改界面、新增Calman灰阶界面

This commit is contained in:
xinzhu.yin
2026-05-27 11:26:28 +08:00
parent a903c17cb3
commit dff4e0df4d
24 changed files with 3327 additions and 386 deletions

View File

@@ -14,13 +14,19 @@ from PIL import Image, ImageTk
from app.services import ai_image as _svc
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
# ---------------- 面板创建 ----------------
def create_ai_image_panel(self):
def create_ai_image_panel(self: "PQAutomationApp"):
"""创建 AI 图片对话面板,并注册到面板管理。"""
frame = ttk.Frame(self.content_frame)
self.ai_image_frame = frame
@@ -190,12 +196,12 @@ def create_ai_image_panel(self):
reload_ai_image_list(self)
def toggle_ai_image_panel(self):
def toggle_ai_image_panel(self: "PQAutomationApp"):
"""切换 AI 图片面板显隐。"""
self.show_panel("ai_image")
def _get_app_base_dir(self) -> str:
def _get_app_base_dir(self: "PQAutomationApp") -> str:
"""返回应用根目录settings 的上一级)。"""
if getattr(self, "config_file", None):
return os.path.dirname(os.path.dirname(self.config_file))
@@ -207,7 +213,7 @@ def _get_app_base_dir(self) -> str:
# ---------------- 列表 / 选中 ----------------
def reload_ai_image_list(self, auto_select_first=True):
def reload_ai_image_list(self: "PQAutomationApp", auto_select_first=True):
"""重新扫描缓存并刷新列表。
按 ``session_id`` 分组:每个会话用一行不可选的分隔头(``── 会话 #N · 时间 ──``
@@ -289,7 +295,7 @@ def _format_list_label(rec: _svc.AIImageRecord) -> str:
return f"{size_tag}{name_line}"
def _on_list_select(self):
def _on_list_select(self: "PQAutomationApp"):
sel = self.ai_image_listbox.curselection()
if not sel:
return
@@ -311,7 +317,7 @@ def _on_list_select(self):
_select_record(self, rec)
def _select_record(self, rec: _svc.AIImageRecord):
def _select_record(self: "PQAutomationApp", rec: _svc.AIImageRecord):
self.ai_image_current = rec
self.ai_image_meta_var.set(
f"{os.path.basename(rec.image_path)} | {rec.created_at}"
@@ -322,7 +328,7 @@ def _select_record(self, rec: _svc.AIImageRecord):
# ---------------- 预览绘制 ----------------
def _redraw_preview(self):
def _redraw_preview(self: "PQAutomationApp"):
rec = getattr(self, "ai_image_current", None)
canvas = self.ai_image_canvas
canvas.delete("all")
@@ -347,7 +353,7 @@ def _redraw_preview(self):
# ---------------- 发送 / 保存 / 删除 ----------------
def _start_new_session(self):
def _start_new_session(self: "PQAutomationApp"):
"""开启新的对话会话,后续生成将使用新的 session_id。"""
if getattr(self, "_ai_image_requesting", False):
messagebox.showinfo("提示", "请等待当前请求完成")
@@ -357,14 +363,14 @@ def _start_new_session(self):
reload_ai_image_list(self, auto_select_first=False)
def _session_id_for_row(self, row: int) -> str:
def _session_id_for_row(self: "PQAutomationApp", row: int) -> str:
session_map = getattr(self, "_ai_image_row_session_map", None) or []
if row < 0 or row >= len(session_map):
return ""
return session_map[row] or ""
def _switch_to_session(self, session_id: str, show_message: bool = True, target_record_id: str = ""):
def _switch_to_session(self: "PQAutomationApp", session_id: str, show_message: bool = True, target_record_id: str = ""):
sid = (session_id or "").strip()
if not sid:
return
@@ -389,7 +395,7 @@ def _switch_to_session(self, session_id: str, show_message: bool = True, target_
messagebox.showinfo("提示", "已切换到所选历史对话")
def _update_request_progress(self):
def _update_request_progress(self: "PQAutomationApp"):
if not getattr(self, "_ai_image_requesting", False):
self._ai_image_progress_job = None
return
@@ -398,7 +404,7 @@ def _update_request_progress(self):
self._ai_image_progress_job = self.root.after(900, lambda: _update_request_progress(self))
def _send_prompt(self):
def _send_prompt(self: "PQAutomationApp"):
if getattr(self, "_ai_image_requesting", False):
return
prompt = self.ai_image_input.get("1.0", tk.END).strip()
@@ -439,7 +445,7 @@ def _send_prompt(self):
)
def _set_requesting(self, flag: bool):
def _set_requesting(self: "PQAutomationApp", flag: bool):
self._ai_image_requesting = flag
try:
self.ai_image_send_btn.configure(state=tk.DISABLED if flag else tk.NORMAL)
@@ -465,7 +471,7 @@ def _set_requesting(self, flag: bool):
pass
def _on_request_done(self, record, exc, req_seq):
def _on_request_done(self: "PQAutomationApp", record, exc, req_seq):
# 旧请求回调(例如用户已点击停止后)直接忽略
if req_seq != getattr(self, "_ai_image_active_seq", 0):
return
@@ -493,7 +499,7 @@ def _on_request_done(self, record, exc, req_seq):
break
def _stop_request(self):
def _stop_request(self: "PQAutomationApp"):
"""停止当前生成任务(协作取消:屏蔽后续回调并恢复 UI"""
if not getattr(self, "_ai_image_requesting", False):
return
@@ -505,7 +511,7 @@ def _stop_request(self):
self.ai_image_status_var.set("已停止生成")
def _save_current(self):
def _save_current(self: "PQAutomationApp"):
rec = getattr(self, "ai_image_current", None)
if rec is None:
messagebox.showinfo("提示", "请先选择一张图片")
@@ -526,7 +532,7 @@ def _save_current(self):
messagebox.showerror("保存失败", str(exc))
def _delete_current(self):
def _delete_current(self: "PQAutomationApp"):
rec = getattr(self, "ai_image_current", None)
if rec is None:
messagebox.showinfo("提示", "请先选择一张图片")
@@ -537,7 +543,7 @@ def _delete_current(self):
reload_ai_image_list(self)
def _rename_current(self):
def _rename_current(self: "PQAutomationApp"):
"""弹窗让用户修改当前记录的展示标题(保存到侧车 ``title`` 字段,原始 prompt 不变)。"""
rec = getattr(self, "ai_image_current", None)
if rec is None:
@@ -580,7 +586,7 @@ def _rename_current(self):
# ---------------- 发送到 UCD ----------------
def _show_list_context_menu(self, event):
def _show_list_context_menu(self: "PQAutomationApp", event):
"""在图片列表上显示右键菜单,并根据状态启用/禁用项。"""
try:
row = self.ai_image_listbox.nearest(event.y)
@@ -619,7 +625,7 @@ def _show_list_context_menu(self, event):
self.ai_image_menu.grab_release()
def _send_to_ucd(self):
def _send_to_ucd(self: "PQAutomationApp"):
"""把当前选中的 AI 图片通过 UCD 发送到显示设备。"""
rec = getattr(self, "ai_image_current", None)
if rec is None:
@@ -730,3 +736,29 @@ def _build_ucd_resized_image(image_path: str, target_w: int, target_h: int) -> s
resized = img.convert("RGB").resize((target_w, target_h), Image.LANCZOS)
resized.save(out_path, format="PNG")
return out_path
class AIImagePanelMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
create_ai_image_panel = create_ai_image_panel
toggle_ai_image_panel = toggle_ai_image_panel
_get_app_base_dir = _get_app_base_dir
reload_ai_image_list = reload_ai_image_list
_on_list_select = _on_list_select
_select_record = _select_record
_redraw_preview = _redraw_preview
_start_new_session = _start_new_session
_session_id_for_row = _session_id_for_row
_switch_to_session = _switch_to_session
_update_request_progress = _update_request_progress
_send_prompt = _send_prompt
_set_requesting = _set_requesting
_on_request_done = _on_request_done
_stop_request = _stop_request
_save_current = _save_current
_delete_current = _delete_current
_rename_current = _rename_current
_show_list_context_menu = _show_list_context_menu
_send_to_ucd = _send_to_ucd

View File

@@ -0,0 +1,987 @@
"""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 _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(ax, title: str) -> None:
ax.set_title(title, color=_FG, fontsize=9, pad=4)
ax.set_facecolor(_AX_BG)
ax.grid(True, color=_GRID, alpha=0.35, linewidth=0.6)
ax.tick_params(colors=_FG, labelsize=8)
for spine in ax.spines.values():
spine.set_color("#8a8a8a")
def create_calman_panel(self: "PQAutomationApp") -> None:
"""创建 CALMAN 风格灰阶测试面板,注册到 panel_manager。"""
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_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=_DARK_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=_DARK_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")
ttk.Label(
control_row,
textvariable=self.calman_elapsed_var,
foreground="#d0d0d0",
).pack(side=tk.LEFT)
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: --")
for idx, v in enumerate(
(
self.calman_avg_de_var,
self.calman_avg_cct_var,
self.calman_contrast_var,
self.calman_avg_gamma_var,
)
):
tk.Label(
metrics_row,
textvariable=v,
anchor=tk.CENTER,
fg="#f2f2f2",
bg="#373737",
font=("微软雅黑", 10, "bold"),
).grid(row=0, column=idx, sticky=tk.EW, padx=2)
# ---------------------------- 顶部右:按钮列 ----------------------------
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="待机")
ttk.Label(
btn_col,
textvariable=self.calman_status_var,
foreground="#555",
wraplength=150,
justify=tk.LEFT,
).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: --"
)
ttk.Label(
btn_col,
textvariable=self.calman_reading_var,
font=("Consolas", 9),
foreground="#1f6fb2",
wraplength=160,
justify=tk.LEFT,
).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):
widget.bind("<Button-1>", lambda _e, pp=p: send_patch(self, pp))
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: --"
)
tk.Label(
left,
textvariable=self.calman_reading_var,
justify=tk.LEFT,
font=("Consolas", 10),
fg="#e5e5e5",
bg="#323232",
width=22,
padx=4,
pady=4,
).pack(fill=tk.X)
xy_fig = Figure(figsize=(2.6, 2.2), dpi=90, facecolor=_DARK_BG)
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=_DARK_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))
style = ttk.Style()
style.configure("Calman.Treeview", rowheight=22, font=("Consolas", 9))
style.configure("Calman.Treeview.Heading", font=("微软雅黑", 9, "bold"))
self.calman_metric_tree.configure(style="Calman.Treeview")
self.calman_data_tree.configure(style="Calman.Treeview")
right.bind("<Configure>", lambda _e: _adaptive_matrix_columns(self))
_refresh_metric_table(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")
# ---------------------------------------------------------------------------
# 发送 / 测量
# ---------------------------------------------------------------------------
def send_patch(self: "PQAutomationApp", pct: int) -> None:
"""点击色块时,发送对应灰阶图案到信号发生器。"""
if not self.signal_service.is_connected:
messagebox.showwarning("提示", "请先连接 UCD323 设备")
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)
def worker():
try:
self.signal_service.send_solid_rgb((rgb_val, rgb_val, rgb_val))
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:
self._dispatch_ui(self.log_gui.log, f"发送失败: {exc}", "error")
self._dispatch_ui(self.calman_status_var.set, f"发送失败: {exc}")
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()
self._dispatch_ui(self.calman_status_var.set, f"采集 {pct}% 中...")
rec = _measure_once(self, pct)
if rec is None:
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
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")
def worker():
seq_t0 = time.perf_counter()
try:
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():
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}%"
)
self._dispatch_ui(_highlight_patch, self, pct)
try:
self.signal_service.send_solid_rgb(
(rgb_val, rgb_val, rgb_val)
)
except Exception as exc:
self._dispatch_ui(
self.log_gui.log, f"发送 {pct}% 失败: {exc}", "error"
)
continue
self.calman_current_level = pct
# 等待稳定,停止事件触发时尽快退出
if self.calman_stop_event.wait(settle):
break
rec = _measure_once(self, pct)
if rec is None:
continue
self.calman_results[pct] = rec
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:
self._dispatch_ui(self.calman_status_var.set, "连续测试完成")
self._dispatch_ui(self.log_gui.log, "CALMAN: 连续测试完成", "success")
return
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:
self.calman_stop_event.set()
self.calman_status_var.set("正在停止...")
else:
self.calman_status_var.set("当前没有运行中的连续测试")
def clear_results(self: "PQAutomationApp") -> None:
"""清空结果表和图表。"""
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 散点。"""
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(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(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(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(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:
ax = self.calman_xy_ax
ax.clear()
_style_axes(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="#ffffff", 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="#000000", 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="#c7c7c7", 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:
"""重绘下方矩阵表。"""
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="#f5f7fa")
self.calman_metric_tree.tag_configure("even", background="#ffffff")
self.calman_data_tree.tag_configure("odd", background="#f5f7fa")
self.calman_data_tree.tag_configure("even", background="#ffffff")
first, last = self.calman_data_tree.yview()
self.calman_table_ysb.set(first, last)
class CalmanPanelMixin:
"""挂载本模块的自由函数到 PQAutomationApp。"""
create_calman_panel = create_calman_panel
toggle_calman_panel = toggle_calman_panel

View File

@@ -1,4 +1,4 @@
"""CCT 参数面板及其处理函数(与主文件重构版本保持同步)。"""
"""CCT 参数面板及其处理函数(与主文件重构版本保持同步)。"""
import time
import traceback
@@ -8,8 +8,14 @@ import ttkbootstrap as ttk
import algorithm.pq_algorithm as pq_algorithm
from typing import TYPE_CHECKING
def create_cct_params_frame(self):
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
def create_cct_params_frame(self: "PQAutomationApp"):
"""创建色度参数设置区域 - 屏模组、SDR、HDR 独立(增加色域参考标准选择 + 单步调试按钮)"""
# ==================== 屏模组色度参数 Frame ====================
@@ -330,7 +336,7 @@ def create_cct_params_frame(self):
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
def _get_cct_var_dict(self, test_type):
def _get_cct_var_dict(self: "PQAutomationApp", test_type):
"""按测试类型返回 CCT 变量映射。"""
if test_type == "sdr_movie":
return {
@@ -354,7 +360,7 @@ def _get_cct_var_dict(self, test_type):
}
def _parse_cct_float(self, var, default):
def _parse_cct_float(self: "PQAutomationApp", var, default):
"""读取并解析 CCT 输入值,失败时回落默认值。"""
try:
value = var.get().strip()
@@ -365,7 +371,7 @@ def _parse_cct_float(self, var, default):
return default
def _save_cct_params_for(self, test_type):
def _save_cct_params_for(self: "PQAutomationApp", test_type):
"""保存指定测试类型的 CCT 参数。"""
try:
default_params = self.config.get_default_cct_params(test_type)
@@ -384,7 +390,7 @@ def _save_cct_params_for(self, test_type):
pass
def _handle_cct_focus_out(self, var, default_value, save_func, label):
def _handle_cct_focus_out(self: "PQAutomationApp", var, default_value, save_func, label):
"""统一处理 CCT 参数失焦校验并保存。"""
try:
value = var.get().strip()
@@ -414,27 +420,27 @@ def _handle_cct_focus_out(self, var, default_value, save_func, label):
self.log_gui.log(f"处理 {label} 参数失败: {str(e)}", level="error")
def on_sdr_cct_param_focus_out(self, var, default_value):
def on_sdr_cct_param_focus_out(self: "PQAutomationApp", var, default_value):
"""SDR 色度参数失去焦点时的处理。"""
_handle_cct_focus_out(self, var, default_value, self.save_sdr_cct_params, "SDR")
def save_sdr_cct_params(self):
def save_sdr_cct_params(self: "PQAutomationApp"):
"""保存 SDR 色度参数。"""
_save_cct_params_for(self, "sdr_movie")
def on_hdr_cct_param_focus_out(self, var, default_value):
def on_hdr_cct_param_focus_out(self: "PQAutomationApp", var, default_value):
"""HDR 色度参数失去焦点时的处理。"""
_handle_cct_focus_out(self, var, default_value, self.save_hdr_cct_params, "HDR")
def save_hdr_cct_params(self):
def save_hdr_cct_params(self: "PQAutomationApp"):
"""保存 HDR 色度参数。"""
_save_cct_params_for(self, "hdr_movie")
def recalculate_cct(self):
def recalculate_cct(self: "PQAutomationApp"):
"""重新计算并绘制色度图"""
try:
# 1. 保存新参数
@@ -496,7 +502,7 @@ def recalculate_cct(self):
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
def recalculate_gamut(self):
def recalculate_gamut(self: "PQAutomationApp"):
"""重新计算并绘制色域图(使用新的参考标准)"""
try:
# 1. 收起配置项
@@ -644,17 +650,17 @@ def recalculate_gamut(self):
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
def on_cct_param_focus_out(self, var, default_value):
def on_cct_param_focus_out(self: "PQAutomationApp", var, default_value):
"""色度参数失去焦点时的处理 - 空值恢复默认"""
_handle_cct_focus_out(self, var, default_value, self.save_cct_params, "屏模组")
def save_cct_params(self):
def save_cct_params(self: "PQAutomationApp"):
"""保存色度参数 - 简化版"""
_save_cct_params_for(self, self.config.current_test_type)
def reload_cct_params(self):
def reload_cct_params(self: "PQAutomationApp"):
"""切换测试类型时重新加载色度参数"""
try:
current_type = self.config.current_test_type
@@ -676,7 +682,7 @@ def reload_cct_params(self):
self.log_gui.log(f"重新加载色度参数失败: {str(e)}", level="error")
def toggle_cct_params_frame(self):
def toggle_cct_params_frame(self: "PQAutomationApp"):
"""根据测试类型和测试项的选中状态显示对应参数框"""
selected_items = self.get_selected_test_items()
current_test_type = self.config.current_test_type
@@ -718,7 +724,7 @@ _GAMUT_REF_CONFIGS = {
}
def _on_gamut_ref_changed(self, test_type, event=None):
def _on_gamut_ref_changed(self: "PQAutomationApp", test_type, event=None):
cfg = _GAMUT_REF_CONFIGS[test_type]
try:
new_ref = getattr(self, cfg["var_attr"]).get()
@@ -732,13 +738,38 @@ def _on_gamut_ref_changed(self, test_type, event=None):
self.log_gui.log(f"保存 {cfg['label']} 色域参考标准失败: {str(e)}", level="error")
def on_screen_gamut_ref_changed(self, event=None):
def on_screen_gamut_ref_changed(self: "PQAutomationApp", event=None):
_on_gamut_ref_changed(self, "screen_module", event)
def on_sdr_gamut_ref_changed(self, event=None):
def on_sdr_gamut_ref_changed(self: "PQAutomationApp", event=None):
_on_gamut_ref_changed(self, "sdr_movie", event)
def on_hdr_gamut_ref_changed(self, event=None):
def on_hdr_gamut_ref_changed(self: "PQAutomationApp", event=None):
_on_gamut_ref_changed(self, "hdr_movie", event)
class CctPanelMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
create_cct_params_frame = create_cct_params_frame
_get_cct_var_dict = _get_cct_var_dict
_parse_cct_float = _parse_cct_float
_save_cct_params_for = _save_cct_params_for
_handle_cct_focus_out = _handle_cct_focus_out
on_sdr_cct_param_focus_out = on_sdr_cct_param_focus_out
save_sdr_cct_params = save_sdr_cct_params
on_hdr_cct_param_focus_out = on_hdr_cct_param_focus_out
save_hdr_cct_params = save_hdr_cct_params
recalculate_cct = recalculate_cct
recalculate_gamut = recalculate_gamut
on_cct_param_focus_out = on_cct_param_focus_out
save_cct_params = save_cct_params
reload_cct_params = reload_cct_params
toggle_cct_params_frame = toggle_cct_params_frame
_on_gamut_ref_changed = _on_gamut_ref_changed
on_screen_gamut_ref_changed = on_screen_gamut_ref_changed
on_sdr_gamut_ref_changed = on_sdr_gamut_ref_changed
on_hdr_gamut_ref_changed = on_hdr_gamut_ref_changed

View File

@@ -1,4 +1,4 @@
"""自定义模板结果面板Step 6 重构)。"""
"""自定义模板结果面板Step 6 重构)。"""
import threading
import time
@@ -11,7 +11,13 @@ import numpy as np
from app.data_range_converter import convert_pattern_params
def create_custom_template_result_panel(self):
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
def create_custom_template_result_panel(self: "PQAutomationApp"):
"""创建客户模板结果显示区域(黑底表格)"""
self.custom_result_frame = ttk.LabelFrame(
self.custom_template_tab_frame, text="客户模板结果显示"
@@ -151,7 +157,7 @@ def create_custom_template_result_panel(self):
table_container.grid_columnconfigure(0, weight=1)
def show_custom_result_context_menu(self, event):
def show_custom_result_context_menu(self: "PQAutomationApp", event):
"""显示客户模板结果右键菜单"""
if not hasattr(self, "custom_result_tree") or not hasattr(
self, "custom_result_menu"
@@ -197,7 +203,7 @@ def show_custom_result_context_menu(self, event):
self.custom_result_menu.grab_release()
def set_custom_result_table_locked(self, locked):
def set_custom_result_table_locked(self: "PQAutomationApp", locked):
"""锁定/解锁客户模板结果表(测试期间禁选择、禁右键)"""
if not hasattr(self, "custom_result_tree"):
return
@@ -208,7 +214,7 @@ def set_custom_result_table_locked(self, locked):
pass
def start_custom_row_single_step(self):
def start_custom_row_single_step(self: "PQAutomationApp"):
"""单步测试当前选中行:发送该行 pattern 并覆盖该行测量结果"""
if not hasattr(self, "custom_result_tree"):
return
@@ -252,7 +258,7 @@ def start_custom_row_single_step(self):
).start()
def _clear_custom_result_row(self, item_id, row_no):
def _clear_custom_result_row(self: "PQAutomationApp", item_id, row_no):
"""单步测试开始前清空指定行的测量数据"""
if not hasattr(self, "custom_result_tree"):
return
@@ -281,7 +287,7 @@ def _clear_custom_result_row(self, item_id, row_no):
self.custom_result_tree.see(item_id)
def _run_custom_row_single_step(self, item_id, row_no):
def _run_custom_row_single_step(self: "PQAutomationApp", item_id, row_no):
"""后台执行客户模板单步测试"""
try:
self._dispatch_ui(self.status_var.set, f"单步测试第 {row_no} 行...")
@@ -352,7 +358,7 @@ def _run_custom_row_single_step(self, item_id, row_no):
self._dispatch_ui(self.status_var.set, "单步测试失败")
def _update_custom_result_row(self, item_id, row_no, result_data):
def _update_custom_result_row(self: "PQAutomationApp", item_id, row_no, result_data):
"""覆盖更新客户模板结果表中指定行"""
def fmt(value, digits=4):
@@ -394,7 +400,7 @@ def _update_custom_result_row(self, item_id, row_no, result_data):
self.custom_result_tree.item(item_id, values=new_values)
def copy_custom_result_table(self):
def copy_custom_result_table(self: "PQAutomationApp"):
"""复制客户模板结果表格到剪贴板(不含标题行/No./Pattern"""
if not hasattr(self, "custom_result_tree"):
return
@@ -434,7 +440,7 @@ def copy_custom_result_table(self):
if hasattr(self, "log_gui"):
self.log_gui.log(f"已复制客户模板表格数据({len(items)} 行)", level="success")
def clear_custom_template_results(self):
def clear_custom_template_results(self: "PQAutomationApp"):
"""清空客户模板结果表格"""
if not hasattr(self, "custom_result_tree"):
return
@@ -442,7 +448,7 @@ def clear_custom_template_results(self):
self.custom_result_tree.delete(item)
def auto_expand_custom_result_view(self):
def auto_expand_custom_result_view(self: "PQAutomationApp"):
"""当客户模板表格有数据时,自动扩展窗口以尽量完整显示所有列"""
if not hasattr(self, "custom_result_tree"):
return
@@ -480,7 +486,7 @@ def auto_expand_custom_result_view(self):
self.log_gui.log(f"自动扩展客户模板窗口失败: {str(e)}", level="error")
def append_custom_template_result(self, row_no, result_data):
def append_custom_template_result(self: "PQAutomationApp", row_no, result_data):
"""追加一条客户模板结果到表格"""
def fmt(value, digits=4):
@@ -523,7 +529,7 @@ def append_custom_template_result(self, row_no, result_data):
self.auto_expand_custom_result_view()
def start_custom_template_test(self):
def start_custom_template_test(self: "PQAutomationApp"):
"""开始客户模板测试SDR"""
if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
@@ -571,7 +577,7 @@ def start_custom_template_test(self):
self.test_thread.daemon = True
self.test_thread.start()
def update_custom_button_visibility(self):
def update_custom_button_visibility(self: "PQAutomationApp"):
"""只在 SDR 测试时显示客户模版按钮"""
if not hasattr(self, "custom_btn") or not hasattr(self, "test_type_var"):
return
@@ -625,7 +631,7 @@ def update_custom_button_visibility(self):
# self.log_gui.log("已填充 147 行客户模板测试数据", level="success")
def export_custom_template_excel(self):
def export_custom_template_excel(self: "PQAutomationApp"):
"""将客户模板结果表导出为 Excel 文件14 列完整数据)"""
if not hasattr(self, "custom_result_tree"):
return
@@ -773,7 +779,7 @@ def export_custom_template_excel(self):
messagebox.showerror("错误", f"导出失败:{str(e)}")
def export_custom_template_charts(self):
def export_custom_template_charts(self: "PQAutomationApp"):
"""生成客户模板图表xy 色度散点图 + Lv 亮度曲线图,保存为 PNG"""
if not hasattr(self, "custom_result_tree"):
return
@@ -910,3 +916,24 @@ def export_custom_template_charts(self):
if hasattr(self, "log_gui"):
self.log_gui.log(f"生成图表失败: {str(e)}", level="error")
messagebox.showerror("错误", f"生成图表失败:{str(e)}")
class CustomTemplatePanelMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
create_custom_template_result_panel = create_custom_template_result_panel
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
_clear_custom_result_row = _clear_custom_result_row
_run_custom_row_single_step = _run_custom_row_single_step
_update_custom_result_row = _update_custom_result_row
copy_custom_result_table = copy_custom_result_table
clear_custom_template_results = clear_custom_template_results
auto_expand_custom_result_view = auto_expand_custom_result_view
append_custom_template_result = append_custom_template_result
start_custom_template_test = start_custom_template_test
update_custom_button_visibility = update_custom_button_visibility
export_custom_template_excel = export_custom_template_excel
export_custom_template_charts = export_custom_template_charts

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
"""主布局面板创建函数Step 6 重构)。"""
"""主布局面板创建函数Step 6 重构)。"""
import re
import tkinter as tk
@@ -8,7 +8,13 @@ from drivers.UCD323_Enum import UCDEnum
from app.views.collapsing_frame import CollapsingFrame
from app.resources import load_icon
def create_floating_config_panel(self):
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
def create_floating_config_panel(self: "PQAutomationApp"):
"""创建右上角悬浮配置框"""
cf = CollapsingFrame(self.control_frame_top)
cf.pack(fill="both")
@@ -53,7 +59,7 @@ def create_floating_config_panel(self):
self.config_panel_frame.btn.configure(image="closed")
def create_test_items_content(self):
def create_test_items_content(self: "PQAutomationApp"):
"""创建测试项目选项卡内容"""
# 创建测试项目字典,用于管理不同测试类型的选项
self.test_items = {
@@ -96,7 +102,7 @@ def create_test_items_content(self):
self.create_cct_params_frame()
def create_signal_format_content(self):
def create_signal_format_content(self: "PQAutomationApp"):
"""创建信号格式选项卡内容"""
self.signal_tabs = ttk.Notebook(self.signal_format_frame)
self.signal_tabs.pack(fill=tk.BOTH, expand=True)
@@ -314,7 +320,7 @@ def create_signal_format_content(self):
self.signal_tabs.tab(2, state="disabled") # 禁用 HDR
def create_connection_content(self):
def create_connection_content(self: "PQAutomationApp"):
"""创建设备连接区域"""
# 创建设备连接区域的主框架
com_frame = ttk.Frame(self.connection_frame)
@@ -424,7 +430,7 @@ def create_connection_content(self):
ca_channel_combo.bind("<<ComboboxSelected>>", self.update_config)
def create_test_type_frame(self):
def create_test_type_frame(self: "PQAutomationApp"):
"""创建测试类型选择区域(侧边栏形式)"""
# 设置测试类型变量
self.test_type_var = tk.StringVar(value="screen_module")
@@ -503,6 +509,26 @@ def create_test_type_frame(self):
)
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)
# 测试版水印标签(版本 x.x.0.0 时显示)
from app_version import is_beta_version, APP_VERSION
if is_beta_version():
@@ -528,9 +554,13 @@ def create_test_type_frame(self):
self.panels["single_step"]["button"] = self.single_step_btn
if "pantone_baseline" in self.panels:
self.panels["pantone_baseline"]["button"] = self.pantone_baseline_btn
if "gamma_pattern" in self.panels:
self.panels["gamma_pattern"]["button"] = self.gamma_pattern_btn
if "calman" in self.panels:
self.panels["calman"]["button"] = self.calman_btn
def update_config_info_display(self):
def update_config_info_display(self: "PQAutomationApp"):
"""更新配置信息显示"""
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
current_config = self.config.get_current_config()
@@ -547,7 +577,7 @@ def update_config_info_display(self):
self.update_sidebar_selection()
def create_operation_frame(self):
def create_operation_frame(self: "PQAutomationApp"):
"""创建操作按钮区域"""
operation_frame = ttk.Frame(self.control_frame_top)
operation_frame.pack(fill=tk.X, padx=5, pady=10)
@@ -594,7 +624,7 @@ def create_operation_frame(self):
self.update_custom_button_visibility()
def on_screen_module_timing_changed(self, event=None):
def on_screen_module_timing_changed(self: "PQAutomationApp", event=None):
"""屏模组信号格式改变时的回调"""
try:
selected_timing = self.screen_module_timing_var.get()
@@ -632,7 +662,7 @@ def on_screen_module_timing_changed(self, event=None):
self.log_gui.log(f"屏模组信号格式更改失败: {str(e)}", level="error")
def on_sdr_timing_changed(self, event=None):
def on_sdr_timing_changed(self: "PQAutomationApp", event=None):
"""SDR测试分辨率改变时的回调"""
try:
selected_timing = self.sdr_timing_var.get()
@@ -650,7 +680,7 @@ def on_sdr_timing_changed(self, event=None):
self.log_gui.log(f"SDR测试分辨率更改失败: {str(e)}", level="error")
def on_sdr_output_format_changed(self, event=None):
def on_sdr_output_format_changed(self: "PQAutomationApp", event=None):
"""SDR 色彩格式改变时的回调"""
try:
fmt = self.sdr_output_format_var.get()
@@ -674,7 +704,7 @@ def on_sdr_output_format_changed(self, event=None):
self.log_gui.log(f"SDR色彩格式更改失败: {str(e)}", level="error")
def on_hdr_output_format_changed(self, event=None):
def on_hdr_output_format_changed(self: "PQAutomationApp", event=None):
"""HDR 色彩格式改变时的回调"""
try:
fmt = self.hdr_output_format_var.get()
@@ -700,7 +730,7 @@ def on_hdr_output_format_changed(self, event=None):
self.log_gui.log(f"HDR色彩格式更改失败: {str(e)}", level="error")
def update_test_items(self):
def update_test_items(self: "PQAutomationApp"):
"""根据当前测试类型更新测试项目复选框"""
# 先隐藏所有测试项目框架
for config in self.test_items.values():
@@ -747,7 +777,7 @@ def update_test_items(self):
self.toggle_cct_params_frame()
def on_test_type_change(self):
def on_test_type_change(self: "PQAutomationApp"):
"""根据测试类型更新内容区域"""
# 更新配置信息显示
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
@@ -756,3 +786,22 @@ def on_test_type_change(self):
# SDR 选中时显示客户模版按钮
self.update_custom_button_visibility()
class MainLayoutMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
create_floating_config_panel = create_floating_config_panel
create_test_items_content = create_test_items_content
create_signal_format_content = create_signal_format_content
create_connection_content = create_connection_content
create_test_type_frame = create_test_type_frame
update_config_info_display = update_config_info_display
create_operation_frame = create_operation_frame
on_screen_module_timing_changed = on_screen_module_timing_changed
on_sdr_timing_changed = on_sdr_timing_changed
on_sdr_output_format_changed = on_sdr_output_format_changed
on_hdr_output_format_changed = on_hdr_output_format_changed
update_test_items = update_test_items
on_test_type_change = on_test_type_change

View File

@@ -11,11 +11,17 @@ from tkinter import filedialog, messagebox
import ttkbootstrap as ttk
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
_TEMPLATE_FILE = "pantone_2670_colors.xlsx"
def create_pantone_baseline_panel(self):
def create_pantone_baseline_panel(self: "PQAutomationApp"):
"""创建 Pantone 认证摸底测试面板。"""
frame = ttk.Frame(self.content_frame)
self.pantone_baseline_frame = frame
@@ -149,12 +155,12 @@ def create_pantone_baseline_panel(self):
_set_button_states(self)
def toggle_pantone_baseline_panel(self):
def toggle_pantone_baseline_panel(self: "PQAutomationApp"):
"""切换 Pantone 认证摸底测试面板。"""
self.show_panel("pantone_baseline")
def _get_settings_dir(self):
def _get_settings_dir(self: "PQAutomationApp"):
"""返回 settings 绝对目录,避免依赖当前工作目录。"""
if getattr(self, "config_file", None):
return os.path.dirname(self.config_file)
@@ -168,7 +174,7 @@ def _get_settings_dir(self):
return os.path.join(base_dir, "settings")
def _load_patterns(self):
def _load_patterns(self: "PQAutomationApp"):
path = os.path.join(_get_settings_dir(self), _TEMPLATE_FILE)
if not os.path.isfile(path):
raise FileNotFoundError(f"未找到模板文件: {path}")
@@ -201,7 +207,7 @@ def _load_patterns(self):
return patterns
def _start_pantone_baseline(self):
def _start_pantone_baseline(self: "PQAutomationApp"):
if self._pantone_running:
messagebox.showinfo("提示", "Pantone 任务正在执行")
return
@@ -247,7 +253,7 @@ def _start_pantone_baseline(self):
_launch_worker(self, start_index=0, settle=settle)
def _resume_pantone_baseline(self):
def _resume_pantone_baseline(self: "PQAutomationApp"):
if self._pantone_running:
messagebox.showinfo("提示", "Pantone 任务正在执行")
return
@@ -291,7 +297,7 @@ def _resume_pantone_baseline(self):
_launch_worker(self, start_index=self._pantone_next_index, settle=settle)
def _launch_worker(self, start_index, settle):
def _launch_worker(self: "PQAutomationApp", start_index, settle):
total = self._pantone_target_count or len(self.pantone_patterns)
def worker():
@@ -401,7 +407,7 @@ def _launch_worker(self, start_index, settle):
threading.Thread(target=worker, daemon=True).start()
def _append_result_row(self, record, total):
def _append_result_row(self: "PQAutomationApp", record, total):
self.pantone_tree.insert(
"",
tk.END,
@@ -423,7 +429,7 @@ def _append_result_row(self, record, total):
self.pantone_tree.see(children[-1])
def _pause_pantone_baseline(self):
def _pause_pantone_baseline(self: "PQAutomationApp"):
if not self._pantone_running:
messagebox.showinfo("提示", "当前没有运行中的任务")
return
@@ -433,7 +439,7 @@ def _pause_pantone_baseline(self):
self._pantone_control_event.set()
def _end_pantone_baseline(self):
def _end_pantone_baseline(self: "PQAutomationApp"):
if self._pantone_running:
self._pantone_stop_requested = True
self.pantone_status_var.set("结束中...")
@@ -448,7 +454,7 @@ def _end_pantone_baseline(self):
_set_button_states(self)
def _clear_results(self):
def _clear_results(self: "PQAutomationApp"):
if self._pantone_running:
messagebox.showinfo("提示", "任务执行中,无法清空")
return
@@ -463,7 +469,7 @@ def _clear_results(self):
_set_button_states(self)
def _set_button_states(self):
def _set_button_states(self: "PQAutomationApp"):
if self._pantone_running:
self.pantone_start_btn.configure(state=tk.DISABLED)
self.pantone_pause_btn.configure(state=tk.NORMAL)
@@ -479,7 +485,7 @@ def _set_button_states(self):
self.pantone_resume_btn.configure(state=tk.NORMAL if can_resume else tk.DISABLED)
def _save_as_template(self):
def _save_as_template(self: "PQAutomationApp"):
if not self.pantone_results:
messagebox.showinfo("提示", "暂无可导出的结果")
return
@@ -502,7 +508,7 @@ def _save_as_template(self):
messagebox.showerror("保存失败", f"写入 xlsx 失败: {exc}")
def _resolve_results_dir(self):
def _resolve_results_dir(self: "PQAutomationApp"):
if getattr(self, "config_file", None):
root_dir = os.path.dirname(os.path.dirname(self.config_file))
else:
@@ -514,7 +520,7 @@ def _resolve_results_dir(self):
return results_dir
def _auto_save_template(self):
def _auto_save_template(self: "PQAutomationApp"):
results_dir = _resolve_results_dir(self)
target_count = len(self.pantone_results)
filename = (
@@ -526,7 +532,7 @@ def _auto_save_template(self):
return path
def _write_template_xlsx(self, path):
def _write_template_xlsx(self: "PQAutomationApp", path):
# 优先复制 settings 模板,再覆盖数据区;没有模板时自动创建同结构表。
template_path = os.path.join(_get_settings_dir(self), _TEMPLATE_FILE)
from openpyxl import load_workbook, Workbook
@@ -560,3 +566,25 @@ def _write_template_xlsx(self, path):
ws.cell(row=idx, column=6, value=float(item["y"]))
wb.save(path)
class PantoneBaselinePanelMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
create_pantone_baseline_panel = create_pantone_baseline_panel
toggle_pantone_baseline_panel = toggle_pantone_baseline_panel
_get_settings_dir = _get_settings_dir
_load_patterns = _load_patterns
_start_pantone_baseline = _start_pantone_baseline
_resume_pantone_baseline = _resume_pantone_baseline
_launch_worker = _launch_worker
_append_result_row = _append_result_row
_pause_pantone_baseline = _pause_pantone_baseline
_end_pantone_baseline = _end_pantone_baseline
_clear_results = _clear_results
_set_button_states = _set_button_states
_save_as_template = _save_as_template
_resolve_results_dir = _resolve_results_dir
_auto_save_template = _auto_save_template
_write_template_xlsx = _write_template_xlsx

View File

@@ -1,4 +1,4 @@
"""侧边面板(日志 / Local Dimming / 调试)"""
"""侧边面板(日志 / Local Dimming / 调试)"""
import traceback
import tkinter as tk
@@ -7,7 +7,13 @@ import ttkbootstrap as ttk
from app.views.pq_log_gui import PQLogGUI
from app.views.pq_debug_panel import PQDebugPanel
def create_log_panel(self):
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
def create_log_panel(self: "PQAutomationApp"):
"""创建日志面板"""
self.log_frame = ttk.Frame(self.content_frame)
self.log_gui = PQLogGUI(self.log_frame)
@@ -22,7 +28,7 @@ def create_log_panel(self):
) # button会在后面设置
def create_local_dimming_panel(self):
def create_local_dimming_panel(self: "PQAutomationApp"):
"""创建 Local Dimming 测试面板 - 手动控制版"""
self.local_dimming_frame = ttk.Frame(self.content_frame)
@@ -172,12 +178,12 @@ def create_local_dimming_panel(self):
self.current_ld_percentage = None
def toggle_local_dimming_panel(self):
def toggle_local_dimming_panel(self: "PQAutomationApp"):
"""切换 Local Dimming 面板显示"""
self.show_panel("local_dimming")
def toggle_log_panel(self):
def toggle_log_panel(self: "PQAutomationApp"):
"""切换日志面板的显示状态"""
self.show_panel("log")
@@ -226,7 +232,7 @@ DEBUG_PANEL_CONFIGS = {
}
def _toggle_debug_panel(self, test_type):
def _toggle_debug_panel(self: "PQAutomationApp", test_type):
"""打开/关闭对应测试类型的单步调试面板(独立窗口)。"""
cfg = DEBUG_PANEL_CONFIGS[test_type]
win_attr = cfg["window_attr"]
@@ -288,20 +294,20 @@ def _toggle_debug_panel(self, test_type):
win.update_idletasks()
def toggle_screen_debug_panel(self):
def toggle_screen_debug_panel(self: "PQAutomationApp"):
_toggle_debug_panel(self, "screen_module")
def toggle_sdr_debug_panel(self):
def toggle_sdr_debug_panel(self: "PQAutomationApp"):
_toggle_debug_panel(self, "sdr_movie")
def toggle_hdr_debug_panel(self):
def toggle_hdr_debug_panel(self: "PQAutomationApp"):
_toggle_debug_panel(self, "hdr_movie")
def update_sidebar_selection(self):
def update_sidebar_selection(self: "PQAutomationApp"):
"""更新侧边栏按钮的选中状态"""
# 重置所有按钮样式为默认
self.screen_module_btn.configure(style="Sidebar.TButton")
@@ -316,3 +322,18 @@ def update_sidebar_selection(self):
self.sdr_movie_btn.configure(style="SidebarSelected.TButton")
elif current_type == "hdr_movie":
self.hdr_movie_btn.configure(style="SidebarSelected.TButton")
class SidePanelsMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
create_log_panel = create_log_panel
create_local_dimming_panel = create_local_dimming_panel
toggle_local_dimming_panel = toggle_local_dimming_panel
toggle_log_panel = toggle_log_panel
_toggle_debug_panel = _toggle_debug_panel
toggle_screen_debug_panel = toggle_screen_debug_panel
toggle_sdr_debug_panel = toggle_sdr_debug_panel
toggle_hdr_debug_panel = toggle_hdr_debug_panel
update_sidebar_selection = update_sidebar_selection

View File

@@ -13,6 +13,12 @@ from tkinter import filedialog, messagebox
import ttkbootstrap as ttk
from PIL import Image
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
@@ -27,7 +33,7 @@ _DEFAULT_SAMPLES = [
]
def create_single_step_panel(self):
def create_single_step_panel(self: "PQAutomationApp"):
"""创建单步调试面板。"""
frame = ttk.Frame(self.content_frame)
self.single_step_frame = frame
@@ -246,12 +252,12 @@ def create_single_step_panel(self):
_load_default_samples(self)
def toggle_single_step_panel(self):
def toggle_single_step_panel(self: "PQAutomationApp"):
"""切换单步调试面板。"""
self.show_panel("single_step")
def _load_default_samples(self):
def _load_default_samples(self: "PQAutomationApp"):
self.single_step_samples = [dict(item) for item in _DEFAULT_SAMPLES]
_refresh_sample_list(self, select_index=0 if self.single_step_samples else None)
self.single_step_status_var.set(
@@ -259,7 +265,7 @@ def _load_default_samples(self):
)
def _refresh_sample_list(self, select_index=None):
def _refresh_sample_list(self: "PQAutomationApp", select_index=None):
self.single_step_listbox.delete(0, tk.END)
for sample in self.single_step_samples:
self.single_step_listbox.insert(
@@ -280,14 +286,14 @@ def _refresh_sample_list(self, select_index=None):
self.single_step_status_var.set("样本列表为空")
def _on_sample_select(self):
def _on_sample_select(self: "PQAutomationApp"):
selection = self.single_step_listbox.curselection()
if not selection:
return
_select_sample(self, selection[0])
def _select_sample(self, index):
def _select_sample(self: "PQAutomationApp", index):
sample = self.single_step_samples[index]
self.single_step_current_index = index
self.single_step_name_var.set(sample["name"])
@@ -297,7 +303,7 @@ def _select_sample(self, index):
self.single_step_status_var.set(f"当前样本: {sample['name']}")
def _import_samples_csv(self):
def _import_samples_csv(self: "PQAutomationApp"):
path = filedialog.askopenfilename(
title="选择单步调试样本 CSV",
filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")],
@@ -334,7 +340,7 @@ def _import_samples_csv(self):
self.log_gui.log(f"单步调试样本已导入: {len(samples)}", level="success")
def _delete_current_sample(self):
def _delete_current_sample(self: "PQAutomationApp"):
if self.single_step_current_index is None:
return
removed = self.single_step_samples.pop(self.single_step_current_index)
@@ -343,7 +349,7 @@ def _delete_current_sample(self):
self.single_step_status_var.set(f"已删除样本: {removed['name']}")
def _upsert_sample(self):
def _upsert_sample(self: "PQAutomationApp"):
try:
sample = {
"name": self.single_step_name_var.get().strip(),
@@ -387,7 +393,7 @@ def _format_float(value):
return f"{number:.4f}"
def _build_color_patch(self, hex_value):
def _build_color_patch(self: "PQAutomationApp", hex_value):
if not self.signal_service.is_connected:
raise RuntimeError("请先连接 UCD323 设备")
width, height = self.signal_service.current_resolution()
@@ -401,7 +407,7 @@ def _build_color_patch(self, hex_value):
return file_path
def _send_current_patch(self):
def _send_current_patch(self: "PQAutomationApp"):
if self.single_step_current_index is None:
messagebox.showinfo("提示", "请先选择一个样本")
return
@@ -428,7 +434,7 @@ def _send_current_patch(self):
threading.Thread(target=worker, daemon=True).start()
def _measure_current_sample(self):
def _measure_current_sample(self: "PQAutomationApp"):
if self.single_step_current_index is None:
messagebox.showinfo("提示", "请先选择一个样本")
return
@@ -457,7 +463,7 @@ def _measure_current_sample(self):
threading.Thread(target=worker, daemon=True).start()
def _commit_result(self):
def _commit_result(self: "PQAutomationApp"):
if self.single_step_current_index is None:
messagebox.showinfo("提示", "请先选择一个样本")
return
@@ -509,14 +515,14 @@ def _commit_result(self):
self.single_step_status_var.set(f"已记录结果ΔE2000={record['delta_e']}")
def _clear_results(self):
def _clear_results(self: "PQAutomationApp"):
self.single_step_results = []
for item in self.single_step_result_tree.get_children():
self.single_step_result_tree.delete(item)
self.single_step_status_var.set("结果已清空")
def _export_results_csv(self):
def _export_results_csv(self: "PQAutomationApp"):
if not self.single_step_results:
messagebox.showinfo("提示", "暂无可导出的调试结果")
return
@@ -547,4 +553,25 @@ def _export_results_csv(self):
self.log_gui.log(f"单步调试结果已导出: {path}", level="success")
self.single_step_status_var.set(f"结果已导出: {os.path.basename(path)}")
except Exception as exc:
messagebox.showerror("导出失败", f"写入 CSV 失败: {exc}")
messagebox.showerror("导出失败", f"写入 CSV 失败: {exc}")
class SingleStepPanelMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
create_single_step_panel = create_single_step_panel
toggle_single_step_panel = toggle_single_step_panel
_load_default_samples = _load_default_samples
_refresh_sample_list = _refresh_sample_list
_on_sample_select = _on_sample_select
_select_sample = _select_sample
_import_samples_csv = _import_samples_csv
_delete_current_sample = _delete_current_sample
_upsert_sample = _upsert_sample
_build_color_patch = _build_color_patch
_send_current_patch = _send_current_patch
_measure_current_sample = _measure_current_sample
_commit_result = _commit_result
_clear_results = _clear_results
_export_results_csv = _export_results_csv