Compare commits
5 Commits
dc096a9805
...
2d8d4119e3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d8d4119e3 | ||
|
|
92cba114a9 | ||
|
|
22f8b12690 | ||
|
|
9c15751dd0 | ||
|
|
3dd383b982 |
@@ -231,6 +231,12 @@ class BaseIO:
|
|||||||
self.__ref_count = TSI_Clean()
|
self.__ref_count = TSI_Clean()
|
||||||
logging.info(f"[UniTAP] BaseIO.del: {self}; Ref count: {self.__ref_count}")
|
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:
|
def get_device_count(self) -> int:
|
||||||
if self.__ref_count > 0:
|
if self.__ref_count > 0:
|
||||||
return TSIX_DEV_GetDeviceCount()
|
return TSIX_DEV_GetDeviceCount()
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ class TsiLib:
|
|||||||
self.__io.cleanup()
|
self.__io.cleanup()
|
||||||
logging.info(f"[UniTAP] TsiLib.cleanup {self}")
|
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):
|
def __del__(self):
|
||||||
"""
|
"""
|
||||||
Automatically call the destructor after the script completes.
|
Automatically call the destructor after the script completes.
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ def get_config_path(self: "PQAutomationApp"):
|
|||||||
return config_file
|
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"):
|
def load_pq_config(self: "PQAutomationApp"):
|
||||||
"""加载PQ配置(兼容打包后的程序)"""
|
"""加载PQ配置(兼容打包后的程序)"""
|
||||||
try:
|
try:
|
||||||
@@ -44,12 +53,15 @@ def load_pq_config(self: "PQAutomationApp"):
|
|||||||
with open(self.config_file, "r", encoding="utf-8") as f:
|
with open(self.config_file, "r", encoding="utf-8") as f:
|
||||||
config_dict = json.load(f)
|
config_dict = json.load(f)
|
||||||
self.config.from_dict(config_dict)
|
self.config.from_dict(config_dict)
|
||||||
|
_apply_acquisition_timing(self)
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log("配置文件加载成功", level="success")
|
self.log_gui.log("配置文件加载成功", level="success")
|
||||||
else:
|
else:
|
||||||
|
_apply_acquisition_timing(self)
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log("配置文件不存在,使用默认配置", level="error")
|
self.log_gui.log("配置文件不存在,使用默认配置", level="error")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
_apply_acquisition_timing(self)
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"加载配置文件失败: {str(e)},使用默认配置", level="error")
|
self.log_gui.log(f"加载配置文件失败: {str(e)},使用默认配置", level="error")
|
||||||
|
|
||||||
|
|||||||
@@ -78,26 +78,89 @@ class ConnectionController:
|
|||||||
|
|
||||||
# -- UCD 连接 ------------------------------------------------
|
# -- UCD 连接 ------------------------------------------------
|
||||||
|
|
||||||
def connect_ucd(self, display: str) -> bool:
|
def _pick_ucd_device(self, ucd_list: list[str], display: str) -> str | None:
|
||||||
"""打开指定 UCD 设备。成功返回 True。"""
|
"""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":
|
if self._device.state.name != "CLOSED":
|
||||||
try:
|
try:
|
||||||
self._device.close()
|
self._device.close()
|
||||||
except Exception: # noqa: BLE001
|
return
|
||||||
pass
|
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:
|
try:
|
||||||
self._device.open(DeviceInfo.parse(display))
|
self._device.open(DeviceInfo.parse(display))
|
||||||
return True
|
return True
|
||||||
except UcdError as exc:
|
except UcdError as exc:
|
||||||
self._log(f"设备 {display} 异常,UCD323 连接失败: {exc}", level="error")
|
last_error = exc
|
||||||
return False
|
self._log(
|
||||||
|
f"设备 {display} 连接失败 ({attempt + 1}/2): {exc}",
|
||||||
|
level="error",
|
||||||
|
)
|
||||||
except Exception as exc: # noqa: BLE001
|
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
|
return False
|
||||||
|
|
||||||
def disconnect_ucd(self) -> None:
|
def disconnect_ucd(self) -> None:
|
||||||
try:
|
try:
|
||||||
self._device.close()
|
self._device.close()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
try:
|
||||||
|
self._device.force_reset()
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
pass
|
pass
|
||||||
self._log("UCD连接已断开", level="info")
|
self._log("UCD连接已断开", level="info")
|
||||||
@@ -158,7 +221,11 @@ class ConnectionController:
|
|||||||
|
|
||||||
def worker():
|
def worker():
|
||||||
try:
|
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._dispatch_ui(
|
||||||
app.update_connection_indicator,
|
app.update_connection_indicator,
|
||||||
app.ucd_status_indicator, ucd_ok,
|
app.ucd_status_indicator, ucd_ok,
|
||||||
@@ -185,7 +252,19 @@ class ConnectionController:
|
|||||||
|
|
||||||
def worker():
|
def worker():
|
||||||
try:
|
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._dispatch_ui(
|
||||||
app.update_connection_indicator,
|
app.update_connection_indicator,
|
||||||
app.ucd_status_indicator,
|
app.ucd_status_indicator,
|
||||||
@@ -262,9 +341,15 @@ class ConnectionController:
|
|||||||
app = self._app
|
app = self._app
|
||||||
com_ports = self.list_com_ports()
|
com_ports = self.list_com_ports()
|
||||||
ucd_list = self.list_ucd_devices()
|
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:
|
selected = self._pick_ucd_device(ucd_list, app.ucd_list_var.get())
|
||||||
app.ucd_list_var.set(ucd_list[0] if ucd_list else "")
|
app.ucd_list_var.set(selected or "")
|
||||||
app.ucd_list_combo.config(values=ucd_list)
|
app.ucd_list_combo.config(values=ucd_list)
|
||||||
|
|
||||||
if app.ca_com_var.get() not in com_ports:
|
if app.ca_com_var.get() not in com_ports:
|
||||||
|
|||||||
@@ -655,6 +655,12 @@ class PQConfig:
|
|||||||
"ca_channel": "0",
|
"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 = {
|
self.custom_pattern = {
|
||||||
"pattern_mode": "SolidColor",
|
"pattern_mode": "SolidColor",
|
||||||
@@ -710,6 +716,7 @@ class PQConfig:
|
|||||||
"test_types": self.current_test_types,
|
"test_types": self.current_test_types,
|
||||||
"device_config": self.device_config,
|
"device_config": self.device_config,
|
||||||
"custom_pattern": self.custom_pattern,
|
"custom_pattern": self.custom_pattern,
|
||||||
|
"acquisition_timing": self.acquisition_timing,
|
||||||
}
|
}
|
||||||
|
|
||||||
def from_dict(self, config_dict):
|
def from_dict(self, config_dict):
|
||||||
@@ -728,6 +735,17 @@ class PQConfig:
|
|||||||
self.device_config = config_dict.get("device_config", self.device_config)
|
self.device_config = config_dict.get("device_config", self.device_config)
|
||||||
self.custom_pattern = config_dict.get("custom_pattern", self.custom_pattern)
|
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):
|
def save_to_file(self, filename):
|
||||||
"""将配置保存到文件"""
|
"""将配置保存到文件"""
|
||||||
with open(filename, "w", encoding="utf-8") as f:
|
with open(filename, "w", encoding="utf-8") as f:
|
||||||
|
|||||||
@@ -356,7 +356,8 @@ def send_fix_pattern(self: "PQAutomationApp", mode):
|
|||||||
1, int(getattr(self, "pattern_progress_log_step", 5))
|
1, int(getattr(self, "pattern_progress_log_step", 5))
|
||||||
)
|
)
|
||||||
self.log_gui.log(
|
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")
|
, level="info")
|
||||||
|
|
||||||
display_names = session.display_names
|
display_names = session.display_names
|
||||||
|
|||||||
@@ -111,6 +111,12 @@ class _UcdSdkBackend:
|
|||||||
|
|
||||||
def search_device(self):
|
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()
|
available_devices = self.lUniTAP.get_list_of_available_devices()
|
||||||
return available_devices if available_devices else []
|
return available_devices if available_devices else []
|
||||||
|
|
||||||
@@ -122,6 +128,13 @@ class _UcdSdkBackend:
|
|||||||
if self.dev is not None or self.status:
|
if self.dev is not None or self.status:
|
||||||
self._force_cleanup()
|
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])
|
device_id = int(device_name.split(":")[0])
|
||||||
temp_dev = self.lUniTAP.open(device_id)
|
temp_dev = self.lUniTAP.open(device_id)
|
||||||
|
|
||||||
@@ -143,7 +156,7 @@ class _UcdSdkBackend:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._force_cleanup()
|
self.hard_reset()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
@@ -154,32 +167,42 @@ class _UcdSdkBackend:
|
|||||||
self._stop_audio_output()
|
self._stop_audio_output()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
self._close_device_object(self.dev)
|
self._close_device_object(self.dev)
|
||||||
|
except Exception:
|
||||||
|
log.exception("UcdSdk.close device object failed")
|
||||||
|
finally:
|
||||||
self._reset_state()
|
self._reset_state()
|
||||||
self.lUniTAP = None
|
try:
|
||||||
|
self._reinit_tsi_lib()
|
||||||
for i in range(3):
|
except Exception:
|
||||||
gc.collect()
|
log.exception("UcdSdk.close reinit failed")
|
||||||
|
|
||||||
time.sleep(2.0)
|
|
||||||
|
|
||||||
self.lUniTAP = UniTAP.TsiLib()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
def hard_reset(self) -> None:
|
||||||
|
"""丢弃失效句柄并重建 TSI(意外断连后重连前调用)。"""
|
||||||
|
self.dev = None
|
||||||
|
self.role = None
|
||||||
self._reset_state()
|
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
|
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()
|
gc.collect()
|
||||||
time.sleep(2.0)
|
time.sleep(2.0)
|
||||||
self.lUniTAP = UniTAP.TsiLib()
|
self.lUniTAP = UniTAP.TsiLib()
|
||||||
except Exception as init_error:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _reset_state(self):
|
def _reset_state(self):
|
||||||
"""重置所有运行时状态(不关闭设备句柄)"""
|
"""重置所有运行时状态(不关闭设备句柄)"""
|
||||||
@@ -1055,6 +1078,26 @@ class UCD323Device(IUcdDevice):
|
|||||||
self._info = None
|
self._info = None
|
||||||
self._bus.publish(ConnectionChanged(DeviceKind.UCD, False, prev_serial))
|
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:
|
def configure(self, signal: SignalFormat, timing: TimingSpec) -> bool:
|
||||||
|
|||||||
@@ -25,7 +25,138 @@ def _result_bg_color() -> str:
|
|||||||
return "#FFFFFF"
|
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()
|
bg = _result_bg_color()
|
||||||
|
|
||||||
@@ -50,6 +181,9 @@ def apply_result_chart_theme(self: "PQAutomationApp"):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
|
if sync_draw:
|
||||||
|
canvas.draw()
|
||||||
|
else:
|
||||||
canvas.draw_idle()
|
canvas.draw_idle()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -1111,6 +1245,9 @@ def on_chart_tab_changed(self: "PQAutomationApp", event):
|
|||||||
if not selected_tab:
|
if not selected_tab:
|
||||||
return
|
return
|
||||||
self._last_tab_index = self.chart_notebook.index(selected_tab)
|
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:
|
except Exception as e:
|
||||||
self.log_gui.log(f"Tab切换事件处理失败: {str(e)}", level="error")
|
self.log_gui.log(f"Tab切换事件处理失败: {str(e)}", level="error")
|
||||||
|
|
||||||
@@ -1129,6 +1266,8 @@ class ChartFrameMixin:
|
|||||||
init_contrast_chart = init_contrast_chart
|
init_contrast_chart = init_contrast_chart
|
||||||
init_accuracy_chart = init_accuracy_chart
|
init_accuracy_chart = init_accuracy_chart
|
||||||
apply_result_chart_theme = apply_result_chart_theme
|
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
|
_init_accuracy_result_table = _init_accuracy_result_table
|
||||||
clear_accuracy_result_table = clear_accuracy_result_table
|
clear_accuracy_result_table = clear_accuracy_result_table
|
||||||
update_accuracy_result_table = update_accuracy_result_table
|
update_accuracy_result_table = update_accuracy_result_table
|
||||||
|
|||||||
@@ -3,88 +3,219 @@
|
|||||||
register_panel / show_panel / hide_all_panels —— 在右侧栏不同浮动面板间切换。
|
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:
|
if TYPE_CHECKING:
|
||||||
from pqAutomationApp import PQAutomationApp
|
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):
|
def register_panel(self: "PQAutomationApp", panel_name, frame, button, visible_attr):
|
||||||
"""注册一个面板到管理系统"""
|
"""注册一个面板到管理系统"""
|
||||||
self.panels[panel_name] = {
|
self.panels[panel_name] = {
|
||||||
"frame": frame,
|
"frame": frame,
|
||||||
"button": button,
|
"button": button,
|
||||||
|
"button_label": None,
|
||||||
|
"sidebar_item": None,
|
||||||
"visible_attr": visible_attr,
|
"visible_attr": visible_attr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def show_panel(self: "PQAutomationApp", panel_name):
|
def setup_view_stack(self: "PQAutomationApp") -> None:
|
||||||
"""显示指定面板,隐藏其他所有面板"""
|
"""将所有工具面板叠放在 view_stack 上,切换时仅调整 Z 序。"""
|
||||||
if panel_name not in self.panels:
|
if getattr(self, "_view_stack_ready", False):
|
||||||
return
|
return
|
||||||
|
|
||||||
# 如果当前面板就是要显示的面板,则隐藏它
|
self.main_view.place(relx=0, rely=0, relwidth=1, relheight=1)
|
||||||
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"):
|
|
||||||
"""隐藏所有面板,显示主内容区域"""
|
|
||||||
# 隐藏所有注册的面板
|
|
||||||
for panel_name, panel_info in self.panels.items():
|
for panel_name, panel_info in self.panels.items():
|
||||||
panel_info["frame"].pack_forget()
|
if panel_name == "local_dimming":
|
||||||
if panel_info["button"]:
|
continue
|
||||||
panel_info["button"].configure(style="Sidebar.TButton")
|
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)
|
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(
|
self.control_frame_top.pack(
|
||||||
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
|
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
|
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
|
self.current_panel = None
|
||||||
|
|
||||||
if hasattr(self, "_sync_save_button_state"):
|
if hasattr(self, "_sync_save_button_state"):
|
||||||
@@ -104,12 +243,128 @@ def hide_all_panels(self: "PQAutomationApp"):
|
|||||||
pass
|
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:
|
class PanelManagerMixin:
|
||||||
"""由 tools/refactor_to_mixins.py 自动生成。
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
"""
|
"""
|
||||||
|
create_tool_panel_sidebar_item = create_tool_panel_sidebar_item
|
||||||
register_panel = register_panel
|
register_panel = register_panel
|
||||||
|
setup_view_stack = setup_view_stack
|
||||||
show_panel = show_panel
|
show_panel = show_panel
|
||||||
hide_all_panels = hide_all_panels
|
hide_all_panels = hide_all_panels
|
||||||
|
refresh_tool_panel_sidebar_theme = refresh_tool_panel_sidebar_theme
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ def _on_tree_motion(self: "PQAutomationApp", event):
|
|||||||
|
|
||||||
def create_ai_image_panel(self: "PQAutomationApp"):
|
def create_ai_image_panel(self: "PQAutomationApp"):
|
||||||
"""创建 AI 图片对话面板,并注册到面板管理。"""
|
"""创建 AI 图片对话面板,并注册到面板管理。"""
|
||||||
frame = ttk.Frame(self.content_frame)
|
frame = ttk.Frame(self.view_stack)
|
||||||
self.ai_image_frame = frame
|
self.ai_image_frame = frame
|
||||||
|
|
||||||
# 内部状态
|
# 内部状态
|
||||||
@@ -413,9 +413,9 @@ def create_ai_image_panel(self: "PQAutomationApp"):
|
|||||||
def toggle_ai_image_panel(self: "PQAutomationApp"):
|
def toggle_ai_image_panel(self: "PQAutomationApp"):
|
||||||
"""切换 AI 图片面板显隐。"""
|
"""切换 AI 图片面板显隐。"""
|
||||||
self.show_panel("ai_image")
|
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):
|
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:
|
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"):
|
def refresh_ai_image_theme(self: "PQAutomationApp"):
|
||||||
"""刷新 AI 图片面板中的主题相关控件。"""
|
"""刷新 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"):
|
if hasattr(self, "_apply_ai_image_list_style"):
|
||||||
self._apply_ai_image_list_style()
|
self._apply_ai_image_list_style()
|
||||||
tip = getattr(self, "_ai_image_tooltip", None)
|
tip = getattr(self, "_ai_image_tooltip", None)
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ def _refresh_calman_config_summary(self: "PQAutomationApp") -> None:
|
|||||||
def create_calman_panel(self: "PQAutomationApp") -> None:
|
def create_calman_panel(self: "PQAutomationApp") -> None:
|
||||||
"""创建 CALMAN 风格灰阶测试面板,注册到 panel_manager。"""
|
"""创建 CALMAN 风格灰阶测试面板,注册到 panel_manager。"""
|
||||||
palette = _get_calman_palette()
|
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_visible = False
|
||||||
self.calman_levels = list(DEFAULT_LEVELS_PCT)
|
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)
|
# 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)
|
_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)
|
self.root.after_idle(lambda: _finish_calman_panel_init(self))
|
||||||
_refresh_calman_config_summary(self)
|
|
||||||
_update_target_strip(self)
|
|
||||||
_update_actual_strip(self)
|
|
||||||
_redraw_calman_charts(self)
|
|
||||||
|
|
||||||
# 注册到统一面板管理(按钮稍后由 main_layout 注入)
|
# 注册到统一面板管理(按钮稍后由 main_layout 注入)
|
||||||
self.register_panel("calman", self.calman_frame, None, "calman_visible")
|
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:
|
def toggle_calman_panel(self: "PQAutomationApp") -> None:
|
||||||
"""切换 CALMAN 灰阶面板显示。"""
|
"""切换 CALMAN 灰阶面板显示。"""
|
||||||
self.show_panel("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)
|
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 散点。"""
|
"""根据 calman_results 重绘四张图和 xy 散点。"""
|
||||||
palette = _get_calman_palette()
|
palette = _get_calman_palette()
|
||||||
if hasattr(self, "calman_fig"):
|
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_ylim(1.8, 2.8)
|
||||||
a4.set_xlabel("", fontsize=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()
|
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()
|
palette = _get_calman_palette()
|
||||||
if hasattr(self, "calman_xy_fig"):
|
if hasattr(self, "calman_xy_fig"):
|
||||||
self.calman_xy_fig.patch.set_facecolor(palette["figure_bg"])
|
self.calman_xy_fig.patch.set_facecolor(palette["figure_bg"])
|
||||||
@@ -1245,6 +1276,9 @@ def _redraw_xy_chart(self: "PQAutomationApp") -> None:
|
|||||||
last = recs[-1]
|
last = recs[-1]
|
||||||
ax.plot([last["x"]], [last["y"]], marker="o", color="#ffcc00", markersize=5)
|
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)
|
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()
|
self.calman_xy_canvas.draw_idle()
|
||||||
|
|
||||||
|
|
||||||
@@ -1337,11 +1371,18 @@ def _refresh_metric_table(self: "PQAutomationApp") -> None:
|
|||||||
self.calman_table_ysb.set(first, last)
|
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 灰阶调试面板的表格与图表颜色。"""
|
"""主题切换后刷新 Calman 灰阶调试面板的表格与图表颜色。"""
|
||||||
if not hasattr(self, "calman_frame"):
|
if not hasattr(self, "calman_frame"):
|
||||||
return
|
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()
|
palette = _get_calman_palette()
|
||||||
|
|
||||||
if hasattr(self, "calman_elapsed_label"):
|
if hasattr(self, "calman_elapsed_label"):
|
||||||
@@ -1365,7 +1406,11 @@ def refresh_calman_theme(self: "PQAutomationApp") -> None:
|
|||||||
|
|
||||||
_refresh_metric_table(self)
|
_refresh_metric_table(self)
|
||||||
_refresh_calman_config_summary(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:
|
class CalmanPanelMixin:
|
||||||
@@ -1374,3 +1419,4 @@ class CalmanPanelMixin:
|
|||||||
create_calman_panel = create_calman_panel
|
create_calman_panel = create_calman_panel
|
||||||
toggle_calman_panel = toggle_calman_panel
|
toggle_calman_panel = toggle_calman_panel
|
||||||
refresh_calman_theme = refresh_calman_theme
|
refresh_calman_theme = refresh_calman_theme
|
||||||
|
_on_calman_panel_shown = _on_calman_panel_shown
|
||||||
|
|||||||
@@ -172,6 +172,14 @@ def _apply_custom_result_theme(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
def refresh_custom_template_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)
|
_apply_custom_result_theme(self)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ _GENERATORS = {
|
|||||||
|
|
||||||
def create_gamma_pattern_panel(self: "PQAutomationApp"):
|
def create_gamma_pattern_panel(self: "PQAutomationApp"):
|
||||||
"""创建灰阶 Pattern 配置面板。"""
|
"""创建灰阶 Pattern 配置面板。"""
|
||||||
frame = ttk.Frame(self.content_frame)
|
frame = ttk.Frame(self.view_stack)
|
||||||
self.gamma_pattern_frame = frame
|
self.gamma_pattern_frame = frame
|
||||||
self.gamma_pattern_visible = False
|
self.gamma_pattern_visible = False
|
||||||
|
|
||||||
|
|||||||
@@ -171,8 +171,32 @@ def create_test_items_content(self: "PQAutomationApp"):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# 根据当前测试类型创建复选框
|
# 预创建各测试类型的复选框,切换时只改显隐与勾选状态,避免重复构建控件。
|
||||||
self.test_vars = {}
|
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()
|
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")
|
).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w")
|
||||||
|
|
||||||
panel_buttons = [
|
panel_buttons = [
|
||||||
("ai_image_btn", "AI 图片", self.toggle_ai_image_panel),
|
("ai_image_btn", "ai_image", "AI 图片", self.toggle_ai_image_panel),
|
||||||
("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel),
|
("pantone_baseline_btn", "pantone_baseline", "Pantone 摸底", self.toggle_pantone_baseline_panel),
|
||||||
("gamma_pattern_btn", "Gamma Pattern编辑", self.toggle_gamma_pattern_panel),
|
("gamma_pattern_btn", "gamma_pattern", "Gamma Pattern编辑", self.toggle_gamma_pattern_panel),
|
||||||
("calman_btn", "CALMAN 灰阶", self.toggle_calman_panel),
|
("calman_btn", "calman", "CALMAN 灰阶", self.toggle_calman_panel),
|
||||||
]
|
]
|
||||||
for attr, text, cmd in panel_buttons:
|
for attr, panel_name, text, cmd in panel_buttons:
|
||||||
btn = ttk.Button(
|
item = self.create_tool_panel_sidebar_item(self.sidebar_frame, text, cmd)
|
||||||
self.sidebar_frame,
|
item["pack"]()
|
||||||
text=text,
|
setattr(self, attr, item["row"])
|
||||||
style="Sidebar.TButton",
|
if hasattr(self, "panels") and panel_name in self.panels:
|
||||||
command=cmd,
|
self.panels[panel_name]["button_label"] = text
|
||||||
takefocus=False,
|
self.panels[panel_name]["sidebar_item"] = item
|
||||||
)
|
self.panels[panel_name]["button"] = item["row"]
|
||||||
btn.pack(fill=tk.X, padx=0, pady=1)
|
|
||||||
setattr(self, attr, btn)
|
|
||||||
|
|
||||||
# 测试版水印标签(版本 x.x.0.0 时显示)
|
# 测试版水印标签(版本 x.x.0.0 时显示)
|
||||||
from app_version import is_beta_version, APP_VERSION
|
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)
|
self.theme_toggle_btn.pack(fill=tk.X, padx=0, pady=(0, 2), side=tk.BOTTOM)
|
||||||
_refresh_theme_toggle_label(self)
|
_refresh_theme_toggle_label(self)
|
||||||
|
|
||||||
# 注册面板按钮
|
# 注册面板侧栏按钮
|
||||||
if hasattr(self, "panels"):
|
if hasattr(self, "panels"):
|
||||||
if "ai_image" in self.panels:
|
if "ai_image" in self.panels:
|
||||||
self.panels["ai_image"]["button"] = self.ai_image_btn
|
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="切换深色模式")
|
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:
|
def _on_toggle_theme(self: "PQAutomationApp") -> None:
|
||||||
"""切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。"""
|
"""切换主题:保持窗口可见,同步批量刷新避免卡顿与分批变色。"""
|
||||||
# 在测试进行时禁止切换主题,避免影响测量稳定性
|
|
||||||
if getattr(self, "testing", False):
|
if getattr(self, "testing", False):
|
||||||
try:
|
try:
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
@@ -859,62 +985,14 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
from app.views.theme_manager import toggle_theme
|
from app.views.theme_manager import toggle_theme
|
||||||
|
|
||||||
|
_cancel_pending_ui_jobs(self)
|
||||||
|
self._theme_transition = True
|
||||||
|
try:
|
||||||
toggle_theme()
|
toggle_theme()
|
||||||
# apply_modern_styles()
|
_refresh_all_theme_widgets(self)
|
||||||
_refresh_theme_toggle_label(self)
|
finally:
|
||||||
if hasattr(self, "refresh_connection_indicators"):
|
self._theme_transition = False
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def update_config_info_display(self: "PQAutomationApp"):
|
def update_config_info_display(self: "PQAutomationApp"):
|
||||||
@@ -1240,33 +1318,17 @@ def update_test_items(self: "PQAutomationApp"):
|
|||||||
frame = config["frame"]
|
frame = config["frame"]
|
||||||
frame.pack(fill=tk.X, padx=5, pady=5)
|
frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
# 添加测试类型标签
|
config["type_label"].configure(
|
||||||
type_label = ttk.Label(
|
|
||||||
frame,
|
|
||||||
text=self.get_test_type_name(current_test_type),
|
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(
|
saved_test_items = self.config.current_test_types[current_test_type].get(
|
||||||
"test_items", []
|
"test_items", []
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加复选框
|
for var_name, var in config["checkbox_vars"].items():
|
||||||
toggle_bootstyle = "success-round-toggle"
|
var.set(var_name in saved_test_items)
|
||||||
for i, (text, var_name) in enumerate(config["items"]):
|
|
||||||
is_checked = var_name in saved_test_items
|
|
||||||
var = tk.BooleanVar(value=is_checked)
|
|
||||||
|
|
||||||
self.test_vars[f"{current_test_type}_{var_name}"] = var
|
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"):
|
if hasattr(self, "chart_notebook"):
|
||||||
self.update_chart_tabs_state()
|
self.update_chart_tabs_state()
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ _TEMPLATE_FILE = "pantone_2670_colors.xlsx"
|
|||||||
|
|
||||||
def create_pantone_baseline_panel(self: "PQAutomationApp"):
|
def create_pantone_baseline_panel(self: "PQAutomationApp"):
|
||||||
"""创建 Pantone 认证摸底测试面板。"""
|
"""创建 Pantone 认证摸底测试面板。"""
|
||||||
frame = ttk.Frame(self.content_frame)
|
frame = ttk.Frame(self.view_stack)
|
||||||
self.pantone_baseline_frame = frame
|
self.pantone_baseline_frame = frame
|
||||||
self.pantone_baseline_visible = False
|
self.pantone_baseline_visible = False
|
||||||
self.pantone_patterns = []
|
self.pantone_patterns = []
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
def create_log_panel(self: "PQAutomationApp"):
|
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 = PQLogGUI(self.log_frame)
|
||||||
self.log_gui.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
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"):
|
def create_local_dimming_panel(self: "PQAutomationApp"):
|
||||||
"""创建 Local Dimming 测试面板。"""
|
"""创建 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)
|
main_container = ttk.Frame(self.local_dimming_frame, padding=10)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ PQ 单步调试面板
|
|||||||
支持屏模组、SDR、HDR 三种测试类型的单步调试功能
|
支持屏模组、SDR、HDR 三种测试类型的单步调试功能
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
import ttkbootstrap as ttk
|
import ttkbootstrap as ttk
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
@@ -840,8 +842,12 @@ class PQDebugPanel:
|
|||||||
# ==================== 构建对比数据 ====================
|
# ==================== 构建对比数据 ====================
|
||||||
comparison = {}
|
comparison = {}
|
||||||
|
|
||||||
if test_item == "gamma":
|
if test_item in ("gamma", "eotf"):
|
||||||
# ========== Gamma 测试:只对比 x, y, lv ==========
|
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"] = (
|
comparison["x"] = (
|
||||||
original_data[0],
|
original_data[0],
|
||||||
new_data[0],
|
new_data[0],
|
||||||
@@ -857,23 +863,10 @@ class PQDebugPanel:
|
|||||||
new_data[2],
|
new_data[2],
|
||||||
new_data[2] - original_data[2],
|
new_data[2] - original_data[2],
|
||||||
)
|
)
|
||||||
|
comparison["γ"] = (
|
||||||
elif test_item == "eotf":
|
original_gamma,
|
||||||
# ========== EOTF 测试:只对比 x, y, lv ==========
|
new_gamma,
|
||||||
comparison["x"] = (
|
new_gamma - original_gamma,
|
||||||
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],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif test_item == "accuracy":
|
elif test_item == "accuracy":
|
||||||
@@ -983,6 +976,16 @@ class PQDebugPanel:
|
|||||||
diff_str = f"{diff:+.2f}"
|
diff_str = f"{diff:+.2f}"
|
||||||
threshold = 0.5
|
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
|
else: # x, y
|
||||||
# x, y 使用 4 位小数
|
# x, y 使用 4 位小数
|
||||||
original_str = f"{original:.4f}"
|
original_str = f"{original:.4f}"
|
||||||
@@ -991,7 +994,9 @@ class PQDebugPanel:
|
|||||||
threshold = 0.001
|
threshold = 0.001
|
||||||
|
|
||||||
# 判断差异
|
# 判断差异
|
||||||
if abs(diff) > threshold:
|
if threshold is None:
|
||||||
|
tag = "normal"
|
||||||
|
elif abs(diff) > threshold:
|
||||||
tag = "warning"
|
tag = "warning"
|
||||||
else:
|
else:
|
||||||
tag = "normal"
|
tag = "normal"
|
||||||
@@ -1114,6 +1119,79 @@ class PQDebugPanel:
|
|||||||
"100% Yellow",
|
"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):
|
def _get_delta_e_from_results(self, test_type, color_name):
|
||||||
"""从原始测试结果中读取 ΔE 值"""
|
"""从原始测试结果中读取 ΔE 值"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import time
|
|||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
import traceback
|
import traceback
|
||||||
import matplotlib
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from app_version import APP_NAME, APP_VERSION, get_app_title
|
from app_version import APP_NAME, APP_VERSION, get_app_title
|
||||||
from app.ucd import (
|
from app.ucd import (
|
||||||
@@ -61,7 +60,7 @@ from app.plots.plot_gamut import PlotGamutMixin
|
|||||||
from app.views.chart_frame import ChartFrameMixin
|
from app.views.chart_frame import ChartFrameMixin
|
||||||
from app.config_io import ConfigIOMixin
|
from app.config_io import ConfigIOMixin
|
||||||
from app.tests.local_dimming import LocalDimmingMixin
|
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
|
from app.runner.test_runner import TestRunnerMixin
|
||||||
|
|
||||||
plt.rcParams["font.family"] = ["sans-serif"]
|
plt.rcParams["font.family"] = ["sans-serif"]
|
||||||
@@ -123,13 +122,12 @@ class PQAutomationApp(
|
|||||||
# 连接控制器:统一管理 CA/UCD 生命周期。
|
# 连接控制器:统一管理 CA/UCD 生命周期。
|
||||||
# 旧的 check_com_connections / disconnect_com_connections 等模块级
|
# 旧的 check_com_connections / disconnect_com_connections 等模块级
|
||||||
# 函数仍以类属性形式挂在 PQAutomationApp 上,内部全部委托给本对象。
|
# 函数仍以类属性形式挂在 PQAutomationApp 上,内部全部委托给本对象。
|
||||||
from app.device.connection import ConnectionController
|
|
||||||
self.connection = ConnectionController(self)
|
self.connection = ConnectionController(self)
|
||||||
|
|
||||||
# 初始化测试状态
|
# 初始化测试状态
|
||||||
self.testing = False
|
self.testing = False
|
||||||
self.test_thread = None
|
self.test_thread = None
|
||||||
# 采集节奏参数:默认在稳定性与速度之间取平衡,可按现场情况再微调。
|
# 采集节奏参数由 settings/pq_config.json → acquisition_timing 加载(见 load_pq_config)。
|
||||||
self.pattern_settle_time = 0.4
|
self.pattern_settle_time = 0.4
|
||||||
self.pattern_progress_log_step = 5
|
self.pattern_progress_log_step = 5
|
||||||
|
|
||||||
@@ -158,6 +156,7 @@ class PQAutomationApp(
|
|||||||
self.current_panel = None # 当前显示的面板名称
|
self.current_panel = None # 当前显示的面板名称
|
||||||
self.panels = {} # 存储所有面板的信息
|
self.panels = {} # 存储所有面板的信息
|
||||||
self.log_visible = False
|
self.log_visible = False
|
||||||
|
self._theme_transition = False
|
||||||
|
|
||||||
# 创建左侧面板
|
# 创建左侧面板
|
||||||
self.left_frame = ttk.Frame(self.main_frame, width=208)
|
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
|
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(
|
self.control_frame_top.pack(
|
||||||
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
|
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(
|
self.control_frame_middle.pack(
|
||||||
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
|
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(
|
self.control_frame_bottom.pack(
|
||||||
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
|
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_result_chart_frame()
|
||||||
# 创建客户模板结果显示区域(黑底表格)
|
# 创建客户模板结果显示区域(黑底表格)
|
||||||
self.create_custom_template_result_panel()
|
self.create_custom_template_result_panel()
|
||||||
|
self.setup_view_stack()
|
||||||
# 在所有控件创建完成后,统一初始化测试类型
|
# 在所有控件创建完成后,统一初始化测试类型
|
||||||
self.root.after(100, self.initialize_default_test_type)
|
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.tab(i, state="normal")
|
||||||
|
|
||||||
self.signal_tabs.select(target_tab)
|
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):
|
for i in range(4):
|
||||||
if i != target_tab:
|
if i != target_tab:
|
||||||
@@ -478,7 +474,19 @@ class PQAutomationApp(
|
|||||||
self._switch_signal_format_tabs(test_type)
|
self._switch_signal_format_tabs(test_type)
|
||||||
self._sync_custom_template_tab_visibility(test_type)
|
self._sync_custom_template_tab_visibility(test_type)
|
||||||
self.sync_gamut_toolbar()
|
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):
|
def _save_chart_snapshot(self, test_type: str, chart_name: str, args: tuple):
|
||||||
"""保存某次绘图的参数,以便切换测试类型时可以重绘。"""
|
"""保存某次绘图的参数,以便切换测试类型时可以重绘。"""
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"current_test_type": "sdr_movie",
|
"current_test_type": "local_dimming",
|
||||||
"test_types": {
|
"test_types": {
|
||||||
"screen_module": {
|
"screen_module": {
|
||||||
"name": "屏模组性能测试",
|
"name": "屏模组性能测试",
|
||||||
@@ -10,10 +10,10 @@
|
|||||||
"contrast"
|
"contrast"
|
||||||
],
|
],
|
||||||
"timing": "DMT 1920x 1080 @ 60Hz",
|
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||||
"data_range": "Full",
|
"data_range": "Limited",
|
||||||
"color_format": "RGB",
|
"color_format": "RGB",
|
||||||
"bpc": 8,
|
"bpc": 8,
|
||||||
"colorimetry": "sRGB",
|
"colorimetry": "DCI-P3",
|
||||||
"patterns": {
|
"patterns": {
|
||||||
"gamut": "rgb",
|
"gamut": "rgb",
|
||||||
"gamma": "gray",
|
"gamma": "gray",
|
||||||
@@ -105,5 +105,9 @@
|
|||||||
"measurement_bit_depth": 8,
|
"measurement_bit_depth": 8,
|
||||||
"measurement_max_value": 0,
|
"measurement_max_value": 0,
|
||||||
"pattern_params": []
|
"pattern_params": []
|
||||||
|
},
|
||||||
|
"acquisition_timing": {
|
||||||
|
"pattern_settle_time": 0.4,
|
||||||
|
"pattern_progress_log_step": 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user