Compare commits

...

5 Commits

Author SHA1 Message Date
xinzhu.yin
2d8d4119e3 继续优化UI加载速度 2026-06-22 10:52:51 +08:00
xinzhu.yin
92cba114a9 优化UI加载速度 2026-06-22 10:40:39 +08:00
xinzhu.yin
22f8b12690 修改UCD逻辑、添加关闭界面的x 2026-06-22 10:31:19 +08:00
xinzhu.yin
9c15751dd0 将 pattern 切换后的等待时间设置添加到pq_config 2026-06-16 15:56:49 +08:00
xinzhu.yin
3dd383b982 修改UCD连接需要重启错误 2026-06-16 14:31:00 +08:00
19 changed files with 1032 additions and 258 deletions

View File

@@ -231,6 +231,12 @@ class BaseIO:
self.__ref_count = TSI_Clean()
logging.info(f"[UniTAP] BaseIO.del: {self}; Ref count: {self.__ref_count}")
def rescan_devices(self):
if self.__ref_count > 0:
TSIX_DEV_RescanDevices()
return
logging.error("[UniTAP] BaseIO.rescan_devices call without TSI_Init")
def get_device_count(self) -> int:
if self.__ref_count > 0:
return TSIX_DEV_GetDeviceCount()

View File

@@ -70,6 +70,11 @@ class TsiLib:
self.__io.cleanup()
logging.info(f"[UniTAP] TsiLib.cleanup {self}")
def rescan_devices(self):
"""Rescan USB for newly connected TSI devices without tearing down the library."""
self.__io.rescan_devices()
logging.info(f"[UniTAP] TsiLib.rescan_devices {self}")
def __del__(self):
"""
Automatically call the destructor after the script completes.

View File

@@ -36,6 +36,15 @@ def get_config_path(self: "PQAutomationApp"):
return config_file
def _apply_acquisition_timing(self: "PQAutomationApp"):
"""将 PQConfig 中的采集节奏同步到 app 运行时属性。"""
timing = getattr(self.config, "acquisition_timing", {})
self.pattern_settle_time = max(0.2, float(timing.get("pattern_settle_time", 0.4)))
self.pattern_progress_log_step = max(
1, int(timing.get("pattern_progress_log_step", 5))
)
def load_pq_config(self: "PQAutomationApp"):
"""加载PQ配置兼容打包后的程序"""
try:
@@ -44,12 +53,15 @@ def load_pq_config(self: "PQAutomationApp"):
with open(self.config_file, "r", encoding="utf-8") as f:
config_dict = json.load(f)
self.config.from_dict(config_dict)
_apply_acquisition_timing(self)
if hasattr(self, "log_gui"):
self.log_gui.log("配置文件加载成功", level="success")
else:
_apply_acquisition_timing(self)
if hasattr(self, "log_gui"):
self.log_gui.log("配置文件不存在,使用默认配置", level="error")
except Exception as e:
_apply_acquisition_timing(self)
if hasattr(self, "log_gui"):
self.log_gui.log(f"加载配置文件失败: {str(e)},使用默认配置", level="error")

View File

@@ -78,26 +78,89 @@ class ConnectionController:
# -- UCD 连接 ------------------------------------------------
def connect_ucd(self, display: str) -> bool:
"""打开指定 UCD 设备。成功返回 True。"""
def _pick_ucd_device(self, ucd_list: list[str], display: str) -> str | None:
"""Prefer devices not marked in-use; fall back to full list."""
usable = [d for d in ucd_list if "(in use)" not in d.lower()]
if display in usable:
return display
if usable:
return usable[0]
if display in ucd_list:
return display
return ucd_list[0] if ucd_list else None
def _refresh_ucd_selection(self, display: str) -> tuple[list[str], str | None]:
"""Rescan devices and return (device list, valid selection or None)."""
ucd_list = self.list_ucd_devices()
return ucd_list, self._pick_ucd_device(ucd_list, display)
def _sync_ucd_combo(self, ucd_list: list[str], selected: str) -> None:
app = self._app
app.ucd_list_combo.config(values=ucd_list)
app.ucd_list_var.set(selected)
app.update_config()
def _prepare_ucd_reconnect(self) -> None:
"""Release stale SDK/native handles before a new open attempt."""
if self._device.state.name != "CLOSED":
try:
self._device.close()
except Exception: # noqa: BLE001
pass
return
except Exception as exc: # noqa: BLE001
self._log(f"关闭旧 UCD 会话失败,将强制重置: {exc}", level="info")
try:
self._device.force_reset()
except Exception as exc: # noqa: BLE001
self._log(f"强制重置 UCD SDK 失败: {exc}", level="error")
def connect_ucd(self, display: str, *, rescan: bool = True) -> bool:
"""打开指定 UCD 设备。成功返回 True。"""
if rescan:
_, display = self._refresh_ucd_selection(display)
if not display:
self._log("未检测到 UCD 设备,请确认电源已打开后重试", level="error")
return False
self._prepare_ucd_reconnect()
last_error: Exception | None = None
for attempt in range(2):
try:
self._device.open(DeviceInfo.parse(display))
return True
except UcdError as exc:
self._log(f"设备 {display} 异常UCD323 连接失败: {exc}", level="error")
return False
last_error = exc
self._log(
f"设备 {display} 连接失败 ({attempt + 1}/2): {exc}",
level="error",
)
except Exception as exc: # noqa: BLE001
self._log(f"UCD323 连接异常: {exc}", level="error")
last_error = exc
self._log(
f"UCD323 连接异常 ({attempt + 1}/2): {exc}",
level="error",
)
if attempt == 0:
try:
self._device.force_reset()
except Exception as exc: # noqa: BLE001
self._log(f"重连前强制重置 UCD 失败: {exc}", level="error")
if rescan:
_, display = self._refresh_ucd_selection(display)
if not display:
return False
if last_error is not None:
self._log(f"UCD323 重连失败: {last_error}", level="error")
return False
def disconnect_ucd(self) -> None:
try:
self._device.close()
except Exception: # noqa: BLE001
try:
self._device.force_reset()
except Exception: # noqa: BLE001
pass
self._log("UCD连接已断开", level="info")
@@ -158,7 +221,11 @@ class ConnectionController:
def worker():
try:
ucd_ok = self.connect_ucd(app.ucd_list_var.get())
ucd_list, selected = self._refresh_ucd_selection(app.ucd_list_var.get())
app._dispatch_ui(self._sync_ucd_combo, ucd_list, selected or "")
ucd_ok = False
if selected:
ucd_ok = self.connect_ucd(selected, rescan=False)
app._dispatch_ui(
app.update_connection_indicator,
app.ucd_status_indicator, ucd_ok,
@@ -185,7 +252,19 @@ class ConnectionController:
def worker():
try:
ucd_ok = self.connect_ucd(app.ucd_list_var.get())
ucd_list, selected = self._refresh_ucd_selection(app.ucd_list_var.get())
app._dispatch_ui(self._sync_ucd_combo, ucd_list, selected or "")
if not selected:
app._dispatch_ui(
app.update_connection_indicator,
app.ucd_status_indicator,
False,
)
app._dispatch_ui(app.status_var.set, "未检测到 UCD 设备")
app._dispatch_ui(self._enable_widgets)
return
ucd_ok = self.connect_ucd(selected, rescan=False)
app._dispatch_ui(
app.update_connection_indicator,
app.ucd_status_indicator,
@@ -262,9 +341,15 @@ class ConnectionController:
app = self._app
com_ports = self.list_com_ports()
ucd_list = self.list_ucd_devices()
if not ucd_list or all("(in use)" in d.lower() for d in ucd_list):
try:
self._device.force_reset()
except Exception as exc: # noqa: BLE001
self._log(f"刷新时重置 UCD SDK: {exc}", level="info")
ucd_list = self.list_ucd_devices()
if app.ucd_list_var.get() not in ucd_list:
app.ucd_list_var.set(ucd_list[0] if ucd_list else "")
selected = self._pick_ucd_device(ucd_list, app.ucd_list_var.get())
app.ucd_list_var.set(selected or "")
app.ucd_list_combo.config(values=ucd_list)
if app.ca_com_var.get() not in com_ports:

View File

@@ -655,6 +655,12 @@ class PQConfig:
"ca_channel": "0",
}
# ---- 采集节奏settings/pq_config.json → acquisition_timing----
self.acquisition_timing = {
"pattern_settle_time": 0.4,
"pattern_progress_log_step": 5,
}
# ---- 自定义图案(用户可变)----
self.custom_pattern = {
"pattern_mode": "SolidColor",
@@ -710,6 +716,7 @@ class PQConfig:
"test_types": self.current_test_types,
"device_config": self.device_config,
"custom_pattern": self.custom_pattern,
"acquisition_timing": self.acquisition_timing,
}
def from_dict(self, config_dict):
@@ -728,6 +735,17 @@ class PQConfig:
self.device_config = config_dict.get("device_config", self.device_config)
self.custom_pattern = config_dict.get("custom_pattern", self.custom_pattern)
loaded_timing = config_dict.get("acquisition_timing")
if isinstance(loaded_timing, dict):
if "pattern_settle_time" in loaded_timing:
self.acquisition_timing["pattern_settle_time"] = max(
0.2, float(loaded_timing["pattern_settle_time"])
)
if "pattern_progress_log_step" in loaded_timing:
self.acquisition_timing["pattern_progress_log_step"] = max(
1, int(loaded_timing["pattern_progress_log_step"])
)
def save_to_file(self, filename):
"""将配置保存到文件"""
with open(filename, "w", encoding="utf-8") as f:

View File

@@ -356,7 +356,8 @@ def send_fix_pattern(self: "PQAutomationApp", mode):
1, int(getattr(self, "pattern_progress_log_step", 5))
)
self.log_gui.log(
f"采集等待时间: {settle_time:.2f}s(可通过 pattern_settle_time 调整)"
f"采集等待时间: {settle_time:.2f}s"
f"(可在 settings/pq_config.json 的 acquisition_timing.pattern_settle_time 调整)"
, level="info")
display_names = session.display_names

View File

@@ -111,6 +111,12 @@ class _UcdSdkBackend:
def search_device(self):
"""搜索可用设备"""
self._ensure_tsi_lib()
try:
self.lUniTAP.rescan_devices()
except Exception:
log.exception("UcdSdk.search_device rescan failed; reinitializing TsiLib")
self._reinit_tsi_lib()
available_devices = self.lUniTAP.get_list_of_available_devices()
return available_devices if available_devices else []
@@ -122,6 +128,13 @@ class _UcdSdkBackend:
if self.dev is not None or self.status:
self._force_cleanup()
self._ensure_tsi_lib()
try:
self.lUniTAP.rescan_devices()
except Exception:
log.exception("UcdSdk.open rescan failed; reinitializing TsiLib")
self._reinit_tsi_lib()
device_id = int(device_name.split(":")[0])
temp_dev = self.lUniTAP.open(device_id)
@@ -143,7 +156,7 @@ class _UcdSdkBackend:
return True
except Exception as e:
self._force_cleanup()
self.hard_reset()
return False
def close(self):
@@ -154,32 +167,42 @@ class _UcdSdkBackend:
self._stop_audio_output()
except Exception:
pass
try:
self._close_device_object(self.dev)
except Exception:
log.exception("UcdSdk.close device object failed")
finally:
self._reset_state()
self.lUniTAP = None
for i in range(3):
gc.collect()
time.sleep(2.0)
self.lUniTAP = UniTAP.TsiLib()
try:
self._reinit_tsi_lib()
except Exception:
log.exception("UcdSdk.close reinit failed")
return True
except Exception as e:
def hard_reset(self) -> None:
"""丢弃失效句柄并重建 TSI意外断连后重连前调用"""
self.dev = None
self.role = None
self._reset_state()
self._reinit_tsi_lib()
try:
def _ensure_tsi_lib(self) -> None:
if self.lUniTAP is None:
self.lUniTAP = UniTAP.TsiLib()
def _reinit_tsi_lib(self) -> None:
"""Destroy and recreate TsiLib so the native driver rescans USB devices."""
old = self.lUniTAP
self.lUniTAP = None
if old is not None:
try:
old.cleanup()
except Exception:
log.exception("UcdSdk._reinit_tsi_lib cleanup failed")
for _ in range(3):
gc.collect()
time.sleep(2.0)
self.lUniTAP = UniTAP.TsiLib()
except Exception as init_error:
pass
return False
def _reset_state(self):
"""重置所有运行时状态(不关闭设备句柄)"""
@@ -1055,6 +1078,26 @@ class UCD323Device(IUcdDevice):
self._info = None
self._bus.publish(ConnectionChanged(DeviceKind.UCD, False, prev_serial))
def force_reset(self) -> None:
"""释放失效的 SDK 句柄,软件状态复位为 CLOSED意外断连后重连前调用"""
with self._acquire_device_lock("force_reset"):
was_open = self._state != UcdState.CLOSED
prev_serial = self._info.serial if self._info else None
try:
self._sdk.hard_reset()
except Exception: # noqa: BLE001
log.exception("UCD force_reset SDK failed")
self._state = UcdState.CLOSED
self._curr_signal = None
self._curr_timing = None
self._curr_pattern = None
self._last_applied = None
self._info = None
if was_open:
self._bus.publish(
ConnectionChanged(DeviceKind.UCD, False, prev_serial)
)
# --- 信号 / 图案配置 ---
def configure(self, signal: SignalFormat, timing: TimingSpec) -> bool:

View File

@@ -25,7 +25,138 @@ def _result_bg_color() -> str:
return "#FFFFFF"
def apply_result_chart_theme(self: "PQAutomationApp"):
def _refresh_ax_theme_in_place(ax, palette) -> None:
"""就地更新已有坐标轴的主题色,无需清空重绘。"""
ax.set_facecolor(palette["card_bg"])
for spine in ax.spines.values():
spine.set_color(palette["border"])
ax.tick_params(axis="both", colors=palette["fg"])
try:
ax.xaxis.label.set_color(palette["fg"])
ax.yaxis.label.set_color(palette["fg"])
except Exception:
pass
try:
if ax.title.get_text():
ax.title.set_color(palette["fg"])
except Exception:
pass
for artist in getattr(ax, "texts", []):
try:
artist.set_color(palette["fg"])
except Exception:
pass
for line in ax.get_lines():
# 保留曲线本身颜色,仅刷新网格等辅助线
pass
for gridline in ax.get_xgridlines() + ax.get_ygridlines():
try:
gridline.set_color(palette["border"])
gridline.set_alpha(0.35)
except Exception:
pass
_CHART_THEME_SPECS = {
"gamut": ("gamut_chart_frame", "gamut_fig", "gamut_canvas"),
"gamma": ("gamma_chart_frame", "gamma_fig", "gamma_canvas"),
"eotf": ("eotf_chart_frame", "eotf_fig", "eotf_canvas"),
"cct": ("cct_chart_frame", "cct_fig", "cct_canvas"),
"contrast": ("contrast_chart_frame", "contrast_fig", "contrast_canvas"),
"accuracy": ("accuracy_chart_frame", "accuracy_fig", "accuracy_canvas"),
}
def _get_active_chart_key(self: "PQAutomationApp") -> str | None:
"""返回当前选中的结果图 Tab 对应的 chart key。"""
notebook = getattr(self, "chart_notebook", None)
if notebook is None:
return None
try:
selected = notebook.select()
if not selected:
return "gamut"
for key, (frame_attr, _, _) in _CHART_THEME_SPECS.items():
frame = getattr(self, frame_attr, None)
if frame is not None and str(frame) == selected:
return key
except Exception:
pass
return "gamut"
def _apply_fig_theme_colors(fig, canvas, palette, bg) -> None:
"""更新 Figure/Axes 主题色,不触发绘制。"""
fig.patch.set_facecolor(bg)
suptitle = getattr(fig, "_suptitle", None)
if suptitle is not None:
try:
suptitle.set_color(palette["fg"])
except Exception:
pass
for ax in fig.axes:
_refresh_ax_theme_in_place(ax, palette)
if canvas is not None:
try:
widget = canvas.get_tk_widget()
widget.configure(bg=bg, highlightthickness=0)
except Exception:
pass
def flush_chart_theme_if_dirty(self: "PQAutomationApp", chart_key: str | None = None) -> None:
"""绘制主题已更新但尚未 draw 的图表(通常在切换到该 Tab 时调用)。"""
dirty = getattr(self, "_charts_theme_dirty", None)
if not dirty:
return
keys = [chart_key] if chart_key and chart_key in dirty else list(dirty)
for key in keys:
spec = _CHART_THEME_SPECS.get(key)
if spec is None:
continue
canvas = getattr(self, spec[2], None)
if canvas is None:
continue
try:
canvas.draw()
except Exception:
pass
dirty.discard(key)
def refresh_result_charts_theme(
self: "PQAutomationApp", *, sync_draw: bool = True, visible_only: bool = True
) -> None:
"""就地刷新结果区 Matplotlib 图表主题色;默认仅绘制当前可见 Tab。"""
palette = get_theme_palette()
bg = _result_bg_color()
active_key = _get_active_chart_key(self) if visible_only else None
dirty = getattr(self, "_charts_theme_dirty", None)
if dirty is None:
dirty = set()
self._charts_theme_dirty = dirty
for key, (_, fig_attr, canvas_attr) in _CHART_THEME_SPECS.items():
fig = getattr(self, fig_attr, None)
canvas = getattr(self, canvas_attr, None)
if fig is None or canvas is None:
continue
_apply_fig_theme_colors(fig, canvas, palette, bg)
should_draw = sync_draw and ((not visible_only) or key == active_key)
if should_draw:
try:
canvas.draw()
except Exception:
pass
dirty.discard(key)
else:
dirty.add(key)
def apply_result_chart_theme(self: "PQAutomationApp", *, sync_draw: bool = False):
"""统一刷新结果图画布背景,使其跟随浅/深色主题。"""
bg = _result_bg_color()
@@ -50,6 +181,9 @@ def apply_result_chart_theme(self: "PQAutomationApp"):
except Exception:
pass
try:
if sync_draw:
canvas.draw()
else:
canvas.draw_idle()
except Exception:
pass
@@ -1111,6 +1245,9 @@ def on_chart_tab_changed(self: "PQAutomationApp", event):
if not selected_tab:
return
self._last_tab_index = self.chart_notebook.index(selected_tab)
active_key = _get_active_chart_key(self)
if active_key:
flush_chart_theme_if_dirty(self, active_key)
except Exception as e:
self.log_gui.log(f"Tab切换事件处理失败: {str(e)}", level="error")
@@ -1129,6 +1266,8 @@ class ChartFrameMixin:
init_contrast_chart = init_contrast_chart
init_accuracy_chart = init_accuracy_chart
apply_result_chart_theme = apply_result_chart_theme
refresh_result_charts_theme = refresh_result_charts_theme
flush_chart_theme_if_dirty = flush_chart_theme_if_dirty
_init_accuracy_result_table = _init_accuracy_result_table
clear_accuracy_result_table = clear_accuracy_result_table
update_accuracy_result_table = update_accuracy_result_table

View File

@@ -3,88 +3,219 @@
register_panel / show_panel / hide_all_panels —— 在右侧栏不同浮动面板间切换。
"""
import tkinter as tk
from __future__ import annotations
from typing import TYPE_CHECKING
import tkinter as tk
import tkinter.font as tkfont
from typing import TYPE_CHECKING, Any, Callable
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
# 叠层切换lift/lower避免 pack_forget 触发 Matplotlib 等大面板重绘。
_STACK_PANELS = frozenset(
{"log", "ai_image", "pantone_baseline", "gamma_pattern", "calman", "single_step"}
)
def create_tool_panel_sidebar_item(
self: "PQAutomationApp", parent: tk.Misc, label: str, command: Callable[[], None]
) -> dict[str, Any]:
"""创建工具面板侧栏项:名称左对齐,选中时 × 贴右端,整体可点击。"""
from app.views.modern_styles import _contrast_text, _is_dark, get_theme_palette
row = tk.Frame(parent, bd=0, highlightthickness=0, cursor="hand2")
name_lbl = tk.Label(row, text=label, anchor=tk.W, bd=0, padx=18, pady=9)
close_lbl = tk.Label(
row,
text="×",
anchor=tk.CENTER,
bd=0,
padx=0,
pady=9,
width=2,
font=("Segoe UI", 9),
)
name_lbl.pack(side=tk.LEFT, fill=tk.X, expand=True)
item: dict[str, Any] = {
"row": row,
"name_label": name_lbl,
"close_label": close_lbl,
"label": label,
"selected": False,
"hover": False,
}
def _apply_style() -> None:
palette = get_theme_palette()
sidebar_bg = palette["sidebar_bg"]
sidebar_hover = palette["sidebar_hover"]
sidebar_selected = palette["sidebar_selected"]
sidebar_fg = palette["sidebar_fg"]
selected_fg = _contrast_text(
sidebar_selected,
dark_text=palette["badge_fg"],
light_text=sidebar_fg,
)
if item["selected"]:
bg = sidebar_selected
fg = selected_fg
name_font = ("Segoe UI Semibold", 10)
elif item["hover"]:
bg = sidebar_hover
fg = "#ffffff" if _is_dark(sidebar_hover) else sidebar_fg
name_font = ("Segoe UI", 10)
else:
bg = sidebar_bg
fg = sidebar_fg
name_font = ("Segoe UI", 10)
row.configure(bg=bg)
for widget in (name_lbl, close_lbl):
widget.configure(
bg=bg,
fg=fg,
activebackground=bg,
activeforeground=fg,
)
name_lbl.configure(font=name_font)
def _truncate_name() -> None:
full = item["label"]
if not item["selected"]:
name_lbl.configure(text=full)
return
row.update_idletasks()
close_w = close_lbl.winfo_width() if close_lbl.winfo_ismapped() else 20
avail = max(24, row.winfo_width() - close_w - 18 - 14)
font = tkfont.Font(font=name_lbl.cget("font"))
if font.measure(full) <= avail:
name_lbl.configure(text=full)
return
for i in range(len(full), 0, -1):
candidate = full[:i].rstrip() + ""
if font.measure(candidate) <= avail:
name_lbl.configure(text=candidate)
return
name_lbl.configure(text="")
def set_selected(selected: bool) -> None:
item["selected"] = selected
if selected:
close_lbl.pack(side=tk.RIGHT, padx=(0, 14))
else:
close_lbl.pack_forget()
_apply_style()
row.after_idle(_truncate_name)
def _on_click(_event=None) -> None:
command()
def _on_enter(_event=None) -> None:
if not item["selected"]:
item["hover"] = True
_apply_style()
def _on_leave(_event=None) -> None:
px, py = row.winfo_pointerxy()
rx, ry = row.winfo_rootx(), row.winfo_rooty()
rw, rh = row.winfo_width(), row.winfo_height()
if rx <= px < rx + rw and ry <= py < ry + rh:
return
item["hover"] = False
_apply_style()
def _on_configure(_event=None) -> None:
if item["selected"]:
_truncate_name()
for widget in (row, name_lbl, close_lbl):
widget.bind("<Button-1>", _on_click)
widget.bind("<Enter>", _on_enter)
widget.bind("<Leave>", _on_leave)
row.bind("<Configure>", _on_configure)
item["set_selected"] = set_selected
item["apply_theme"] = _apply_style
item["pack"] = lambda **kw: row.pack(fill=tk.X, padx=0, pady=1, **kw)
_apply_style()
return item
def _set_tool_panel_sidebar_selected(
panel_info: dict[str, Any], selected: bool
) -> None:
sidebar_item = panel_info.get("sidebar_item")
if sidebar_item is not None:
sidebar_item["set_selected"](selected)
return
button = panel_info.get("button")
if button is not None:
button.configure(
style="SidebarSelected.TButton" if selected else "Sidebar.TButton"
)
def refresh_tool_panel_sidebar_theme(self: "PQAutomationApp") -> None:
"""主题切换后刷新工具面板侧栏项配色。"""
for panel_info in self.panels.values():
sidebar_item = panel_info.get("sidebar_item")
if sidebar_item is not None:
sidebar_item["apply_theme"]()
def register_panel(self: "PQAutomationApp", panel_name, frame, button, visible_attr):
"""注册一个面板到管理系统"""
self.panels[panel_name] = {
"frame": frame,
"button": button,
"button_label": None,
"sidebar_item": None,
"visible_attr": visible_attr,
}
def show_panel(self: "PQAutomationApp", panel_name):
"""显示指定面板,隐藏其他所有面板"""
if panel_name not in self.panels:
def setup_view_stack(self: "PQAutomationApp") -> None:
"""将所有工具面板叠放在 view_stack 上,切换时仅调整 Z 序。"""
if getattr(self, "_view_stack_ready", False):
return
# 如果当前面板就是要显示的面板,则隐藏它
if self.current_panel == panel_name:
self.hide_all_panels()
# 如果当前测试类型是 Local Dimming则在关闭日志等面板后自动恢复 Local Dimming 面板
try:
if (
getattr(self, "config", None)
and getattr(self.config, "current_test_type", None) == "local_dimming"
and panel_name != "local_dimming"
):
self.show_panel("local_dimming")
except Exception:
pass
return
# 隐藏所有面板
self.hide_all_panels()
# 显示指定面板
panel_info = self.panels[panel_name]
# 隐藏主内容区域。
# Local Dimming 作为并列测试类型时,需要保留顶部配置区,
# 让用户在面板上方直接看到并修改配置项。
if panel_name == "local_dimming":
# 重新按“自适应高度”布局顶部配置区,避免其占用可扩展空间把
# Local Dimming 主面板整体向下挤出大块空白。
self.control_frame_top.pack_forget()
self.control_frame_top.pack(
side=tk.TOP, fill=tk.X, expand=False, padx=0, pady=5
)
self.control_frame_middle.pack_forget()
self.control_frame_bottom.pack_forget()
else:
self.control_frame_top.pack_forget()
self.control_frame_middle.pack_forget()
self.control_frame_bottom.pack_forget()
# 显示目标面板
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: "PQAutomationApp"):
"""隐藏所有面板,显示主内容区域"""
# 隐藏所有注册的面板
self.main_view.place(relx=0, rely=0, relwidth=1, relheight=1)
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")
if panel_name == "local_dimming":
continue
panel_info["frame"].place(relx=0, rely=0, relwidth=1, relheight=1)
panel_info["frame"].lower()
self._view_stack_ready = True
def _lower_stack_panels(self: "PQAutomationApp") -> None:
for panel_name, panel_info in self.panels.items():
if panel_name in _STACK_PANELS:
panel_info["frame"].lower()
def _clear_panel_selection(self: "PQAutomationApp") -> None:
for panel_info in self.panels.values():
_set_tool_panel_sidebar_selected(panel_info, False)
setattr(self, panel_info["visible_attr"], False)
# 显示主内容区域
def _restore_main_view_layout(self: "PQAutomationApp") -> None:
"""恢复主界面三区布局,并收起嵌入的 Local Dimming 面板。"""
if getattr(self, "_ld_embedded", False):
self.local_dimming_frame.pack_forget()
self._ld_embedded = False
self.control_frame_top.pack_forget()
self.control_frame_middle.pack_forget()
self.control_frame_bottom.pack_forget()
self.control_frame_top.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
@@ -95,6 +226,14 @@ def hide_all_panels(self: "PQAutomationApp"):
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
def _show_main_content(self: "PQAutomationApp") -> None:
"""显示主内容区并清除当前面板状态。"""
setup_view_stack(self)
_lower_stack_panels(self)
_restore_main_view_layout(self)
self.main_view.lift()
_clear_panel_selection(self)
self.current_panel = None
if hasattr(self, "_sync_save_button_state"):
@@ -104,12 +243,128 @@ def hide_all_panels(self: "PQAutomationApp"):
pass
def _notify_panel_shown(self: "PQAutomationApp", panel_name: str) -> None:
"""面板显示后的轻量刷新(延迟执行,不阻塞切换)。"""
if getattr(self, "_theme_transition", False):
return
if panel_name == "calman":
refresh = getattr(self, "_on_calman_panel_shown", None)
if refresh is not None:
self.root.after_idle(refresh)
return
if panel_name == "ai_image" and getattr(self, "_ai_image_theme_dirty", False):
self.root.after_idle(lambda: self.refresh_ai_image_theme())
return
if panel_name == "log" and hasattr(self, "log_gui") and hasattr(
self.log_gui, "refresh_log_theme"
):
self.root.after_idle(self.log_gui.refresh_log_theme)
def _show_stack_panel(self: "PQAutomationApp", panel_name: str) -> None:
"""叠层模式显示工具面板。"""
setup_view_stack(self)
panel_info = self.panels[panel_name]
_set_tool_panel_sidebar_selected(panel_info, True)
previous = self.current_panel
if previous == "local_dimming":
self.local_dimming_frame.pack_forget()
self._ld_embedded = False
elif previous is not None and previous in _STACK_PANELS:
prev_info = self.panels[previous]
_set_tool_panel_sidebar_selected(prev_info, False)
setattr(self, prev_info["visible_attr"], False)
prev_info["frame"].lower()
self.main_view.lower()
panel_info["frame"].lift()
setattr(self, panel_info["visible_attr"], True)
self.current_panel = panel_name
_notify_panel_shown(self, panel_name)
def _show_local_dimming_panel(self: "PQAutomationApp") -> None:
"""Local Dimming保留顶部配置区下方嵌入 LD 面板。"""
setup_view_stack(self)
panel_info = self.panels["local_dimming"]
if self.current_panel in _STACK_PANELS:
prev_info = self.panels[self.current_panel]
_set_tool_panel_sidebar_selected(prev_info, False)
setattr(self, prev_info["visible_attr"], False)
prev_info["frame"].lower()
_set_tool_panel_sidebar_selected(panel_info, True)
_lower_stack_panels(self)
self.main_view.lift()
self.control_frame_top.pack_forget()
self.control_frame_top.pack(
side=tk.TOP, fill=tk.X, expand=False, padx=0, pady=5
)
self.control_frame_middle.pack_forget()
self.control_frame_bottom.pack_forget()
self.local_dimming_frame.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5
)
self._ld_embedded = True
setattr(self, panel_info["visible_attr"], True)
self.current_panel = "local_dimming"
def _maybe_restore_local_dimming_panel(
self: "PQAutomationApp", closed_panel: str
) -> None:
"""关闭工具面板后Local Dimming 测试类型下自动恢复 LD 面板。"""
if closed_panel == "local_dimming":
return
try:
if (
getattr(self, "config", None)
and getattr(self.config, "current_test_type", None) == "local_dimming"
):
self.show_panel("local_dimming")
except Exception:
pass
def show_panel(self: "PQAutomationApp", panel_name):
"""显示指定面板,隐藏其他所有面板"""
if panel_name not in self.panels:
return
if self.current_panel == panel_name:
_show_main_content(self)
_maybe_restore_local_dimming_panel(self, panel_name)
return
if panel_name == "local_dimming":
_show_local_dimming_panel(self)
return
if panel_name in _STACK_PANELS:
_show_stack_panel(self, panel_name)
return
# 未知面板类型:回退到叠层显示
_show_stack_panel(self, panel_name)
def hide_all_panels(self: "PQAutomationApp"):
"""隐藏所有面板,显示主内容区域"""
_show_main_content(self)
class PanelManagerMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
create_tool_panel_sidebar_item = create_tool_panel_sidebar_item
register_panel = register_panel
setup_view_stack = setup_view_stack
show_panel = show_panel
hide_all_panels = hide_all_panels
refresh_tool_panel_sidebar_theme = refresh_tool_panel_sidebar_theme

