修改Calman灰阶中结果图显示、修改UI主题样式应用

This commit is contained in:
xinzhu.yin
2026-06-04 10:36:15 +08:00
parent 3aa975c4d3
commit 49d82da8b9
16 changed files with 597 additions and 210 deletions

View File

@@ -272,6 +272,49 @@ def disconnect_com_connections(self: "PQAutomationApp"):
self.connection.disconnect_all()
def _get_ca_measure_lock(self: "PQAutomationApp"):
lock = getattr(self, "_ca_measure_lock", None)
if lock is None:
lock = threading.RLock()
self._ca_measure_lock = lock
return lock
def _read_ca_display(self: "PQAutomationApp", mode: int):
"""在锁内切换 CA410 Display 模式并立即读取,避免模式串扰。"""
if getattr(self, "ca", None) is None:
raise RuntimeError("请先连接 CA410 色度计")
with _get_ca_measure_lock(self):
self.ca.set_Display(mode)
return self.ca.readAllDisplay()
def read_ca_xyLv(self: "PQAutomationApp"):
"""读取 xy/Lv/XYZDisplay 0"""
return _read_ca_display(self, 0)
def read_ca_tcp_duv(self: "PQAutomationApp"):
"""读取 Tcp/duv/Lv/XYZDisplay 1"""
return _read_ca_display(self, 1)
def read_ca_uvLv(self: "PQAutomationApp"):
"""读取 u'/v'/Lv/XYZDisplay 5"""
return _read_ca_display(self, 5)
def read_ca_xyz(self: "PQAutomationApp"):
"""读取 XYZDisplay 7"""
return _read_ca_display(self, 7)
def read_ca_lambda_pe(self: "PQAutomationApp"):
"""读取 λd/Pe/Lv/XYZDisplay 8"""
return _read_ca_display(self, 8)
__all__ = [
"ConnectionController",
# 兼容层
@@ -298,3 +341,10 @@ class DeviceConnectionMixin:
check_port_connection = check_port_connection
enable_com_widgets = enable_com_widgets
disconnect_com_connections = disconnect_com_connections
_get_ca_measure_lock = _get_ca_measure_lock
_read_ca_display = _read_ca_display
read_ca_xyLv = read_ca_xyLv
read_ca_tcp_duv = read_ca_tcp_duv
read_ca_uvLv = read_ca_uvLv
read_ca_xyz = read_ca_xyz
read_ca_lambda_pe = read_ca_lambda_pe

View File

@@ -394,8 +394,7 @@ def send_fix_pattern(self: "PQAutomationApp", mode):
# 测量数据
if mode == "custom":
result = []
self.ca.set_Display(1)
tcp, duv, lv, X, Y, Z = self.ca.readAllDisplay()
tcp, duv, lv, X, Y, Z = self.read_ca_tcp_duv()
if should_log_detail:
self.log_gui.log(
@@ -403,8 +402,7 @@ def send_fix_pattern(self: "PQAutomationApp", mode):
f"X={X:.4f}, Y={Y:.4f}, Z={Z:.4f}"
, level="success")
self.ca.set_Display(8)
lambda_, Pe, lv, X, Y, Z = self.ca.readAllDisplay()
lambda_, Pe, lv, X, Y, Z = self.read_ca_lambda_pe()
if should_log_detail:
self.log_gui.log(
@@ -449,9 +447,7 @@ def send_fix_pattern(self: "PQAutomationApp", mode):
self.log_gui.log(f"{i+1} 行实时结果写入失败: {str(e)}", level="error")
else:
self.ca.set_xyLv_Display()
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
x, y, lv, X, Y, Z = self.read_ca_xyLv()
results.append([x, y, lv, X, Y, Z])
if should_log_detail:

View File

@@ -171,7 +171,7 @@ def _build_ld_result_row(test_item, pattern_label, value, x="--", y="--"):
def _measure_ld_row(self: "PQAutomationApp", test_item, pattern_label):
"""读取一次 CA410 数据并包装为表格行。"""
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
if lv is None:
raise RuntimeError(f"{pattern_label} 采集失败")
return _build_ld_result_row(test_item, pattern_label, lv, x, y), lv
@@ -549,7 +549,7 @@ def measure_ld_luminance(self: "PQAutomationApp"):
def measure():
try:
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
except Exception as e:
self._dispatch_ui(self.log_gui.log, f"采集异常: {str(e)}")
return

View File

@@ -36,37 +36,188 @@ def _is_dark(color: str) -> bool:
return (r * 299 + g * 587 + b * 114) / 1000 < 128
def apply_modern_styles() -> None:
"""注册或刷新现代化样式集。可在主题切换后再次调用。"""
style = ttk.Style()
theme = style.colors # ttkbootstrap.style.Colors
def _contrast_text(color: str, *, dark_text: str, light_text: str) -> str:
return dark_text if _is_dark(color) else light_text
bg = theme.bg # 主背景
fg = theme.fg # 主前景
def get_theme_palette() -> dict[str, str]:
"""返回当前主题的语义色板,供 ttk / tk 自定义控件共用。"""
style = ttk.Style()
theme = style.colors
bg = theme.bg
fg = theme.fg
primary = theme.primary
secondary = theme.secondary
success = theme.success
info = theme.info
warning = theme.warning
danger = theme.danger
dark = theme.dark
border = theme.border
inputbg = theme.inputbg
inputfg = getattr(theme, "inputfg", fg)
dark_theme = _is_dark(bg)
select_bg = getattr(theme, "selectbg", _mix(primary, bg, 0.30 if dark_theme else 0.12))
select_fg = getattr(theme, "selectfg", "#ffffff" if _is_dark(select_bg) else fg)
# 卡片背景:在主背景上轻微偏移,营造层级感
card_bg = _mix(bg, "#ffffff", 0.04) if dark_theme else _mix(bg, "#000000", 0.025)
card_border = _mix(bg, fg, 0.18) if dark_theme else _mix(bg, "#000000", 0.10)
# 配置项 header 用 secondary 主题色
header_bg = secondary
header_fg = "#ffffff" if _is_dark(secondary) else "#1a1a1a"
if dark_theme:
card_bg = _mix(bg, "#ffffff", 0.04)
card_border = _mix(bg, fg, 0.18)
header_fg = _contrast_text(
"#444A51",
dark_text="#ffffff",
light_text="#1a1a1a",
)
sidebar_bg = _mix(dark, bg, 0.18)
sidebar_hover = _mix(sidebar_bg, "#ffffff", 0.07)
sidebar_selected = _mix(sidebar_bg, "#ffffff", 0.14)
sidebar_fg = _mix(fg, "#ffffff", 0.04)
sidebar_muted = _mix(sidebar_fg, sidebar_bg, 0.45)
muted_fg = _mix(fg, bg, 0.32)
disabled_fg = _mix(fg, bg, 0.42)
disabled_bg = _mix(inputbg, bg, 0.18)
disabled_border = _mix(border, fg, 0.22)
readonly_bg = _mix(inputbg, "#ffffff", 0.06)
success_fg = _mix(success, "#ffffff", 0.08)
warning_fg = _mix(warning, "#ffffff", 0.06)
info_fg = _mix(info, "#ffffff", 0.06)
statusbar_bg = _mix(bg, "#ffffff", 0.06)
tooltip_bg = _mix(inputbg, bg, 0.08)
tooltip_fg = inputfg
tooltip_border = _mix(border, fg, 0.20)
surface_alt_bg = _mix(card_bg, "#ffffff", 0.05)
surface_hover_bg = _mix(card_bg, "#ffffff", 0.09)
badge_bg = _mix(danger, bg, 0.12)
badge_fg = "#ffffff"
focus = _mix(primary, "#ffffff", 0.18)
config_bg = _mix("#444A51", bg, 0.30)
else:
card_bg = inputbg
card_border = border
header_fg = bg
config_bg = _mix(primary, bg, 0.25)
sidebar_bg = _mix(primary, bg, 0.82)
sidebar_hover = _mix(primary, bg, 0.72)
sidebar_selected = primary
sidebar_fg = fg
sidebar_muted = _mix(fg, sidebar_bg, 0.35)
muted_fg = _mix(fg, bg, 0.38)
disabled_fg = _mix(fg, bg, 0.55)
disabled_bg = _mix(bg, border, 0.18)
disabled_border = _mix(border, bg, 0.18)
readonly_bg = _mix(inputbg, primary, 0.04)
success_fg = success
warning_fg = _mix(warning, fg, 0.18)
info_fg = info
statusbar_bg = _mix(bg, dark, 0.04)
tooltip_bg = inputbg
tooltip_fg = inputfg
tooltip_border = border
surface_alt_bg = _mix(bg, dark, 0.03)
surface_hover_bg = _mix(bg, dark, 0.05)
badge_bg = danger
badge_fg = "#ffffff"
focus = _mix(primary, bg, 0.20)
return {
"bg": bg,
"fg": fg,
"primary": primary,
"secondary": secondary,
"success": success,
"info": info,
"warning": warning,
"danger": danger,
"border": border,
"input_bg": inputbg,
"input_fg": inputfg,
"select_bg": select_bg,
"select_fg": select_fg,
"card_bg": card_bg,
"card_border": card_border,
"header_fg": header_fg,
"sidebar_bg": sidebar_bg,
"sidebar_hover": sidebar_hover,
"sidebar_selected": sidebar_selected,
"sidebar_fg": sidebar_fg,
"sidebar_muted": sidebar_muted,
"muted_fg": muted_fg,
"disabled_fg": disabled_fg,
"disabled_bg": disabled_bg,
"disabled_border": disabled_border,
"readonly_bg": readonly_bg,
"success_fg": success_fg,
"warning_fg": warning_fg,
"info_fg": info_fg,
"statusbar_bg": statusbar_bg,
"tooltip_bg": tooltip_bg,
"tooltip_fg": tooltip_fg,
"tooltip_border": tooltip_border,
"surface_alt_bg": surface_alt_bg,
"surface_hover_bg": surface_hover_bg,
"badge_bg": badge_bg,
"badge_fg": badge_fg,
"focus": focus,
"config_bg": config_bg,
}
def apply_listbox_theme(widget) -> None:
"""将 tk.Listbox 颜色同步到当前主题。"""
palette = get_theme_palette()
widget.configure(
background=palette["input_bg"],
foreground=palette["input_fg"],
highlightbackground=palette["border"],
highlightcolor=palette["focus"],
selectbackground=palette["select_bg"],
selectforeground=palette["select_fg"],
disabledforeground=palette["disabled_fg"],
)
def apply_tooltip_theme(toplevel, label) -> None:
"""将 tooltip 的 tk.Toplevel / Label 同步到当前主题。"""
palette = get_theme_palette()
toplevel.configure(background=palette["tooltip_border"])
label.configure(
bg=palette["tooltip_bg"],
fg=palette["tooltip_fg"],
highlightbackground=palette["tooltip_border"],
)
def apply_modern_styles() -> None:
"""注册或刷新现代化样式集。可在主题切换后再次调用。"""
style = ttk.Style()
palette = get_theme_palette()
bg = palette["bg"]
fg = palette["fg"]
primary = palette["primary"]
secondary = palette["secondary"]
info = palette["info"]
card_bg = palette["card_bg"]
card_border = palette["card_border"]
header_bg = palette["config_bg"]
header_fg = palette["header_fg"]
dark_theme = _is_dark(bg)
header_hover_bg = _mix(secondary, "#ffffff", 0.08) if _is_dark(secondary) else _mix(secondary, "#000000", 0.08)
preview_fg = _mix(header_fg, header_bg, 0.35)
sidebar_bg = _mix(dark, bg, 0.18) if dark_theme else _mix(primary, "#000000", 0.10)
sidebar_hover = _mix(sidebar_bg, "#ffffff", 0.07) if dark_theme else _mix(sidebar_bg, "#000000", 0.06)
sidebar_selected = _mix(sidebar_bg, "#ffffff", 0.14) if dark_theme else _mix(sidebar_bg, "#000000", 0.10)
# 侧栏背景在浅色主题下也偏深,文字颜色需按侧栏亮度自适应,避免“黑字不明显”。
sidebar_fg = "#F4F8FD" if _is_dark(sidebar_bg) else _mix(fg, bg, 0.05)
sidebar_muted = _mix(sidebar_fg, sidebar_bg, 0.45)
sidebar_bg = palette["sidebar_bg"]
sidebar_hover = palette["sidebar_hover"]
sidebar_selected = palette["sidebar_selected"]
sidebar_fg = palette["sidebar_fg"]
sidebar_muted = palette["sidebar_muted"]
muted_fg = palette["muted_fg"]
disabled_fg = palette["disabled_fg"]
disabled_bg = palette["disabled_bg"]
disabled_border = palette["disabled_border"]
readonly_bg = palette["readonly_bg"]
success_fg = palette["success_fg"]
warning_fg = palette["warning_fg"]
# ---------------- 卡片 ----------------
style.configure(
@@ -134,6 +285,12 @@ def apply_modern_styles() -> None:
font=("Segoe UI", 9),
)
# ---------------- 通用文字语义 ----------------
style.configure("Muted.TLabel", background=bg, foreground=muted_fg)
style.configure("SuccessState.TLabel", background=bg, foreground=success_fg)
style.configure("WarningState.TLabel", background=bg, foreground=warning_fg)
style.configure("InfoState.TLabel", background=bg, foreground=palette["info_fg"])
# ---------------- 顶部工具条 ----------------
style.configure("Toolbar.TFrame", background=bg, borderwidth=0)
# 工具条上的次要按钮(清理配置等)
@@ -168,9 +325,17 @@ def apply_modern_styles() -> None:
style.configure(
"SidebarBrand.TLabel",
background=brand_bg,
foreground="#ffffff",
foreground=palette["badge_fg"],
font=("Segoe UI Semibold", 12),
)
style.configure(
"SidebarBadge.TLabel",
background=palette["badge_bg"],
foreground=palette["badge_fg"],
font=("微软雅黑", 8, "bold"),
anchor="center",
padding=(6, 2),
)
# ---------------- 结果区无边框标题行 ----------------
style.configure("ResultHeader.TFrame", background=bg, borderwidth=0)
@@ -182,7 +347,7 @@ def apply_modern_styles() -> None:
)
# ---------------- 状态栏 ----------------
statusbar_bg = _mix(bg, "#000000", 0.06) if not dark_theme else _mix(bg, "#ffffff", 0.06)
statusbar_bg = palette["statusbar_bg"]
statusbar_fg = _mix(fg, bg, 0.15)
style.configure(
"StatusBar.TFrame",
@@ -204,6 +369,33 @@ def apply_modern_styles() -> None:
padding=(10, 4),
)
# ---------------- 深色禁用态 / 只读态增强 ----------------
style.map(
"TLabel",
foreground=[("disabled", disabled_fg)],
)
style.map(
"TButton",
foreground=[("disabled", disabled_fg)],
background=[("disabled", disabled_bg)],
bordercolor=[("disabled", disabled_border)],
darkcolor=[("disabled", disabled_bg)],
lightcolor=[("disabled", disabled_bg)],
)
style.map(
"TEntry",
foreground=[("disabled", disabled_fg)],
fieldbackground=[("disabled", disabled_bg), ("readonly", readonly_bg)],
bordercolor=[("disabled", disabled_border), ("readonly", disabled_border)],
)
style.map(
"TCombobox",
foreground=[("disabled", disabled_fg), ("readonly", fg)],
fieldbackground=[("disabled", disabled_bg), ("readonly", readonly_bg)],
bordercolor=[("disabled", disabled_border), ("readonly", disabled_border)],
arrowcolor=[("disabled", disabled_fg), ("readonly", muted_fg)],
)
# ---------------- Sidebar 按钮(保留兼容名) ----------------
style.configure(
"Sidebar.TButton",
@@ -225,7 +417,7 @@ def apply_modern_styles() -> None:
style.configure(
"SidebarSelected.TButton",
background=sidebar_selected,
foreground="#ffffff",
foreground=_contrast_text(sidebar_selected, dark_text=palette["badge_fg"], light_text=sidebar_fg),
font=("Segoe UI Semibold", 10),
padding=(18, 9),
borderwidth=0,

View File

@@ -15,6 +15,7 @@ import ttkbootstrap as ttk
from PIL import Image, ImageTk
from app.services import ai_image as _svc
from app.views.modern_styles import apply_tooltip_theme, get_theme_palette
from typing import TYPE_CHECKING
@@ -26,17 +27,19 @@ logger = logging.getLogger(__name__)
def _theme_colors():
style = ttk.Style()
colors = style.colors
palette = get_theme_palette()
return {
"bg": colors.bg,
"fg": colors.fg,
"muted": colors.secondary,
"input_bg": colors.inputbg,
"input_fg": colors.inputfg,
"select_bg": colors.selectbg,
"select_fg": colors.selectfg,
"border": colors.border,
"bg": palette["bg"],
"fg": palette["fg"],
"muted": palette["muted_fg"],
"input_bg": palette["input_bg"],
"input_fg": palette["input_fg"],
"select_bg": palette["select_bg"],
"select_fg": palette["select_fg"],
"border": palette["border"],
"tooltip_bg": palette["tooltip_bg"],
"tooltip_fg": palette["tooltip_fg"],
"tooltip_border": palette["tooltip_border"],
}
@@ -95,8 +98,6 @@ def _show_tree_tooltip(self: "PQAutomationApp", text: str, x_root: int, y_root:
text="",
justify=tk.LEFT,
anchor=tk.W,
bg="#ffffff",
fg="#1f2937",
relief=tk.SOLID,
bd=1,
padx=8,
@@ -104,6 +105,7 @@ def _show_tree_tooltip(self: "PQAutomationApp", text: str, x_root: int, y_root:
font=("微软雅黑", 9),
wraplength=520,
)
apply_tooltip_theme(tip, label)
label.pack(fill=tk.BOTH, expand=True)
self._ai_image_tooltip = tip
self._ai_image_tooltip_label = label
@@ -114,6 +116,7 @@ def _show_tree_tooltip(self: "PQAutomationApp", text: str, x_root: int, y_root:
self._ai_image_tooltip_item = item_id
label.configure(text=text)
apply_tooltip_theme(tip, label)
tip.geometry(f"+{x_root + 14}+{y_root + 18}")
tip.deiconify()
tip.lift()
@@ -1225,6 +1228,16 @@ def _build_ucd_resized_image(image_path: str, target_w: int, target_h: int) -> s
return out_path
def refresh_ai_image_theme(self: "PQAutomationApp"):
"""刷新 AI 图片面板中的主题相关控件。"""
if hasattr(self, "_apply_ai_image_list_style"):
self._apply_ai_image_list_style()
tip = getattr(self, "_ai_image_tooltip", None)
label = getattr(self, "_ai_image_tooltip_label", None)
if tip is not None and label is not None:
apply_tooltip_theme(tip, label)
class AIImagePanelMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
@@ -1249,3 +1262,5 @@ class AIImagePanelMixin:
_rename_current = _rename_current
_show_list_context_menu = _show_list_context_menu
_send_to_ucd = _send_to_ucd
_apply_ai_image_list_style = _apply_ai_image_list_style
refresh_ai_image_theme = refresh_ai_image_theme

View File

@@ -22,6 +22,7 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from app.tests.color_accuracy import calculate_delta_e_2000
from app.views.modern_styles import get_theme_palette
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
@@ -35,10 +36,6 @@ D65_X = 0.3127
D65_Y = 0.3290
TARGET_CCT = 6504
TARGET_GAMMA = 2.2
_DARK_BG = "#2f2f2f"
_AX_BG = "#262626"
_FG = "#d8d8d8"
_GRID = "#5b5b5b"
DE_FORMULAS = ["2000", "94", "76"]
@@ -60,7 +57,7 @@ def _contrast_fg(gray_value: int) -> str:
def _set_canvas_patch(canvas: tk.Canvas, color: str, text: str) -> None:
"""统一设置色块 Canvas 颜色与文字,避免主题对 Label 背景渲染的干扰。"""
gray = int(color[1:3], 16)
canvas.configure(bg=color, highlightbackground="#666666")
canvas.configure(bg=color, highlightbackground=get_theme_palette()["border"])
canvas.itemconfigure("patch_bg", fill=color, outline=color)
canvas.itemconfigure("patch_text", text=text, fill=_contrast_fg(gray))
@@ -115,6 +112,7 @@ def _get_calman_palette() -> dict[str, str]:
"""根据当前主题生成 Calman 调试面板色板。"""
style = ttk.Style()
colors = style.colors
theme_palette = get_theme_palette()
bg = colors.bg
fg = colors.fg
dark_mode = _is_dark_hex(bg)
@@ -131,22 +129,22 @@ def _get_calman_palette() -> dict[str, str]:
reading_fg = _mix(fg, "#ffffff", 0.06)
status_fg = _mix(fg, bg, 0.35)
reading_accent = colors.info
xy_series = "#d7dce4"
d65_mark = "#ffffff"
xy_series = _mix(fg, "#ffffff", 0.10)
d65_mark = _mix(fg, "#ffffff", 0.04)
else:
figure_bg = _mix(bg, "#dfe7ef", 0.45)
axes_bg = _mix(bg, "#eff4f9", 0.72)
grid = _mix("#5f6f82", axes_bg, 0.55)
tree_bg = "#ffffff"
tree_even = "#ffffff"
tree_bg = theme_palette["input_bg"]
tree_even = theme_palette["input_bg"]
tree_odd = "#f3f7fb"
heading_bg = _mix(colors.primary, "#ffffff", 0.82)
reading_bg = _mix(bg, "#e7eef5", 0.58)
reading_fg = fg
status_fg = _mix(fg, bg, 0.25)
reading_accent = _mix(colors.info, "#000000", 0.25)
xy_series = "#1f2a36"
d65_mark = "#253142"
xy_series = _mix(fg, bg, 0.18)
d65_mark = _mix(fg, bg, 0.28)
return {
"figure_bg": figure_bg,
@@ -166,31 +164,59 @@ def _get_calman_palette() -> dict[str, str]:
"tree_heading_bg": heading_bg,
"tree_heading_fg": reading_fg,
"tree_select": _mix(colors.info, figure_bg, 0.35),
"patch_border": theme_palette["border"],
"patch_border_alt": _mix(theme_palette["border"], theme_palette["fg"], 0.12),
"patch_focus": theme_palette["focus"],
"xy_series": xy_series,
"d65_mark": d65_mark,
}
def _xyY_to_rgb_balance(x: float, y: float, big_y: float) -> tuple[float, float, float]:
"""把 xyY 近似映射到 RGB 比例,并归一到平均值 100"""
"""按 D65 同亮度参考计算 RGB BalanceCalman 常见口径)"""
if y <= 0 or big_y <= 0:
return float("nan"), float("nan"), float("nan")
big_x = (x * big_y) / y
big_z = ((1.0 - x - y) * big_y) / y
def _xyY_to_xyz(cx: float, cy: float, cy_big: float) -> tuple[float, float, float]:
if cy <= 0:
return float("nan"), float("nan"), float("nan")
cx_big = (cx * cy_big) / cy
cz_big = ((1.0 - cx - cy) * cy_big) / cy
return cx_big, cy_big, cz_big
r = (3.2406 * big_x) + (-1.5372 * big_y) + (-0.4986 * big_z)
g = (-0.9689 * big_x) + (1.8758 * big_y) + (0.0415 * big_z)
b = (0.0557 * big_x) + (-0.2040 * big_y) + (1.0570 * big_z)
def _xyz_to_linear_rgb(cx_big: float, cy_big: float, cz_big: float) -> tuple[float, float, float]:
rr = (3.2406 * cx_big) + (-1.5372 * cy_big) + (-0.4986 * cz_big)
gg = (-0.9689 * cx_big) + (1.8758 * cy_big) + (0.0415 * cz_big)
bb = (0.0557 * cx_big) + (-0.2040 * cy_big) + (1.0570 * cz_big)
return rr, gg, bb
r = max(r, 0.0)
g = max(g, 0.0)
b = max(b, 0.0)
mx, my, mz = _xyY_to_xyz(x, y, big_y)
tx, ty, tz = _xyY_to_xyz(D65_X, D65_Y, big_y)
mr, mg, mb = _xyz_to_linear_rgb(mx, my, mz)
tr, tg, tb = _xyz_to_linear_rgb(tx, ty, tz)
avg = (r + g + b) / 3.0
if avg <= 0:
eps = 1e-9
if tr <= eps or tg <= eps or tb <= eps:
return float("nan"), float("nan"), float("nan")
return (r / avg) * 100.0, (g / avg) * 100.0, (b / avg) * 100.0
rr = (mr / tr) * 100.0
gg = (mg / tg) * 100.0
bb = (mb / tb) * 100.0
# 明显异常值视为无效,避免图表被离群点拉坏。
if not (math.isfinite(rr) and math.isfinite(gg) and math.isfinite(bb)):
return float("nan"), float("nan"), float("nan")
if rr < 0 or gg < 0 or bb < 0:
return float("nan"), float("nan"), float("nan")
return rr, gg, bb
def _target_gamma_loglog_curve(pct: int) -> float:
"""Calman风格目标曲线低灰阶从 1.8 过渡并逐步逼近 2.2。"""
if pct <= 0:
return 1.8
return TARGET_GAMMA - 0.4 * math.exp(-pct / 6.0)
def _style_axes(self: "PQAutomationApp", ax, title: str) -> None:
@@ -480,6 +506,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
self.calman_actual_patch_cells = []
self.calman_target_patch_canvases = []
self.calman_target_hexes = []
patch_palette = _get_calman_palette()
for idx, pct in enumerate(self.calman_levels):
rgb = _pct_to_gray_rgb(pct)
color = _rgb_to_hex(rgb)
@@ -493,7 +520,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
bd=1,
relief="solid",
highlightthickness=1,
highlightbackground="#808080",
highlightbackground=patch_palette["patch_border_alt"],
cursor="hand2",
)
actual_cell.grid(row=0, column=idx, padx=1, pady=1, sticky=tk.NSEW)
@@ -520,7 +547,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
bd=1,
relief="solid",
highlightthickness=1,
highlightbackground="#9c9c9c",
highlightbackground=patch_palette["patch_border"],
cursor="hand2",
)
cell.grid(row=1, column=idx, padx=1, pady=1, sticky=tk.NSEW)
@@ -722,7 +749,7 @@ def send_patch(self: "PQAutomationApp", pct: int) -> None:
def _measure_once(self: "PQAutomationApp", pct: int) -> dict | None:
"""采集一次 CA410并组装一条记录含 CCT/Gamma/ΔE2000"""
try:
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
x, y, lv, X, Y, Z = self.read_ca_xyLv()
except Exception as exc:
self._dispatch_ui(self.log_gui.log, f"CA410 采集异常: {exc}", "error")
return None
@@ -974,20 +1001,21 @@ def clear_results(self: "PQAutomationApp") -> None:
def _highlight_patch(self: "PQAutomationApp", pct: int) -> None:
"""高亮当前选中色块。"""
palette = _get_calman_palette()
try:
idx = self.calman_levels.index(pct)
except ValueError:
return
for i, cell in enumerate(self.calman_patch_cells):
if i == idx:
cell.configure(highlightbackground="#1f6fb2", highlightthickness=2)
cell.configure(highlightbackground=palette["patch_focus"], highlightthickness=2)
else:
cell.configure(highlightbackground="#9c9c9c", highlightthickness=1)
cell.configure(highlightbackground=palette["patch_border"], highlightthickness=1)
for i, cell in enumerate(self.calman_actual_cells):
if i == idx:
cell.configure(highlightbackground="#1f6fb2", highlightthickness=2)
cell.configure(highlightbackground=palette["patch_focus"], highlightthickness=2)
else:
cell.configure(highlightbackground="#808080", highlightthickness=1)
cell.configure(highlightbackground=palette["patch_border_alt"], highlightthickness=1)
total_cols = len(self.calman_levels) + 1 # 含 metric 列
col_index = idx + 1
@@ -1082,10 +1110,18 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
pcts = [r["pct"] for r in recs]
de_vals = [r["de2000"] if r["de2000"] == r["de2000"] else 0 for r in recs]
lum_vals = [r["Y"] if r["Y"] == r["Y"] else 0 for r in recs]
rgb_r = [r["rgb_r"] for r in recs if r["rgb_r"] == r["rgb_r"]]
rgb_g = [r["rgb_g"] for r in recs if r["rgb_g"] == r["rgb_g"]]
rgb_b = [r["rgb_b"] for r in recs if r["rgb_b"] == r["rgb_b"]]
rgb_pcts = [r["pct"] for r in recs if r["rgb_r"] == r["rgb_r"]]
rgb_recs = [
r for r in recs
if (
r.get("rgb_r") == r.get("rgb_r")
and r.get("rgb_g") == r.get("rgb_g")
and r.get("rgb_b") == r.get("rgb_b")
)
]
rgb_pcts = [r["pct"] for r in rgb_recs]
rgb_r = [r["rgb_r"] for r in rgb_recs]
rgb_g = [r["rgb_g"] for r in rgb_recs]
rgb_b = [r["rgb_b"] for r in rgb_recs]
gamma_pcts = [r["pct"] for r in recs if r["gamma"] == r["gamma"]]
gamma_vals = [r["gamma"] for r in recs if r["gamma"] == r["gamma"]]
cct_vals = [r["cct"] for r in recs if r["cct"] == r["cct"]]
@@ -1123,6 +1159,16 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
a1.set_ylim(bottom=0)
a1.set_xlabel("", fontsize=8)
rgb_ylim_low = 95.0
rgb_ylim_high = 105.0
if rgb_recs:
rgb_values = rgb_r + rgb_g + rgb_b
rgb_min = min(rgb_values + [100.0])
rgb_max = max(rgb_values + [100.0])
pad = max(0.8, (rgb_max - rgb_min) * 0.15)
rgb_ylim_low = min(95.0, rgb_min - pad)
rgb_ylim_high = max(105.0, rgb_max + pad)
# RGB Balance 线图
a2 = self.calman_ax_rgb_line
a2.clear()
@@ -1133,36 +1179,38 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
a2.plot(rgb_pcts, rgb_b, "-", color="#4a73ff", linewidth=1.2)
a2.axhline(100, color="#9e9e9e", lw=0.8, ls="--")
a2.set_xlim(-2, 102)
a2.set_ylim(95, 105)
a2.set_ylim(rgb_ylim_low, rgb_ylim_high)
a2.set_xlabel("", fontsize=8)
# RGB Balance 条图(用最后一个点)
a3 = self.calman_ax_rgb_bar
a3.clear()
_style_axes(self, a3, "RGB Balance")
if recs:
last = recs[-1]
if rgb_recs:
last = rgb_recs[-1]
bars = [
last["rgb_r"] if last["rgb_r"] == last["rgb_r"] else 100,
last["rgb_g"] if last["rgb_g"] == last["rgb_g"] else 100,
last["rgb_b"] if last["rgb_b"] == last["rgb_b"] else 100,
last["rgb_r"],
last["rgb_g"],
last["rgb_b"],
]
a3.bar([0, 1, 2], bars, color=["#ff4d4d", "#4caf50", "#4a73ff"], width=0.7)
a3.set_xticks([0, 1, 2], ["R", "G", "B"])
else:
a3.set_xticks([0, 1, 2], ["R", "G", "B"])
a3.set_ylim(95, 105)
a3.set_ylim(rgb_ylim_low, rgb_ylim_high)
a3.set_xlabel("", fontsize=8)
# Gamma
a4 = self.calman_ax_gamma
a4.clear()
_style_axes(self, a4, "Gamma Log/Log")
target_pcts = list(self.calman_levels)
target_vals = [_target_gamma_loglog_curve(p) for p in target_pcts]
a4.plot(target_pcts, target_vals, "-", color="#f4ff00", linewidth=1.8)
if gamma_pcts:
a4.plot(gamma_pcts, gamma_vals, "-", color="#ffe24d", linewidth=1.3)
a4.axhline(TARGET_GAMMA, color="#9e9e9e", lw=0.8, ls="--")
a4.plot(gamma_pcts, gamma_vals, "-", color="#8f8f8f", linewidth=2.0)
a4.set_xlim(-2, 102)
a4.set_ylim(1.6, 2.8)
a4.set_ylim(1.8, 2.8)
a4.set_xlabel("", fontsize=8)
self.calman_canvas.draw_idle()
@@ -1232,14 +1280,20 @@ def _refresh_metric_table(self: "PQAutomationApp") -> None:
"""重绘下方矩阵表。"""
_apply_calman_tree_style(self)
palette = _get_calman_palette()
ref_white_y = self.calman_results.get(100, {}).get("Y")
def _target_y_abs(pctx):
if pctx is None:
return "-"
if ref_white_y is None or ref_white_y != ref_white_y or ref_white_y <= 0:
return "-"
return _safe_float(ref_white_y * ((pctx / 100.0) ** TARGET_GAMMA), "{:.3f}")
metrics = [
("x CIE31", lambda r: _safe_float(r.get("x")) if r else "-"),
("y CIE31", lambda r: _safe_float(r.get("y")) if r else "-"),
("Y", lambda r: _safe_float(r.get("Y"), "{:.3f}") if r else "-"),
(
"Target Y",
lambda _r, pctx=None: _safe_float((pctx / 100.0) ** TARGET_GAMMA, "{:.4f}") if pctx and pctx > 0 else ("0.0000" if pctx == 0 else "-"),
),
("Target Y", lambda _r, pctx=None: _target_y_abs(pctx)),
("Gamma Log/Log", lambda r: _safe_float(r.get("gamma"), "{:.3f}") if r else "-"),
("CCT", lambda r: _safe_float(r.get("cct"), "{:.0f}") if r else "-"),
("dE 2000", lambda r: _safe_float(r.get("de2000"), "{:.3f}") if r else "-"),

View File

@@ -10,6 +10,7 @@ import colour
import numpy as np
from app.data_range_converter import convert_pattern_params
from app.views.modern_styles import get_theme_palette
from typing import TYPE_CHECKING
@@ -28,37 +29,12 @@ def create_custom_template_result_panel(self: "PQAutomationApp"):
table_container = tk.Frame(
self.custom_result_frame,
bg="#000000",
highlightthickness=1,
highlightbackground="#5a5a5a",
)
table_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.custom_result_table_container = table_container
style = ttk.Style()
style.configure(
"CustomResult.Treeview",
background="#000000",
fieldbackground="#000000",
foreground="#ffffff",
rowheight=28,
borderwidth=0,
)
style.configure(
"CustomResult.Treeview.Heading",
background="#2f2f2f",
foreground="#f5f5f5",
font=("Microsoft YaHei", 10, "bold"),
relief="flat",
)
style.map(
"CustomResult.Treeview",
background=[("selected", "#1f4e79")],
foreground=[("selected", "#ffffff")],
)
style.map(
"CustomResult.Treeview.Heading",
background=[("active", "#3b3b3b")],
)
_apply_custom_result_theme(self)
columns = (
"Pattern",
@@ -157,6 +133,70 @@ def create_custom_template_result_panel(self: "PQAutomationApp"):
table_container.grid_columnconfigure(0, weight=1)
def _apply_custom_result_theme(self: "PQAutomationApp"):
palette = get_theme_palette()
container = getattr(self, "custom_result_table_container", None)
if container is not None:
container.configure(
bg=palette["input_bg"],
highlightbackground=palette["border"],
highlightcolor=palette["border"],
)
style = ttk.Style()
style.configure(
"CustomResult.Treeview",
background=palette["input_bg"],
fieldbackground=palette["input_bg"],
foreground=palette["input_fg"],
rowheight=28,
borderwidth=0,
)
style.configure(
"CustomResult.Treeview.Heading",
background=palette["surface_alt_bg"],
foreground=palette["muted_fg"],
font=("Microsoft YaHei", 10, "bold"),
relief="flat",
)
style.map(
"CustomResult.Treeview",
background=[("selected", palette["select_bg"])],
foreground=[("selected", palette["select_fg"])],
)
style.map(
"CustomResult.Treeview.Heading",
background=[("active", palette["surface_hover_bg"])],
)
def refresh_custom_template_theme(self: "PQAutomationApp"):
"""刷新客户模板结果表的主题色。"""
_apply_custom_result_theme(self)
def _set_custom_template_tab_visible(self: "PQAutomationApp", visible: bool):
"""控制客户模板结果 TAB 的显示与隐藏。"""
if not hasattr(self, "chart_notebook") or not hasattr(self, "custom_template_tab_frame"):
return
tab_id = str(self.custom_template_tab_frame)
current_tabs = list(self.chart_notebook.tabs())
if visible:
if tab_id not in current_tabs:
self.chart_notebook.add(self.custom_template_tab_frame, text="客户模板结果显示")
self.chart_notebook.select(self.custom_template_tab_frame)
return
if tab_id in current_tabs:
current_selected = self.chart_notebook.select()
self.chart_notebook.forget(self.custom_template_tab_frame)
remaining_tabs = list(self.chart_notebook.tabs())
if current_selected == tab_id and remaining_tabs:
self.chart_notebook.select(remaining_tabs[0])
def show_custom_result_context_menu(self: "PQAutomationApp", event):
"""显示客户模板结果右键菜单"""
if not hasattr(self, "custom_result_tree") or not hasattr(
@@ -322,11 +362,9 @@ def _run_custom_row_single_step(self: "PQAutomationApp", item_id, row_no):
time.sleep(self.pattern_settle_time)
# 测量显示模式1读取 Tcp/duv/Lv显示模式8读取 λd/Pe/Lv 与 XYZ。
self.ca.set_Display(1)
tcp, duv, lv, _, _, _ = self.ca.readAllDisplay()
tcp, duv, lv, _, _, _ = self.read_ca_tcp_duv()
self.ca.set_Display(8)
lambda_d, pe, lv, X, Y, Z = self.ca.readAllDisplay()
lambda_d, pe, lv, X, Y, Z = self.read_ca_lambda_pe()
xy = colour.XYZ_to_xy(np.array([X, Y, Z]))
u_prime, v_prime, _ = colour.XYZ_to_CIE1976UCS(np.array([X, Y, Z]))
@@ -532,9 +570,6 @@ def append_custom_template_result(self: "PQAutomationApp", row_no, result_data):
def start_custom_template_test(self: "PQAutomationApp"):
"""开始客户模板测试SDR"""
if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
self.chart_notebook.select(self.custom_template_tab_frame)
if self.ca is None or self.ucd is None:
messagebox.showerror("错误", "请先连接CA410和信号发生器")
return
@@ -569,8 +604,10 @@ def start_custom_template_test(self: "PQAutomationApp"):
self.custom_btn.config(state=tk.NORMAL)
self.status_var.set("测试已取消")
self.set_custom_result_table_locked(False)
_set_custom_template_tab_visible(self, False)
return
_set_custom_template_tab_visible(self, True)
self.set_custom_result_table_locked(True)
self.test_thread = threading.Thread(target=self.run_custom_sdr_test, args=([],))
@@ -923,6 +960,7 @@ class CustomTemplatePanelMixin:
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
create_custom_template_result_panel = create_custom_template_result_panel
_set_custom_template_tab_visible = _set_custom_template_tab_visible
show_custom_result_context_menu = show_custom_result_context_menu
set_custom_result_table_locked = set_custom_result_table_locked
start_custom_row_single_step = start_custom_row_single_step
@@ -937,3 +975,4 @@ class CustomTemplatePanelMixin:
update_custom_button_visibility = update_custom_button_visibility
export_custom_template_excel = export_custom_template_excel
export_custom_template_charts = export_custom_template_charts
refresh_custom_template_theme = refresh_custom_template_theme

View File

@@ -171,7 +171,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
ttk.Label(
title_row,
text="Gamma / CCT / 对比度 / EOTF 共用此列表)",
foreground="#888",
style="Muted.TLabel",
).pack(side=tk.LEFT, padx=(8, 0))
# ===== 预设管理行 =====
@@ -207,7 +207,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
).pack(side=tk.LEFT, padx=2)
self._gamma_active_label = ttk.Label(
preset_row1, text="", foreground="#0a8", font=("微软雅黑", 9, "bold")
preset_row1, text="", style="SuccessState.TLabel", font=("微软雅黑", 9, "bold")
)
self._gamma_active_label.pack(side=tk.LEFT, padx=(10, 0))
@@ -230,7 +230,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
# 描述行
self._gamma_meta_label = ttk.Label(
preset_box, text="", foreground="#666", font=("微软雅黑", 9)
preset_box, text="", style="Muted.TLabel", font=("微软雅黑", 9)
)
self._gamma_meta_label.pack(anchor=tk.W, pady=(6, 0))
@@ -350,7 +350,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
paste_frame.pack(fill=tk.X, pady=(10, 0))
ttk.Label(
paste_frame, text="每行R,G,B 或 R G B\n或:灰度% (如 50%)",
foreground="#888", justify=tk.LEFT,
style="Muted.TLabel", justify=tk.LEFT,
).pack(anchor=tk.W)
ttk.Button(
paste_frame, text="从剪贴板导入",
@@ -363,7 +363,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
bottom.pack(fill=tk.X, pady=(10, 0))
self._gamma_validate_label = ttk.Label(
bottom, text="", foreground="#666", justify=tk.LEFT
bottom, text="", style="Muted.TLabel", justify=tk.LEFT
)
self._gamma_validate_label.pack(anchor=tk.W)
@@ -435,16 +435,16 @@ def _update_active_label(self: "PQAutomationApp"):
current = self._gamma_current_preset
if active and current == active and not self._gamma_dirty:
self._gamma_active_label.config(
text=f"✔ 当前激活:{active}", foreground="#0a8"
text=f"✔ 当前激活:{active}", style="SuccessState.TLabel"
)
elif active:
extra = "(有未保存改动)" if self._gamma_dirty else ""
self._gamma_active_label.config(
text=f"● 激活:{active} 编辑中:{current or '-'}{extra}",
foreground="#a60" if self._gamma_dirty else "#888",
style="WarningState.TLabel" if self._gamma_dirty else "Muted.TLabel",
)
else:
self._gamma_active_label.config(text="● 未激活任何预设", foreground="#888")
self._gamma_active_label.config(text="● 未激活任何预设", style="Muted.TLabel")
def _on_preset_selected(self: "PQAutomationApp"):
@@ -1023,11 +1023,11 @@ def _run_validation(self: "PQAutomationApp"):
if not msgs:
text = f"✔ 校验通过(共 {len(params)} 点)"
color = "#0a8"
style_name = "SuccessState.TLabel"
else:
text = f"{len(params)} 点 | " + " ".join(msgs)
color = "#a60" if any(m.startswith("") for m in msgs) else "#666"
self._gamma_validate_label.config(text=text, foreground=color)
style_name = "WarningState.TLabel" if any(m.startswith("") for m in msgs) else "Muted.TLabel"
self._gamma_validate_label.config(text=text, style=style_name)
# ============================================================

View File

@@ -757,13 +757,10 @@ def create_test_type_frame(self: "PQAutomationApp"):
# 测试版水印标签(版本 x.x.0.0 时显示)
from app_version import is_beta_version, APP_VERSION
if is_beta_version():
beta_lbl = tk.Label(
beta_lbl = ttk.Label(
self.sidebar_frame,
text=f"[测试版] v{APP_VERSION}",
foreground="#ffffff",
background="#cc3300",
font=("微软雅黑", 8, "bold"),
anchor="center",
style="SidebarBadge.TLabel",
)
beta_lbl.pack(fill=tk.X, side=tk.BOTTOM, padx=4, pady=(6, 4))
@@ -809,6 +806,7 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None:
"""切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。"""
from app.views.theme_manager import toggle_theme
toggle_theme()
# apply_modern_styles()
_refresh_theme_toggle_label(self)
if hasattr(self, "apply_result_chart_theme"):
try:
@@ -820,6 +818,21 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None:
self.log_gui.refresh_log_theme()
except Exception:
pass
if hasattr(self, "refresh_ai_image_theme"):
try:
self.refresh_ai_image_theme()
except Exception:
pass
if hasattr(self, "refresh_single_step_theme"):
try:
self.refresh_single_step_theme()
except Exception:
pass
if hasattr(self, "refresh_custom_template_theme"):
try:
self.refresh_custom_template_theme()
except Exception:
pass
if hasattr(self, "refresh_calman_theme"):
try:
self.refresh_calman_theme()

View File

@@ -63,7 +63,7 @@ def create_pantone_baseline_panel(self: "PQAutomationApp"):
ttk.Label(config_row, textvariable=self.pantone_progress_var).pack(
side=tk.RIGHT, padx=(8, 0)
)
ttk.Label(config_row, textvariable=self.pantone_status_var, foreground="#666").pack(
ttk.Label(config_row, textvariable=self.pantone_status_var, style="Muted.TLabel").pack(
side=tk.RIGHT
)
@@ -336,7 +336,7 @@ def _launch_worker(self: "PQAutomationApp", start_index, settle):
end_state = "paused"
break
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
if lv is None:
raise RuntimeError(f"{i + 1} 组 CA410 采集失败")

View File

@@ -57,7 +57,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
window_frame,
text="点击按钮发送对应百分比的白色窗口(黑色背景 + 居中白色矩形)",
font=("", 9),
foreground="#28a745",
style="SuccessState.TLabel",
).pack(pady=(0, 8))
# 第一行1%, 2%, 5%, 10%, 18%
@@ -96,7 +96,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
pattern_frame,
text="手动发送棋盘格、瞬时峰值、黑场图案,再点击采集当前亮度",
font=("", 9),
foreground="#28a745",
style="SuccessState.TLabel",
).pack(pady=(0, 8))
pattern_row = ttk.Frame(pattern_frame)
@@ -155,7 +155,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
measure_btn_frame,
text="亮度: -- cd/m² | x: -- | y: --",
font=("Consolas", 10),
foreground="#007bff",
style="InfoState.TLabel",
)
self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0))

View File

@@ -13,6 +13,8 @@ from tkinter import filedialog, messagebox
import ttkbootstrap as ttk
from PIL import Image
from app.views.modern_styles import apply_listbox_theme
from typing import TYPE_CHECKING
if TYPE_CHECKING:
@@ -59,7 +61,7 @@ def create_single_step_panel(self: "PQAutomationApp"):
ttk.Label(
title_row,
text="录入目标色块,发送纯色色块并采集 xyY / ΔE2000。",
foreground="#666",
style="Muted.TLabel",
).pack(side=tk.LEFT, padx=(12, 0))
left = ttk.LabelFrame(root, text="样本列表", padding=8)
@@ -73,11 +75,8 @@ def create_single_step_panel(self: "PQAutomationApp"):
activestyle="none",
font=("微软雅黑", 9),
highlightthickness=1,
highlightbackground="#d8d8d8",
highlightcolor="#4a90e2",
selectbackground="#2b6cb0",
selectforeground="#ffffff",
)
apply_listbox_theme(self.single_step_listbox)
self.single_step_listbox.pack(fill=tk.BOTH, expand=True)
self.single_step_listbox.bind(
"<<ListboxSelect>>", lambda e: _on_sample_select(self)
@@ -154,7 +153,7 @@ def create_single_step_panel(self: "PQAutomationApp"):
ttk.Label(
form_frame,
textvariable=self.single_step_status_var,
foreground="#666",
style="Muted.TLabel",
).grid(row=2, column=2, columnspan=4, sticky=tk.W, pady=4)
action_row = ttk.Frame(form_frame)
@@ -444,7 +443,7 @@ def _measure_current_sample(self: "PQAutomationApp"):
def worker():
try:
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
if lv is None:
raise RuntimeError("CA410 未返回有效亮度")
self._dispatch_ui(self.single_step_measured_x_var.set, f"{x:.4f}")
@@ -556,6 +555,12 @@ def _export_results_csv(self: "PQAutomationApp"):
messagebox.showerror("导出失败", f"写入 CSV 失败: {exc}")
def refresh_single_step_theme(self: "PQAutomationApp"):
"""刷新单步调试中 tk.Listbox 的主题色。"""
if hasattr(self, "single_step_listbox"):
apply_listbox_theme(self.single_step_listbox)
class SingleStepPanelMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
@@ -575,3 +580,4 @@ class SingleStepPanelMixin:
_commit_result = _commit_result
_clear_results = _clear_results
_export_results_csv = _export_results_csv
refresh_single_step_theme = refresh_single_step_theme

View File

@@ -802,7 +802,7 @@ class PQDebugPanel:
time.sleep(1.5)
# 测量数据
x, y, lv, X, Y, Z = self.app.ca.readAllDisplay()
x, y, lv, X, Y, Z = self.app.read_ca_xyLv()
self.app.log_gui.log(
f"测量完成: x={x:.4f}, y={y:.4f}, lv={lv:.2f}, "

View File

@@ -19,20 +19,44 @@ from app.views.modern_styles import apply_modern_styles
_PREFS_PATH = Path("settings/ui_preferences.json")
# 浅色主题:沿用旧的 yeti首发布兼容
LIGHT_THEME = "yeti"
# 浅色主题:自定义轻量蓝灰色板,恢复旧版浅色观感
LIGHT_THEME = "calman_light"
# 深色主题:自定义 Calman 风格
DARK_THEME = "calman_dark"
_LEGACY_LIGHT_THEMES = {"yeti"}
_CALMAN_LIGHT_COLORS = {
"primary": "#1755a6",
"secondary": "#2B6CB0",
"success": "#2F9E44",
"info": "#247BA0",
"warning": "#C98700",
"danger": "#CC3300",
"light": "#F7FAFC",
"dark": "#1F2A36",
"bg": "#F5F8FB",
"fg": "#1F2933",
"selectbg": "#2B6CB0",
"selectfg": "#FFFFFF",
"border": "#C8D4E3",
"inputfg": "#243240",
"inputbg": "#FFFFFF",
"active": "#D9E6F2",
}
# ----------------------------------------------------------------------
# Calman 风格深色主题色板(参考实测截图取色)
# Calman 风格深色主题色板
# ----------------------------------------------------------------------
_CALMAN_DARK_COLORS = {
"primary": "#343A41", # 主色改为炭灰,避免大面积亮蓝
"secondary": "#444A51", # 中性深灰(用于 header / 分组背景)
# "primary": "#2A2F36",
# "secondary": "#444A51",
"primary": "#6FAFCC",
"secondary": "#AEAEAE",
"success": "#4FB960",
"info": "#6FAFCC", # 降低饱和度,只做少量点缀
"info": "#6FAFCC",
"warning": "#F2A93B",
"danger": "#E0524A",
"light": "#BFC6CE", # 高亮文本
@@ -51,14 +75,27 @@ _CALMAN_DARK_COLORS = {
def register_themes() -> None:
"""把自定义深色主题注册到 ttkbootstrap可重复调用幂等"""
style = Style()
if LIGHT_THEME not in style.theme_names():
light_def = ThemeDefinition(
name=LIGHT_THEME,
themetype="light",
colors=_CALMAN_LIGHT_COLORS,
)
style.register_theme(light_def)
if DARK_THEME in style.theme_names():
return
theme_def = ThemeDefinition(
dark_def = ThemeDefinition(
name=DARK_THEME,
themetype="dark",
colors=_CALMAN_DARK_COLORS,
)
style.register_theme(theme_def)
style.register_theme(dark_def)
def _normalize_theme_name(name: Optional[str]) -> str:
if not name or name in _LEGACY_LIGHT_THEMES:
return LIGHT_THEME
return name
# ----------------------------------------------------------------------
@@ -101,7 +138,7 @@ def apply_initial_theme() -> str:
返回最终生效的主题名。
"""
register_themes()
name = get_saved_theme() or LIGHT_THEME
name = _normalize_theme_name(get_saved_theme())
style = Style()
if name not in style.theme_names():
name = LIGHT_THEME
@@ -113,6 +150,7 @@ def apply_initial_theme() -> str:
def set_theme(name: str) -> str:
"""切换到指定主题,持久化偏好,并刷新自定义样式。"""
register_themes()
name = _normalize_theme_name(name)
style = Style()
if name not in style.theme_names():
name = LIGHT_THEME

View File

@@ -388,50 +388,31 @@ class PQAutomationApp(
if hasattr(self, "log_gui"):
self.log_gui.log(f"切换信号格式失败: {str(e)}", level="error")
def _switch_chart_tabs_by_test_type(self, test_type):
"""按测试类型切换 Gamma/EOTF 与客户模板结果 Tab"""
if not hasattr(self, "chart_notebook"):
def _sync_custom_template_tab_visibility(self, test_type):
"""按测试类型与客户模板结果状态同步客户模板 Tab 可见性"""
if not hasattr(self, "_set_custom_template_tab_visible"):
return
try:
def _safe_insert_tab(frame, text, target_pos=1):
"""安全插入 Tab有目标位置则插入否则追加到末尾。"""
tabs = list(self.chart_notebook.tabs())
if not tabs or target_pos >= len(tabs):
self.chart_notebook.add(frame, text=text)
return
before_tab_id = tabs[target_pos]
self.chart_notebook.insert(before_tab_id, frame, text=text)
# 客户模板结果 Tab 只属于 SDR Movie。
if test_type != "sdr_movie":
self._set_custom_template_tab_visible(False)
return
current_tabs = list(self.chart_notebook.tabs())
gamma_tab_id = str(self.gamma_chart_frame)
eotf_tab_id = str(self.eotf_chart_frame)
has_custom_rows = False
tree = getattr(self, "custom_result_tree", None)
if tree is not None:
try:
has_custom_rows = len(tree.get_children()) > 0
except Exception:
has_custom_rows = False
if test_type == "hdr_movie":
if gamma_tab_id in current_tabs:
self.chart_notebook.forget(self.gamma_chart_frame)
if eotf_tab_id not in current_tabs:
_safe_insert_tab(self.eotf_chart_frame, "EOTF 曲线", target_pos=1)
else:
if eotf_tab_id in current_tabs:
self.chart_notebook.forget(self.eotf_chart_frame)
if gamma_tab_id not in current_tabs:
_safe_insert_tab(self.gamma_chart_frame, "Gamma 曲线", target_pos=1)
custom_tab_id = str(self.custom_template_tab_frame)
current_tabs = list(self.chart_notebook.tabs())
if test_type == "sdr_movie":
if custom_tab_id not in current_tabs:
self.chart_notebook.add(self.custom_template_tab_frame, text="客户模板结果显示")
else:
if custom_tab_id in current_tabs:
self.chart_notebook.forget(self.custom_template_tab_frame)
self.chart_notebook.update_idletasks()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"切换 Gamma/EOTF Tab 失败: {str(e)}", level="error")
# SDR 下仅在客户模板测试进行中,或已有客户模板结果时显示。
show_tab = has_custom_rows or (
getattr(self, "testing", False)
and getattr(self, "test_type_var", None) is not None
and self.test_type_var.get() == "sdr_movie"
)
self._set_custom_template_tab_visible(show_tab)
def change_test_type(self, test_type):
"""切换测试类型"""
@@ -453,7 +434,7 @@ class PQAutomationApp(
self.update_sidebar_selection()
self.on_test_type_change()
self._switch_signal_format_tabs(test_type)
self._switch_chart_tabs_by_test_type(test_type)
self._sync_custom_template_tab_visibility(test_type)
self.sync_gamut_toolbar()
self._restore_charts_for_type(test_type)

View File

@@ -6,11 +6,11 @@
"test_items": [
"gamma"
],
"timing": "OVT 1280x 720 @ 120Hz",
"timing": "DMT 1920x 1080 @ 60Hz",
"data_range": "Full",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "DCI-P3",
"colorimetry": "sRGB",
"patterns": {
"gamut": "rgb",
"gamma": "gray",
@@ -22,15 +22,18 @@
"x_tolerance": 0.003,
"y_ideal": 0.329,
"y_tolerance": 0.003
},
"gamut_reference": "DCI-P3"
}
},
"sdr_movie": {
"name": "SDR Movie测试",
"test_items": [
"gamut",
"gamma",
"cct",
"contrast",
"accuracy"
],
"timing": "DMT 1680x 1050 @ 60Hz",
"timing": "DMT 1920x 1080 @ 60Hz",
"data_range": "Full",
"color_format": "RGB",
"bpc": 8,
@@ -48,7 +51,7 @@
"y_ideal": 0.329,
"y_tolerance": 0.003
},
"gamut_reference": "DCI-P3"
"gamut_reference": "BT.709"
},
"hdr_movie": {
"name": "HDR Movie测试",