Files
pqAutomationApp/pqAutomationApp.py

856 lines
34 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 drivers.ucd_driver import UCD323Device
from app.ucd_domain import EventBus
from app.services.ucd_service import SignalService
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.custom_template_panel import CustomTemplatePanelMixin
from app.views.panels.side_panels import SidePanelsMixin
from app.views.panels.cct_panel import CctPanelMixin
from app.views.panels.main_layout import MainLayoutMixin
from app.views.panels.ai_image_panel import AIImagePanelMixin
from app.views.panels.single_step_panel import SingleStepPanelMixin
from app.views.panels.pantone_baseline_panel import PantoneBaselinePanelMixin
from app.views.panels.gamma_pattern_panel import GammaPatternPanelMixin
from app.views.panels.calman_panel import CalmanPanelMixin
from app.views.panel_manager import PanelManagerMixin
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 PlotAccuracyMixin
from app.plots.plot_cct import PlotCctMixin
from app.plots.plot_contrast import PlotContrastMixin
from app.plots.plot_eotf import PlotEotfMixin
from app.plots.plot_gamma import PlotGammaMixin
from app.plots.plot_gamut import PlotGamutMixin
from app.views.chart_frame import ChartFrameMixin
from app.config_io import ConfigIOMixin
from app.tests.local_dimming import LocalDimmingMixin
from app.services import PatternService
from app.device.connection import DeviceConnectionMixin
from app.runner.test_runner import TestRunnerMixin
plt.rcParams["font.family"] = ["sans-serif"]
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
class PQAutomationApp(
ConfigIOMixin,
ChartFrameMixin,
MainLayoutMixin,
CctPanelMixin,
DeviceConnectionMixin,
CustomTemplatePanelMixin,
SidePanelsMixin,
AIImagePanelMixin,
SingleStepPanelMixin,
PantoneBaselinePanelMixin,
GammaPatternPanelMixin,
CalmanPanelMixin,
LocalDimmingMixin,
PanelManagerMixin,
TestRunnerMixin,
PlotGamutMixin,
PlotGammaMixin,
PlotEotfMixin,
PlotCctMixin,
PlotContrastMixin,
PlotAccuracyMixin,
):
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() # 信号发生器(旧接口,过渡期保留)
# 新架构EventBus + 设备抽象 + 服务层。
# UCD323Device 内部委托 self.ucd保证零行为变更
# 新代码统一走 self.signal_service。
self.event_bus = EventBus()
self.ucd_device = UCD323Device(self.event_bus, self.ucd)
self.signal_service = SignalService(self.ucd_device, self.event_bus)
# 连接控制器:统一管理 CA/UCD 生命周期。
# 旧的 check_com_connections / disconnect_com_connections 等模块级
# 函数仍以类属性形式挂在 PQAutomationApp 上,内部全部委托给本对象。
from app.device.connection import ConnectionController
self.connection = ConnectionController(self)
# 初始化测试状态
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()
# 图表快照:按 test_type 缓存最近一次各图表的绘制参数,切换类型时可恢复
self._chart_snapshots: dict = {} # {test_type: {chart_name: args_tuple}}
# 加载上次保存的设置
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=208)
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, style="Sidebar.TFrame")
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()
# 创建右侧结果显示区域(无边框,纯 Frame让图表占满
self.result_frame = ttk.Frame(self.control_frame_middle)
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()
# 创建 Gamma 测试图案配置面板
self.create_gamma_pattern_panel()
# 创建 CALMAN 风格灰阶测试面板
self.create_calman_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)
# 状态栏(现代化扁平条,跟随 ttkbootstrap 主题)
self.status_var = tk.StringVar(value="\u25cf 就绪")
status_container = ttk.Frame(root, style="StatusBar.TFrame")
status_container.pack(side=tk.BOTTOM, fill=tk.X)
self.status_bar = ttk.Label(
status_container,
textvariable=self.status_var,
style="StatusBar.TLabel",
anchor=tk.W,
)
self.status_bar.pack(side=tk.LEFT, fill=tk.X, expand=True)
# 右侧版本号
ttk.Label(
status_container,
text=f"v{APP_VERSION}",
style="StatusBarAccent.TLabel",
anchor=tk.E,
).pack(side=tk.RIGHT)
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")
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"]
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,
"local_dimming": 3,
}
target_tab = tab_mapping.get(test_type, 0)
for i in range(4):
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()
elif target_tab == 3:
self.local_dimming_signal_frame.tkraise()
for i in range(4):
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:
self.chart_notebook.forget(self.gamma_chart_frame)
if eotf_tab_id not in current_tabs:
insert_pos = min(1, len(self.chart_notebook.tabs()))
self.chart_notebook.insert(insert_pos, self.eotf_chart_frame, text="EOTF 曲线")
else:
if eotf_tab_id in current_tabs:
self.chart_notebook.forget(self.eotf_chart_frame)
if gamma_tab_id not in current_tabs:
insert_pos = min(1, len(self.chart_notebook.tabs()))
self.chart_notebook.insert(insert_pos, 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",
"gamma_pattern",
):
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)
self.sync_gamut_toolbar()
self._restore_charts_for_type(test_type)
def _save_chart_snapshot(self, test_type: str, chart_name: str, args: tuple):
"""保存某次绘图的参数,以便切换测试类型时可以重绘。"""
if test_type not in self._chart_snapshots:
self._chart_snapshots[test_type] = {}
self._chart_snapshots[test_type][chart_name] = args
def _restore_charts_for_type(self, test_type: str):
"""
切换测试类型后恢复图表显示:
- 该类型有历史结果 → 切换活跃结果 + 重绘所有已缓存图表
- 该类型无历史结果 → 清空图表 + 禁用保存按钮
"""
self.results.set_active(test_type)
snapshots = self._chart_snapshots.get(test_type)
if not snapshots:
self.clear_chart()
if hasattr(self, "save_btn"):
self.save_btn.config(state=tk.DISABLED)
return
for chart_name, args in snapshots.items():
plot_fn = getattr(self, f"plot_{chart_name}", None)
if plot_fn:
try:
plot_fn(*args)
except Exception:
pass
def _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 测试,请设置正确的图像模式"
if test_type == "local_dimming":
return "Local Dimming 为手动模式,请在右侧面板发送图案并采集数据"
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,
app=self,
)
# 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)}")
# 纯算法函数:作为 staticmethod 保留在主类(不依赖 self且 calculate_xxx
# 的命名空间由历史代码以 self.calculate_xxx 调用)。
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)
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测试"
elif test_type == "local_dimming":
return "Local Dimming"
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避免误覆盖其它测试类型配置。
if self.config.current_test_type == "screen_module":
self.config.set_current_timing(self.screen_module_timing_var.get())
elif (
self.config.current_test_type == "sdr_movie"
and hasattr(self, "sdr_timing_var")
):
self.config.set_current_timing(self.sdr_timing_var.get())
elif (
self.config.current_test_type == "local_dimming"
and hasattr(self, "local_dimming_timing_var")
):
self.config.set_current_timing(self.local_dimming_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()
# 同步刷新顶部 header 折叠预览(现代化布局新增)
if hasattr(self, "refresh_config_preview"):
try:
self.refresh_config_preview()
except Exception:
pass
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()
# 先以浅色主题启动 Window再根据用户偏好含自定义 Calman 深色主题)切换
root = ttk.Window(themename="yeti")
from app.views.theme_manager import apply_initial_theme
apply_initial_theme()
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()