Files
pqAutomationApp/app/views/panels/main_layout.py
2026-06-05 16:58:46 +08:00

1259 lines
47 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""主布局面板创建函数Step 6 重构)。"""
import re
import tkinter as tk
import ttkbootstrap as ttk
from drivers.UCD323_Enum import UCDEnum
from app.views.collapsing_frame import CollapsingFrame
from app.resources import load_icon
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
# ---------------------------------------------------------------------------
# 内部工具:现代化卡片容器
# ---------------------------------------------------------------------------
def _make_card(parent, icon: str, title: str) -> ttk.Frame:
"""构造一个"卡片"容器(圆角描边 + 顶部图标/标题)。
返回值是 outer card frame通过 ``outer._body`` 拿到内部 body Frame
旧代码里习惯把控件挂在 ``self.connection_frame`` 等"卡片本身"上,所以
保留 outer 作为对外别名,但实际控件父级换成 body 以避开边框 padding。
"""
outer = ttk.Frame(parent, style="Card.TFrame", padding=12)
header = ttk.Frame(outer, style="CardInner.TFrame")
header.pack(fill="x", pady=(0, 8))
ttk.Label(header, text=icon, style="CardIcon.TLabel").pack(side="left", padx=(0, 8))
ttk.Label(header, text=title, style="CardTitle.TLabel").pack(side="left")
body = ttk.Frame(outer, style="CardInner.TFrame")
body.pack(fill="both", expand=True)
outer._body = body # type: ignore[attr-defined]
return outer
def create_floating_config_panel(self: "PQAutomationApp"):
"""创建顶部"配置项"现代化折叠面板
"""
cf = CollapsingFrame(self.control_frame_top)
cf.pack(fill="both")
self._config_collapsing = cf
# 配置项主体容器(卡片宿主)
self.config_panel_frame = ttk.Frame(cf)
# 折叠预览:呈现"测试类型 · 已选测试项"
self.config_preview_var = tk.StringVar(value="")
self.toolbar_actions_frame = None
def _header_actions(parent: ttk.Frame):
# 暴露给 create_operation_frame 使用
self.toolbar_actions_frame = parent
cf.add(
self.config_panel_frame,
title="配置项",
preview_textvariable=self.config_preview_var,
header_actions=_header_actions,
)
# 卡片三栏
self.config_content_frame = ttk.Frame(self.config_panel_frame)
self.config_content_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
config_row_frame = ttk.Frame(self.config_content_frame)
config_row_frame.pack(fill=tk.X, expand=False)
# 设备连接 卡片
connection_card = _make_card(config_row_frame, icon="01", title="设备连接")
connection_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6))
self.connection_frame = connection_card._body # type: ignore[attr-defined]
# 测试项目 卡片
test_items_card = _make_card(config_row_frame, icon="02", title="测试项目")
test_items_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=6)
self.test_items_frame = test_items_card._body # type: ignore[attr-defined]
# 信号格式 卡片
signal_card = _make_card(config_row_frame, icon="03", title="信号格式")
signal_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(6, 0))
self.signal_format_frame = signal_card._body # type: ignore[attr-defined]
# 创建卡片内部内容
self.create_connection_content()
self.create_test_items_content()
self.create_signal_format_content()
# 默认收起 —— 与旧版行为保持一致
self.config_panel_frame.grid_remove()
self.config_panel_frame.btn.configure(image="closed")
# 初始化预览文本
refresh_config_preview(self)
def refresh_config_preview(self: "PQAutomationApp") -> None:
"""根据当前测试类型 + 已选测试项刷新折叠预览。"""
if not hasattr(self, "config_preview_var"):
return
type_labels = {
"screen_module": "屏模组",
"sdr_movie": "SDR Movie",
"hdr_movie": "HDR Movie",
"local_dimming": "Local Dimming",
}
current_type = getattr(self.config, "current_test_type", "")
type_label = type_labels.get(current_type, "")
item_labels = []
if current_type and hasattr(self, "test_items"):
info = self.test_items.get(current_type, {})
label_map = {code: name for name, code in info.get("items", [])}
if hasattr(self, "test_vars"):
for code, var in self.test_vars.items():
try:
if var.get():
item_labels.append(label_map.get(code, code))
except Exception:
pass
parts = []
if type_label:
parts.append(f"[{type_label}]")
if item_labels:
# 限制宽度,避免顶部条过挤
shown = item_labels[:5]
suffix = " \u2026" if len(item_labels) > 5 else ""
parts.append(" \u00b7 ".join(shown) + suffix)
self.config_preview_var.set(" ".join(parts))
def create_test_items_content(self: "PQAutomationApp"):
"""创建测试项目选项卡内容"""
# 创建测试项目字典,用于管理不同测试类型的选项
self.test_items = {
"screen_module": {
"frame": ttk.Frame(self.test_items_frame),
"items": [
("色域", "gamut"),
("Gamma", "gamma"),
("色度", "cct"),
("对比度", "contrast"),
],
},
"sdr_movie": {
"frame": ttk.Frame(self.test_items_frame),
"items": [
("色域", "gamut"),
("Gamma", "gamma"),
("色度", "cct"),
("对比度", "contrast"),
("色准", "accuracy"),
],
},
"hdr_movie": {
"frame": ttk.Frame(self.test_items_frame),
"items": [
("色域", "gamut"),
("EOTF", "eotf"),
("色度", "cct"),
("对比度", "contrast"),
("色准", "accuracy"),
],
},
"local_dimming": {
"frame": ttk.Frame(self.test_items_frame),
"items": [],
},
}
# 根据当前测试类型创建复选框
self.test_vars = {}
self.update_test_items()
# 创建色度参数设置框架
self.create_cct_params_frame()
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)
# ==================== 屏模组格式设置 ====================
self.screen_module_signal_frame = ttk.Frame(self.signal_tabs)
self.screen_module_signal_frame.grid_columnconfigure(0, weight=0)
self.screen_module_signal_frame.grid_columnconfigure(1, weight=1)
self.signal_tabs.add(self.screen_module_signal_frame, text="屏模组测试")
screen_cfg = self.config.current_test_types.get("screen_module", {})
self.screen_module_timing_var = tk.StringVar(
value=screen_cfg.get("timing", "DMT 1920x 1080 @ 60Hz")
)
ttk.Label(self.screen_module_signal_frame, text="分辨率:").grid(
row=0, column=0, sticky=tk.W, padx=5, pady=2
)
screen_module_timing_combo = ttk.Combobox(
self.screen_module_signal_frame,
textvariable=self.screen_module_timing_var,
values=UCDEnum.TimingInfo.get_formatted_resolution_list(),
state="readonly",
)
screen_module_timing_combo.bind(
"<<ComboboxSelected>>", self.on_screen_module_timing_changed
)
screen_module_timing_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
ttk.Label(self.screen_module_signal_frame, text="色彩空间:").grid(
row=1, column=0, sticky=tk.W, padx=5, pady=2
)
self.screen_module_color_space_var = tk.StringVar(
value=screen_cfg.get("colorimetry", "sRGB")
)
screen_module_color_space_combo = ttk.Combobox(
self.screen_module_signal_frame,
textvariable=self.screen_module_color_space_var,
values=["sRGB", "BT.709", "BT.601", "BT.2020", "DCI-P3"],
width=10,
state="readonly",
)
screen_module_color_space_combo.bind(
"<<ComboboxSelected>>", self.on_screen_module_signal_format_changed
)
screen_module_color_space_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
ttk.Label(self.screen_module_signal_frame, text="数据范围:").grid(
row=2, column=0, sticky=tk.W, padx=5, pady=2
)
self.screen_module_data_range_var = tk.StringVar(
value=screen_cfg.get("data_range", UCDEnum.SignalFormat.DataRange.FULL)
)
screen_module_data_range_combo = ttk.Combobox(
self.screen_module_signal_frame,
textvariable=self.screen_module_data_range_var,
values=UCDEnum.SignalFormat.DataRange.get_list(),
width=10,
state="readonly",
)
screen_module_data_range_combo.bind(
"<<ComboboxSelected>>", self.on_screen_module_signal_format_changed
)
screen_module_data_range_combo.grid(row=2, column=1, sticky=tk.W, padx=5, pady=2)
default_screen_bpc = int(screen_cfg.get("bpc", 8))
default_screen_bit_depth = (
f"{default_screen_bpc}bit"
if f"{default_screen_bpc}bit" in UCDEnum.SignalFormat.BitDepth.get_list()
else UCDEnum.SignalFormat.BitDepth.BIT_8
)
ttk.Label(self.screen_module_signal_frame, text="编码位深:").grid(
row=3, column=0, sticky=tk.W, padx=5, pady=2
)
self.screen_module_bit_depth_var = tk.StringVar(value=default_screen_bit_depth)
screen_module_bit_depth_combo = ttk.Combobox(
self.screen_module_signal_frame,
textvariable=self.screen_module_bit_depth_var,
values=UCDEnum.SignalFormat.BitDepth.get_list(),
width=10,
state="readonly",
)
screen_module_bit_depth_combo.bind(
"<<ComboboxSelected>>", self.on_screen_module_signal_format_changed
)
screen_module_bit_depth_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
ttk.Label(self.screen_module_signal_frame, text="色彩格式:").grid(
row=4, column=0, sticky=tk.W, padx=5, pady=2
)
self.screen_module_output_format_var = tk.StringVar(
value=screen_cfg.get("color_format", UCDEnum.SignalFormat.OutputFormat.RGB)
)
screen_module_output_format_combo = ttk.Combobox(
self.screen_module_signal_frame,
textvariable=self.screen_module_output_format_var,
values=UCDEnum.SignalFormat.OutputFormat.get_list(),
width=10,
state="readonly",
)
screen_module_output_format_combo.bind(
"<<ComboboxSelected>>", self.on_screen_module_signal_format_changed
)
screen_module_output_format_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
# ==================== SDR信号格式设置 ====================
self.sdr_signal_frame = ttk.Frame(self.signal_tabs)
# 配置列权重
self.sdr_signal_frame.grid_columnconfigure(0, weight=0)
self.sdr_signal_frame.grid_columnconfigure(1, weight=1)
self.signal_tabs.add(self.sdr_signal_frame, text="SDR测试")
# 色彩空间
ttk.Label(self.sdr_signal_frame, text="色彩空间:").grid(
row=0, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_color_space_var = tk.StringVar(value="BT.709")
sdr_color_space_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_color_space_var,
values=["sRGB", "BT.709", "BT.601", "BT.2020", "DCI-P3"],
width=10,
state="readonly",
)
sdr_color_space_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
# Gamma测试参考值用于Gamma曲线绘制和色准计算
ttk.Label(self.sdr_signal_frame, text="Gamma:").grid(
row=1, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_gamma_type_var = tk.StringVar(value=UCDEnum.SignalFormat.GammaType.GAMMA_22)
sdr_gamma_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_gamma_type_var,
values=UCDEnum.SignalFormat.GammaType.get_list(),
width=10,
state="readonly",
)
sdr_gamma_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
# 数据范围
ttk.Label(self.sdr_signal_frame, text="数据范围:").grid(
row=2, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_data_range_var = tk.StringVar(value=UCDEnum.SignalFormat.DataRange.FULL)
sdr_range_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_data_range_var,
values=UCDEnum.SignalFormat.DataRange.get_list(),
width=10,
state="readonly",
)
sdr_range_combo.grid(row=2, column=1, sticky=tk.W, padx=5, pady=2)
# 编码位深
ttk.Label(self.sdr_signal_frame, text="编码位深:").grid(
row=3, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_bit_depth_var = tk.StringVar(value=UCDEnum.SignalFormat.BitDepth.BIT_8)
sdr_bit_depth_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_bit_depth_var,
values=UCDEnum.SignalFormat.BitDepth.get_list(),
width=10,
state="readonly",
)
sdr_bit_depth_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
# 分辨率
ttk.Label(self.sdr_signal_frame, text="分辨率:").grid(
row=4, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_timing_var = tk.StringVar(
value=self.config.current_test_types.get("sdr_movie", {}).get(
"timing", "DMT 1920x1080@60Hz"
)
)
sdr_timing_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_timing_var,
values=UCDEnum.TimingInfo.get_formatted_resolution_list(),
width=20,
state="readonly",
)
sdr_timing_combo.bind("<<ComboboxSelected>>", self.on_sdr_timing_changed)
sdr_timing_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
# 色彩格式
ttk.Label(self.sdr_signal_frame, text="色彩格式:").grid(
row=5, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_output_format_var = tk.StringVar(value=UCDEnum.SignalFormat.OutputFormat.RGB)
sdr_output_format_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_output_format_var,
values=UCDEnum.SignalFormat.OutputFormat.get_list(),
width=10,
state="readonly",
)
sdr_output_format_combo.bind("<<ComboboxSelected>>", self.on_sdr_output_format_changed)
sdr_output_format_combo.grid(row=5, column=1, sticky=tk.W, padx=5, pady=2)
# ==================== HDR信号格式设置 ====================
self.hdr_signal_frame = ttk.Frame(self.signal_tabs)
# 配置列权重
self.hdr_signal_frame.grid_columnconfigure(0, weight=0)
self.hdr_signal_frame.grid_columnconfigure(1, weight=1)
self.signal_tabs.add(self.hdr_signal_frame, text="HDR")
# 色彩空间
ttk.Label(self.hdr_signal_frame, text="色彩空间:").grid(
row=0, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_color_space_var = tk.StringVar(value="BT.2020")
hdr_color_space_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_color_space_var,
values=["BT.2020", "DCI-P3"],
width=10,
state="readonly",
)
hdr_color_space_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
# Metadata设置
ttk.Label(self.hdr_signal_frame, text="Metadata:").grid(
row=1, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_metadata_frame = ttk.Frame(self.hdr_signal_frame)
self.hdr_metadata_frame.grid(
row=1, column=1, rowspan=2, sticky=tk.W, padx=5, pady=2
)
ttk.Label(self.hdr_metadata_frame, text="MaxCLL:").grid(
row=0, column=0, sticky=tk.W
)
self.hdr_maxcll_var = tk.StringVar(value="1000")
ttk.Entry(
self.hdr_metadata_frame, textvariable=self.hdr_maxcll_var, width=6
).grid(row=0, column=1, padx=2)
ttk.Label(self.hdr_metadata_frame, text="MaxFALL:").grid(
row=1, column=0, sticky=tk.W
)
self.hdr_maxfall_var = tk.StringVar(value="400")
ttk.Entry(
self.hdr_metadata_frame, textvariable=self.hdr_maxfall_var, width=6
).grid(row=1, column=1, padx=2)
# 数据范围
ttk.Label(self.hdr_signal_frame, text="数据范围:").grid(
row=3, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_data_range_var = tk.StringVar(value=UCDEnum.SignalFormat.DataRange.FULL)
hdr_range_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_data_range_var,
values=UCDEnum.SignalFormat.DataRange.get_list(),
width=10,
state="readonly",
)
hdr_range_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
# 编码位深
ttk.Label(self.hdr_signal_frame, text="编码位深:").grid(
row=4, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_bit_depth_var = tk.StringVar(value=UCDEnum.SignalFormat.BitDepth.BIT_8)
hdr_bit_depth_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_bit_depth_var,
values=UCDEnum.SignalFormat.BitDepth.get_list(),
width=10,
state="readonly",
)
hdr_bit_depth_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
# 色彩格式
ttk.Label(self.hdr_signal_frame, text="色彩格式:").grid(
row=5, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_output_format_var = tk.StringVar(value=UCDEnum.SignalFormat.OutputFormat.RGB)
hdr_output_format_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_output_format_var,
values=UCDEnum.SignalFormat.OutputFormat.get_list(),
width=10,
state="readonly",
)
hdr_output_format_combo.bind("<<ComboboxSelected>>", self.on_hdr_output_format_changed)
hdr_output_format_combo.grid(row=5, column=1, sticky=tk.W, padx=5, pady=2)
# ==================== Local Dimming 信号格式设置 ====================
self.local_dimming_signal_frame = ttk.Frame(self.signal_tabs)
self.local_dimming_signal_frame.grid_columnconfigure(0, weight=0)
self.local_dimming_signal_frame.grid_columnconfigure(1, weight=1)
self.signal_tabs.add(self.local_dimming_signal_frame, text="Local Dimming")
ld_cfg = self.config.current_test_types.get("local_dimming", {})
ttk.Label(self.local_dimming_signal_frame, text="分辨率:").grid(
row=0, column=0, sticky=tk.W, padx=5, pady=2
)
self.local_dimming_timing_var = tk.StringVar(
value=ld_cfg.get("timing", "DMT 1920x 1080 @ 60Hz")
)
ld_timing_combo = ttk.Combobox(
self.local_dimming_signal_frame,
textvariable=self.local_dimming_timing_var,
values=UCDEnum.TimingInfo.get_formatted_resolution_list(),
width=20,
state="readonly",
)
ld_timing_combo.bind("<<ComboboxSelected>>", self.on_local_dimming_timing_changed)
ld_timing_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
ttk.Label(self.local_dimming_signal_frame, text="色彩空间:").grid(
row=1, column=0, sticky=tk.W, padx=5, pady=2
)
self.local_dimming_color_space_var = tk.StringVar(
value=ld_cfg.get("colorimetry", "sRGB")
)
ld_color_space_combo = ttk.Combobox(
self.local_dimming_signal_frame,
textvariable=self.local_dimming_color_space_var,
values=["sRGB", "BT.709", "BT.601", "BT.2020", "DCI-P3"],
width=10,
state="readonly",
)
ld_color_space_combo.bind("<<ComboboxSelected>>", self.on_local_dimming_signal_format_changed)
ld_color_space_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
ttk.Label(self.local_dimming_signal_frame, text="数据范围:").grid(
row=2, column=0, sticky=tk.W, padx=5, pady=2
)
self.local_dimming_data_range_var = tk.StringVar(
value=ld_cfg.get("data_range", UCDEnum.SignalFormat.DataRange.FULL)
)
ld_data_range_combo = ttk.Combobox(
self.local_dimming_signal_frame,
textvariable=self.local_dimming_data_range_var,
values=UCDEnum.SignalFormat.DataRange.get_list(),
width=10,
state="readonly",
)
ld_data_range_combo.bind("<<ComboboxSelected>>", self.on_local_dimming_signal_format_changed)
ld_data_range_combo.grid(row=2, column=1, sticky=tk.W, padx=5, pady=2)
default_ld_bpc = int(ld_cfg.get("bpc", 8))
default_ld_bit_depth = (
f"{default_ld_bpc}bit"
if f"{default_ld_bpc}bit" in UCDEnum.SignalFormat.BitDepth.get_list()
else UCDEnum.SignalFormat.BitDepth.BIT_8
)
ttk.Label(self.local_dimming_signal_frame, text="编码位深:").grid(
row=3, column=0, sticky=tk.W, padx=5, pady=2
)
self.local_dimming_bit_depth_var = tk.StringVar(value=default_ld_bit_depth)
ld_bit_depth_combo = ttk.Combobox(
self.local_dimming_signal_frame,
textvariable=self.local_dimming_bit_depth_var,
values=UCDEnum.SignalFormat.BitDepth.get_list(),
width=10,
state="readonly",
)
ld_bit_depth_combo.bind("<<ComboboxSelected>>", self.on_local_dimming_signal_format_changed)
ld_bit_depth_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
ttk.Label(self.local_dimming_signal_frame, text="色彩格式:").grid(
row=4, column=0, sticky=tk.W, padx=5, pady=2
)
self.local_dimming_output_format_var = tk.StringVar(
value=ld_cfg.get("color_format", UCDEnum.SignalFormat.OutputFormat.RGB)
)
ld_output_format_combo = ttk.Combobox(
self.local_dimming_signal_frame,
textvariable=self.local_dimming_output_format_var,
values=UCDEnum.SignalFormat.OutputFormat.get_list(),
width=10,
state="readonly",
)
ld_output_format_combo.bind("<<ComboboxSelected>>", self.on_local_dimming_signal_format_changed)
ld_output_format_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
# ==================== 初始化:默认只启用屏模组 Tab ====================
self.signal_tabs.select(0) # 选中屏模组
self.signal_tabs.tab(1, state="disabled") # 禁用 SDR
self.signal_tabs.tab(2, state="disabled") # 禁用 HDR
self.signal_tabs.tab(3, state="disabled") # 禁用 Local Dimming
def create_connection_content(self: "PQAutomationApp"):
"""创建设备连接区域"""
# 创建设备连接区域的主框架
com_frame = ttk.Frame(self.connection_frame)
com_frame.pack(fill=tk.X, pady=5)
# 获取可用的COM端口列表和UCD设备列表
available_ports = self.get_available_com_ports()
available_ucd_list = self.get_available_ucd_ports()
# 使用网格布局,更整齐
ttk.Label(com_frame, text="UCD列表:").grid(
row=0, column=0, sticky=ttk.W, padx=5, pady=3
)
self.ucd_list_var = tk.StringVar(value=self.config.device_config["ucd_list"])
self.ucd_list_combo = ttk.Combobox(
com_frame,
textvariable=self.ucd_list_var,
values=available_ucd_list,
width=10,
state="readonly",
)
self.ucd_list_combo.grid(row=0, column=1, sticky=ttk.W, padx=5, pady=3)
self.ucd_list_combo.bind("<<ComboboxSelected>>", self.update_config)
# 添加UCD连接状态指示器
self.ucd_status_indicator = tk.Canvas(
com_frame, width=15, height=15, bg="gray", highlightthickness=0
)
self.ucd_status_indicator.grid(row=0, column=2, padx=(10, 20))
# 添加按钮框架
button_frame = ttk.Frame(com_frame)
button_frame.grid(row=3, column=0, columnspan=3, pady=3, sticky="ew")
button_frame.grid_columnconfigure(0, weight=1)
button_frame.grid_columnconfigure(1, weight=1)
button_frame.grid_columnconfigure(2, weight=1)
# connect_icon = load_icon("assets/connect-svgrepo-com.png")
self.check_button = ttk.Button(
button_frame,
# image=connect_icon,
# bootstyle="link",
text="连接",
bootstyle="success",
takefocus=False,
command=self.check_com_connections,
)
# self.check_button.image = connect_icon
self.check_button.grid(row=0, column=0, padx=(0, 4), pady=3, sticky="ew")
# disconnect_icon = load_icon("assets/disconnect-svgrepo-com.png")
# 断开连接按钮
self.disconnect_button = ttk.Button(
button_frame,
# image=disconnect_icon,
# bootstyle="link",
text="断开",
bootstyle="danger",
takefocus=False,
command=self.disconnect_com_connections,
)
# self.disconnect_button.image = disconnect_icon # 防止图标被垃圾回收
self.disconnect_button.grid(row=0, column=1, padx=4, pady=3, sticky="ew")
# refresh_icon = load_icon("assets/refresh-svgrepo-com.png")
self.refresh_button = ttk.Button(
button_frame,
# image=refresh_icon,
# bootstyle="link",
text="刷新",
bootstyle="info",
takefocus=False,
command=self.refresh_com_ports,
)
# self.refresh_button.image = refresh_icon # 防止图标被垃圾回收
self.refresh_button.grid(row=0, column=2, padx=(4, 0), pady=3, sticky="ew")
# CA端口
ttk.Label(com_frame, text="CA端口:").grid(
row=1, column=0, sticky=ttk.W, padx=5, pady=3
)
self.ca_com_var = tk.StringVar(value=self.config.device_config["ca_com"])
self.ca_com_combo = ttk.Combobox(
com_frame,
textvariable=self.ca_com_var,
values=available_ports,
width=10,
state="readonly",
)
self.ca_com_combo.grid(row=1, column=1, sticky=ttk.W, padx=5, pady=3)
self.ca_com_combo.bind("<<ComboboxSelected>>", self.update_config)
# 添加CA连接状态指示器
self.ca_status_indicator = tk.Canvas(
com_frame, width=15, height=15, bg="gray", highlightthickness=0
)
self.ca_status_indicator.grid(row=1, column=2, padx=(10, 20))
self.refresh_connection_indicators()
# 添加CA通道设置
ttk.Label(com_frame, text="CA通道:").grid(
row=2, column=0, sticky=tk.W, padx=5, pady=3
)
self.ca_channel_var = tk.StringVar(
value=self.config.device_config["ca_channel"]
)
ca_channel_combo = ttk.Combobox(
com_frame,
textvariable=self.ca_channel_var,
values=[str(i) for i in range(11)],
width=10,
state="readonly",
)
ca_channel_combo.grid(row=2, column=1, sticky=ttk.W, padx=5, pady=3)
ca_channel_combo.bind("<<ComboboxSelected>>", self.update_config)
def create_test_type_frame(self: "PQAutomationApp"):
"""创建测试类型选择区域(侧边栏形式)。
"""
# 设置测试类型变量
self.test_type_var = tk.StringVar(value="screen_module")
# ---------- 顶部品牌区 ----------
brand = ttk.Frame(self.sidebar_frame, style="SidebarBrand.TFrame")
brand.pack(fill=tk.X, pady=(2, 10))
ttk.Label(
brand,
text="PQ AUTOMATION",
style="SidebarBrand.TLabel",
).pack(side=tk.LEFT, padx=16, pady=11)
# ---------- 分组:测试类型 ----------
ttk.Label(
self.sidebar_frame,
text="测试类型",
style="SidebarSection.TLabel",
).pack(fill=tk.X, padx=16, pady=(2, 6), anchor="w")
test_types = [
("屏模组性能测试", "screen_module"),
("SDR Movie", "sdr_movie"),
("HDR Movie", "hdr_movie"),
("Local Dimming", "local_dimming"),
]
for text, type_value in test_types:
btn = ttk.Button(
master=self.sidebar_frame,
text=text,
style="Sidebar.TButton",
command=lambda v=type_value: self.change_test_type(v),
takefocus=False,
)
btn.pack(fill=tk.X, padx=0, pady=1)
setattr(self, f"{type_value}_btn", btn)
# ---------- 分组:工具面板 ----------
ttk.Label(
self.sidebar_frame,
text="工具面板",
style="SidebarSection.TLabel",
).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w")
panel_buttons = [
("log_btn", "测试日志", self.toggle_log_panel),
("ai_image_btn", "AI 图片", self.toggle_ai_image_panel),
("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel),
("gamma_pattern_btn", "Gamma 图案", self.toggle_gamma_pattern_panel),
("calman_btn", "CALMAN 灰阶", self.toggle_calman_panel),
]
for attr, text, cmd in panel_buttons:
btn = ttk.Button(
self.sidebar_frame,
text=text,
style="Sidebar.TButton",
command=cmd,
takefocus=False,
)
btn.pack(fill=tk.X, padx=0, pady=1)
setattr(self, attr, btn)
# 测试版水印标签(版本 x.x.0.0 时显示)
from app_version import is_beta_version, APP_VERSION
if is_beta_version():
beta_lbl = ttk.Label(
self.sidebar_frame,
text=f"[测试版] v{APP_VERSION}",
style="SidebarBadge.TLabel",
)
beta_lbl.pack(fill=tk.X, side=tk.BOTTOM, padx=4, pady=(6, 4))
# ---------- 主题切换(底部固定) ----------
self.theme_toggle_btn = ttk.Button(
self.sidebar_frame,
text="切换深色模式",
style="Sidebar.TButton",
command=self._on_toggle_theme,
takefocus=False,
)
self.theme_toggle_btn.pack(fill=tk.X, padx=0, pady=(0, 2), side=tk.BOTTOM)
_refresh_theme_toggle_label(self)
# 注册面板按钮
if hasattr(self, "panels"):
if "log" in self.panels:
self.panels["log"]["button"] = self.log_btn
if "ai_image" in self.panels:
self.panels["ai_image"]["button"] = self.ai_image_btn
if "single_step" in self.panels:
self.panels["single_step"]["button"] = getattr(self, "single_step_btn", None)
if "pantone_baseline" in self.panels:
self.panels["pantone_baseline"]["button"] = self.pantone_baseline_btn
if "gamma_pattern" in self.panels:
self.panels["gamma_pattern"]["button"] = self.gamma_pattern_btn
if "calman" in self.panels:
self.panels["calman"]["button"] = self.calman_btn
def _refresh_theme_toggle_label(self: "PQAutomationApp") -> None:
"""根据当前主题刷新切换按钮的文字。"""
from app.views.theme_manager import is_dark
if not hasattr(self, "theme_toggle_btn"):
return
if is_dark():
self.theme_toggle_btn.configure(text="切换浅色模式")
else:
self.theme_toggle_btn.configure(text="切换深色模式")
def _on_toggle_theme(self: "PQAutomationApp") -> None:
"""切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。"""
from app.views.theme_manager import toggle_theme
toggle_theme()
# apply_modern_styles()
_refresh_theme_toggle_label(self)
if hasattr(self, "refresh_connection_indicators"):
try:
self.refresh_connection_indicators()
except Exception:
pass
if hasattr(self, "apply_result_chart_theme"):
try:
self.apply_result_chart_theme()
except Exception:
pass
if hasattr(self, "log_gui") and hasattr(self.log_gui, "refresh_log_theme"):
try:
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()
except Exception:
pass
# 同步刷新侧栏选中态(高亮样式跟随新色板)
if hasattr(self, "update_sidebar_selection"):
try:
self.update_sidebar_selection()
except Exception:
pass
# 以新的 dark_mode 值重绘当前测试类型的所有图表
if hasattr(self, "_chart_snapshots") and hasattr(self, "config"):
test_type = getattr(self.config, "current_test_type", None)
if test_type:
snapshots = self._chart_snapshots.get(test_type, {})
for chart_name, args in snapshots.items():
plot_fn = getattr(self, f"plot_{chart_name}", None)
if plot_fn:
try:
plot_fn(*args)
except Exception:
pass
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()
info_text = f"测试类型: {current_config.get('name', '未知')}\n"
info_text += (
f"测试项目: {', '.join(current_config.get('test_items', []))}\n"
)
info_text += f"信号格式: {current_config.get('signal_format', 'none')}\n"
info_text += f"色彩空间: {current_config.get('color_space', 'unknown')}\n"
info_text += f"位深度: {current_config.get('bit_depth', 'unknown')}"
# 高亮当前选中的测试类型
self.update_sidebar_selection()
def create_operation_frame(self: "PQAutomationApp"):
"""创建操作按钮区域。
新布局:按钮挂到配置项 header 右侧常驻工具条 ``self.toolbar_actions_frame``。
若该容器尚未创建(极端情况下顺序异常),回退到原 control_frame_top 位置,
确保按钮始终可见、调用方代码不破。
"""
parent = getattr(self, "toolbar_actions_frame", None)
fallback = parent is None
if fallback:
parent = ttk.Frame(self.control_frame_top)
parent.pack(fill=tk.X, padx=5, pady=8)
# 用 bootstyle 取代旧 style="xxx.TButton",跟随主题;
# 给按钮统一加 padding触摸友好。
btn_pad = dict(padx=4, pady=0)
self.start_btn = ttk.Button(
parent,
text="\u25b6 开始测试",
command=self.start_test,
bootstyle="success",
padding=(12, 6),
takefocus=False,
)
self.start_btn.pack(side=tk.LEFT, **btn_pad)
self.stop_btn = ttk.Button(
parent,
text="\u25a0 停止",
command=self.stop_test,
bootstyle="danger",
padding=(12, 6),
state=tk.DISABLED,
takefocus=False,
)
self.stop_btn.pack(side=tk.LEFT, **btn_pad)
# 分隔
ttk.Separator(parent, orient="vertical").pack(side=tk.LEFT, fill="y", padx=8, pady=4)
self.save_btn = ttk.Button(
parent,
text="保存结果",
command=self.save_results,
bootstyle="info-outline",
padding=(12, 6),
state=tk.DISABLED,
takefocus=False,
)
self.save_btn.pack(side=tk.LEFT, **btn_pad)
self.custom_btn = ttk.Button(
parent,
text="客户模版",
command=self.start_custom_template_test,
bootstyle="info",
padding=(12, 6),
takefocus=False,
)
self.custom_btn.pack(side=tk.LEFT, **btn_pad)
self.clear_config_btn = ttk.Button(
parent,
text="清理配置",
command=self.clear_config_file,
bootstyle="secondary-outline",
padding=(10, 6),
takefocus=False,
)
self.clear_config_btn.pack(side=tk.LEFT, **btn_pad)
self.update_custom_button_visibility()
def on_screen_module_timing_changed(self: "PQAutomationApp", event=None):
"""屏模组信号格式改变时的回调"""
try:
selected_timing = self.screen_module_timing_var.get()
# 记录日志
self.log_gui.log(f"屏模组信号格式已更改为: {selected_timing}", level="info")
match = re.search(r"(\d+)x(\d+)\s*@\s*(\d+)", selected_timing)
if match:
width = int(match.group(1))
height = int(match.group(2))
refresh_rate = int(match.group(3))
self.log_gui.log(f" ├─ 分辨率: {width}x{height}", level="info")
self.log_gui.log(f" └─ 刷新率: {refresh_rate}Hz", level="info")
# 根据分辨率给出提示
if width >= 3840: # 4K及以上
self.log_gui.log(" 检测到4K分辨率", level="info")
if refresh_rate >= 120:
self.log_gui.log(" 检测到高刷新率", level="info")
# 更新屏模组配置(独立于 current_test_type
self.config.current_test_types.setdefault("screen_module", {})[
"timing"
] = selected_timing
# 如果正在测试,提示用户
if self.testing:
self.log_gui.log("警告: 测试进行中,信号格式更改将在下次测试时生效", level="error")
# 保存配置
self.save_pq_config()
except Exception as e:
self.log_gui.log(f"屏模组信号格式更改失败: {str(e)}", level="error")
def on_screen_module_signal_format_changed(self: "PQAutomationApp", event=None):
"""屏模组 ColorInfo 相关选项变更回调。"""
try:
color_space = self.screen_module_color_space_var.get()
data_range = self.screen_module_data_range_var.get()
bit_depth = self.screen_module_bit_depth_var.get()
output_format = self.screen_module_output_format_var.get()
screen_cfg = self.config.current_test_types.setdefault("screen_module", {})
screen_cfg["colorimetry"] = color_space
screen_cfg["color_format"] = output_format
screen_cfg["bpc"] = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
screen_cfg["data_range"] = data_range
self.log_gui.log(
(
"屏模组信号格式已更新: "
f"色彩空间={color_space}, 数据范围={data_range}, "
f"位深={bit_depth}, 色彩格式={output_format}"
),
level="info",
)
if self.testing:
self.log_gui.log("警告: 测试进行中,格式更改将在下次测试时生效", level="error")
self.save_pq_config()
return
if getattr(self.ucd, "status", False):
ok = self.signal_service.update_signal_format(
color_space=color_space,
data_range=data_range,
bit_depth=bit_depth,
output_format=output_format,
)
if not ok:
self.log_gui.log("屏模组信号格式应用到UCD失败", level="error")
self.save_pq_config()
except Exception as e:
self.log_gui.log(f"屏模组信号格式更改失败: {str(e)}", level="error")
def on_sdr_timing_changed(self: "PQAutomationApp", event=None):
"""SDR测试分辨率改变时的回调"""
try:
selected_timing = self.sdr_timing_var.get()
self.log_gui.log(f"SDR测试分辨率已更改为: {selected_timing}", level="info")
# 直接更新 sdr_movie 的 timing 配置
self.config.current_test_types["sdr_movie"]["timing"] = selected_timing
if self.testing:
self.log_gui.log("警告: 测试进行中,分辨率更改将在下次测试时生效", level="error")
self.save_pq_config()
except Exception as e:
self.log_gui.log(f"SDR测试分辨率更改失败: {str(e)}", level="error")
def on_sdr_output_format_changed(self: "PQAutomationApp", event=None):
"""SDR 色彩格式改变时的回调"""
try:
fmt = self.sdr_output_format_var.get()
self.log_gui.log(f"SDR色彩格式已更改为: {fmt}", level="info")
if self.testing:
self.log_gui.log("警告: 测试进行中,格式更改将在下次测试时生效", level="error")
return
if getattr(self.ucd, "status", False):
ok = self.signal_service.update_signal_format(
color_space=self.sdr_color_space_var.get(),
data_range=self.sdr_data_range_var.get(),
bit_depth=self.sdr_bit_depth_var.get(),
output_format=fmt,
)
if not ok:
self.log_gui.log("SDR色彩格式应用到UCD失败", level="error")
except Exception as e:
self.log_gui.log(f"SDR色彩格式更改失败: {str(e)}", level="error")
def on_hdr_output_format_changed(self: "PQAutomationApp", event=None):
"""HDR 色彩格式改变时的回调"""
try:
fmt = self.hdr_output_format_var.get()
self.log_gui.log(f"HDR色彩格式已更改为: {fmt}", level="info")
if self.testing:
self.log_gui.log("警告: 测试进行中,格式更改将在下次测试时生效", level="error")
return
if getattr(self.ucd, "status", False):
ok = self.signal_service.update_signal_format(
color_space=self.hdr_color_space_var.get(),
data_range=self.hdr_data_range_var.get(),
bit_depth=self.hdr_bit_depth_var.get(),
max_cll=self.hdr_maxcll_var.get(),
max_fall=self.hdr_maxfall_var.get(),
output_format=fmt,
)
if not ok:
self.log_gui.log("HDR色彩格式应用到UCD失败", level="error")
except Exception as e:
self.log_gui.log(f"HDR色彩格式更改失败: {str(e)}", level="error")
def on_local_dimming_timing_changed(self: "PQAutomationApp", event=None):
"""Local Dimming 分辨率改变时的回调。"""
try:
selected_timing = self.local_dimming_timing_var.get()
self.log_gui.log(f"Local Dimming 分辨率已更改为: {selected_timing}", level="info")
self.config.current_test_types.setdefault("local_dimming", {})["timing"] = selected_timing
if self.testing:
self.log_gui.log("警告: 测试进行中,分辨率更改将在下次测试时生效", level="error")
self.save_pq_config()
except Exception as e:
self.log_gui.log(f"Local Dimming 分辨率更改失败: {str(e)}", level="error")
def on_local_dimming_signal_format_changed(self: "PQAutomationApp", event=None):
"""Local Dimming ColorInfo 相关选项变更回调。"""
try:
color_space = self.local_dimming_color_space_var.get()
data_range = self.local_dimming_data_range_var.get()
bit_depth = self.local_dimming_bit_depth_var.get()
output_format = self.local_dimming_output_format_var.get()
ld_cfg = self.config.current_test_types.setdefault("local_dimming", {})
ld_cfg["colorimetry"] = color_space
ld_cfg["color_format"] = output_format
ld_cfg["bpc"] = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
ld_cfg["data_range"] = data_range
self.log_gui.log(
(
"Local Dimming 信号格式已更新: "
f"色彩空间={color_space}, 数据范围={data_range}, "
f"位深={bit_depth}, 色彩格式={output_format}"
),
level="info",
)
if self.testing:
self.log_gui.log("警告: 测试进行中,格式更改将在下次测试时生效", level="error")
self.save_pq_config()
return
if getattr(self.ucd, "status", False):
ok = self.signal_service.update_signal_format(
color_space=color_space,
data_range=data_range,
bit_depth=bit_depth,
output_format=output_format,
)
if not ok:
self.log_gui.log("Local Dimming 信号格式应用到UCD失败", level="error")
self.save_pq_config()
except Exception as e:
self.log_gui.log(f"Local Dimming 信号格式更改失败: {str(e)}", level="error")
def update_test_items(self: "PQAutomationApp"):
"""根据当前测试类型更新测试项目复选框"""
# 先隐藏所有测试项目框架
for config in self.test_items.values():
config["frame"].pack_forget()
current_test_type = self.config.current_test_type
self.test_vars = {}
if current_test_type in self.test_items:
config = self.test_items[current_test_type]
frame = config["frame"]
frame.pack(fill=tk.X, padx=5, pady=5)
# 添加测试类型标签
type_label = ttk.Label(
frame,
text=self.get_test_type_name(current_test_type),
style="primary.TLabel",
)
type_label.grid(row=0, column=0, columnspan=2, sticky=tk.W, padx=5, pady=3)
# 从配置中读取保存的选择状态
saved_test_items = self.config.current_test_types[current_test_type].get(
"test_items", []
)
# 添加复选框
toggle_bootstyle = "success-round-toggle"
for i, (text, var_name) in enumerate(config["items"]):
is_checked = var_name in saved_test_items
var = tk.BooleanVar(value=is_checked)
self.test_vars[f"{current_test_type}_{var_name}"] = var
ttk.Checkbutton(
frame,
text=text,
variable=var,
bootstyle=toggle_bootstyle,
command=self.update_config_and_tabs,
).grid(row=i // 2 + 1, column=i % 2, sticky=tk.W, padx=10, pady=5)
if hasattr(self, "chart_notebook"):
self.update_chart_tabs_state()
if hasattr(self, "cct_params_frame"):
self.toggle_cct_params_frame()
# 同步刷新 header 折叠预览
refresh_config_preview(self)
def on_test_type_change(self: "PQAutomationApp"):
"""根据测试类型更新内容区域"""
# 更新配置信息显示
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
self.update_config_info_display()
# SDR 选中时显示客户模版按钮
self.update_custom_button_visibility()
# Local Dimming 作为并列测试类型时,自动显示其专用面板。
if self.config.current_test_type == "local_dimming":
self.show_panel("local_dimming")
elif getattr(self, "current_panel", None) == "local_dimming":
self.hide_all_panels()
class MainLayoutMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
create_floating_config_panel = create_floating_config_panel
refresh_config_preview = refresh_config_preview
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_toggle_theme = _on_toggle_theme
on_screen_module_timing_changed = on_screen_module_timing_changed
on_screen_module_signal_format_changed = on_screen_module_signal_format_changed
on_sdr_timing_changed = on_sdr_timing_changed
on_sdr_output_format_changed = on_sdr_output_format_changed
on_hdr_output_format_changed = on_hdr_output_format_changed
on_local_dimming_timing_changed = on_local_dimming_timing_changed
on_local_dimming_signal_format_changed = on_local_dimming_signal_format_changed
update_test_items = update_test_items
on_test_type_change = on_test_type_change