View File

@@ -163,7 +163,7 @@ def _on_tree_motion(self: "PQAutomationApp", event):
def create_ai_image_panel(self: "PQAutomationApp"):
"""创建 AI 图片对话面板,并注册到面板管理。"""
frame = ttk.Frame(self.content_frame)
frame = ttk.Frame(self.view_stack)
self.ai_image_frame = frame
# 内部状态
@@ -413,9 +413,9 @@ def create_ai_image_panel(self: "PQAutomationApp"):
def toggle_ai_image_panel(self: "PQAutomationApp"):
"""切换 AI 图片面板显隐。"""
self.show_panel("ai_image")
_apply_ai_image_list_style(self)
self.root.after_idle(lambda: _apply_ai_image_list_style(self))
if not getattr(self, "_ai_image_list_loaded", False):
_start_new_session(self)
self.root.after_idle(lambda: _start_new_session(self))
def _get_app_base_dir(self: "PQAutomationApp") -> str:
@@ -1220,6 +1220,10 @@ def _build_ucd_resized_image(image_path: str, target_w: int, target_h: int) -> s
def refresh_ai_image_theme(self: "PQAutomationApp"):
"""刷新 AI 图片面板中的主题相关控件。"""
if getattr(self, "current_panel", None) != "ai_image":
self._ai_image_theme_dirty = True
return
self._ai_image_theme_dirty = False
if hasattr(self, "_apply_ai_image_list_style"):
self._apply_ai_image_list_style()
tip = getattr(self, "_ai_image_tooltip", None)

