From 1aab2d2453d461890b2d8dd4c99cebdfd7a456e4 Mon Sep 17 00:00:00 2001 From: "xinzhu.yin" Date: Mon, 20 Apr 2026 16:57:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9C=AC=E5=9C=B0=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pqAutomationApp.py | 2316 +++++++++++++++++++++++++++++++++++++-- settings/pq_config.json | 2 +- 2 files changed, 2218 insertions(+), 100 deletions(-) diff --git a/pqAutomationApp.py b/pqAutomationApp.py index 4d2285e..8d868ac 100644 --- a/pqAutomationApp.py +++ b/pqAutomationApp.py @@ -24,8 +24,6 @@ from app.pq.pq_result import PQResult from app.data_range_converter import convert_pattern_params from PIL import Image, ImageTk from app.views.collapsing_frame import CollapsingFrame - -# from app.views.pq_history_gui import PQHistoryGUI from app.views.pq_log_gui import PQLogGUI from colormath.color_objects import xyYColor, LabColor from colormath.color_conversions import convert_color @@ -110,57 +108,6 @@ from app.runner.test_runner import ( test_gamma as _run_test_gamma, test_gamut as _run_test_gamut, ) -from app.views.panel_manager import ( - hide_all_panels as _pm_hide_all_panels, - register_panel as _pm_register_panel, - show_panel as _pm_show_panel, -) -from app.views.panels.side_panels import ( - create_local_dimming_panel as _side_create_local_dimming_panel, - create_log_panel as _side_create_log_panel, - toggle_hdr_debug_panel as _side_toggle_hdr_debug_panel, - toggle_local_dimming_panel as _side_toggle_local_dimming_panel, - toggle_log_panel as _side_toggle_log_panel, - toggle_screen_debug_panel as _side_toggle_screen_debug_panel, - toggle_sdr_debug_panel as _side_toggle_sdr_debug_panel, -) -from app.views.panels.custom_template_panel import ( - _clear_custom_result_row as _ctpl__clear_custom_result_row, - _run_custom_row_single_step as _ctpl__run_custom_row_single_step, - _update_custom_result_row as _ctpl__update_custom_result_row, - append_custom_template_result as _ctpl_append_custom_template_result, - auto_expand_custom_result_view as _ctpl_auto_expand_custom_result_view, - clear_custom_template_results as _ctpl_clear_custom_template_results, - copy_custom_result_table as _ctpl_copy_custom_result_table, - create_custom_template_result_panel as _ctpl_create_custom_template_result_panel, - fill_custom_result_test_data as _ctpl_fill_custom_result_test_data, - set_custom_result_table_locked as _ctpl_set_custom_result_table_locked, - show_custom_result_context_menu as _ctpl_show_custom_result_context_menu, - start_custom_row_single_step as _ctpl_start_custom_row_single_step, - start_custom_template_test as _ctpl_start_custom_template_test, -) -from app.views.panels.main_layout import ( - create_connection_content as _layout_create_connection_content, - create_floating_config_panel as _layout_create_floating_config_panel, - create_operation_frame as _layout_create_operation_frame, - create_signal_format_content as _layout_create_signal_format_content, - create_test_items_content as _layout_create_test_items_content, - create_test_type_frame as _layout_create_test_type_frame, - update_config_info_display as _layout_update_config_info_display, -) -from app.views.panels.cct_panel import ( - create_cct_params_frame as _cct_create_cct_params_frame, - on_cct_param_change as _cct_on_cct_param_change, - on_cct_param_focus_out as _cct_on_cct_param_focus_out, - on_hdr_cct_param_focus_out as _cct_on_hdr_cct_param_focus_out, - on_sdr_cct_param_focus_out as _cct_on_sdr_cct_param_focus_out, - recalculate_cct as _cct_recalculate_cct, - recalculate_gamut as _cct_recalculate_gamut, - reload_cct_params as _cct_reload_cct_params, - save_cct_params as _cct_save_cct_params, - save_hdr_cct_params as _cct_save_hdr_cct_params, - save_sdr_cct_params as _cct_save_sdr_cct_params, -) plt.rcParams["font.family"] = ["sans-serif"] plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"] @@ -283,11 +230,11 @@ class PQAutomationApp: self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) def _dispatch_ui(self, fn, *args, **kwargs): - """把 fn(*args, **kwargs) 调度到 Tk 主线程执行。 + """把 ``fn(*args, **kwargs)`` 调度到 Tk 主线程执行。 - 统一替代散落各处的 self.root.after(0, lambda: ...) 写法: + 统一替代散落各处的 ``self.root.after(0, lambda: ...)`` 写法: - 自动捕获异常并记录日志,避免工作线程静默丢失 UI 更新失败; - - 参数用位置/关键字传入,绕开 lambda 闭包捕获变量的常见坑; + - 参数用位置/关键字传入,绕开 ``lambda`` 闭包捕获变量的常见坑; - 允许在 UI 销毁(如关闭窗口)后安全失败。 """ def _runner(): @@ -325,31 +272,818 @@ class PQAutomationApp: init_contrast_chart = _cf_init_contrast_chart init_accuracy_chart = _cf_init_accuracy_chart clear_chart = _cf_clear_chart - create_floating_config_panel = _layout_create_floating_config_panel + def create_floating_config_panel(self): + """创建右上角悬浮配置框""" + cf = CollapsingFrame(self.control_frame_top) + cf.pack(fill="both") + # 创建悬浮框主容器 + self.config_panel_frame = ttk.Frame(cf) + cf.add(self.config_panel_frame, title="配置项") - create_test_items_content = _layout_create_test_items_content + # 创建一个统一的frame来替代选项卡控件 + self.config_content_frame = ttk.Frame(self.config_panel_frame) + self.config_content_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) - create_cct_params_frame = _cct_create_cct_params_frame + # 创建一个横向排列的Frame + config_row_frame = ttk.Frame(self.config_content_frame) + config_row_frame.pack(fill=tk.X, expand=False, padx=5, pady=5) - on_sdr_cct_param_focus_out = _cct_on_sdr_cct_param_focus_out + # 创建连接内容区域 + self.connection_frame = ttk.LabelFrame(config_row_frame, text="设备连接") + self.connection_frame.pack( + side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5 + ) - save_sdr_cct_params = _cct_save_sdr_cct_params + # 创建测试项目区域 + self.test_items_frame = ttk.LabelFrame(config_row_frame, text="测试项目") + self.test_items_frame.pack( + side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5 + ) - on_hdr_cct_param_focus_out = _cct_on_hdr_cct_param_focus_out + # 创建信号格式区域 + self.signal_format_frame = ttk.LabelFrame(config_row_frame, text="信号格式") + self.signal_format_frame.pack( + side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5 + ) - save_hdr_cct_params = _cct_save_hdr_cct_params + # 创建连接内容 + self.create_connection_content() + # 创建测试项目内容 + self.create_test_items_content() + # 创建信号格式内容 + self.create_signal_format_content() - recalculate_cct = _cct_recalculate_cct + self.config_panel_frame.grid_remove() + self.config_panel_frame.btn.configure(image="closed") - recalculate_gamut = _cct_recalculate_gamut + def create_test_items_content(self): + """创建测试项目选项卡内容""" + # 创建测试项目字典,用于管理不同测试类型的选项 + 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"), + ], + }, + } - on_cct_param_change = _cct_on_cct_param_change + # 根据当前测试类型创建复选框 + self.test_vars = {} + self.update_test_items() - on_cct_param_focus_out = _cct_on_cct_param_focus_out + # 创建色度参数设置框架 + self.create_cct_params_frame() - save_cct_params = _cct_save_cct_params + def create_cct_params_frame(self): + """创建色度参数设置区域 - 屏模组、SDR、HDR 独立(✅ 增加色域参考标准选择 + 单步调试按钮)""" - reload_cct_params = _cct_reload_cct_params + # ==================== 屏模组色度参数 Frame ==================== + self.cct_params_frame = ttk.LabelFrame( + self.test_items_frame, text="色度参数设置(屏模组)" + ) + + # 默认值 + screen_default_cct_params = self.config.get_default_cct_params("screen_module") + + # 从配置读取屏模组参数 + saved_params = self.config.current_test_types.get("screen_module", {}).get( + "cct_params", screen_default_cct_params.copy() + ) + + # 色域参考标准 + saved_gamut_ref = self.config.current_test_types.get("screen_module", {}).get( + "gamut_reference", self.config.get_default_gamut_reference("screen_module") + ) + + # 创建屏模组变量 + self.cct_x_ideal_var = tk.StringVar( + value=str(saved_params.get("x_ideal", 0.3127)) + ) + self.cct_x_tolerance_var = tk.StringVar( + value=str(saved_params.get("x_tolerance", 0.003)) + ) + self.cct_y_ideal_var = tk.StringVar( + value=str(saved_params.get("y_ideal", 0.3290)) + ) + self.cct_y_tolerance_var = tk.StringVar( + value=str(saved_params.get("y_tolerance", 0.003)) + ) + self.screen_gamut_ref_var = tk.StringVar(value=saved_gamut_ref) + + # 创建屏模组输入框(左侧:色度参数) + params = [ + ("x-ideal:", self.cct_x_ideal_var, "x_ideal"), + ("x-tolerance:", self.cct_x_tolerance_var, "x_tolerance"), + ("y-ideal:", self.cct_y_ideal_var, "y_ideal"), + ("y-tolerance:", self.cct_y_tolerance_var, "y_tolerance"), + ] + + for i, (label_text, var, key) in enumerate(params): + ttk.Label(self.cct_params_frame, text=label_text).grid( + row=i, column=0, sticky=tk.W, padx=5, pady=3 + ) + entry = ttk.Entry(self.cct_params_frame, textvariable=var, width=15) + entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3) + + # 绑定失去焦点事件 + default_val = screen_default_cct_params[key] + entry.bind( + "", + lambda e, v=var, d=default_val: self.on_cct_param_focus_out(v, d), + ) + + # 色域参考标准选择(右侧第一行) + ttk.Label(self.cct_params_frame, text="色域参考标准:").grid( + row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3 + ) + screen_gamut_combo = ttk.Combobox( + self.cct_params_frame, + textvariable=self.screen_gamut_ref_var, + values=["BT.2020", "BT.709", "DCI-P3"], + state="disabled", + width=12, + ) + screen_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3) + screen_gamut_combo.bind( + "<>", self.on_screen_gamut_ref_changed + ) + self.screen_gamut_combo = screen_gamut_combo + + # ==================== ✅ 单步调试按钮(右侧第二行)==================== + ttk.Label(self.cct_params_frame, text="单步调试:").grid( + row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3 + ) + + self.screen_debug_btn = ttk.Button( + self.cct_params_frame, + text="打开调试面板", + command=self.toggle_screen_debug_panel, + bootstyle="info-outline", + state=tk.DISABLED, # 初始禁用 + width=15, + ) + self.screen_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3) + + # 重新计算按钮(屏模组) + self.recalc_cct_btn = ttk.Button( + self.cct_params_frame, + text="应用新参数并重绘", + command=self.recalculate_cct, + bootstyle="success", + ) + self.recalc_cct_btn.grid( + row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew" + ) + self.recalc_cct_btn.grid_remove() + + # 色域重新计算按钮 + self.recalc_gamut_btn = ttk.Button( + self.cct_params_frame, + text="应用色域参考并重绘", + command=self.recalculate_gamut, + bootstyle="warning", + ) + self.recalc_gamut_btn.grid( + row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew" + ) + self.recalc_gamut_btn.grid_remove() + + # 提示文字 + ttk.Label( + self.cct_params_frame, + text="提示: 清空输入框将恢复默认值", + font=("SimHei", 8), + foreground="gray", + ).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5) + + # ==================== SDR 色度参数 Frame ==================== + self.sdr_cct_params_frame = ttk.LabelFrame( + self.test_items_frame, text="色度参数设置(SDR)" + ) + + # SDR 默认值 + sdr_default_cct_params = self.config.get_default_cct_params("sdr_movie") + + # 从配置读取 SDR 参数 + sdr_saved_params = self.config.current_test_types.get("sdr_movie", {}).get( + "cct_params", sdr_default_cct_params.copy() + ) + + # 色域参考标准 + sdr_saved_gamut_ref = self.config.current_test_types.get("sdr_movie", {}).get( + "gamut_reference", self.config.get_default_gamut_reference("sdr_movie") + ) + + # 创建 SDR 变量 + self.sdr_cct_x_ideal_var = tk.StringVar( + value=str(sdr_saved_params.get("x_ideal", 0.3127)) + ) + self.sdr_cct_x_tolerance_var = tk.StringVar( + value=str(sdr_saved_params.get("x_tolerance", 0.003)) + ) + self.sdr_cct_y_ideal_var = tk.StringVar( + value=str(sdr_saved_params.get("y_ideal", 0.3290)) + ) + self.sdr_cct_y_tolerance_var = tk.StringVar( + value=str(sdr_saved_params.get("y_tolerance", 0.003)) + ) + self.sdr_gamut_ref_var = tk.StringVar(value=sdr_saved_gamut_ref) + + # 创建 SDR 输入框 + sdr_params = [ + ("x-ideal:", self.sdr_cct_x_ideal_var, "x_ideal"), + ("x-tolerance:", self.sdr_cct_x_tolerance_var, "x_tolerance"), + ("y-ideal:", self.sdr_cct_y_ideal_var, "y_ideal"), + ("y-tolerance:", self.sdr_cct_y_tolerance_var, "y_tolerance"), + ] + + for i, (label_text, var, key) in enumerate(sdr_params): + ttk.Label(self.sdr_cct_params_frame, text=label_text).grid( + row=i, column=0, sticky=tk.W, padx=5, pady=3 + ) + entry = ttk.Entry(self.sdr_cct_params_frame, textvariable=var, width=15) + entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3) + + # 绑定失去焦点事件 + default_val = sdr_default_cct_params[key] + entry.bind( + "", + lambda e, v=var, d=default_val: self.on_sdr_cct_param_focus_out(v, d), + ) + + # 色域参考标准选择(右侧第一行) + ttk.Label(self.sdr_cct_params_frame, text="色域参考标准:").grid( + row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3 + ) + sdr_gamut_combo = ttk.Combobox( + self.sdr_cct_params_frame, + textvariable=self.sdr_gamut_ref_var, + values=["BT.2020", "BT.709", "DCI-P3"], + state="disabled", + width=12, + ) + sdr_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3) + sdr_gamut_combo.bind("<>", self.on_sdr_gamut_ref_changed) + self.sdr_gamut_combo = sdr_gamut_combo + + # ==================== ✅ SDR 单步调试按钮(右侧第二行)==================== + ttk.Label(self.sdr_cct_params_frame, text="单步调试:").grid( + row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3 + ) + + self.sdr_debug_btn = ttk.Button( + self.sdr_cct_params_frame, + text="打开调试面板", + command=self.toggle_sdr_debug_panel, + bootstyle="info-outline", + state=tk.DISABLED, # 初始禁用 + width=15, + ) + self.sdr_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3) + + # 重新计算按钮(SDR) + self.sdr_recalc_cct_btn = ttk.Button( + self.sdr_cct_params_frame, + text="应用新参数并重绘", + command=self.recalculate_cct, + bootstyle="success", + ) + self.sdr_recalc_cct_btn.grid( + row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew" + ) + self.sdr_recalc_cct_btn.grid_remove() + + # 色域重新计算按钮(SDR) + self.sdr_recalc_gamut_btn = ttk.Button( + self.sdr_cct_params_frame, + text="应用色域参考并重绘", + command=self.recalculate_gamut, + bootstyle="warning", + ) + self.sdr_recalc_gamut_btn.grid( + row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew" + ) + self.sdr_recalc_gamut_btn.grid_remove() + + # 提示文字 + ttk.Label( + self.sdr_cct_params_frame, + text="提示: 清空输入框将恢复默认值", + font=("SimHei", 8), + foreground="gray", + ).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5) + + # ==================== HDR 色度参数 Frame ==================== + self.hdr_cct_params_frame = ttk.LabelFrame( + self.test_items_frame, text="色度参数设置(HDR)" + ) + + # HDR 默认值 + hdr_default_cct_params = self.config.get_default_cct_params("hdr_movie") + + # 从配置读取 HDR 参数 + hdr_saved_params = self.config.current_test_types.get("hdr_movie", {}).get( + "cct_params", hdr_default_cct_params.copy() + ) + + # 色域参考标准 + hdr_saved_gamut_ref = self.config.current_test_types.get("hdr_movie", {}).get( + "gamut_reference", self.config.get_default_gamut_reference("hdr_movie") + ) + + # 创建 HDR 变量 + self.hdr_cct_x_ideal_var = tk.StringVar( + value=str(hdr_saved_params.get("x_ideal", 0.3127)) + ) + self.hdr_cct_x_tolerance_var = tk.StringVar( + value=str(hdr_saved_params.get("x_tolerance", 0.003)) + ) + self.hdr_cct_y_ideal_var = tk.StringVar( + value=str(hdr_saved_params.get("y_ideal", 0.3290)) + ) + self.hdr_cct_y_tolerance_var = tk.StringVar( + value=str(hdr_saved_params.get("y_tolerance", 0.003)) + ) + self.hdr_gamut_ref_var = tk.StringVar(value=hdr_saved_gamut_ref) + + # 创建 HDR 输入框 + hdr_params = [ + ("x-ideal:", self.hdr_cct_x_ideal_var, "x_ideal"), + ("x-tolerance:", self.hdr_cct_x_tolerance_var, "x_tolerance"), + ("y-ideal:", self.hdr_cct_y_ideal_var, "y_ideal"), + ("y-tolerance:", self.hdr_cct_y_tolerance_var, "y_tolerance"), + ] + + for i, (label_text, var, key) in enumerate(hdr_params): + ttk.Label(self.hdr_cct_params_frame, text=label_text).grid( + row=i, column=0, sticky=tk.W, padx=5, pady=3 + ) + entry = ttk.Entry(self.hdr_cct_params_frame, textvariable=var, width=15) + entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3) + + # 绑定失去焦点事件 + default_val = hdr_default_cct_params[key] + entry.bind( + "", + lambda e, v=var, d=default_val: self.on_hdr_cct_param_focus_out(v, d), + ) + + # 色域参考标准选择(右侧第一行) + ttk.Label(self.hdr_cct_params_frame, text="色域参考标准:").grid( + row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3 + ) + hdr_gamut_combo = ttk.Combobox( + self.hdr_cct_params_frame, + textvariable=self.hdr_gamut_ref_var, + values=["BT.2020", "BT.709", "DCI-P3"], + state="disabled", + width=12, + ) + hdr_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3) + hdr_gamut_combo.bind("<>", self.on_hdr_gamut_ref_changed) + self.hdr_gamut_combo = hdr_gamut_combo + + # ==================== ✅ HDR 单步调试按钮(右侧第二行)==================== + ttk.Label(self.hdr_cct_params_frame, text="单步调试:").grid( + row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3 + ) + + self.hdr_debug_btn = ttk.Button( + self.hdr_cct_params_frame, + text="打开调试面板", + command=self.toggle_hdr_debug_panel, + bootstyle="info-outline", + state=tk.DISABLED, # 初始禁用 + width=15, + ) + self.hdr_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3) + + # 重新计算按钮(HDR) + self.hdr_recalc_cct_btn = ttk.Button( + self.hdr_cct_params_frame, + text="应用新参数并重绘", + command=self.recalculate_cct, + bootstyle="success", + ) + self.hdr_recalc_cct_btn.grid( + row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew" + ) + self.hdr_recalc_cct_btn.grid_remove() + + # 色域重新计算按钮(HDR) + self.hdr_recalc_gamut_btn = ttk.Button( + self.hdr_cct_params_frame, + text="应用色域参考并重绘", + command=self.recalculate_gamut, + bootstyle="warning", + ) + self.hdr_recalc_gamut_btn.grid( + row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew" + ) + self.hdr_recalc_gamut_btn.grid_remove() + + # 提示文字 + ttk.Label( + self.hdr_cct_params_frame, + text="提示: 清空输入框将恢复默认值", + font=("SimHei", 8), + foreground="gray", + ).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5) + + def _get_cct_var_dict(self, test_type): + """按测试类型返回 CCT 变量映射。""" + if test_type == "sdr_movie": + return { + "x_ideal": self.sdr_cct_x_ideal_var, + "x_tolerance": self.sdr_cct_x_tolerance_var, + "y_ideal": self.sdr_cct_y_ideal_var, + "y_tolerance": self.sdr_cct_y_tolerance_var, + } + if test_type == "hdr_movie": + return { + "x_ideal": self.hdr_cct_x_ideal_var, + "x_tolerance": self.hdr_cct_x_tolerance_var, + "y_ideal": self.hdr_cct_y_ideal_var, + "y_tolerance": self.hdr_cct_y_tolerance_var, + } + return { + "x_ideal": self.cct_x_ideal_var, + "x_tolerance": self.cct_x_tolerance_var, + "y_ideal": self.cct_y_ideal_var, + "y_tolerance": self.cct_y_tolerance_var, + } + + def _parse_cct_float(self, var, default): + """读取并解析 CCT 输入值,失败时回落默认值。""" + try: + value = var.get().strip() + if value == "": + return default + return float(value) + except Exception: + return default + + def _save_cct_params_for(self, test_type): + """保存指定测试类型的 CCT 参数。""" + try: + default_params = self.config.get_default_cct_params(test_type) + var_dict = self._get_cct_var_dict(test_type) + cct_params = { + key: self._parse_cct_float(var_dict[key], default_params[key]) + for key in default_params + } + + if test_type not in self.config.current_test_types: + self.config.current_test_types[test_type] = {} + + self.config.current_test_types[test_type]["cct_params"] = cct_params + self.save_pq_config() + except Exception: + pass + + def _handle_cct_focus_out(self, var, default_value, save_func, label): + """统一处理 CCT 参数失焦校验并保存。""" + try: + value = var.get().strip() + if value == "": + var.set(str(default_value)) + if hasattr(self, "log_gui"): + self.log_gui.log(f"✓ {label} 参数为空,恢复默认值: {default_value}") + else: + try: + float_val = float(value) + if float_val < 0 or float_val > 1: + var.set(str(default_value)) + if hasattr(self, "log_gui"): + self.log_gui.log( + f"⚠️ {label} 参数超出范围,恢复默认值: {default_value}" + ) + except ValueError: + var.set(str(default_value)) + if hasattr(self, "log_gui"): + self.log_gui.log( + f"⚠️ {label} 参数无效,恢复默认值: {default_value}" + ) + + save_func() + except Exception as e: + if hasattr(self, "log_gui"): + self.log_gui.log(f"处理 {label} 参数失败: {str(e)}") + + def on_sdr_cct_param_focus_out(self, var, default_value): + """SDR 色度参数失去焦点时的处理。""" + self._handle_cct_focus_out(var, default_value, self.save_sdr_cct_params, "SDR") + + def save_sdr_cct_params(self): + """保存 SDR 色度参数。""" + self._save_cct_params_for("sdr_movie") + + def on_hdr_cct_param_focus_out(self, var, default_value): + """HDR 色度参数失去焦点时的处理。""" + self._handle_cct_focus_out(var, default_value, self.save_hdr_cct_params, "HDR") + + def save_hdr_cct_params(self): + """保存 HDR 色度参数。""" + self._save_cct_params_for("hdr_movie") + + def recalculate_cct(self): + """重新计算并绘制色度图""" + try: + # 1. 保存新参数 + self.save_cct_params() + self.log_gui.log("✓ 色度参数已更新") + + # 2. 收起配置项 + if hasattr(self, "config_panel_frame"): + try: + if self.config_panel_frame.winfo_viewable(): + self.config_panel_frame.btn.invoke() + self.root.update_idletasks() + time.sleep(0.1) + except: + pass + + # 3. 跳转到色度图Tab + self.chart_notebook.select(self.cct_chart_frame) + self.root.update_idletasks() + + # 4. 检查是否有数据 + if not hasattr(self, "results") or not self.results: + self.log_gui.log("⚠️ 没有测试数据,无法重新绘制") + messagebox.showwarning("警告", "请先完成测试后再重新计算") + return + + # 5. 获取保存的灰阶数据 + gray_data = self.results.get_intermediate_data("shared", "gray") + if not gray_data: + gray_data = self.results.get_intermediate_data("cct", "gray") + + if not gray_data or len(gray_data) < 2: + self.log_gui.log("⚠️ 没有可用的灰阶数据") + messagebox.showwarning("警告", "没有找到色度测试数据") + return + + # 6. 重新计算 CCT + self.log_gui.log("=" * 50) + self.log_gui.log("开始重新计算色度一致性...") + self.log_gui.log("=" * 50) + + import algorithm.pq_algorithm as pq_algorithm + + cct_values = pq_algorithm.calculate_cct_from_results(gray_data) + + # 7. 更新结果 + self.results.set_test_item_result("cct", {"cct_values": cct_values}) + + # 8. 重新绘制色度图 + test_type = self.config.current_test_type + self.plot_cct(test_type) + + self.log_gui.log("✓ 色度图已重新绘制") + self.log_gui.log("=" * 50) + + messagebox.showinfo("成功", "色度图已根据新参数重新绘制!") + + except Exception as e: + self.log_gui.log(f"❌ 重新计算失败: {str(e)}") + import traceback + + self.log_gui.log(traceback.format_exc()) + messagebox.showerror("错误", f"重新计算失败: {str(e)}") + + def recalculate_gamut(self): + """重新计算并绘制色域图(使用新的参考标准)""" + try: + # 1. 收起配置项 + if hasattr(self, "config_panel_frame"): + try: + if self.config_panel_frame.winfo_viewable(): + self.config_panel_frame.btn.invoke() + self.root.update_idletasks() + time.sleep(0.1) + except: + pass + + # 2. 跳转到色域图Tab + self.chart_notebook.select(self.gamut_chart_frame) + self.root.update_idletasks() + + # 3. 检查是否有数据 + if not hasattr(self, "results") or not self.results: + self.log_gui.log("⚠️ 没有测试数据,无法重新绘制") + messagebox.showwarning("警告", "请先完成测试后再重新计算") + return + + # 4. 获取保存的色域数据 + rgb_data = self.results.get_intermediate_data("gamut", "rgb") + + if not rgb_data or len(rgb_data) < 3: + self.log_gui.log("⚠️ 没有可用的色域数据") + messagebox.showwarning("警告", "没有找到色域测试数据") + return + + # 5. 获取当前测试类型 + test_type = self.config.current_test_type + + # 6. 获取用户选择的参考标准 + if test_type == "screen_module": + reference_standard = self.screen_gamut_ref_var.get() + elif test_type == "sdr_movie": + reference_standard = self.sdr_gamut_ref_var.get() + elif test_type == "hdr_movie": + reference_standard = self.hdr_gamut_ref_var.get() + else: + reference_standard = "DCI-P3" + + self.log_gui.log("=" * 50) + self.log_gui.log(f"开始重新计算色域(参考标准: {reference_standard})...") + self.log_gui.log("=" * 50) + + # 7. 重新计算 XY 色域覆盖率 + xy_points = [[result[0], result[1]] for result in rgb_data] + + # 根据参考标准计算 XY 覆盖率 + if reference_standard == "BT.2020": + area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_BT2020( + xy_points + ) + elif reference_standard == "BT.709": + area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_BT709( + xy_points + ) + elif reference_standard == "DCI-P3": + area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_DCIP3( + xy_points + ) + else: + area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_DCIP3( + xy_points + ) + reference_standard = "DCI-P3" + + self.log_gui.log(f"✓ 参考标准: {reference_standard}") + self.log_gui.log(f"✓ XY 色域覆盖率: {coverage_xy:.1f}%") + + # ========== ✅✅✅ 8. 重新计算 UV 色域覆盖率 ========== + # 将 XY 坐标转换为 UV 坐标 + uv_points = [] + for x, y in xy_points: + try: + # XY转UV公式 + denom = -2 * x + 12 * y + 3 + if abs(denom) < 1e-10: + u, v = 0, 0 + else: + u = 4 * x / denom + v = 9 * y / denom + uv_points.append([u, v]) + except ZeroDivisionError: + continue + + self.log_gui.log(f"✓ 转换后的 UV 点数量: {len(uv_points)}") + + # 根据参考标准计算 UV 覆盖率 + if reference_standard == "BT.2020": + area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_BT2020_uv( + uv_points + ) + elif reference_standard == "BT.709": + area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_BT709_uv( + uv_points + ) + elif reference_standard == "DCI-P3": + area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_DCIP3_uv( + uv_points + ) + else: + area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_DCIP3_uv( + uv_points + ) + + self.log_gui.log(f"✓ UV 色域覆盖率: {coverage_uv:.1f}%") + # ======================================================== + + # 9. ✅ 更新结果(同时保存 XY 和 UV 覆盖率) + self.results.set_test_item_result( + "gamut", + { + "area": area_xy, # ← 兼容旧字段 + "coverage": coverage_xy, # ← 兼容旧字段 + "area_xy": area_xy, # ← XY 面积 + "coverage_xy": coverage_xy, # ← XY 覆盖率 + "area_uv": area_uv, # ← UV 面积 + "coverage_uv": coverage_uv, # ← UV 覆盖率 + "uv_coverage": coverage_uv, # ← 兼容字段(Excel 导出用) + "reference": reference_standard, + }, + ) + + self.log_gui.log("✓ 测试结果已更新到 results 对象") + + # 10. 重新绘制色域图 + self.plot_gamut(rgb_data, coverage_xy, test_type) + + self.log_gui.log("✓ 色域图已重新绘制") + self.log_gui.log("=" * 50) + + messagebox.showinfo( + "成功", + f"色域图已根据新参考标准 {reference_standard} 重新绘制!\n\n" + f"XY 覆盖率: {coverage_xy:.1f}%\n" + f"UV 覆盖率: {coverage_uv:.1f}%", + ) + + except Exception as e: + self.log_gui.log(f"❌ 重新计算失败: {str(e)}") + import traceback + + self.log_gui.log(traceback.format_exc()) + messagebox.showerror("错误", f"重新计算失败: {str(e)}") + + def on_cct_param_change(self, var, default_value): + """色度参数改变时的处理 - 空值恢复默认""" + try: + value = var.get().strip() + + if value == "": + # 空值:恢复默认值 + var.set(str(default_value)) + if hasattr(self, "log_gui"): + self.log_gui.log(f"输入框为空,恢复默认值: {default_value}") + else: + # 验证是否为有效数字 + try: + float_val = float(value) + if float_val < 0 or float_val > 1: + var.set(str(default_value)) + if hasattr(self, "log_gui"): + self.log_gui.log( + f"参数超出范围 [0, 1],恢复默认值: {default_value}" + ) + except ValueError: + var.set(str(default_value)) + if hasattr(self, "log_gui"): + self.log_gui.log(f"无效的参数值,恢复默认值: {default_value}") + + # 保存配置 + self.save_cct_params() + + except Exception as e: + if hasattr(self, "log_gui"): + self.log_gui.log(f"处理参数变化失败: {str(e)}") + + def on_cct_param_focus_out(self, var, default_value): + """色度参数失去焦点时的处理 - 空值恢复默认""" + self._handle_cct_focus_out(var, default_value, self.save_cct_params, "屏模组") + + def save_cct_params(self): + """保存色度参数 - 简化版""" + self._save_cct_params_for(self.config.current_test_type) + + def reload_cct_params(self): + """切换测试类型时重新加载色度参数""" + try: + current_type = self.config.current_test_type + saved_params = self.config.current_test_types.get(current_type, {}).get( + "cct_params", None + ) + + if saved_params is None: + saved_params = self.config.get_default_cct_params(current_type) + + # 更新输入框的值 + self.cct_x_ideal_var.set(str(saved_params["x_ideal"])) + self.cct_x_tolerance_var.set(str(saved_params["x_tolerance"])) + self.cct_y_ideal_var.set(str(saved_params["y_ideal"])) + self.cct_y_tolerance_var.set(str(saved_params["y_tolerance"])) + + except Exception as e: + if hasattr(self, "log_gui"): + self.log_gui.log(f"重新加载色度参数失败: {str(e)}") def update_test_items(self): """根据当前测试类型更新测试项目复选框""" @@ -418,9 +1152,281 @@ class PQAutomationApp: } return display_names.get(test_type, test_type) - create_signal_format_content = _layout_create_signal_format_content + def create_signal_format_content(self): + """创建信号格式选项卡内容""" + self.signal_tabs = ttk.Notebook(self.signal_format_frame) + self.signal_tabs.pack(fill=tk.BOTH, expand=True) - create_connection_content = _layout_create_connection_content + # ==================== 屏模组格式设置 ==================== + self.screen_module_signal_frame = ttk.Frame(self.signal_tabs) + self.screen_module_signal_frame.grid_columnconfigure(0, weight=1) + self.signal_tabs.add(self.screen_module_signal_frame, text="屏模组测试") + + self.screen_module_timing_var = tk.StringVar( + value=self.config.current_test_types[self.config.current_test_type][ + "timing" + ] + ) + 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( + "<>", self.on_screen_module_timing_changed + ) + screen_module_timing_combo.grid(row=0, column=0, sticky="ew", padx=5, pady=5) + + # ==================== 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=["BT.709", "BT.601", "BT.2020"], + width=10, + state="readonly", + ) + sdr_color_space_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2) + + # 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="2.2") + sdr_gamma_combo = ttk.Combobox( + self.sdr_signal_frame, + textvariable=self.sdr_gamma_type_var, + values=["2.2", "2.4", "2.6"], + 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="Full") + sdr_range_combo = ttk.Combobox( + self.sdr_signal_frame, + textvariable=self.sdr_data_range_var, + values=["Full", "Limited"], + 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="8bit") + sdr_bit_depth_combo = ttk.Combobox( + self.sdr_signal_frame, + textvariable=self.sdr_bit_depth_var, + values=["8bit", "10bit", "12bit"], + width=10, + state="readonly", + ) + sdr_bit_depth_combo.grid(row=3, 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="Full") + hdr_range_combo = ttk.Combobox( + self.hdr_signal_frame, + textvariable=self.hdr_data_range_var, + values=["Full", "Limited"], + 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="8bit") + hdr_bit_depth_combo = ttk.Combobox( + self.hdr_signal_frame, + textvariable=self.hdr_bit_depth_var, + values=["8bit", "10bit", "12bit"], + width=10, + state="readonly", + ) + hdr_bit_depth_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 + + def create_connection_content(self): + """创建设备连接区域""" + # 创建设备连接区域的主框架 + com_frame = ttk.Frame(self.connection_frame) + com_frame.pack(fill=tk.X, pady=5) + + # 获取可用的COM端口列表 + available_ports = self.get_available_com_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_ports, + 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("<>", 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)) + self.ucd_status_indicator.config(bg="gray") + + # 添加按钮框架 + button_frame = ttk.Frame(com_frame) + button_frame.grid(row=3, column=0, columnspan=3, pady=3, sticky="w") + + connect_icon = load_icon("assets/connect-svgrepo-com.png") + self.check_button = ttk.Button( + button_frame, + image=connect_icon, + bootstyle="link", + takefocus=False, + command=self.check_com_connections, + ) + self.check_button.image = connect_icon + self.check_button.pack(side="left", padx=0, pady=3) + + disconnect_icon = load_icon("assets/disconnect-svgrepo-com.png") + # 断开连接按钮 + self.disconnect_button = ttk.Button( + button_frame, + image=disconnect_icon, + bootstyle="link", + takefocus=False, + command=self.disconnect_com_connections, + ) + self.disconnect_button.image = disconnect_icon # 防止图标被垃圾回收 + self.disconnect_button.pack(side="left", padx=0, pady=3) + + refresh_icon = load_icon("assets/refresh-svgrepo-com.png") + self.refresh_button = ttk.Button( + button_frame, + image=refresh_icon, + bootstyle="link", + takefocus=False, + command=self.refresh_com_ports, + ) + self.refresh_button.image = refresh_icon # 防止图标被垃圾回收 + self.refresh_button.pack(side="left", padx=0, pady=3) + + # 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("<>", 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.ca_status_indicator.config(bg="gray") + + # 添加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("<>", self.update_config) get_available_ucd_ports = _dev_get_available_ucd_ports get_available_com_ports = _dev_get_available_com_ports @@ -430,52 +1436,941 @@ class PQAutomationApp: check_port_connection = _dev_check_port_connection enable_com_widgets = _dev_enable_com_widgets disconnect_com_connections = _dev_disconnect_com_connections - create_test_type_frame = _layout_create_test_type_frame + def create_test_type_frame(self): + """创建测试类型选择区域(侧边栏形式)""" + # 设置测试类型变量 + self.test_type_var = tk.StringVar(value="screen_module") - update_config_info_display = _layout_update_config_info_display + # 创建测试类型按钮并放置在侧边栏 + test_types = [ + ("屏模组性能测试", "screen_module"), + ("SDR Movie测试", "sdr_movie"), + ("HDR Movie测试", "hdr_movie"), + ] - create_operation_frame = _layout_create_operation_frame + for text, type_value in test_types: + btn = ttk.Button( + master=self.sidebar_frame, + text=text, + style="Sidebar.TButton", + padding=10, + command=lambda v=type_value: self.change_test_type(v), + takefocus=False, + ) + btn.pack(fill=tk.X, padx=0, pady=1) - create_custom_template_result_panel = _ctpl_create_custom_template_result_panel + # 保存按钮引用以便后续更新样式 + setattr(self, f"{type_value}_btn", btn) - show_custom_result_context_menu = _ctpl_show_custom_result_context_menu + # 添加分隔线 + ttk.Separator(self.sidebar_frame, orient="horizontal").pack( + fill=tk.X, padx=10, pady=10 + ) - set_custom_result_table_locked = _ctpl_set_custom_result_table_locked + # ✅ 只保留日志按钮 + self.log_btn = ttk.Button( + self.sidebar_frame, + text="测试日志", + style="Sidebar.TButton", + command=self.toggle_log_panel, + takefocus=False, + ) + self.log_btn.pack(fill=tk.X, padx=0, pady=1) - start_custom_row_single_step = _ctpl_start_custom_row_single_step + # Local Dimming 测试按钮 + self.local_dimming_btn = ttk.Button( + self.sidebar_frame, + text="Local Dimming", + style="Sidebar.TButton", + command=self.toggle_local_dimming_panel, + takefocus=False, + ) + self.local_dimming_btn.pack(fill=tk.X, padx=0, pady=1) - _clear_custom_result_row = _ctpl__clear_custom_result_row + # 注册面板按钮(只保留日志) + if hasattr(self, "panels"): + if "log" in self.panels: + self.panels["log"]["button"] = self.log_btn + if "local_dimming" in self.panels: + self.panels["local_dimming"]["button"] = self.local_dimming_btn - _run_custom_row_single_step = _ctpl__run_custom_row_single_step + def update_config_info_display(self): + """更新配置信息显示""" + if hasattr(self, "config") and hasattr(self.config, "get_current_config"): + current_config = self.config.get_current_config() - _update_custom_result_row = _ctpl__update_custom_result_row + 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')}" - copy_custom_result_table = _ctpl_copy_custom_result_table + # 高亮当前选中的测试类型 + self.update_sidebar_selection() - fill_custom_result_test_data = _ctpl_fill_custom_result_test_data + def create_operation_frame(self): + """创建操作按钮区域""" + operation_frame = ttk.Frame(self.control_frame_top) + operation_frame.pack(fill=tk.X, padx=5, pady=10) - clear_custom_template_results = _ctpl_clear_custom_template_results + self.start_btn = ttk.Button( + operation_frame, + text="开始测试", + command=self.start_test, + style="success.TButton", + ) + self.start_btn.pack(side=tk.LEFT, padx=5) - auto_expand_custom_result_view = _ctpl_auto_expand_custom_result_view + self.stop_btn = ttk.Button( + operation_frame, + text="停止测试", + command=self.stop_test, + style="danger.TButton", + state=tk.DISABLED, + ) + self.stop_btn.pack(side=tk.LEFT, padx=5) - append_custom_template_result = _ctpl_append_custom_template_result + self.save_btn = ttk.Button( + operation_frame, + text="保存结果", + command=self.save_results, + state=tk.DISABLED, + ) + self.save_btn.pack(side=tk.LEFT, padx=5) - start_custom_template_test = _ctpl_start_custom_template_test + self.clear_config_btn = ttk.Button( + operation_frame, + text="清理配置", + command=self.clear_config_file, + ) + self.clear_config_btn.pack(side=tk.LEFT, padx=5) + + self.custom_btn = ttk.Button( + operation_frame, + text="客户模版", + command=self.start_custom_template_test, + style="info.TButton", + ) + self.custom_btn.pack(side=tk.LEFT, padx=5) + self.update_custom_button_visibility() + + def create_custom_template_result_panel(self): + """创建客户模板结果显示区域(黑底表格)""" + self.custom_result_frame = ttk.LabelFrame( + self.custom_template_tab_frame, text="客户模板结果显示" + ) + self.custom_result_frame.pack( + side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5 + ) + + 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) + + 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")], + ) + + columns = ( + "Pattern", + "No.", + "X", + "Y", + "Z", + "x", + "y", + "Lv", + "u'", + "v'", + "Tcp", + "duv", + "λd/λc", + "Pe" + ) + + self.custom_result_tree = ttk.Treeview( + table_container, + columns=columns, + show="headings", + height=4, + style="CustomResult.Treeview", + ) + + column_widths = { + "Pattern": 90, + "No.": 60, + "X": 80, + "Y": 80, + "Z": 80, + "x": 80, + "y": 80, + "Lv": 80, + "u'": 80, + "v'": 80, + "Tcp": 90, + "duv": 80, + "λd/λc": 95, + "Pe": 80, + } + + for col in columns: + self.custom_result_tree.heading(col, text=col) + self.custom_result_tree.column( + col, + width=column_widths.get(col, 80), + minwidth=60, + anchor=tk.CENTER, + stretch=False, + ) + + y_scroll = ttk.Scrollbar( + table_container, + orient=tk.VERTICAL, + command=self.custom_result_tree.yview, + ) + x_scroll = ttk.Scrollbar( + table_container, + orient=tk.HORIZONTAL, + command=self.custom_result_tree.xview, + ) + + self.custom_result_tree.configure( + yscrollcommand=y_scroll.set, + xscrollcommand=x_scroll.set, + ) + + self.custom_result_tree.grid(row=0, column=0, sticky="nsew") + y_scroll.grid(row=0, column=1, sticky="ns") + x_scroll.grid(row=1, column=0, sticky="ew") + + # 右键菜单:复制全部数据(Excel 可直接按行列粘贴) + self.custom_result_menu = tk.Menu(self.root, tearoff=0) + self.custom_result_menu.add_command( + label="复制全部数据", + command=self.copy_custom_result_table, + ) + self.custom_result_menu.add_command( + label="单步测试", + command=self.start_custom_row_single_step, + ) + + # self.custom_result_menu.add_separator() + # self.custom_result_menu.add_command( + # label="单步测试", + # command=self.fill_custom_result_test_data, + # ) + self.custom_result_tree.bind("", self.show_custom_result_context_menu) + + table_container.grid_rowconfigure(0, weight=1) + table_container.grid_columnconfigure(0, weight=1) + + def show_custom_result_context_menu(self, event): + """显示客户模板结果右键菜单""" + if not hasattr(self, "custom_result_tree") or not hasattr( + self, "custom_result_menu" + ): + return + + if self.testing: + # 测试进行中锁定客户模板结果表,禁止右键菜单。 + return + + row_id = self.custom_result_tree.identify_row(event.y) + if row_id: + self.custom_result_tree.selection_set(row_id) + self.custom_result_tree.focus(row_id) + + has_rows = len(self.custom_result_tree.get_children()) > 0 + has_selection = len(self.custom_result_tree.selection()) > 0 + can_single_step = ( + has_selection + and self.ca is not None + and self.ucd is not None + and not self.testing + ) + try: + self.custom_result_menu.entryconfigure( + 0, + state=("normal" if has_rows else "disabled"), + ) + self.custom_result_menu.entryconfigure( + 1, + state=("normal" if can_single_step else "disabled"), + ) + self.custom_result_menu.tk_popup(event.x_root, event.y_root) + finally: + self.custom_result_menu.grab_release() + + def set_custom_result_table_locked(self, locked): + """锁定/解锁客户模板结果表(测试期间禁选择、禁右键)""" + if not hasattr(self, "custom_result_tree"): + return + + try: + self.custom_result_tree.configure(selectmode=("none" if locked else "browse")) + except Exception: + pass + + def start_custom_row_single_step(self): + """单步测试当前选中行:发送该行 pattern 并覆盖该行测量结果""" + if not hasattr(self, "custom_result_tree"): + return + + if self.ca is None or self.ucd is None: + messagebox.showerror("错误", "请先连接CA410和信号发生器") + return + + if self.testing: + messagebox.showinfo("提示", "测试进行中,无法执行单步测试") + return + + selected = self.custom_result_tree.selection() + if not selected: + messagebox.showinfo("提示", "请先选中一行再执行单步测试") + return + + item_id = selected[0] + values = self.custom_result_tree.item(item_id, "values") + if not values: + messagebox.showinfo("提示", "选中行没有有效数据") + return + + row_no = None + if len(values) > 1: + try: + row_no = int(float(values[1])) + except Exception: + row_no = None + + if row_no is None or row_no <= 0: + children = list(self.custom_result_tree.get_children()) + row_no = children.index(item_id) + 1 if item_id in children else 1 + + self._clear_custom_result_row(item_id, row_no) + + threading.Thread( + target=self._run_custom_row_single_step, + args=(item_id, row_no), + daemon=True, + ).start() + + def _clear_custom_result_row(self, item_id, row_no): + """单步测试开始前清空指定行的测量数据""" + if not hasattr(self, "custom_result_tree"): + return + + old_values = list(self.custom_result_tree.item(item_id, "values")) + pattern_name = old_values[0] if len(old_values) > 0 else f"P {row_no}" + + cleared_values = ( + pattern_name, + row_no, + "---", + "---", + "---", + "---", + "---", + "---", + "---", + "---", + "---", + "---", + "---", + "---", + ) + + self.custom_result_tree.item(item_id, values=cleared_values) + self.custom_result_tree.see(item_id) + + def _run_custom_row_single_step(self, item_id, row_no): + """后台执行客户模板单步测试""" + try: + self._dispatch_ui(self.status_var.set, f"单步测试第 {row_no} 行...") + self.log_gui.log(f"开始单步测试第 {row_no} 行") + + self.config.set_current_pattern("custom") + + # 与批量 custom 测试保持一致:根据当前 SDR 配置转换 pattern 数据。 + import copy + + data_range = self.sdr_data_range_var.get() + original_params = copy.deepcopy(self.config.default_pattern_temp["pattern_params"]) + converted_params = convert_pattern_params( + pattern_params=original_params, + data_range=data_range, + verbose=False, + ) + + temp_config = self.config.get_temp_config_with_converted_params( + mode="custom", + converted_params=converted_params, + ) + + if row_no > len(converted_params): + self.log_gui.log(f"❌ 行号超出 pattern 范围: {row_no}/{len(converted_params)}") + self._dispatch_ui(self.status_var.set, "单步测试失败:行号超范围") + return + + self.ucd.set_ucd_params(temp_config) + pattern_param = converted_params[row_no - 1] + self.ucd.set_pattern(self.ucd.current_pattern, pattern_param) + self.ucd.run() + + 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() + + self.ca.set_Display(8) + lambda_d, pe, lv, X, Y, Z = self.ca.readAllDisplay() + + xy = colour.XYZ_to_xy(np.array([X, Y, Z])) + u_prime, v_prime, _ = colour.XYZ_to_CIE1976UCS(np.array([X, Y, Z])) + + row_data = { + "X": X, + "Y": Y, + "Z": Z, + "x": xy[0], + "y": xy[1], + "Lv": lv, + "u_prime": u_prime, + "v_prime": v_prime, + "Tcp": tcp, + "duv": duv, + "lambda_d": lambda_d, + "Pe": pe, + } + + self._dispatch_ui( + self._update_custom_result_row, item_id, row_no, row_data + ) + + self.log_gui.log(f"✓ 第 {row_no} 行单步测试完成并已覆盖") + self._dispatch_ui(self.status_var.set, f"第 {row_no} 行单步测试完成") + + except Exception as e: + self.log_gui.log(f"❌ 单步测试失败: {str(e)}") + self._dispatch_ui(self.status_var.set, "单步测试失败") + + def _update_custom_result_row(self, item_id, row_no, result_data): + """覆盖更新客户模板结果表中指定行""" + + def fmt(value, digits=4): + if value is None: + return "--" + if isinstance(value, (int, float, np.floating)): + # CA 返回异常哨兵值(如 -99999999)时,显示为占位符。 + if (not np.isfinite(value)) or value <= -99999998: + return "---" + return f"{value:.{digits}f}" + try: + numeric_value = float(value) + if (not np.isfinite(numeric_value)) or numeric_value <= -99999998: + return "---" + except (TypeError, ValueError): + pass + return str(value) + + old_values = list(self.custom_result_tree.item(item_id, "values")) + pattern_name = old_values[0] if len(old_values) > 0 else f"P {row_no}" + + new_values = ( + pattern_name, + row_no, + fmt(result_data.get("X")), + fmt(result_data.get("Y")), + fmt(result_data.get("Z")), + fmt(result_data.get("x")), + fmt(result_data.get("y")), + fmt(result_data.get("Lv"), 3), + fmt(result_data.get("u_prime")), + fmt(result_data.get("v_prime")), + fmt(result_data.get("Tcp"), 1), + fmt(result_data.get("duv"), 5), + fmt(result_data.get("lambda_d"), 1), + fmt(result_data.get("Pe"), 1), + ) + + self.custom_result_tree.item(item_id, values=new_values) + + def copy_custom_result_table(self): + """复制客户模板结果表格到剪贴板(不含标题行/No./Pattern)""" + if not hasattr(self, "custom_result_tree"): + return + + items = self.custom_result_tree.get_children() + if not items: + messagebox.showinfo("提示", "当前没有可复制的数据") + return + + lines = [] + columns = tuple(self.custom_result_tree["columns"]) + excluded_col_indexes = { + idx + for idx, col_name in enumerate(columns) + if col_name in ("No.", "Pattern") + } + + for item in items: + values = self.custom_result_tree.item(item, "values") + # 跳过 No. 和 Pattern 两列,只保留测量数据列。 + data_values = [ + v for idx, v in enumerate(values) if idx not in excluded_col_indexes + ] + row = [ + str(v).replace("\t", " ").replace("\n", " ") + for v in data_values + ] + lines.append("\t".join(row)) + + clipboard_text = "\n".join(lines) + self.root.clipboard_clear() + self.root.clipboard_append(clipboard_text) + self.root.update_idletasks() + + if hasattr(self, "status_var"): + self.status_var.set(f"已复制 {len(items)} 行客户模板数据到剪贴板") + if hasattr(self, "log_gui"): + self.log_gui.log(f"✓ 已复制客户模板表格数据({len(items)} 行)") + + def fill_custom_result_test_data(self): + """填充 147 行客户模板测试数据(用于界面验证)""" + if not hasattr(self, "custom_result_tree"): + return + + self.clear_custom_template_results() + + pattern_names = [] + if hasattr(self, "config") and hasattr(self.config, "get_temp_pattern_names"): + pattern_names = self.config.get_temp_pattern_names() + + total_rows = 147 + for i in range(1, total_rows + 1): + ratio = (i - 1) / (total_rows - 1) if total_rows > 1 else 0 + row_data = { + "pattern_name": ( + pattern_names[i - 1] if i - 1 < len(pattern_names) else f"P {i}" + ), + "X": 0.8 + ratio * 120, + "Y": 0.9 + ratio * 135, + "Z": 1.1 + ratio * 145, + "x": 0.24 + ratio * 0.10, + "y": 0.26 + ratio * 0.10, + "Lv": 1.0 + ratio * 500, + "u_prime": 0.16 + ratio * 0.12, + "v_prime": 0.42 + ratio * 0.08, + "Tcp": 1800 + ratio * 12000, + "duv": -0.01 + ratio * 0.03, + "lambda_d": 430 + ratio * 200, + "Pe": 10 + ratio * 90, + } + self.append_custom_template_result(i, row_data) + + if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"): + self.chart_notebook.select(self.custom_template_tab_frame) + + if hasattr(self, "status_var"): + self.status_var.set("已填充 147 行客户模板测试数据") + if hasattr(self, "log_gui"): + self.log_gui.log("✓ 已填充 147 行客户模板测试数据") + + def clear_custom_template_results(self): + """清空客户模板结果表格""" + if not hasattr(self, "custom_result_tree"): + return + for item in self.custom_result_tree.get_children(): + self.custom_result_tree.delete(item) + + def auto_expand_custom_result_view(self): + """当客户模板表格有数据时,自动扩展窗口以尽量完整显示所有列""" + if not hasattr(self, "custom_result_tree"): + return + + if len(self.custom_result_tree.get_children()) == 0: + return + + try: + self.root.update_idletasks() + + columns = tuple(self.custom_result_tree["columns"]) + columns_total_width = 0 + for col in columns: + columns_total_width += int(self.custom_result_tree.column(col, "width")) + + left_panel_width = self.left_frame.winfo_width() if hasattr(self, "left_frame") else 180 + if left_panel_width <= 1: + left_panel_width = 180 + + # 列宽 + 左侧导航 + 滚动条/边框/外边距。 + target_width = int(left_panel_width + columns_total_width + 120) + + screen_max_width = max(900, self.root.winfo_screenwidth() - 40) + target_width = min(target_width, screen_max_width) + + current_width = self.root.winfo_width() + current_height = self.root.winfo_height() + + # 只扩不缩,避免用户窗口被反复改变。 + if target_width > current_width: + self.root.geometry(f"{target_width}x{current_height}") + self.root.update_idletasks() + except Exception as e: + if hasattr(self, "log_gui"): + self.log_gui.log(f"⚠️ 自动扩展客户模板窗口失败: {str(e)}") + + def append_custom_template_result(self, row_no, result_data): + """追加一条客户模板结果到表格""" + + def fmt(value, digits=4): + if value is None: + return "--" + if isinstance(value, (int, float, np.floating)): + # CA 返回异常哨兵值(如 -99999999)时,显示为占位符。 + if (not np.isfinite(value)) or value <= -99999998: + return "---" + return f"{value:.{digits}f}" + try: + numeric_value = float(value) + if (not np.isfinite(numeric_value)) or numeric_value <= -99999998: + return "---" + except (TypeError, ValueError): + pass + return str(value) + + row_values = ( + result_data.get("pattern_name", f"P {row_no}"), + row_no, + fmt(result_data.get("X")), + fmt(result_data.get("Y")), + fmt(result_data.get("Z")), + fmt(result_data.get("x")), + fmt(result_data.get("y")), + fmt(result_data.get("Lv"), 3), + fmt(result_data.get("u_prime")), + fmt(result_data.get("v_prime")), + fmt(result_data.get("Tcp"), 1), + fmt(result_data.get("duv"), 5), + fmt(result_data.get("lambda_d"), 1), + fmt(result_data.get("Pe"), 1) + ) + + if hasattr(self, "custom_result_tree"): + item_id = self.custom_result_tree.insert("", tk.END, values=row_values) + # 新增数据后自动跳转到最新行。 + self.custom_result_tree.see(item_id) + self.auto_expand_custom_result_view() + + def start_custom_template_test(self): + """开始客户模板测试(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 + + if self.testing: + messagebox.showinfo("提示", "测试已在进行中") + return + + if hasattr(self, "debug_container"): + self.debug_container.pack_forget() + + self.testing = True + self.start_btn.config(state=tk.DISABLED) + self.stop_btn.config(state=tk.NORMAL) + self.save_btn.config(state=tk.DISABLED) + self.clear_config_btn.config(state=tk.DISABLED) + self.custom_btn.config(state=tk.DISABLED) + self.status_var.set("客户模板测试进行中...") + + self.log_gui.clear_log() + self.clear_custom_template_results() + + confirm = messagebox.askyesno( + "确认测试", "开始客户模板测试(SDR)?\n\n将采集并显示客户模板格式结果。" + ) + + if not confirm: + self.testing = False + self.start_btn.config(state=tk.NORMAL) + self.stop_btn.config(state=tk.DISABLED) + self.clear_config_btn.config(state=tk.NORMAL) + self.custom_btn.config(state=tk.NORMAL) + self.status_var.set("测试已取消") + self.set_custom_result_table_locked(False) + return + + self.set_custom_result_table_locked(True) + + self.test_thread = threading.Thread(target=self.run_custom_sdr_test, args=([],)) + self.test_thread.daemon = True + self.test_thread.start() - register_panel = _pm_register_panel + def register_panel(self, panel_name, frame, button, visible_attr): + """注册一个面板到管理系统""" + self.panels[panel_name] = { + "frame": frame, + "button": button, + "visible_attr": visible_attr, + } - show_panel = _pm_show_panel + def show_panel(self, panel_name): + """显示指定面板,隐藏其他所有面板""" + if panel_name not in self.panels: + return - hide_all_panels = _pm_hide_all_panels + # 如果当前面板就是要显示的面板,则隐藏它 + if self.current_panel == panel_name: + self.hide_all_panels() + return - create_log_panel = _side_create_log_panel + # 隐藏所有面板 + self.hide_all_panels() - create_local_dimming_panel = _side_create_local_dimming_panel + # 显示指定面板 + panel_info = self.panels[panel_name] - toggle_local_dimming_panel = _side_toggle_local_dimming_panel + # 隐藏主内容区域 + self.control_frame_top.pack_forget() + self.control_frame_middle.pack_forget() + self.control_frame_bottom.pack_forget() - toggle_log_panel = _side_toggle_log_panel + # 显示目标面板 + panel_info["frame"].pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 更新按钮样式 + if panel_info["button"]: + panel_info["button"].configure(style="SidebarSelected.TButton") + + # 更新状态 + setattr(self, panel_info["visible_attr"], True) + self.current_panel = panel_name + + def hide_all_panels(self): + """隐藏所有面板,显示主内容区域""" + # 隐藏所有注册的面板 + for panel_name, panel_info in self.panels.items(): + panel_info["frame"].pack_forget() + if panel_info["button"]: + panel_info["button"].configure(style="Sidebar.TButton") + setattr(self, panel_info["visible_attr"], False) + + # 显示主内容区域 + self.control_frame_top.pack( + side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 + ) + self.control_frame_middle.pack( + side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 + ) + self.control_frame_bottom.pack( + side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 + ) + + self.current_panel = None + + def create_log_panel(self): + """创建日志面板""" + self.log_frame = ttk.Frame(self.content_frame) + self.log_gui = PQLogGUI(self.log_frame) + self.log_gui.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 默认隐藏日志面板 + self.log_visible = False + + # 注册到面板管理系统 + self.register_panel( + "log", self.log_frame, None, "log_visible" + ) # button会在后面设置 + + def create_local_dimming_panel(self): + """创建 Local Dimming 测试面板 - 手动控制版""" + self.local_dimming_frame = ttk.Frame(self.content_frame) + + # 主容器 + main_container = ttk.Frame(self.local_dimming_frame, padding=10) + main_container.pack(fill=tk.BOTH, expand=True) + + # ==================== 1. 标题 ==================== + title_frame = ttk.Frame(main_container) + title_frame.pack(fill=tk.X, pady=(0, 10)) + + ttk.Label( + title_frame, + text="🔆 Local Dimming 窗口测试", + font=("微软雅黑", 14, "bold"), + ).pack(side=tk.LEFT) + + # ==================== 2. 窗口百分比按钮 ==================== + window_frame = ttk.LabelFrame( + main_container, text="🔆 窗口百分比(点击发送)", padding=10 + ) + window_frame.pack(fill=tk.X, pady=(0, 10)) + + # 说明文字 + ttk.Label( + window_frame, + text="点击按钮发送对应百分比的白色窗口(黑色背景 + 居中白色矩形)", + font=("", 9), + foreground="#28a745", + ).pack(pady=(0, 8)) + + # 第一行:1%, 2%, 5%, 10%, 18% + row1 = ttk.Frame(window_frame) + row1.pack(fill=tk.X, pady=(0, 5)) + + percentages_row1 = [1, 2, 5, 10, 18] + for p in percentages_row1: + ttk.Button( + row1, + text=f"{p}%", + command=lambda p=p: self.send_ld_window(p), + bootstyle="success", + width=12, + ).pack(side=tk.LEFT, padx=3) + + # 第二行:25%, 50%, 75%, 100% + row2 = ttk.Frame(window_frame) + row2.pack(fill=tk.X) + + percentages_row2 = [25, 50, 75, 100] + for p in percentages_row2: + ttk.Button( + row2, + text=f"{p}%", + command=lambda p=p: self.send_ld_window(p), + bootstyle="success", + width=12, + ).pack(side=tk.LEFT, padx=3) + + # ==================== 4. CA410 采集按钮 ==================== + measure_frame = ttk.LabelFrame(main_container, text="📊 CA410 测量", padding=10) + measure_frame.pack(fill=tk.X, pady=(0, 10)) + + measure_btn_frame = ttk.Frame(measure_frame) + measure_btn_frame.pack(fill=tk.X) + + self.ld_measure_btn = ttk.Button( + measure_btn_frame, + text="📏 采集当前亮度", + command=self.measure_ld_luminance, + bootstyle="primary", + width=15, + ) + self.ld_measure_btn.pack(side=tk.LEFT, padx=(0, 5)) + + # 显示测量结果 + self.ld_result_label = ttk.Label( + measure_btn_frame, + text="亮度: -- cd/m² | x: -- | y: --", + font=("Consolas", 10), + foreground="#007bff", + ) + self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0)) + + # ==================== 5. 测试结果表格 ==================== + result_frame = ttk.LabelFrame(main_container, text="📋 测试记录", padding=10) + result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) + + # Treeview + columns = ("窗口百分比", "亮度 (cd/m²)", "x", "y", "时间") + self.ld_tree = ttk.Treeview( + result_frame, columns=columns, show="headings", height=10 + ) + + for col in columns: + self.ld_tree.heading(col, text=col) + if col == "窗口百分比": + self.ld_tree.column(col, width=100, anchor=tk.CENTER) + elif col == "时间": + self.ld_tree.column(col, width=120, anchor=tk.CENTER) + else: + self.ld_tree.column(col, width=100, anchor=tk.CENTER) + + self.ld_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # 滚动条 + scrollbar = ttk.Scrollbar( + result_frame, orient=tk.VERTICAL, command=self.ld_tree.yview + ) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + self.ld_tree.configure(yscrollcommand=scrollbar.set) + + # ==================== 6. 底部操作按钮 ==================== + bottom_frame = ttk.Frame(main_container) + bottom_frame.pack(fill=tk.X) + + self.ld_clear_btn = ttk.Button( + bottom_frame, + text="🗑️ 清空记录", + command=self.clear_ld_records, + bootstyle="danger-outline", + width=12, + ) + self.ld_clear_btn.pack(side=tk.LEFT, padx=(0, 5)) + + self.ld_save_btn = ttk.Button( + bottom_frame, + text="💾 保存结果", + command=self.save_local_dimming_results, + bootstyle="info", + width=12, + ) + self.ld_save_btn.pack(side=tk.LEFT) + + # 默认隐藏 + self.local_dimming_visible = False + + # 注册到面板管理系统 + self.register_panel( + "local_dimming", + self.local_dimming_frame, + None, + "local_dimming_visible", + ) + + # 初始化当前窗口百分比(用于记录) + self.current_ld_percentage = None + + def toggle_local_dimming_panel(self): + """切换 Local Dimming 面板显示""" + self.show_panel("local_dimming") + + def toggle_log_panel(self): + """切换日志面板的显示状态""" + self.show_panel("log") create_result_chart_frame = _cf_create_result_chart_frame on_chart_tab_changed = _cf_on_chart_tab_changed @@ -2771,6 +4666,7 @@ class PQAutomationApp: on_custom_template_test_completed = _run_on_custom_template_test_completed get_current_test_result = _run_get_current_test_result on_test_error = _run_on_test_error + def get_test_type_name(self, test_type): """获取测试类型的显示名称""" if test_type == "screen_module": @@ -2839,23 +4735,32 @@ class PQAutomationApp: """根据测试类型和测试项的选中状态显示对应参数框""" selected_items = self.get_selected_test_items() current_test_type = self.config.current_test_type - # 默认隐藏所有参数框 + + # ========== 默认隐藏所有参数框 ========== self.cct_params_frame.pack_forget() self.sdr_cct_params_frame.pack_forget() + + # HDR 色度参数框(如果存在的话) if hasattr(self, "hdr_cct_params_frame"): self.hdr_cct_params_frame.pack_forget() - # 根据测试类型和选中项显示对应参数框 + + # ========== 根据测试类型和选中项显示对应参数框 ========== if current_test_type == "screen_module": + # 屏模组:只有色度参数 if "cct" in selected_items: self.cct_params_frame.pack(fill=tk.X, padx=5, pady=5) if hasattr(self, "log_gui"): self.log_gui.log("✓ 显示屏模组色度参数设置") + elif current_test_type == "sdr_movie": + # SDR:只有色度参数(色准不需要参数设置框) if "cct" in selected_items: self.sdr_cct_params_frame.pack(fill=tk.X, padx=5, pady=5) if hasattr(self, "log_gui"): self.log_gui.log("✓ 显示 SDR 色度参数设置") + elif current_test_type == "hdr_movie": + # HDR:只有色度参数(色准不需要参数设置框) if "cct" in selected_items: if hasattr(self, "hdr_cct_params_frame"): self.hdr_cct_params_frame.pack(fill=tk.X, padx=5, pady=5) @@ -2978,11 +4883,224 @@ class PQAutomationApp: except Exception as e: self.log_gui.log(f"保存 HDR 色域参考标准失败: {str(e)}") - toggle_screen_debug_panel = _side_toggle_screen_debug_panel + def toggle_screen_debug_panel(self): + """打开/关闭屏模组单步调试面板(独立窗口)""" + # 如果窗口已存在且可见,关闭它 + if hasattr(self, "debug_window") and self.debug_window.winfo_exists(): + self.debug_window.destroy() + self.screen_debug_btn.config(text="打开调试面板") + self.log_gui.log("✓ 单步调试面板已关闭") + return - toggle_sdr_debug_panel = _side_toggle_sdr_debug_panel + # 创建新窗口 + self.debug_window = ttk.Toplevel(self.root) + self.debug_window.title("🔧 单步调试面板") + self.debug_window.geometry("900x400") + self.debug_window.transient(self.root) - toggle_hdr_debug_panel = _side_toggle_hdr_debug_panel + # 创建调试面板容器 + debug_container = ttk.Frame(self.debug_window, padding=10) + debug_container.pack(fill=tk.BOTH, expand=True) # ← 这个 pack 是对的 + + # 创建调试面板实例 + debug_panel_instance = PQDebugPanel(debug_container, self) + # ← 这里不应该有任何 pack 调用! + + self.log_gui.log("✓ 单步调试面板实例已创建") + + # 重新启用调试(如果有数据) + try: + test_type = self.config.current_test_type + selected_items = self.get_selected_test_items() + + if test_type == "screen_module": + if "gamma" in selected_items: + gray_data = self.results.get_intermediate_data("shared", "gray") + if gray_data: + self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点") + debug_panel_instance.enable_debug( + "screen_module", "gamma", gray_data + ) + self.log_gui.log("✓ 屏模组 Gamma 单步调试已重新启用") + else: + self.log_gui.log(" ✗ 没有可用的灰阶数据") + + if "gamut" in selected_items: + rgb_data = self.results.get_intermediate_data("gamut", "rgb") + if rgb_data: + self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点") + debug_panel_instance.enable_debug( + "screen_module", "rgb", rgb_data + ) + self.log_gui.log("✓ 屏模组 RGB 单步调试已重新启用") + + except Exception as e: + self.log_gui.log(f"⚠️ 加载调试数据失败: {str(e)}") + import traceback + + self.log_gui.log(traceback.format_exc()) + + # 更新按钮文字 + self.screen_debug_btn.config(text="关闭调试面板") + + # 窗口关闭时的回调 + def on_closing(): + self.screen_debug_btn.config(text="打开调试面板") + self.debug_window.destroy() + self.log_gui.log("✓ 单步调试窗口已关闭") + + self.debug_window.protocol("WM_DELETE_WINDOW", on_closing) + self.debug_window.update_idletasks() + + self.log_gui.log("✓ 单步调试面板已打开(独立窗口)") + + def toggle_sdr_debug_panel(self): + """打开/关闭 SDR 单步调试面板(独立窗口)""" + # 如果窗口已存在且可见,关闭它 + if hasattr(self, "sdr_debug_window") and self.sdr_debug_window.winfo_exists(): + self.sdr_debug_window.destroy() + self.sdr_debug_btn.config(text="打开调试面板") + self.log_gui.log("✓ SDR 单步调试面板已关闭") + return + + # 创建新窗口 + self.sdr_debug_window = ttk.Toplevel(self.root) + self.sdr_debug_window.title("🔧 SDR 单步调试面板") + self.sdr_debug_window.geometry("900x400") + self.sdr_debug_window.transient(self.root) + + # 创建调试面板容器 + debug_container = ttk.Frame(self.sdr_debug_window, padding=10) + debug_container.pack(fill=tk.BOTH, expand=True) + + # ✅ 创建调试面板实例(不要对它调用 pack) + debug_panel_instance = PQDebugPanel(debug_container, self) + # ← 删除:debug_panel_instance.pack(...) + + self.log_gui.log("✓ SDR 单步调试面板实例已创建") + + # ✅ 重新启用调试(如果有数据) + try: + selected_items = self.get_selected_test_items() + + if "gamma" in selected_items: + gray_data = self.results.get_intermediate_data("shared", "gray") + if gray_data: + self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点") + debug_panel_instance.enable_debug("sdr_movie", "gamma", gray_data) + self.log_gui.log("✓ SDR Gamma 单步调试已重新启用") + + if "accuracy" in selected_items: + accuracy_data = self.results.get_intermediate_data( + "accuracy", "measured" + ) + if accuracy_data: + self.log_gui.log(f" → 加载 {len(accuracy_data)} 个色准数据点") + debug_panel_instance.enable_debug( + "sdr_movie", "accuracy", accuracy_data + ) + self.log_gui.log("✓ SDR 色准单步调试已重新启用") + + if "gamut" in selected_items: + rgb_data = self.results.get_intermediate_data("gamut", "rgb") + if rgb_data: + self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点") + debug_panel_instance.enable_debug("sdr_movie", "rgb", rgb_data) + self.log_gui.log("✓ SDR RGB 单步调试已重新启用") + + except Exception as e: + self.log_gui.log(f"⚠️ 加载 SDR 调试数据失败: {str(e)}") + import traceback + + self.log_gui.log(traceback.format_exc()) + + # 更新按钮文字 + self.sdr_debug_btn.config(text="关闭调试面板") + + # 窗口关闭时的回调 + def on_closing(): + self.sdr_debug_btn.config(text="打开调试面板") + self.sdr_debug_window.destroy() + self.log_gui.log("✓ SDR 单步调试窗口已关闭") + + self.sdr_debug_window.protocol("WM_DELETE_WINDOW", on_closing) + self.sdr_debug_window.update_idletasks() + + self.log_gui.log("✓ SDR 单步调试面板已打开(独立窗口)") + + def toggle_hdr_debug_panel(self): + """打开/关闭 HDR 单步调试面板(独立窗口)""" + # 如果窗口已存在且可见,关闭它 + if hasattr(self, "hdr_debug_window") and self.hdr_debug_window.winfo_exists(): + self.hdr_debug_window.destroy() + self.hdr_debug_btn.config(text="打开调试面板") + self.log_gui.log("✓ HDR 单步调试面板已关闭") + return + + # 创建新窗口 + self.hdr_debug_window = ttk.Toplevel(self.root) + self.hdr_debug_window.title("🔧 HDR 单步调试面板") + self.hdr_debug_window.geometry("900x400") + self.hdr_debug_window.transient(self.root) + + # 创建调试面板容器 + debug_container = ttk.Frame(self.hdr_debug_window, padding=10) + debug_container.pack(fill=tk.BOTH, expand=True) + + # ✅ 创建调试面板实例(不要对它调用 pack) + debug_panel_instance = PQDebugPanel(debug_container, self) + # ← 删除:debug_panel_instance.pack(...) + + self.log_gui.log("✓ HDR 单步调试面板实例已创建") + + # ✅ 重新启用调试(如果有数据) + try: + selected_items = self.get_selected_test_items() + + if "eotf" in selected_items: + gray_data = self.results.get_intermediate_data("shared", "gray") + if gray_data: + self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点") + debug_panel_instance.enable_debug("hdr_movie", "eotf", gray_data) + self.log_gui.log("✓ HDR EOTF 单步调试已重新启用") + + if "accuracy" in selected_items: + accuracy_data = self.results.get_intermediate_data( + "accuracy", "measured" + ) + if accuracy_data: + self.log_gui.log(f" → 加载 {len(accuracy_data)} 个色准数据点") + debug_panel_instance.enable_debug( + "hdr_movie", "accuracy", accuracy_data + ) + self.log_gui.log("✓ HDR 色准单步调试已重新启用") + + if "gamut" in selected_items: + rgb_data = self.results.get_intermediate_data("gamut", "rgb") + if rgb_data: + self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点") + debug_panel_instance.enable_debug("hdr_movie", "rgb", rgb_data) + self.log_gui.log("✓ HDR RGB 单步调试已重新启用") + + except Exception as e: + self.log_gui.log(f"⚠️ 加载 HDR 调试数据失败: {str(e)}") + import traceback + + self.log_gui.log(traceback.format_exc()) + + # 更新按钮文字 + self.hdr_debug_btn.config(text="关闭调试面板") + + # 窗口关闭时的回调 + def on_closing(): + self.hdr_debug_btn.config(text="打开调试面板") + self.hdr_debug_window.destroy() + self.log_gui.log("✓ HDR 单步调试窗口已关闭") + + self.hdr_debug_window.protocol("WM_DELETE_WINDOW", on_closing) + self.hdr_debug_window.update_idletasks() + + self.log_gui.log("✓ HDR 单步调试面板已打开(独立窗口)") clear_config_file = _cfg_clear_config_file start_local_dimming_test = _ld_start_local_dimming_test diff --git a/settings/pq_config.json b/settings/pq_config.json index 02b8611..d7b733a 100644 --- a/settings/pq_config.json +++ b/settings/pq_config.json @@ -1,5 +1,5 @@ { - "current_test_type": "sdr_movie", + "current_test_type": "screen_module", "test_types": { "screen_module": { "name": "屏模组性能测试",