diff --git a/app/device/connection.py b/app/device/connection.py index 37aa885..709d448 100644 --- a/app/device/connection.py +++ b/app/device/connection.py @@ -72,30 +72,26 @@ def check_com_connections(self): try: # 检测TV连接 ucd_connected = self.check_port_connection(is_ucd=True) - self.root.after( - 0, - lambda: self.update_connection_indicator( - self.ucd_status_indicator, ucd_connected - ), + self._dispatch_ui( + self.update_connection_indicator, + self.ucd_status_indicator, ucd_connected, ) # 检测CA连接 ca_connected = self.check_port_connection(is_ucd=False) - self.root.after( - 0, - lambda: self.update_connection_indicator( - self.ca_status_indicator, ca_connected - ), + self._dispatch_ui( + self.update_connection_indicator, + self.ca_status_indicator, ca_connected, ) # 更新状态栏 - self.root.after(0, lambda: self.status_var.set("连接检测完成")) + self._dispatch_ui(self.status_var.set, "连接检测完成") # 重新启用所有控件 - self.root.after(0, self.enable_com_widgets) + self._dispatch_ui(self.enable_com_widgets) except Exception as e: - self.root.after(0, lambda: self.log_gui.log(f"连接检测出错: {e}")) - self.root.after(0, self.enable_com_widgets) + self._dispatch_ui(self.log_gui.log, f"连接检测出错: {e}") + self._dispatch_ui(self.enable_com_widgets) # 启动线程 threading.Thread(target=check_connections, daemon=True).start() diff --git a/app/runner/test_runner.py b/app/runner/test_runner.py index c19a73c..839b336 100644 --- a/app/runner/test_runner.py +++ b/app/runner/test_runner.py @@ -55,13 +55,13 @@ def run_test(self, test_type, test_items): # 测试完成后更新UI状态 if self.testing: # 如果没有被中途停止 - self.root.after(0, self.on_test_completed) + self._dispatch_ui(self.on_test_completed) except Exception as e: self.log_gui.log(f"测试过程中发生错误: {str(e)}") import traceback self.log_gui.log(traceback.format_exc()) - self.root.after(0, self.on_test_error) + self._dispatch_ui(self.on_test_error) def run_screen_module_test(self, test_items): @@ -154,7 +154,7 @@ def run_custom_sdr_test(self, test_items): self.test_custom_sdr() if self.testing: - self.root.after(0, self.on_custom_template_test_completed) + self._dispatch_ui(self.on_custom_template_test_completed) def run_sdr_movie_test(self, test_items): diff --git a/app/tests/local_dimming.py b/app/tests/local_dimming.py index f2677b8..af73463 100644 --- a/app/tests/local_dimming.py +++ b/app/tests/local_dimming.py @@ -154,10 +154,10 @@ def start_local_dimming_test(self): log("=" * 60) self.ld_test_results = results - self.root.after(0, lambda: self.update_ld_results(results)) - self.root.after(0, lambda: self.ld_start_btn.config(state=tk.NORMAL)) - self.root.after(0, lambda: self.ld_stop_btn.config(state=tk.DISABLED)) - self.root.after(0, lambda: self.ld_save_btn.config(state=tk.NORMAL)) + self._dispatch_ui(self.update_ld_results, results) + self._dispatch_ui(self.ld_start_btn.config, state=tk.NORMAL) + self._dispatch_ui(self.ld_stop_btn.config, state=tk.DISABLED) + self._dispatch_ui(self.ld_save_btn.config, state=tk.NORMAL) threading.Thread(target=worker, daemon=True).start() @@ -192,14 +192,14 @@ def send_ld_window(self, percentage): try: image_path = _ensure_window_image(width, height, percentage) except Exception as e: - self.root.after(0, lambda: self.log_gui.log(f"❌ 图像生成失败: {e}")) + self._dispatch_ui(self.log_gui.log, f"❌ 图像生成失败: {e}") return ok = send_image_pattern(self.ucd, image_path) msg = ( f"✅ {percentage}% 窗口已发送" if ok else f"❌ {percentage}% 窗口发送失败" ) - self.root.after(0, lambda: self.log_gui.log(msg)) + self._dispatch_ui(self.log_gui.log, msg) threading.Thread(target=send, daemon=True).start() @@ -219,23 +219,24 @@ def measure_ld_luminance(self): try: x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay() except Exception as e: - self.root.after(0, lambda: self.log_gui.log(f"❌ 采集异常: {str(e)}")) + self._dispatch_ui(self.log_gui.log, f"❌ 采集异常: {str(e)}") return if lv is None: - self.root.after(0, lambda: self.log_gui.log("❌ 采集失败")) + self._dispatch_ui(self.log_gui.log, "❌ 采集失败") return timestamp = datetime.datetime.now().strftime("%H:%M:%S") - self.root.after(0, lambda: self.ld_result_label.config( - text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}" - )) - self.root.after(0, lambda: self.ld_tree.insert( - "", tk.END, + self._dispatch_ui( + self.ld_result_label.config, + text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}", + ) + self._dispatch_ui( + self.ld_tree.insert, "", tk.END, values=( f"{self.current_ld_percentage}%", f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}", timestamp, ), - )) - self.root.after(0, lambda: self.log_gui.log(f"✅ 采集完成: {lv:.2f} cd/m²")) + ) + self._dispatch_ui(self.log_gui.log, f"✅ 采集完成: {lv:.2f} cd/m²") threading.Thread(target=measure, daemon=True).start() diff --git a/app/views/panels/custom_template_panel.py b/app/views/panels/custom_template_panel.py index c67a1de..33766c6 100644 --- a/app/views/panels/custom_template_panel.py +++ b/app/views/panels/custom_template_panel.py @@ -273,7 +273,7 @@ def _clear_custom_result_row(self, item_id, row_no): def _run_custom_row_single_step(self, item_id, row_no): """后台执行客户模板单步测试""" try: - self.root.after(0, lambda: self.status_var.set(f"单步测试第 {row_no} 行...")) + self._dispatch_ui(self.status_var.set, f"单步测试第 {row_no} 行...") self.log_gui.log(f"开始单步测试第 {row_no} 行") self.config.set_current_pattern("custom") @@ -296,7 +296,7 @@ def _run_custom_row_single_step(self, item_id, row_no): if row_no > len(converted_params): self.log_gui.log(f"❌ 行号超出 pattern 范围: {row_no}/{len(converted_params)}") - self.root.after(0, lambda: self.status_var.set("单步测试失败:行号超范围")) + self._dispatch_ui(self.status_var.set, "单步测试失败:行号超范围") return self.ucd.set_ucd_params(temp_config) @@ -331,17 +331,16 @@ def _run_custom_row_single_step(self, item_id, row_no): "Pe": pe, } - self.root.after( - 0, - lambda: self._update_custom_result_row(item_id, row_no, row_data), + self._dispatch_ui( + self._update_custom_result_row, item_id, row_no, row_data ) self.log_gui.log(f"✓ 第 {row_no} 行单步测试完成并已覆盖") - self.root.after(0, lambda: self.status_var.set(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.root.after(0, lambda: self.status_var.set("单步测试失败")) + self._dispatch_ui(self.status_var.set, "单步测试失败") def _update_custom_result_row(self, item_id, row_no, result_data): diff --git a/app/views/pq_debug_panel.py b/app/views/pq_debug_panel.py index 059c8d9..41d347c 100644 --- a/app/views/pq_debug_panel.py +++ b/app/views/pq_debug_panel.py @@ -1064,37 +1064,26 @@ class PQDebugPanel: def _enable_test_button(self, test_type, test_item): """启用测试按钮""" + dispatch = self.app._dispatch_ui if test_type == "screen_module": if test_item == "gamma": - self.app.root.after(0, lambda: self.screen_test_btn.config(state=tk.NORMAL)) + dispatch(self.screen_test_btn.config, state=tk.NORMAL) elif test_item == "rgb": - self.app.root.after(0, lambda: self.screen_rgb_test_btn.config(state=tk.NORMAL)) + dispatch(self.screen_rgb_test_btn.config, state=tk.NORMAL) elif test_type == "sdr_movie": if test_item == "gamma": - self.app.root.after( - 0, lambda: self.sdr_gamma_test_btn.config(state=tk.NORMAL) - ) + dispatch(self.sdr_gamma_test_btn.config, state=tk.NORMAL) elif test_item == "accuracy": - self.app.root.after( - 0, lambda: self.sdr_accuracy_test_btn.config(state=tk.NORMAL) - ) + dispatch(self.sdr_accuracy_test_btn.config, state=tk.NORMAL) elif test_item == "rgb": - self.app.root.after( - 0, lambda: self.sdr_rgb_test_btn.config(state=tk.NORMAL) - ) + dispatch(self.sdr_rgb_test_btn.config, state=tk.NORMAL) elif test_type == "hdr_movie": if test_item == "eotf": - self.app.root.after( - 0, lambda: self.hdr_eotf_test_btn.config(state=tk.NORMAL) - ) + dispatch(self.hdr_eotf_test_btn.config, state=tk.NORMAL) elif test_item == "accuracy": - self.app.root.after( - 0, lambda: self.hdr_accuracy_test_btn.config(state=tk.NORMAL) - ) + dispatch(self.hdr_accuracy_test_btn.config, state=tk.NORMAL) elif test_item == "rgb": - self.app.root.after( - 0, lambda: self.hdr_rgb_test_btn.config(state=tk.NORMAL) - ) + dispatch(self.hdr_rgb_test_btn.config, state=tk.NORMAL) # ==================== 辅助方法 ==================== diff --git a/pqAutomationApp.py b/pqAutomationApp.py index 74151b3..aebf9b9 100644 --- a/pqAutomationApp.py +++ b/pqAutomationApp.py @@ -283,6 +283,29 @@ class PQAutomationApp: ) 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 + get_config_path = _cfg_get_config_path def initialize_default_test_type(self): """初始化默认测试类型(在所有控件创建完成后调用)""" diff --git a/pqAutomationApp.spec b/pqAutomationApp.spec index 256a600..369c6ad 100644 --- a/pqAutomationApp.spec +++ b/pqAutomationApp.spec @@ -70,7 +70,52 @@ a = Analysis( pathex=[], binaries=[], datas=[('assets', 'assets'), ('UniTAP', 'UniTAP')], - hiddenimports=[], + hiddenimports=[ + # app 包的子模块在主文件里是静态 import 的,但惰性调用 / 属性绑定 + # 场景较多,显式列出可避免 PyInstaller 漏打包。 + 'app', + 'app.config_io', + 'app.data_range_converter', + 'app.resources', + 'app.device', + 'app.device.connection', + 'app.pq', + 'app.pq.pq_config', + 'app.pq.pq_result', + 'app.plots', + 'app.plots.plot_accuracy', + 'app.plots.plot_cct', + 'app.plots.plot_contrast', + 'app.plots.plot_eotf', + 'app.plots.plot_gamma', + 'app.plots.plot_gamut', + 'app.runner', + 'app.runner.test_runner', + 'app.tests', + 'app.tests.color_accuracy', + 'app.tests.eotf', + 'app.tests.gamma', + 'app.tests.gamut', + 'app.tests.local_dimming', + 'app.views', + 'app.views.chart_frame', + 'app.views.collapsing_frame', + 'app.views.panel_manager', + 'app.views.pq_debug_panel', + 'app.views.pq_log_gui', + 'app.views.panels', + 'app.views.panels.cct_panel', + 'app.views.panels.custom_template_panel', + 'app.views.panels.main_layout', + 'app.views.panels.side_panels', + 'drivers', + 'drivers.baseSerail', + 'drivers.caSerail', + 'drivers.tvSerail', + 'drivers.UCD323_Enum', + 'drivers.UCD323_Function', + 'drivers.ucd_helpers', + ], hookspath=[], hooksconfig={}, runtime_hooks=[], diff --git a/settings/pq_config.json b/settings/pq_config.json index d7b733a..02b8611 100644 --- a/settings/pq_config.json +++ b/settings/pq_config.json @@ -1,5 +1,5 @@ { - "current_test_type": "screen_module", + "current_test_type": "sdr_movie", "test_types": { "screen_module": { "name": "屏模组性能测试",