View File

@@ -317,7 +317,7 @@ def _refresh_calman_config_summary(self: "PQAutomationApp") -> None:
def create_calman_panel(self: "PQAutomationApp") -> None:
"""创建 CALMAN 风格灰阶测试面板,注册到 panel_manager。"""
palette = _get_calman_palette()
self.calman_frame = ttk.Frame(self.content_frame)
self.calman_frame = ttk.Frame(self.view_stack)
self.calman_visible = False
self.calman_levels = list(DEFAULT_LEVELS_PCT)
# level_pct -> dict(pct, x, y, Y, X, Z, cct, gamma, de2000, rgb_r, rgb_g, rgb_b, time)
@@ -677,22 +677,50 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
_apply_calman_tree_style(self)
right.bind("<Configure>", lambda _e: _adaptive_matrix_columns(self))
right.bind("<Configure>", lambda _e: _schedule_adaptive_matrix_columns(self))
_refresh_metric_table(self)
_refresh_calman_config_summary(self)
_update_target_strip(self)
_update_actual_strip(self)
_redraw_calman_charts(self)
self.root.after_idle(lambda: _finish_calman_panel_init(self))
# 注册到统一面板管理(按钮稍后由 main_layout 注入)
self.register_panel("calman", self.calman_frame, None, "calman_visible")
def _finish_calman_panel_init(self: "PQAutomationApp") -> None:
"""延迟初始化 CALMAN 图表与表格,避免阻塞主窗口首屏。"""
_refresh_metric_table(self)
_refresh_calman_config_summary(self)
_update_target_strip(self)
_update_actual_strip(self)
_redraw_calman_charts(self)
self._calman_panel_initialized = True
def _on_calman_panel_shown(self: "PQAutomationApp") -> None:
"""面板切到前台时只做轻量刷新,不重绘 Matplotlib。"""
_refresh_calman_config_summary(self)
_schedule_adaptive_matrix_columns(self)
if getattr(self, "_calman_theme_dirty", False):
_redraw_calman_charts(self)
self._calman_theme_dirty = False
def _schedule_adaptive_matrix_columns(self: "PQAutomationApp") -> None:
if getattr(self, "_theme_transition", False):
return
job = getattr(self, "_calman_matrix_col_job", None)
if job is not None:
try:
self.root.after_cancel(job)
except Exception:
pass
self._calman_matrix_col_job = self.root.after_idle(
lambda: _adaptive_matrix_columns(self)
)
def toggle_calman_panel(self: "PQAutomationApp") -> None:
"""切换 CALMAN 灰阶面板显示。"""
self.show_panel("calman")
_refresh_calman_config_summary(self)
# ---------------------------------------------------------------------------
@@ -1093,7 +1121,7 @@ def _adaptive_matrix_columns(self: "PQAutomationApp") -> None:
self.calman_data_tree.column(str(p), width=width, minwidth=min_w, stretch=False)
def _redraw_calman_charts(self: "PQAutomationApp") -> None:
def _redraw_calman_charts(self: "PQAutomationApp", *, sync_draw: bool = False) -> None:
"""根据 calman_results 重绘四张图和 xy 散点。"""
palette = _get_calman_palette()
if hasattr(self, "calman_fig"):
@@ -1213,12 +1241,15 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
a4.set_ylim(1.8, 2.8)
a4.set_xlabel("", fontsize=8)
if sync_draw or getattr(self, "_theme_transition", False):
self.calman_canvas.draw()
else:
self.calman_canvas.draw_idle()
_redraw_xy_chart(self)
_redraw_xy_chart(self, sync_draw=sync_draw)
def _redraw_xy_chart(self: "PQAutomationApp") -> None:
def _redraw_xy_chart(self: "PQAutomationApp", *, sync_draw: bool = False) -> None:
palette = _get_calman_palette()
if hasattr(self, "calman_xy_fig"):
self.calman_xy_fig.patch.set_facecolor(palette["figure_bg"])
@@ -1245,6 +1276,9 @@ def _redraw_xy_chart(self: "PQAutomationApp") -> None:
last = recs[-1]
ax.plot([last["x"]], [last["y"]], marker="o", color="#ffcc00", markersize=5)
ax.plot([last["x"], D65_X], [last["y"], D65_Y], color=_mix(palette["fg"], palette["axes_bg"], 0.4), linewidth=0.8)
if sync_draw or getattr(self, "_theme_transition", False):
self.calman_xy_canvas.draw()
else:
self.calman_xy_canvas.draw_idle()
@@ -1337,11 +1371,18 @@ def _refresh_metric_table(self: "PQAutomationApp") -> None:
self.calman_table_ysb.set(first, last)
def refresh_calman_theme(self: "PQAutomationApp") -> None:
def refresh_calman_theme(self: "PQAutomationApp", *, redraw_charts: bool | None = None) -> None:
"""主题切换后刷新 Calman 灰阶调试面板的表格与图表颜色。"""
if not hasattr(self, "calman_frame"):
return
if getattr(self, "current_panel", None) != "calman":
self._calman_theme_dirty = True
return
if redraw_charts is None:
redraw_charts = True
palette = _get_calman_palette()
if hasattr(self, "calman_elapsed_label"):
@@ -1365,7 +1406,11 @@ def refresh_calman_theme(self: "PQAutomationApp") -> None:
_refresh_metric_table(self)
_refresh_calman_config_summary(self)
_redraw_calman_charts(self)
if redraw_charts:
_redraw_calman_charts(self, sync_draw=getattr(self, "_theme_transition", False))
self._calman_theme_dirty = False
else:
self._calman_theme_dirty = True
class CalmanPanelMixin:
@@ -1374,3 +1419,4 @@ class CalmanPanelMixin:
create_calman_panel = create_calman_panel
toggle_calman_panel = toggle_calman_panel
refresh_calman_theme = refresh_calman_theme
_on_calman_panel_shown = _on_calman_panel_shown

