"""自定义模板结果面板(Step 6 重构)。""" import threading import time from tkinter import messagebox import tkinter as tk import ttkbootstrap as ttk import colour import numpy as np from app.data_range_converter import convert_pattern_params from typing import TYPE_CHECKING if TYPE_CHECKING: from pqAutomationApp import PQAutomationApp def create_custom_template_result_panel(self: "PQAutomationApp"): """创建客户模板结果显示区域(黑底表格)""" 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.export_custom_template_excel, ) self.custom_result_menu.add_command( label="生成图表", command=self.export_custom_template_charts, ) 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: "PQAutomationApp", 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.entryconfigure( 3, state=("normal" if has_rows else "disabled"), ) self.custom_result_menu.entryconfigure( 4, state=("normal" if has_rows 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: "PQAutomationApp", 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: "PQAutomationApp"): """单步测试当前选中行:发送该行 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 _clear_custom_result_row(self, item_id, row_no) threading.Thread( target=_run_custom_row_single_step, args=(self, item_id, row_no), daemon=True, ).start() def _clear_custom_result_row(self: "PQAutomationApp", 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: "PQAutomationApp", item_id, row_no): """后台执行客户模板单步测试""" try: self._dispatch_ui(self.status_var.set, f"单步测试第 {row_no} 行...") self.log_gui.log(f"开始单步测试第 {row_no} 行", level="info") 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)}", level="error") self._dispatch_ui(self.status_var.set, "单步测试失败:行号超范围") return pattern_param = converted_params[row_no - 1] self.signal_service.apply_and_run(temp_config, pattern_param) 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( _update_custom_result_row, self, item_id, row_no, row_data ) self.log_gui.log(f"第 {row_no} 行单步测试完成并已覆盖", level="success") self._dispatch_ui(self.status_var.set, f"第 {row_no} 行单步测试完成") except Exception as e: self.log_gui.log(f"单步测试失败: {str(e)}", level="error") self._dispatch_ui(self.status_var.set, "单步测试失败") def _update_custom_result_row(self: "PQAutomationApp", 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: "PQAutomationApp"): """复制客户模板结果表格到剪贴板(不含标题行/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)} 行)", level="success") def clear_custom_template_results(self: "PQAutomationApp"): """清空客户模板结果表格""" 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: "PQAutomationApp"): """当客户模板表格有数据时,自动扩展窗口以尽量完整显示所有列""" 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)}", level="error") def append_custom_template_result(self: "PQAutomationApp", 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: "PQAutomationApp"): """开始客户模板测试(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() def update_custom_button_visibility(self: "PQAutomationApp"): """只在 SDR 测试时显示客户模版按钮""" if not hasattr(self, "custom_btn") or not hasattr(self, "test_type_var"): return if self.test_type_var.get() == "sdr_movie": if not self.custom_btn.winfo_manager(): self.custom_btn.pack(side=tk.LEFT, padx=5) else: if self.custom_btn.winfo_manager(): self.custom_btn.pack_forget() # 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 行客户模板测试数据", level="success") def export_custom_template_excel(self: "PQAutomationApp"): """将客户模板结果表导出为 Excel 文件(14 列完整数据)""" if not hasattr(self, "custom_result_tree"): return items = self.custom_result_tree.get_children() if not items: messagebox.showinfo("提示", "当前没有可导出的数据") return import datetime from tkinter import filedialog default_name = ( f"客户模板结果_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" ) save_path = filedialog.asksaveasfilename( title="保存客户模板 Excel 报告", defaultextension=".xlsx", filetypes=[("Excel 文件", "*.xlsx")], initialfile=default_name, ) if not save_path: return try: from openpyxl import Workbook from openpyxl.styles import Alignment, Border, Font, PatternFill, Side wb = Workbook() ws = wb.active ws.title = "客户模板测试结果" columns = tuple(self.custom_result_tree["columns"]) num_cols = len(columns) # 列字母辅助(A-N,共 14 列,全在单字母范围内) def col_letter(idx_1based): return chr(64 + idx_1based) last_col = col_letter(num_cols) # ---- 标题行 ---- ws.merge_cells(f"A1:{last_col}1") ws["A1"] = "客户模板测试结果" ws["A1"].font = Font(name="微软雅黑", size=16, bold=True, color="FFFFFF") ws["A1"].fill = PatternFill( start_color="4472C4", end_color="4472C4", fill_type="solid" ) ws["A1"].alignment = Alignment(horizontal="center", vertical="center") ws.row_dimensions[1].height = 35 # 写入测试时间 ws.merge_cells(f"A2:{last_col}2") ws["A2"] = ( f"测试时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) ws["A2"].font = Font(name="微软雅黑", size=10, color="CCCCCC") ws["A2"].fill = PatternFill( start_color="2F2F2F", end_color="2F2F2F", fill_type="solid" ) ws["A2"].alignment = Alignment(horizontal="left", vertical="center") ws.row_dimensions[2].height = 20 # ---- 表头行 ---- thin = Side(style="thin") border = Border(left=thin, right=thin, top=thin, bottom=thin) header_font = Font(name="微软雅黑", size=10, bold=True, color="FFFFFF") header_fill = PatternFill( start_color="70AD47", end_color="70AD47", fill_type="solid" ) header_align = Alignment( horizontal="center", vertical="center", wrap_text=True ) for col_idx, col_name in enumerate(columns, start=1): cell = ws.cell(row=3, column=col_idx, value=col_name) cell.font = header_font cell.fill = header_fill cell.alignment = header_align cell.border = border ws.row_dimensions[3].height = 22 # ---- 数据行 ---- data_font = Font(name="微软雅黑", size=10) data_align = Alignment(horizontal="center", vertical="center") # 数值列(跳过 Pattern 和 No.)以 4 位小数格式输出 numeric_col_indices = set(range(2, num_cols)) for row_offset, item in enumerate(items): row_num = 4 + row_offset values = self.custom_result_tree.item(item, "values") for col_idx, value in enumerate(values, start=1): cell = ws.cell(row=row_num, column=col_idx) cell.font = data_font cell.alignment = data_align cell.border = border # 占位符保持文本,非占位符数值列尝试转为浮点数 if ( col_idx - 1 in numeric_col_indices and str(value) not in ("---", "--", "") ): try: cell.value = float(value) cell.number_format = "0.0000" except (ValueError, TypeError): cell.value = value else: cell.value = value ws.row_dimensions[row_num].height = 20 # ---- 列宽 ---- col_widths = { "Pattern": 14, "No.": 8, "X": 11, "Y": 11, "Z": 11, "x": 10, "y": 10, "Lv": 10, "u'": 10, "v'": 10, "Tcp": 12, "duv": 10, "\u03bbd/\u03bbc": 12, "Pe": 10, } for col_idx, col_name in enumerate(columns, start=1): ws.column_dimensions[col_letter(col_idx)].width = col_widths.get( col_name, 11 ) wb.save(save_path) if hasattr(self, "status_var"): self.status_var.set("已导出客户模板 Excel 报告") if hasattr(self, "log_gui"): self.log_gui.log( f"已导出客户模板 Excel 报告: {save_path}", level="success" ) messagebox.showinfo("成功", f"Excel 报告已保存到:\n{save_path}") except Exception as e: if hasattr(self, "log_gui"): self.log_gui.log(f"导出 Excel 失败: {str(e)}", level="error") messagebox.showerror("错误", f"导出失败:{str(e)}") def export_custom_template_charts(self: "PQAutomationApp"): """生成客户模板图表:xy 色度散点图 + Lv 亮度曲线图,保存为 PNG""" if not hasattr(self, "custom_result_tree"): return items = self.custom_result_tree.get_children() if not items: messagebox.showinfo("提示", "当前没有可绘制的数据") return import datetime from tkinter import filedialog default_name = ( f"客户模板图表_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.png" ) save_path = filedialog.asksaveasfilename( title="保存客户模板图表", defaultextension=".png", filetypes=[("PNG 图片", "*.png")], initialfile=default_name, ) if not save_path: return try: import matplotlib.pyplot as plt columns = tuple(self.custom_result_tree["columns"]) col_idx_map = {col: idx for idx, col in enumerate(columns)} pattern_names, x_vals, y_vals, lv_vals = [], [], [], [] for item in items: vals = self.custom_result_tree.item(item, "values") pattern_names.append(str(vals[col_idx_map.get("Pattern", 0)])) for container, key, fallback in ( (x_vals, "x", 5), (y_vals, "y", 6), (lv_vals, "Lv", 7), ): raw = vals[col_idx_map.get(key, fallback)] try: v = float(raw) container.append(v if np.isfinite(v) and v > -99999998 else None) except (ValueError, TypeError): container.append(None) # ---- 绘图 ---- fig, (ax_xy, ax_lv) = plt.subplots( 1, 2, figsize=(16, 7), facecolor="#1a1a2e" ) # ── 左图:xy 色度散点图 ── ax_xy.set_facecolor("#0f0f23") ax_xy.set_xlim(0, 0.8) ax_xy.set_ylim(0, 0.9) ax_xy.set_xlabel("x", color="#cccccc", fontsize=11) ax_xy.set_ylabel("y", color="#cccccc", fontsize=11) ax_xy.set_title("xy 色度图", color="#ffffff", fontsize=13, fontweight="bold") ax_xy.tick_params(colors="#aaaaaa", which="both") for spine in ax_xy.spines.values(): spine.set_color("#444444") ax_xy.grid(color="#333333", linestyle="--", linewidth=0.5, alpha=0.7) # D65 白点标注 ax_xy.scatter( [0.3127], [0.3290], c="#ffffff", s=100, zorder=5, marker="+", linewidths=2, label="D65", ) valid_pairs = [ (x, y, i) for i, (x, y) in enumerate(zip(x_vals, y_vals)) if x is not None and y is not None ] if valid_pairs: xs, ys, idxs = zip(*valid_pairs) sc = ax_xy.scatter( xs, ys, c=idxs, cmap="plasma", s=60, zorder=4, edgecolors="#cccccc", linewidths=0.5, alpha=0.9, ) cbar = fig.colorbar(sc, ax=ax_xy, pad=0.01) cbar.set_label("测量序号", color="#cccccc", fontsize=10) cbar.ax.yaxis.set_tick_params(color="#aaaaaa") plt.setp(cbar.ax.yaxis.get_ticklabels(), color="#aaaaaa") ax_xy.legend( fontsize=9, facecolor="#2a2a3e", edgecolor="#555555", labelcolor="#cccccc", ) # ── 右图:Lv 亮度曲线 ── ax_lv.set_facecolor("#0f0f23") ax_lv.set_title( "Lv 亮度曲线", color="#ffffff", fontsize=13, fontweight="bold" ) ax_lv.set_xlabel("测量序号", color="#cccccc", fontsize=11) ax_lv.set_ylabel("Lv (cd/m²)", color="#cccccc", fontsize=11) ax_lv.tick_params(colors="#aaaaaa", which="both") for spine in ax_lv.spines.values(): spine.set_color("#444444") ax_lv.grid(color="#333333", linestyle="--", linewidth=0.5, alpha=0.7) valid_lv = [(i + 1, lv) for i, lv in enumerate(lv_vals) if lv is not None] if valid_lv: seq, lvs = zip(*valid_lv) ax_lv.plot( seq, lvs, color="#4fc3f7", linewidth=1.5, marker="o", markersize=4, markerfacecolor="#ff8c00", markeredgecolor="#ff8c00", ) ax_lv.fill_between(seq, lvs, alpha=0.15, color="#4fc3f7") plt.tight_layout(pad=2.0) fig.savefig(save_path, dpi=200, bbox_inches="tight", facecolor=fig.get_facecolor()) plt.close(fig) if hasattr(self, "status_var"): self.status_var.set("已生成客户模板图表") if hasattr(self, "log_gui"): self.log_gui.log( f"已生成客户模板图表: {save_path}", level="success" ) messagebox.showinfo("成功", f"图表已保存到:\n{save_path}") except Exception as e: if hasattr(self, "log_gui"): self.log_gui.log(f"生成图表失败: {str(e)}", level="error") messagebox.showerror("错误", f"生成图表失败:{str(e)}") class CustomTemplatePanelMixin: """由 tools/refactor_to_mixins.py 自动生成。 把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。 """ create_custom_template_result_panel = create_custom_template_result_panel show_custom_result_context_menu = show_custom_result_context_menu set_custom_result_table_locked = set_custom_result_table_locked start_custom_row_single_step = start_custom_row_single_step _clear_custom_result_row = _clear_custom_result_row _run_custom_row_single_step = _run_custom_row_single_step _update_custom_result_row = _update_custom_result_row copy_custom_result_table = copy_custom_result_table clear_custom_template_results = clear_custom_template_results auto_expand_custom_result_view = auto_expand_custom_result_view append_custom_template_result = append_custom_template_result start_custom_template_test = start_custom_template_test update_custom_button_visibility = update_custom_button_visibility export_custom_template_excel = export_custom_template_excel export_custom_template_charts = export_custom_template_charts