import ttkbootstrap as ttk import tkinter as tk from tkinter import messagebox, filedialog import threading import time import os import datetime import traceback import matplotlib.pyplot as plt from app_version import APP_NAME, APP_VERSION, get_app_title from drivers.UCD323_Function import UCDController from app.pq.pq_config import PQConfig from app.pq.pq_result import PQResultStore from app.export import ( save_result_images as _save_result_images_impl, export_excel_report as _export_excel_report_impl, EXCEL_EXPORT_CONFIG as _EXCEL_EXPORT_CONFIG, ) from app.views.panels import custom_template_panel as _ctp from app.views.panels import side_panels as _sp from app.views.panels import cct_panel as _ccp from app.views.panels import main_layout as _main from app.views.panels import ai_image_panel as _aip from app.views.panels import single_step_panel as _ssp from app.views.panels import pantone_baseline_panel as _pbp from app.views import panel_manager as PM from app.logging_setup import setup_logging, attach_gui_handler # Step 0/1 重构:资源工具和纯算法已迁移到 app/ 包,这里重新导入以保持 # 对原函数名/方法名的向后兼容(老代码内部仍用 self.calculate_* 调用)。 from app.resources import ( backgroud_style_set, get_resource_path, load_icon, ) from app.tests.color_accuracy import ( calculate_color_accuracy as _calc_color_accuracy, calculate_delta_e_2000 as _calc_delta_e_2000, get_accuracy_color_standards as _get_accuracy_color_standards, ) from app.tests.eotf import calculate_pq_curve as _calc_pq_curve from app.tests.gamma import calculate_gamma as _calc_gamma from app.tests.gamut import calculate_gamut_coverage as _calc_gamut_coverage from app.plots.plot_accuracy import plot_accuracy as _plot_accuracy from app.plots.plot_cct import plot_cct as _plot_cct from app.plots.plot_contrast import plot_contrast as _plot_contrast from app.plots.plot_eotf import plot_eotf as _plot_eotf from app.plots.plot_gamma import plot_gamma as _plot_gamma from app.plots.plot_gamut import plot_gamut as _plot_gamut from app.views.chart_frame import ( clear_chart as _cf_clear_chart, create_result_chart_frame as _cf_create_result_chart_frame, init_accuracy_chart as _cf_init_accuracy_chart, init_cct_chart as _cf_init_cct_chart, init_contrast_chart as _cf_init_contrast_chart, init_eotf_chart as _cf_init_eotf_chart, init_gamma_chart as _cf_init_gamma_chart, init_gamut_chart as _cf_init_gamut_chart, on_chart_tab_changed as _cf_on_chart_tab_changed, update_chart_tabs_state as _cf_update_chart_tabs_state, ) from app.config_io import ( clear_config_file as _cfg_clear_config_file, get_config_path as _cfg_get_config_path, load_pq_config as _cfg_load_pq_config, save_pq_config as _cfg_save_pq_config, ) from app.tests.local_dimming import ( clear_ld_records as _ld_clear_ld_records, measure_ld_luminance as _ld_measure_ld_luminance, save_local_dimming_results as _ld_save_local_dimming_results, send_ld_window as _ld_send_ld_window, start_local_dimming_test as _ld_start_local_dimming_test, stop_local_dimming_test as _ld_stop_local_dimming_test, update_ld_results as _ld_update_ld_results, ) from app.services import PatternService from app.device.connection import ( check_com_connections as _dev_check_com_connections, check_port_connection as _dev_check_port_connection, disconnect_com_connections as _dev_disconnect_com_connections, enable_com_widgets as _dev_enable_com_widgets, get_available_com_ports as _dev_get_available_com_ports, get_available_ucd_ports as _dev_get_available_ucd_ports, refresh_com_ports as _dev_refresh_com_ports, update_connection_indicator as _dev_update_connection_indicator, ) from app.runner.test_runner import ( get_current_test_result as _run_get_current_test_result, new_pq_results as _run_new_pq_results, on_custom_template_test_completed as _run_on_custom_template_test_completed, on_test_completed as _run_on_test_completed, on_test_error as _run_on_test_error, run_custom_sdr_test as _run_run_custom_sdr_test, run_hdr_movie_test as _run_run_hdr_movie_test, run_screen_module_test as _run_run_screen_module_test, run_sdr_movie_test as _run_run_sdr_movie_test, run_test as _run_run_test, send_fix_pattern as _run_send_fix_pattern, test_cct as _run_test_cct, test_color_accuracy as _run_test_color_accuracy, test_contrast as _run_test_contrast, test_custom_sdr as _run_test_custom_sdr, test_eotf as _run_test_eotf, test_gamma as _run_test_gamma, test_gamut as _run_test_gamut, ) plt.rcParams["font.family"] = ["sans-serif"] plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"] class PQAutomationApp: def __init__(self, root): self.root = root self.root.title(get_app_title()) self.root.geometry("900x650") self.root.minsize(900, 650) self.root.state("zoomed") self.root.protocol("WM_DELETE_WINDOW", self.on_closing) self.app_name = APP_NAME self.app_version = APP_VERSION self.config_cleared = False # 初始化设备连接状态 self.ca = None # CA410色度计 self.ucd = UCDController() # 信号发生器 # 初始化测试状态 self.testing = False self.test_thread = None # 采集节奏参数:默认在稳定性与速度之间取平衡,可按现场情况再微调。 self.pattern_settle_time = 0.4 self.pattern_progress_log_step = 5 # 创建主框架 self.main_frame = ttk.Frame(root) self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) backgroud_style_set() # 创建配置对象 self.config = PQConfig() self.pattern_service = PatternService(self) # 结果管理器:按 test_type 保留每次测试结果,始终存在,避免未初始化错误 self.results = PQResultStore() # 加载上次保存的设置 self.config_file = self.get_config_path() self.load_pq_config() # 如果加载的配置不是屏模组,强制切换为屏模组 if self.config.current_test_type != "screen_module": self.config.set_current_test_type("screen_module") # 初始化侧边栏功能显示状态 - 使用统一的页面管理 self.current_panel = None # 当前显示的面板名称 self.panels = {} # 存储所有面板的信息 self.log_visible = False # 创建左侧面板 self.left_frame = ttk.Frame(self.main_frame, width=180) self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=0, pady=5) self.left_frame.pack_propagate(False) # 创建左侧导航栏 self.sidebar_frame = ttk.Frame(self.left_frame, bootstyle="primary") self.sidebar_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=5) # self.sidebar_frame.pack_propagate(False) # 创建右侧内容区域 self.content_frame = ttk.Frame(self.main_frame) self.content_frame.pack( side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5 ) # 创建右侧内容区域的上中下三个分区 self.control_frame_top = ttk.Frame(self.content_frame) self.control_frame_top.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 ) self.control_frame_middle = ttk.Frame(self.content_frame) self.control_frame_middle.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 ) self.control_frame_bottom = ttk.Frame(self.content_frame) self.control_frame_bottom.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 ) # 创建右上角悬浮配置框 self.create_floating_config_panel() # 创建右侧结果显示区域 self.result_frame = ttk.LabelFrame(self.control_frame_middle, text="测试结果") self.result_frame.pack( side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=0, pady=5 ) # 创建日志显示区域 self.create_log_panel() # 创建 Local Dimming 面板 self.create_local_dimming_panel() # 创建 AI 图片对话面板 self.create_ai_image_panel() # # 创建单步调试面板 # self.create_single_step_panel() # 创建 Pantone 认证摸底测试面板 self.create_pantone_baseline_panel() # 创建测试类型选择区域 self.create_test_type_frame() # 创建操作按钮区域 self.create_operation_frame() # 创建结果图表区域 self.create_result_chart_frame() # 创建客户模板结果显示区域(黑底表格) self.create_custom_template_result_panel() # 在所有控件创建完成后,统一初始化测试类型 self.root.after(100, self.initialize_default_test_type) # 状态栏 self.status_var = tk.StringVar(value="就绪") self.status_bar = ttk.Label( root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W ) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) def _dispatch_ui(self, fn, *args, **kwargs): """把 ``fn(*args, **kwargs)`` 调度到 Tk 主线程执行。 统一替代散落各处的 ``self.root.after(0, lambda: ...)`` 写法: - 自动捕获异常并记录日志,避免工作线程静默丢失 UI 更新失败; - 参数用位置/关键字传入,绕开 ``lambda`` 闭包捕获变量的常见坑; - 允许在 UI 销毁(如关闭窗口)后安全失败。 """ def _runner(): try: fn(*args, **kwargs) except Exception as exc: log = getattr(self, "log_gui", None) if log is not None: try: log.log(f"UI 调度异常: {exc}") except Exception: pass try: self.root.after(0, _runner) except Exception: pass def initialize_default_test_type(self): try: # 初始设置为屏模组 self.change_test_type("screen_module") except Exception as e: if hasattr(self, "log_gui"): self.log_gui.log(f"初始化默认测试类型失败: {str(e)}", level="error") get_config_path = _cfg_get_config_path load_pq_config = _cfg_load_pq_config save_pq_config = _cfg_save_pq_config register_panel = PM.register_panel show_panel = PM.show_panel hide_all_panels = PM.hide_all_panels init_gamut_chart = _cf_init_gamut_chart init_gamma_chart = _cf_init_gamma_chart init_eotf_chart = _cf_init_eotf_chart init_cct_chart = _cf_init_cct_chart init_contrast_chart = _cf_init_contrast_chart init_accuracy_chart = _cf_init_accuracy_chart clear_chart = _cf_clear_chart create_result_chart_frame = _cf_create_result_chart_frame on_chart_tab_changed = _cf_on_chart_tab_changed create_floating_config_panel = _main.create_floating_config_panel create_test_items_content = _main.create_test_items_content create_signal_format_content = _main.create_signal_format_content create_connection_content = _main.create_connection_content create_operation_frame = _main.create_operation_frame create_test_type_frame = _main.create_test_type_frame update_config_info_display = _main.update_config_info_display on_screen_module_timing_changed = _main.on_screen_module_timing_changed update_test_items = _main.update_test_items on_test_type_change = _main.on_test_type_change create_cct_params_frame = _ccp.create_cct_params_frame on_sdr_cct_param_focus_out = _ccp.on_sdr_cct_param_focus_out save_sdr_cct_params = _ccp.save_sdr_cct_params on_hdr_cct_param_focus_out = _ccp.on_hdr_cct_param_focus_out save_hdr_cct_params = _ccp.save_hdr_cct_params recalculate_cct = _ccp.recalculate_cct recalculate_gamut = _ccp.recalculate_gamut on_cct_param_focus_out = _ccp.on_cct_param_focus_out save_cct_params = _ccp.save_cct_params reload_cct_params = _ccp.reload_cct_params toggle_cct_params_frame = _ccp.toggle_cct_params_frame on_screen_gamut_ref_changed = _ccp.on_screen_gamut_ref_changed on_sdr_gamut_ref_changed = _ccp.on_sdr_gamut_ref_changed on_hdr_gamut_ref_changed = _ccp.on_hdr_gamut_ref_changed get_available_ucd_ports = _dev_get_available_ucd_ports get_available_com_ports = _dev_get_available_com_ports refresh_com_ports = _dev_refresh_com_ports check_com_connections = _dev_check_com_connections update_connection_indicator = _dev_update_connection_indicator check_port_connection = _dev_check_port_connection enable_com_widgets = _dev_enable_com_widgets disconnect_com_connections = _dev_disconnect_com_connections create_custom_template_result_panel = _ctp.create_custom_template_result_panel show_custom_result_context_menu = _ctp.show_custom_result_context_menu set_custom_result_table_locked = _ctp.set_custom_result_table_locked start_custom_row_single_step = _ctp.start_custom_row_single_step copy_custom_result_table = _ctp.copy_custom_result_table clear_custom_template_results = _ctp.clear_custom_template_results auto_expand_custom_result_view = _ctp.auto_expand_custom_result_view append_custom_template_result = _ctp.append_custom_template_result start_custom_template_test = _ctp.start_custom_template_test update_custom_button_visibility = _ctp.update_custom_button_visibility create_log_panel = _sp.create_log_panel create_local_dimming_panel = _sp.create_local_dimming_panel toggle_local_dimming_panel = _sp.toggle_local_dimming_panel toggle_log_panel = _sp.toggle_log_panel update_sidebar_selection = _sp.update_sidebar_selection # ---- AI 图片对话面板 ---- create_ai_image_panel = _aip.create_ai_image_panel toggle_ai_image_panel = _aip.toggle_ai_image_panel reload_ai_image_list = _aip.reload_ai_image_list # ---- 单步调试面板 ---- create_single_step_panel = _ssp.create_single_step_panel toggle_single_step_panel = _ssp.toggle_single_step_panel # ---- Pantone 认证摸底测试面板 ---- create_pantone_baseline_panel = _pbp.create_pantone_baseline_panel toggle_pantone_baseline_panel = _pbp.toggle_pantone_baseline_panel # ---- 单步调试面板(统一实现,委托到 side_panels 模块) ---- _toggle_debug_panel = _sp._toggle_debug_panel toggle_screen_debug_panel = _sp.toggle_screen_debug_panel toggle_sdr_debug_panel = _sp.toggle_sdr_debug_panel toggle_hdr_debug_panel = _sp.toggle_hdr_debug_panel clear_config_file = _cfg_clear_config_file start_local_dimming_test = _ld_start_local_dimming_test update_ld_results = _ld_update_ld_results stop_local_dimming_test = _ld_stop_local_dimming_test send_ld_window = _ld_send_ld_window measure_ld_luminance = _ld_measure_ld_luminance clear_ld_records = _ld_clear_ld_records save_local_dimming_results = _ld_save_local_dimming_results def _save_current_cct_params(self, swallow_errors=True): """按当前测试类型分发保存对应的 CCT 参数。""" try: current_type = self.config.current_test_type if current_type == "screen_module": self.save_cct_params() elif current_type == "sdr_movie": self.save_sdr_cct_params() elif current_type == "hdr_movie" and hasattr(self, "save_hdr_cct_params"): self.save_hdr_cct_params() except Exception as e: if not swallow_errors: raise if hasattr(self, "log_gui"): self.log_gui.log(f"保存参数失败: {str(e)}", level="error") def _save_cct_params_before_test_type_switch(self): """切换测试类型前,按当前类型保存色度参数。""" if not hasattr(self, "cct_x_ideal_var"): return self._save_current_cct_params() def _set_gamut_combos_state(self, state, success_msg=None, error_prefix="色域参考标准状态切换失败"): """统一切换三个色域参考下拉框的状态。""" try: for attr in ("screen_gamut_combo", "sdr_gamut_combo", "hdr_gamut_combo"): combo = getattr(self, attr, None) if combo is not None: combo.configure(state=state) if success_msg and hasattr(self, "log_gui"): self.log_gui.log(success_msg, level="success") except Exception as e: if hasattr(self, "log_gui"): self.log_gui.log(f"{error_prefix}: {str(e)}", level="error") def _hide_recalc_buttons(self, include_gamut=False): """隐藏重新计算按钮。include_gamut=True 时同时隐藏色域重算按钮。""" attrs = ["recalc_cct_btn", "sdr_recalc_cct_btn", "hdr_recalc_cct_btn"] if include_gamut: attrs += ["recalc_gamut_btn", "sdr_recalc_gamut_btn", "hdr_recalc_gamut_btn"] hidden = 0 for attr in attrs: btn = getattr(self, attr, None) if btn is None: continue try: btn.grid_remove() hidden += 1 except Exception: pass return hidden def _disable_debug_panel(self): """禁用并隐藏单步调试面板(统一实现)。""" if hasattr(self, "debug_panel"): try: self.debug_panel.disable_all_debug() self.log_gui.log("单步调试已禁用", level="success") except Exception as e: self.log_gui.log(f"禁用单步调试失败: {str(e)}", level="error") if hasattr(self, "debug_container"): try: self.debug_container.pack_forget() self.log_gui.log("单步调试面板已隐藏", level="success") except Exception as e: self.log_gui.log(f"隐藏调试面板失败: {str(e)}", level="error") def _set_config_panel_btn_state(self, state): """统一设置配置面板按钮状态(disabled/normal)。""" if not hasattr(self, "config_panel_frame"): return try: self.config_panel_frame.btn.configure(state=state) except Exception: pass def _apply_current_test_type(self, test_type): """更新 UI 变量与配置中的当前测试类型。""" self.test_type_var.set(test_type) if hasattr(self, "config") and hasattr(self.config, "set_current_test_type"): success = self.config.set_current_test_type(test_type) if not success and hasattr(self, "log_gui"): self.log_gui.log(f"切换测试类型失败: {test_type}", level="error") def _switch_signal_format_tabs(self, test_type): """切换信号格式 Tab 到目标测试类型。""" if not hasattr(self, "signal_tabs"): if hasattr(self, "log_gui"): self.log_gui.log("signal_tabs 尚未创建", level="error") return try: tab_mapping = { "screen_module": 0, "sdr_movie": 1, "hdr_movie": 2, } target_tab = tab_mapping.get(test_type, 0) for i in range(3): self.signal_tabs.tab(i, state="normal") self.signal_tabs.select(target_tab) self.signal_tabs.update() self.root.update_idletasks() if target_tab == 0: self.screen_module_signal_frame.tkraise() elif target_tab == 1: self.sdr_signal_frame.tkraise() elif target_tab == 2: self.hdr_signal_frame.tkraise() for i in range(3): if i != target_tab: self.signal_tabs.tab(i, state="disabled") if hasattr(self, "log_gui"): tab_names = ["屏模组测试", "SDR测试", "HDR"] except Exception as e: if hasattr(self, "log_gui"): self.log_gui.log(f"切换信号格式失败: {str(e)}", level="error") def _switch_chart_tabs_by_test_type(self, test_type): """按测试类型切换 Gamma/EOTF 与客户模板结果 Tab。""" if not hasattr(self, "chart_notebook"): return try: current_tabs = list(self.chart_notebook.tabs()) gamma_tab_id = str(self.gamma_chart_frame) eotf_tab_id = str(self.eotf_chart_frame) if test_type == "hdr_movie": if gamma_tab_id in current_tabs: gamma_index = current_tabs.index(gamma_tab_id) self.chart_notebook.forget(gamma_index) if eotf_tab_id not in current_tabs: self.chart_notebook.insert(1, self.eotf_chart_frame, text="EOTF 曲线") else: if eotf_tab_id in current_tabs: eotf_index = current_tabs.index(eotf_tab_id) self.chart_notebook.forget(eotf_index) if gamma_tab_id not in current_tabs: self.chart_notebook.insert(1, self.gamma_chart_frame, text="Gamma 曲线") custom_tab_id = str(self.custom_template_tab_frame) current_tabs = list(self.chart_notebook.tabs()) if test_type == "sdr_movie": if custom_tab_id not in current_tabs: self.chart_notebook.add(self.custom_template_tab_frame, text="客户模板结果显示") else: if custom_tab_id in current_tabs: self.chart_notebook.forget(self.custom_template_tab_frame) self.chart_notebook.update_idletasks() except Exception as e: if hasattr(self, "log_gui"): self.log_gui.log(f"切换 Gamma/EOTF Tab 失败: {str(e)}", level="error") def change_test_type(self, test_type): """切换测试类型""" # 切换测试类型时,自动隐藏日志面板和 Local Dimming 面板 if self.current_panel in ( "log", "local_dimming", "ai_image", "single_step", "pantone_baseline", ): self.hide_all_panels() self._save_cct_params_before_test_type_switch() self._apply_current_test_type(test_type) # 更新测试项目和侧边栏 self.update_test_items() self.update_sidebar_selection() self.on_test_type_change() self._switch_signal_format_tabs(test_type) self._switch_chart_tabs_by_test_type(test_type) def _check_start_preconditions(self): """检查开始测试前置条件:设备连接 & 未在测试中。""" if self.ca is None or self.ucd is None: messagebox.showerror("错误", "请先连接CA410和信号发生器") return False if self.testing: messagebox.showinfo("提示", "测试已在进行中") return False return True def _collapse_config_panel_for_test(self): """收起配置项并禁用其按钮。""" if not hasattr(self, "config_panel_frame"): return try: if self.config_panel_frame.winfo_viewable(): self.config_panel_frame.btn.invoke() self.root.update_idletasks() time.sleep(0.2) except Exception: pass self._set_config_panel_btn_state("disabled") def _set_ui_testing_state(self): """切换主 UI 到测试中状态(按钮禁用、状态栏、清空日志/图表)。""" 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.status_var.set("测试进行中...") self.log_gui.clear_log() self.clear_chart() def _prepare_ui_for_test_start(self): """为开始测试准备 UI(禁用调试、收起配置、禁用色域、隐藏重算按钮、切换状态)。""" self._disable_debug_panel() self._collapse_config_panel_for_test() self._set_gamut_combos_state("disabled", error_prefix="禁用色域参考标准失败") self._hide_recalc_buttons(include_gamut=False) self._set_ui_testing_state() def _rollback_test_start(self): """用户取消测试时恢复 UI 状态。""" 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.status_var.set("测试已取消") self._set_config_panel_btn_state("normal") def _build_test_start_message(self, test_type): """根据测试类型生成确认弹框提示文案。""" if test_type == "screen_module": return "开始屏模组性能测试,请 byPass All PQ" if test_type == "sdr_movie": return "开始 SDR Movie 测试,请设置正确的图像模式" if test_type == "hdr_movie": return "开始 HDR Movie 测试,请设置正确的图像模式" return f"开始{self.get_test_type_name(test_type)}测试" def _launch_test_thread(self, test_type, test_items): """在新线程中执行测试。""" self.test_thread = threading.Thread( target=self.run_test, args=(test_type, test_items) ) self.test_thread.daemon = True self.test_thread.start() def start_test(self): """开始测试""" if not self._check_start_preconditions(): return test_type = self.test_type_var.get() test_items = self.get_selected_test_items() if not test_items: messagebox.showinfo("提示", "请至少选择一个测试项目") return self._prepare_ui_for_test_start() message = self._build_test_start_message(test_type) if not messagebox.askyesno("确认测试", message): self._rollback_test_start() return self._launch_test_thread(test_type, test_items) def _confirm_stop_test(self): """弹出确认停止测试对话框。""" return messagebox.askyesno( "确认停止测试", "测试正在进行中,确定要停止吗?\n\n 停止后将放弃本次测试的所有数据,无法保存。", icon="warning", ) def _signal_stop_and_update_ui(self): """设置停止标志并立即更新 UI 以反馈给用户。""" self.testing = False self.log_gui.log("=" * 50, level="separator") self.log_gui.log("正在停止测试...", level="info") self.stop_btn.config(state=tk.DISABLED) self.status_var.set("正在停止测试,请稍候...") self.root.update() def _wait_for_test_thread(self, timeout_seconds=5): """等待测试线程结束,最多 timeout_seconds 秒,同时保持 UI 响应。""" if not (self.test_thread and self.test_thread.is_alive()): return self.log_gui.log("等待测试线程结束...", level="info") for _ in range(int(timeout_seconds * 10)): if not self.test_thread.is_alive(): break time.sleep(0.1) self.root.update() if self.test_thread.is_alive(): self.log_gui.log("测试线程未能正常结束,将在后台继续等待", level="error") else: self.log_gui.log("测试线程已结束", level="success") def _clear_test_data(self): """清空测试结果对象与中间数据缓存。""" try: self.log_gui.log("清理测试数据...", level="info") if hasattr(self, "results") and self.results is not None: self.results.clear() self.log_gui.log(" 测试结果对象已清空", level="success") for attr in [ "gamut_results", "gamma_results", "cct_results", "contrast_results", "accuracy_results", ]: if hasattr(self, attr): setattr(self, attr, None) self.log_gui.log(" 所有中间数据已清空", level="success") except Exception as e: self.log_gui.log(f"清理数据时出错: {str(e)}", level="error") def _clear_charts_and_tables(self): """清空图表与客户模板结果表格,并跳转到色域图 Tab。""" try: self.clear_chart() self.log_gui.log("图表已清空", level="success") except Exception as e: self.log_gui.log(f"清空图表时出错: {str(e)}", level="error") try: self.clear_custom_template_results() self.log_gui.log("客户模板结果表格已清空", level="success") except Exception as e: self.log_gui.log(f"清空客户模板结果表格失败: {str(e)}", level="error") try: if hasattr(self, "chart_notebook"): self.chart_notebook.select(self.gamut_chart_frame) self.root.update_idletasks() except Exception as e: self.log_gui.log(f"跳转到色域图失败: {str(e)}", level="error") def _restore_ui_after_stop(self): """恢复主按钮与状态栏到非测试态。""" self.set_custom_result_table_locked(False) self.start_btn.config(state=tk.NORMAL) self.stop_btn.config(state=tk.DISABLED) self.save_btn.config(state=tk.DISABLED) self.clear_config_btn.config(state=tk.NORMAL) if hasattr(self, "custom_btn"): self.custom_btn.config(state=tk.NORMAL) self.status_var.set("测试已停止 - 数据已清空") if hasattr(self, "config_panel_frame"): self._set_config_panel_btn_state("normal") def _finalize_stop_test(self): """延迟执行的清理流程总入口(由 root.after 调用)。""" self._clear_test_data() self._clear_charts_and_tables() self._restore_ui_after_stop() self._set_gamut_combos_state( "disabled", success_msg="色域参考标准已禁用", error_prefix="禁用色域参考标准失败", ) hidden = self._hide_recalc_buttons(include_gamut=True) if hidden > 0: self.log_gui.log(f"已隐藏 {hidden} 个重新计算按钮", level="success") self._disable_debug_panel() self.log_gui.log("=" * 50, level="separator") self.log_gui.log("测试已停止,所有数据已清空", level="success") messagebox.showinfo( "测试已停止", "测试已停止,本次测试数据已清空。\n\n可以重新开始新的测试。", ) def stop_test(self): """停止测试 - 放弃本次所有数据(完全集成版)""" if not self.testing: return if not self._confirm_stop_test(): return self._signal_stop_and_update_ui() self._wait_for_test_thread(timeout_seconds=5) self.root.after(1000, self._finalize_stop_test) # ==================== 保存测试结果 ==================== def save_results(self): """保存测试结果(图片 + Excel)。实现委派给 app.export。""" save_dir = filedialog.askdirectory(title="选择保存测试结果的目录") if not save_dir: return try: timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") test_type = self.get_test_type_name(self.test_type_var.get()) result_dir = os.path.join(save_dir, f"{test_type}_{timestamp}") os.makedirs(result_dir, exist_ok=True) current_test_type = self.test_type_var.get() selected_items = self.get_selected_test_items() log = self.log_gui.log log(f"保存测试类型: {current_test_type}", level="info") log(f"已选测试项: {selected_items}", level="info") # 1) 图片 _save_result_images_impl( result_dir, current_test_type, selected_items, lambda attr: getattr(self, attr, None), log, ) # 2) Excel if (current_test_type in _EXCEL_EXPORT_CONFIG and hasattr(self, "results") and self.results): _export_excel_report_impl( result_dir, current_test_type, selected_items, self.results, log, ) # 3) 成功提示 log("=" * 50, level="separator") log(f"测试结果已保存到目录: {result_dir}", level="success") messagebox.showinfo("成功", f"测试结果已保存到目录:\n{result_dir}") except Exception as e: self.log_gui.log(f"保存测试结果失败: {str(e)}", level="error") import traceback self.log_gui.log(traceback.format_exc(), level="error") messagebox.showerror("错误", f"保存测试结果失败: {str(e)}") new_pq_results = _run_new_pq_results run_test = _run_run_test run_screen_module_test = _run_run_screen_module_test run_custom_sdr_test = _run_run_custom_sdr_test run_sdr_movie_test = _run_run_sdr_movie_test run_hdr_movie_test = _run_run_hdr_movie_test send_fix_pattern = _run_send_fix_pattern test_custom_sdr = _run_test_custom_sdr test_gamut = _run_test_gamut test_gamma = _run_test_gamma test_eotf = _run_test_eotf test_cct = _run_test_cct test_contrast = _run_test_contrast test_color_accuracy = _run_test_color_accuracy calculate_delta_e_2000 = staticmethod(_calc_delta_e_2000) get_accuracy_color_standards = staticmethod(_get_accuracy_color_standards) calculate_gamut_coverage = staticmethod(_calc_gamut_coverage) calculate_gamma = staticmethod(_calc_gamma) calculate_color_accuracy = staticmethod(_calc_color_accuracy) calculate_pq_curve = staticmethod(_calc_pq_curve) plot_gamut = _plot_gamut plot_gamma = _plot_gamma plot_eotf = _plot_eotf plot_cct = _plot_cct plot_contrast = _plot_contrast plot_accuracy = _plot_accuracy on_test_completed = _run_on_test_completed 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 update_chart_tabs_state = _cf_update_chart_tabs_state def get_test_type_name(self, test_type): """获取测试类型的显示名称""" if test_type == "screen_module": return "屏模组性能测试" elif test_type == "sdr_movie": return "SDR Movie测试" elif test_type == "hdr_movie": return "HDR Movie测试" return test_type def get_selected_test_items(self): """获取当前选中的测试项""" selected_items = [] for var_name, var in self.test_vars.items(): if var.get(): selected_items.append(var_name.split("_")[-1]) return selected_items def update_config(self, event=None): """更新配置""" try: self.config.set_device_config( self.ca_com_var.get(), self.ucd_list_var.get(), self.ca_channel_var.get(), ) # 保存当前选中的测试项到配置 self.config.set_current_test_items(self.get_selected_test_items()) # 待修改为三种测试类型的timing值 self.config.set_current_timing(self.screen_module_timing_var.get()) # 自动保存配置到文件 self.save_pq_config() except Exception as e: self.log_gui.log(f"更新配置失败: {str(e)}", level="error") def update_config_and_tabs(self): """更新配置并同步Tab状态""" self.update_config() self.update_chart_tabs_state() # 根据当前测试类型保存对应参数 if "cct" in self.get_selected_test_items(): self._save_current_cct_params() # 控制参数框的显示 self.toggle_cct_params_frame() def on_closing(self): """窗口关闭时的处理""" try: # 检查是否清理了配置 if not self.config_cleared: # 保存配置 self.save_pq_config() else: print("配置已清理,不再保存") # 断开设备连接 if self.ucd.status: self.ucd.close() if self.ca is not None: self.ca.close() # 关闭窗口 self.root.destroy() except Exception as e: print(f"关闭窗口时出错: {str(e)}") self.root.destroy() def main(): try: setup_logging() # root = tk.Tk() root = ttk.Window(themename="yeti") app = PQAutomationApp(root) # GUI 创建完成后,把 logging 记录同步到日志面板 if hasattr(app, "log_gui"): attach_gui_handler(app.log_gui) root.mainloop() except Exception as e: print("程序发生错误:", e) traceback.print_exc() finally: pass if __name__ == "__main__": main()