View File

@@ -172,6 +172,14 @@ def _apply_custom_result_theme(self: "PQAutomationApp"):
def refresh_custom_template_theme(self: "PQAutomationApp"):
"""刷新客户模板结果表的主题色。"""
notebook = getattr(self, "chart_notebook", None)
tab_frame = getattr(self, "custom_template_tab_frame", None)
if notebook is not None and tab_frame is not None:
try:
if str(tab_frame) not in notebook.tabs():
return
except Exception:
pass
_apply_custom_result_theme(self)

View File

@@ -148,7 +148,7 @@ _GENERATORS = {
def create_gamma_pattern_panel(self: "PQAutomationApp"):
"""创建灰阶 Pattern 配置面板。"""
frame = ttk.Frame(self.content_frame)
frame = ttk.Frame(self.view_stack)
self.gamma_pattern_frame = frame
self.gamma_pattern_visible = False

View File

@@ -171,8 +171,32 @@ def create_test_items_content(self: "PQAutomationApp"):
},
}
# 根据当前测试类型创建复选框
# 预创建各测试类型复选框,切换时只改显隐与勾选状态,避免重复构建控件。
self.test_vars = {}
toggle_bootstyle = "success-round-toggle"
for type_key, config in self.test_items.items():
frame = config["frame"]
type_label = ttk.Label(
frame,
text=self.get_test_type_name(type_key),
style="primary.TLabel",
)
type_label.grid(row=0, column=0, columnspan=2, sticky=tk.W, padx=5, pady=3)
config["type_label"] = type_label
checkbox_vars: dict[str, tk.BooleanVar] = {}
for i, (text, var_name) in enumerate(config["items"]):
var = tk.BooleanVar(value=False)
ttk.Checkbutton(
frame,
text=text,
variable=var,
bootstyle=toggle_bootstyle,
command=self.update_config_and_tabs,
).grid(row=i // 2 + 1, column=i % 2, sticky=tk.W, padx=10, pady=5)
checkbox_vars[var_name] = var
config["checkbox_vars"] = checkbox_vars
self.update_test_items()
# 创建色度参数设置框架
@@ -774,21 +798,19 @@ def create_test_type_frame(self: "PQAutomationApp"):
).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w")
panel_buttons = [
("ai_image_btn", "AI 图片", self.toggle_ai_image_panel),
("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel),
("gamma_pattern_btn", "Gamma Pattern编辑", self.toggle_gamma_pattern_panel),
("calman_btn", "CALMAN 灰阶", self.toggle_calman_panel),
("ai_image_btn", "ai_image", "AI 图片", self.toggle_ai_image_panel),
("pantone_baseline_btn", "pantone_baseline", "Pantone 摸底", self.toggle_pantone_baseline_panel),
("gamma_pattern_btn", "gamma_pattern", "Gamma Pattern编辑", self.toggle_gamma_pattern_panel),
("calman_btn", "calman", "CALMAN 灰阶", self.toggle_calman_panel),
]
for attr, text, cmd in panel_buttons:
btn = ttk.Button(
self.sidebar_frame,
text=text,
style="Sidebar.TButton",
command=cmd,
takefocus=False,
)
btn.pack(fill=tk.X, padx=0, pady=1)
setattr(self, attr, btn)
for attr, panel_name, text, cmd in panel_buttons:
item = self.create_tool_panel_sidebar_item(self.sidebar_frame, text, cmd)
item["pack"]()
setattr(self, attr, item["row"])
if hasattr(self, "panels") and panel_name in self.panels:
self.panels[panel_name]["button_label"] = text
self.panels[panel_name]["sidebar_item"] = item
self.panels[panel_name]["button"] = item["row"]
# 测试版水印标签(版本 x.x.0.0 时显示)
from app_version import is_beta_version, APP_VERSION
@@ -822,7 +844,7 @@ def create_test_type_frame(self: "PQAutomationApp"):
self.theme_toggle_btn.pack(fill=tk.X, padx=0, pady=(0, 2), side=tk.BOTTOM)
_refresh_theme_toggle_label(self)
# 注册面板按钮
# 注册面板侧栏按钮
if hasattr(self, "panels"):
if "ai_image" in self.panels:
self.panels["ai_image"]["button"] = self.ai_image_btn
@@ -847,9 +869,113 @@ def _refresh_theme_toggle_label(self: "PQAutomationApp") -> None:
self.theme_toggle_btn.configure(text="切换深色模式")
def _replot_charts_for_current_theme(self: "PQAutomationApp") -> None:
"""刷新结果区图表配色(仅同步绘制当前可见 Tab"""
if hasattr(self, "refresh_result_charts_theme"):
try:
self.refresh_result_charts_theme(sync_draw=True, visible_only=True)
return
except Exception:
pass
test_type = None
if hasattr(self, "config"):
test_type = getattr(self.config, "current_test_type", None)
snapshots = {}
if test_type and hasattr(self, "_chart_snapshots"):
snapshots = self._chart_snapshots.get(test_type) or {}
if not snapshots:
if hasattr(self, "apply_result_chart_theme"):
self.apply_result_chart_theme(sync_draw=True)
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 _cancel_pending_ui_jobs(self: "PQAutomationApp") -> None:
"""取消可能在主题切换后继续触发的延迟 UI 任务。"""
for attr in ("_calman_matrix_col_job", "_chart_restore_job"):
job = getattr(self, attr, None)
if job is None:
continue
try:
self.root.after_cancel(job)
except Exception:
pass
setattr(self, attr, None)
def _refresh_all_theme_widgets(self: "PQAutomationApp") -> None:
"""同步刷新主题相关控件:优先处理可见区域,隐藏面板延后。"""
_refresh_theme_toggle_label(self)
# 侧栏 / 连接指示 / 选中态 —— 始终可见,必须立即更新
for method_name in (
"refresh_tool_panel_sidebar_theme",
"refresh_connection_indicators",
):
method = getattr(self, method_name, None)
if method is not None:
try:
method()
except Exception:
pass
current_panel = getattr(self, "current_panel", None)
if current_panel == "ai_image" and hasattr(self, "refresh_ai_image_theme"):
try:
self.refresh_ai_image_theme()
except Exception:
pass
else:
self._ai_image_theme_dirty = True
if current_panel == "single_step" and hasattr(self, "refresh_single_step_theme"):
try:
self.refresh_single_step_theme()
except Exception:
pass
if current_panel == "log" and hasattr(self, "log_gui") and hasattr(
self.log_gui, "refresh_log_theme"
):
try:
self.log_gui.refresh_log_theme()
except Exception:
pass
if hasattr(self, "refresh_custom_template_theme"):
try:
self.refresh_custom_template_theme()
except Exception:
pass
if hasattr(self, "refresh_calman_theme"):
try:
self.refresh_calman_theme()
except Exception:
pass
if hasattr(self, "update_sidebar_selection"):
try:
self.update_sidebar_selection()
except Exception:
pass
_replot_charts_for_current_theme(self)
def _on_toggle_theme(self: "PQAutomationApp") -> None:
"""切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签"""
# 在测试进行时禁止切换主题,避免影响测量稳定性
"""切换主题:保持窗口可见,同步批量刷新避免卡顿与分批变色"""
if getattr(self, "testing", False):
try:
if hasattr(self, "log_gui"):
@@ -859,62 +985,14 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None:
return
from app.views.theme_manager import toggle_theme
_cancel_pending_ui_jobs(self)
self._theme_transition = True
try:
toggle_theme()
# apply_modern_styles()
_refresh_theme_toggle_label(self)
if hasattr(self, "refresh_connection_indicators"):
try:
self.refresh_connection_indicators()
except Exception:
pass
if hasattr(self, "apply_result_chart_theme"):
try:
self.apply_result_chart_theme()
except Exception:
pass
if hasattr(self, "log_gui") and hasattr(self.log_gui, "refresh_log_theme"):
try:
self.log_gui.refresh_log_theme()
except Exception:
pass
if hasattr(self, "refresh_ai_image_theme"):
try:
self.refresh_ai_image_theme()
except Exception:
pass
if hasattr(self, "refresh_single_step_theme"):
try:
self.refresh_single_step_theme()
except Exception:
pass
if hasattr(self, "refresh_custom_template_theme"):
try:
self.refresh_custom_template_theme()
except Exception:
pass
if hasattr(self, "refresh_calman_theme"):
try:
self.refresh_calman_theme()
except Exception:
pass
# 同步刷新侧栏选中态(高亮样式跟随新色板)
if hasattr(self, "update_sidebar_selection"):
try:
self.update_sidebar_selection()
except Exception:
pass
# 以新的 dark_mode 值重绘当前测试类型的所有图表
if hasattr(self, "_chart_snapshots") and hasattr(self, "config"):
test_type = getattr(self.config, "current_test_type", None)
if test_type:
snapshots = self._chart_snapshots.get(test_type, {})
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
_refresh_all_theme_widgets(self)
finally:
self._theme_transition = False
def update_config_info_display(self: "PQAutomationApp"):
@@ -1240,33 +1318,17 @@ def update_test_items(self: "PQAutomationApp"):
frame = config["frame"]
frame.pack(fill=tk.X, padx=5, pady=5)
# 添加测试类型标签
type_label = ttk.Label(
frame,
config["type_label"].configure(
text=self.get_test_type_name(current_test_type),
style="primary.TLabel",
)
type_label.grid(row=0, column=0, columnspan=2, sticky=tk.W, padx=5, pady=3)
# 从配置中读取保存的选择状态
saved_test_items = self.config.current_test_types[current_test_type].get(
"test_items", []
)
# 添加复选框
toggle_bootstyle = "success-round-toggle"
for i, (text, var_name) in enumerate(config["items"]):
is_checked = var_name in saved_test_items
var = tk.BooleanVar(value=is_checked)
for var_name, var in config["checkbox_vars"].items():
var.set(var_name in saved_test_items)
self.test_vars[f"{current_test_type}_{var_name}"] = var
ttk.Checkbutton(
frame,
text=text,
variable=var,
bootstyle=toggle_bootstyle,
command=self.update_config_and_tabs,
).grid(row=i // 2 + 1, column=i % 2, sticky=tk.W, padx=10, pady=5)
if hasattr(self, "chart_notebook"):
self.update_chart_tabs_state()

View File

@@ -23,7 +23,7 @@ _TEMPLATE_FILE = "pantone_2670_colors.xlsx"
def create_pantone_baseline_panel(self: "PQAutomationApp"):
"""创建 Pantone 认证摸底测试面板。"""
frame = ttk.Frame(self.content_frame)
frame = ttk.Frame(self.view_stack)
self.pantone_baseline_frame = frame
self.pantone_baseline_visible = False
self.pantone_patterns = []

View File

@@ -15,7 +15,7 @@ if TYPE_CHECKING:
def create_log_panel(self: "PQAutomationApp"):
"""创建日志面板"""
self.log_frame = ttk.Frame(self.content_frame)
self.log_frame = ttk.Frame(self.view_stack)
self.log_gui = PQLogGUI(self.log_frame)
self.log_gui.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
@@ -30,7 +30,7 @@ def create_log_panel(self: "PQAutomationApp"):
def create_local_dimming_panel(self: "PQAutomationApp"):
"""创建 Local Dimming 测试面板。"""
self.local_dimming_frame = ttk.Frame(self.content_frame)
self.local_dimming_frame = ttk.Frame(self.main_view)
# 主容器
main_container = ttk.Frame(self.local_dimming_frame, padding=10)

View File

@@ -3,6 +3,8 @@ PQ 单步调试面板
支持屏模组、SDR、HDR 三种测试类型的单步调试功能
"""
import copy
import ttkbootstrap as ttk
import tkinter as tk
from tkinter import messagebox
@@ -840,8 +842,12 @@ class PQDebugPanel:
# ==================== 构建对比数据 ====================
comparison = {}
if test_item == "gamma":
# ========== Gamma 测试:只对比 x, y, lv ==========
if test_item in ("gamma", "eotf"):
result_key = "gamma" if test_item == "gamma" else "eotf"
original_gamma = self._get_gamma_from_results(result_key, index)
new_gamma = self._calculate_gamma_at_index(
test_type, test_item, index, new_data
)
comparison["x"] = (
original_data[0],
new_data[0],
@@ -857,23 +863,10 @@ class PQDebugPanel:
new_data[2],
new_data[2] - original_data[2],
)
elif test_item == "eotf":
# ========== EOTF 测试:只对比 x, y, lv ==========
comparison["x"] = (
original_data[0],
new_data[0],
new_data[0] - original_data[0],
)
comparison["y"] = (
original_data[1],
new_data[1],
new_data[1] - original_data[1],
)
comparison["lv"] = (
original_data[2],
new_data[2],
new_data[2] - original_data[2],
comparison["γ"] = (
original_gamma,
new_gamma,
new_gamma - original_gamma,
)
elif test_item == "accuracy":
@@ -983,6 +976,16 @@ class PQDebugPanel:
diff_str = f"{diff:+.2f}"
threshold = 0.5
elif key == "γ":
original_str = self._format_gamma_value(original)
new_str = self._format_gamma_value(new)
if original_str == "--" or new_str == "--":
diff_str = "--"
threshold = None
else:
diff_str = f"{diff:+.2f}"
threshold = 0.05
else: # x, y
# x, y 使用 4 位小数
original_str = f"{original:.4f}"
@@ -991,7 +994,9 @@ class PQDebugPanel:
threshold = 0.001
# 判断差异
if abs(diff) > threshold:
if threshold is None:
tag = "normal"
elif abs(diff) > threshold:
tag = "warning"
else:
tag = "normal"
@@ -1114,6 +1119,79 @@ class PQDebugPanel:
"100% Yellow",
]
def _format_gamma_value(self, gamma):
"""格式化 Gamma 值,无效时显示 --"""
if gamma is None or gamma < 0.5 or gamma > 5.0:
return "--"
return f"{gamma:.2f}"
def _get_gamma_calc_params(self, results):
"""获取 Gamma 计算参数(与批量测试保持一致)"""
config_max_value = self.app.config.current_pattern.get(
"measurement_max_value", 10
)
try:
max_index_fix = int(config_max_value)
except (ValueError, TypeError):
max_index_fix = 10
actual_max_index = len(results) - 1
if max_index_fix > actual_max_index:
max_index_fix = actual_max_index
pattern_params = self.app.config.default_pattern_gray.get(
"pattern_params", None
)
return max_index_fix, pattern_params
def _get_gamma_from_results(self, result_key, index):
"""从原始测试结果中读取指定灰阶的 Gamma 值"""
try:
if result_key not in self.app.results.test_items:
return 0.0
result_data = self.app.results.test_items[result_key].final_result
if not result_data:
return 0.0
gamma_list = result_data.get(result_key, [])
if index >= len(gamma_list):
return 0.0
item = gamma_list[index]
return item[3] if len(item) > 3 else 0.0
except Exception as e:
self.app.log_gui.log(f"读取 Gamma 失败: {str(e)}", level="error")
return 0.0
def _calculate_gamma_at_index(self, test_type, test_item, index, new_data):
"""用单步测量更新对应灰阶后,重新计算该点的 Gamma 值"""
try:
key = f"{test_type}_{test_item}"
results = copy.deepcopy(self.original_data[key])
updated = list(results[index])
updated[0] = new_data[0]
updated[1] = new_data[1]
updated[2] = new_data[2]
for i in range(3, min(len(updated), len(new_data))):
updated[i] = new_data[i]
results[index] = updated
max_index_fix, pattern_params = self._get_gamma_calc_params(results)
results_with_gamma, _ = self.app.calculate_gamma(
results, max_index_fix, pattern_params
)
if index >= len(results_with_gamma):
return 0.0
return results_with_gamma[index][3]
except Exception as e:
self.app.log_gui.log(f"计算 Gamma 失败: {str(e)}", level="error")
return 0.0
def _get_delta_e_from_results(self, test_type, color_name):
"""从原始测试结果中读取 ΔE 值"""
try:

View File

@@ -6,7 +6,6 @@ import time
import os
import datetime
import traceback
import matplotlib
import matplotlib.pyplot as plt
from app_version import APP_NAME, APP_VERSION, get_app_title
from app.ucd import (
@@ -61,7 +60,7 @@ 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.device.connection import DeviceConnectionMixin
from app.device.connection import DeviceConnectionMixin, ConnectionController
from app.runner.test_runner import TestRunnerMixin
plt.rcParams["font.family"] = ["sans-serif"]
@@ -123,13 +122,12 @@ class PQAutomationApp(
# 连接控制器:统一管理 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
# 采集节奏参数:默认在稳定性与速度之间取平衡,可按现场情况再微调
# 采集节奏参数由 settings/pq_config.json → acquisition_timing 加载(见 load_pq_config
self.pattern_settle_time = 0.4
self.pattern_progress_log_step = 5
@@ -158,6 +156,7 @@ class PQAutomationApp(
self.current_panel = None # 当前显示的面板名称
self.panels = {} # 存储所有面板的信息
self.log_visible = False
self._theme_transition = False
# 创建左侧面板
self.left_frame = ttk.Frame(self.main_frame, width=208)
@@ -175,18 +174,25 @@ class PQAutomationApp(
side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5
)
# 叠层容器:主界面与工具面板同尺寸叠放,切换时用 lift 而非 pack_forget。
self.view_stack = ttk.Frame(self.content_frame)
self.view_stack.pack(fill=tk.BOTH, expand=True)
self.main_view = ttk.Frame(self.view_stack)
self._ld_embedded = False
self._view_stack_ready = False
# 创建右侧内容区域的上中下三个分区
self.control_frame_top = ttk.Frame(self.content_frame)
self.control_frame_top = ttk.Frame(self.main_view)
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 = ttk.Frame(self.main_view)
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 = ttk.Frame(self.main_view)
self.control_frame_bottom.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
@@ -224,6 +230,7 @@ class PQAutomationApp(
self.create_result_chart_frame()
# 创建客户模板结果显示区域(黑底表格)
self.create_custom_template_result_panel()
self.setup_view_stack()
# 在所有控件创建完成后,统一初始化测试类型
self.root.after(100, self.initialize_default_test_type)
@@ -408,17 +415,6 @@ class PQAutomationApp(
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:
@@ -478,7 +474,19 @@ class PQAutomationApp(
self._switch_signal_format_tabs(test_type)
self._sync_custom_template_tab_visibility(test_type)
self.sync_gamut_toolbar()
self._restore_charts_for_type(test_type)
self._schedule_chart_restore(test_type)
def _schedule_chart_restore(self, test_type: str) -> None:
"""延迟恢复图表,避免阻塞测试类型切换时的 UI 响应。"""
job = getattr(self, "_chart_restore_job", None)
if job is not None:
try:
self.root.after_cancel(job)
except Exception:
pass
self._chart_restore_job = self.root.after_idle(
lambda tt=test_type: self._restore_charts_for_type(tt)
)
def _save_chart_snapshot(self, test_type: str, chart_name: str, args: tuple):
"""保存某次绘图的参数,以便切换测试类型时可以重绘。"""

View File

@@ -1,5 +1,5 @@
{
"current_test_type": "sdr_movie",
"current_test_type": "local_dimming",
"test_types": {
"screen_module": {
"name": "屏模组性能测试",
@@ -10,10 +10,10 @@
"contrast"
],
"timing": "DMT 1920x 1080 @ 60Hz",
"data_range": "Full",
"data_range": "Limited",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "sRGB",
"colorimetry": "DCI-P3",
"patterns": {
"gamut": "rgb",
"gamma": "gray",
@@ -105,5 +105,9 @@
"measurement_bit_depth": 8,
"measurement_max_value": 0,
"pattern_params": []
},
"acquisition_timing": {
"pattern_settle_time": 0.4,
"pattern_progress_log_step": 5
}
}