修改引用逻辑、新增Pattern更改界面、新增Calman灰阶界面
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
"""配置文件 I/O(Step 4 重构)。
|
"""配置文件 I/O(Step 4 重构)。
|
||||||
|
|
||||||
从 pqAutomationApp.PQAutomationApp 中搬迁。每个函数第一行 `self = app`
|
从 pqAutomationApp.PQAutomationApp 中搬迁。每个函数第一行 `self = app`
|
||||||
以保留原有 `self.xxx` 属性访问不变。
|
以保留原有 `self.xxx` 属性访问不变。
|
||||||
@@ -8,7 +8,13 @@ import json
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
def get_config_path(self):
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_path(self: "PQAutomationApp"):
|
||||||
"""获取配置文件的完整路径(兼容打包后的程序)"""
|
"""获取配置文件的完整路径(兼容打包后的程序)"""
|
||||||
|
|
||||||
# 判断是否是打包后的程序
|
# 判断是否是打包后的程序
|
||||||
@@ -30,7 +36,7 @@ def get_config_path(self):
|
|||||||
return config_file
|
return config_file
|
||||||
|
|
||||||
|
|
||||||
def load_pq_config(self):
|
def load_pq_config(self: "PQAutomationApp"):
|
||||||
"""加载PQ配置(兼容打包后的程序)"""
|
"""加载PQ配置(兼容打包后的程序)"""
|
||||||
try:
|
try:
|
||||||
# 使用 self.config_file(已经是动态路径)
|
# 使用 self.config_file(已经是动态路径)
|
||||||
@@ -48,7 +54,7 @@ def load_pq_config(self):
|
|||||||
self.log_gui.log(f"加载配置文件失败: {str(e)},使用默认配置", level="error")
|
self.log_gui.log(f"加载配置文件失败: {str(e)},使用默认配置", level="error")
|
||||||
|
|
||||||
|
|
||||||
def save_pq_config(self):
|
def save_pq_config(self: "PQAutomationApp"):
|
||||||
"""保存PQ配置(兼容打包后的程序)"""
|
"""保存PQ配置(兼容打包后的程序)"""
|
||||||
try:
|
try:
|
||||||
# 确保目录存在
|
# 确保目录存在
|
||||||
@@ -61,7 +67,7 @@ def save_pq_config(self):
|
|||||||
self.log_gui.log(f"保存配置文件失败: {str(e)}", level="error")
|
self.log_gui.log(f"保存配置文件失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
def clear_config_file(self):
|
def clear_config_file(self: "PQAutomationApp"):
|
||||||
"""清理配置文件(兼容打包后的程序)"""
|
"""清理配置文件(兼容打包后的程序)"""
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
|
|
||||||
@@ -82,3 +88,13 @@ def clear_config_file(self):
|
|||||||
self.log_gui.log(f"配置文件清理失败: {str(e)}", level="error")
|
self.log_gui.log(f"配置文件清理失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigIOMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
get_config_path = get_config_path
|
||||||
|
load_pq_config = load_pq_config
|
||||||
|
save_pq_config = save_pq_config
|
||||||
|
clear_config_file = clear_config_file
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""设备连接管理(UCD323 / CA410)。
|
"""设备连接管理(UCD323 / CA410)。
|
||||||
|
|
||||||
重构目标
|
重构目标
|
||||||
---------
|
---------
|
||||||
@@ -26,6 +26,12 @@ from app.ucd_domain import ConnectionChanged, UcdError
|
|||||||
from drivers.caSerail import CASerail
|
from drivers.caSerail import CASerail
|
||||||
from drivers.ucd_driver import DeviceInfo
|
from drivers.ucd_driver import DeviceInfo
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from app.ucd_domain import EventBus
|
from app.ucd_domain import EventBus
|
||||||
from drivers.ucd_driver import UCD323Device
|
from drivers.ucd_driver import UCD323Device
|
||||||
@@ -231,38 +237,38 @@ class ConnectionController:
|
|||||||
# 挂接一并删除,让 GUI 直接调用 ``self.connection.xxx``。
|
# 挂接一并删除,让 GUI 直接调用 ``self.connection.xxx``。
|
||||||
|
|
||||||
|
|
||||||
def get_available_ucd_ports(self):
|
def get_available_ucd_ports(self: "PQAutomationApp"):
|
||||||
return self.connection.list_ucd_devices()
|
return self.connection.list_ucd_devices()
|
||||||
|
|
||||||
|
|
||||||
def get_available_com_ports(self):
|
def get_available_com_ports(self: "PQAutomationApp"):
|
||||||
return self.connection.list_com_ports()
|
return self.connection.list_com_ports()
|
||||||
|
|
||||||
|
|
||||||
def refresh_com_ports(self):
|
def refresh_com_ports(self: "PQAutomationApp"):
|
||||||
self.connection.refresh_ports()
|
self.connection.refresh_ports()
|
||||||
|
|
||||||
|
|
||||||
def check_com_connections(self):
|
def check_com_connections(self: "PQAutomationApp"):
|
||||||
self.connection.check_all_async()
|
self.connection.check_all_async()
|
||||||
|
|
||||||
|
|
||||||
def update_connection_indicator(self, indicator, connected):
|
def update_connection_indicator(self: "PQAutomationApp", indicator, connected):
|
||||||
indicator.config(bg="green" if connected else "red")
|
indicator.config(bg="green" if connected else "red")
|
||||||
|
|
||||||
|
|
||||||
def check_port_connection(self, is_ucd=True):
|
def check_port_connection(self: "PQAutomationApp", is_ucd=True):
|
||||||
"""[已弃用] 旗参数反模式;保留仅为兼容旧调用点。"""
|
"""[已弃用] 旗参数反模式;保留仅为兼容旧调用点。"""
|
||||||
if is_ucd:
|
if is_ucd:
|
||||||
return self.connection.connect_ucd(self.ucd_list_var.get())
|
return self.connection.connect_ucd(self.ucd_list_var.get())
|
||||||
return self.connection.connect_ca()
|
return self.connection.connect_ca()
|
||||||
|
|
||||||
|
|
||||||
def enable_com_widgets(self):
|
def enable_com_widgets(self: "PQAutomationApp"):
|
||||||
self.connection._enable_widgets()
|
self.connection._enable_widgets()
|
||||||
|
|
||||||
|
|
||||||
def disconnect_com_connections(self):
|
def disconnect_com_connections(self: "PQAutomationApp"):
|
||||||
self.connection.disconnect_all()
|
self.connection.disconnect_all()
|
||||||
|
|
||||||
|
|
||||||
@@ -278,3 +284,17 @@ __all__ = [
|
|||||||
"enable_com_widgets",
|
"enable_com_widgets",
|
||||||
"disconnect_com_connections",
|
"disconnect_com_connections",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceConnectionMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
get_available_ucd_ports = get_available_ucd_ports
|
||||||
|
get_available_com_ports = get_available_com_ports
|
||||||
|
refresh_com_ports = refresh_com_ports
|
||||||
|
check_com_connections = check_com_connections
|
||||||
|
update_connection_indicator = update_connection_indicator
|
||||||
|
check_port_connection = check_port_connection
|
||||||
|
enable_com_widgets = enable_com_widgets
|
||||||
|
disconnect_com_connections = disconnect_com_connections
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_accuracy 原样搬迁
|
|||||||
|
|
||||||
from matplotlib.patches import Rectangle
|
from matplotlib.patches import Rectangle
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
def plot_accuracy(self, accuracy_data, test_type):
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
|
||||||
"""绘制色准测试结果 - 29色显示 - 简洁版布局(显示 Gamma)"""
|
"""绘制色准测试结果 - 29色显示 - 简洁版布局(显示 Gamma)"""
|
||||||
|
|
||||||
self.accuracy_ax.clear()
|
self.accuracy_ax.clear()
|
||||||
@@ -319,3 +325,10 @@ def plot_accuracy(self, accuracy_data, test_type):
|
|||||||
|
|
||||||
self.accuracy_canvas.draw()
|
self.accuracy_canvas.draw()
|
||||||
self.chart_notebook.select(self.accuracy_chart_frame)
|
self.chart_notebook.select(self.accuracy_chart_frame)
|
||||||
|
|
||||||
|
|
||||||
|
class PlotAccuracyMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
plot_accuracy = plot_accuracy
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
"""CCT / 色度一致性绘制。
|
"""CCT / 色度一致性绘制。
|
||||||
|
|
||||||
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_cct 原样搬迁。
|
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_cct 原样搬迁。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
def plot_cct(self, test_type):
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def plot_cct(self: "PQAutomationApp", test_type):
|
||||||
"""绘制 x 和 y 坐标分离图 - 每个点标注纵坐标值"""
|
"""绘制 x 和 y 坐标分离图 - 每个点标注纵坐标值"""
|
||||||
|
|
||||||
self.cct_fig.clear()
|
self.cct_fig.clear()
|
||||||
@@ -322,3 +328,10 @@ def plot_cct(self, test_type):
|
|||||||
self.chart_notebook.select(self.cct_chart_frame)
|
self.chart_notebook.select(self.cct_chart_frame)
|
||||||
|
|
||||||
self.log_gui.log("xy 色度坐标图绘制完成", level="success")
|
self.log_gui.log("xy 色度坐标图绘制完成", level="success")
|
||||||
|
|
||||||
|
|
||||||
|
class PlotCctMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
plot_cct = plot_cct
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_contrast 原样搬迁
|
|||||||
|
|
||||||
from matplotlib.patches import Rectangle
|
from matplotlib.patches import Rectangle
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
def plot_contrast(self, contrast_data, test_type):
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def plot_contrast(self: "PQAutomationApp", contrast_data, test_type):
|
||||||
"""绘制对比度测试结果 - 固定布局版本"""
|
"""绘制对比度测试结果 - 固定布局版本"""
|
||||||
|
|
||||||
# 清空并重置
|
# 清空并重置
|
||||||
@@ -165,3 +171,10 @@ def plot_contrast(self, contrast_data, test_type):
|
|||||||
|
|
||||||
self.contrast_canvas.draw()
|
self.contrast_canvas.draw()
|
||||||
self.chart_notebook.select(self.contrast_chart_frame)
|
self.chart_notebook.select(self.contrast_chart_frame)
|
||||||
|
|
||||||
|
|
||||||
|
class PlotContrastMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
plot_contrast = plot_contrast
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
"""EOTF 曲线绘制(HDR)。
|
"""EOTF 曲线绘制(HDR)。
|
||||||
|
|
||||||
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_eotf 原样搬迁。
|
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_eotf 原样搬迁。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
def plot_eotf(self, L_bar, results_with_eotf_list, test_type):
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def plot_eotf(self: "PQAutomationApp", L_bar, results_with_eotf_list, test_type):
|
||||||
"""绘制 EOTF 曲线 + 数据表格(HDR 专用,包含实测亮度)"""
|
"""绘制 EOTF 曲线 + 数据表格(HDR 专用,包含实测亮度)"""
|
||||||
|
|
||||||
# ========== 1. 清空并重置左侧曲线 ==========
|
# ========== 1. 清空并重置左侧曲线 ==========
|
||||||
@@ -146,3 +152,10 @@ def plot_eotf(self, L_bar, results_with_eotf_list, test_type):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
self.log_gui.log("EOTF 曲线 + 数据表格绘制完成", level="success")
|
self.log_gui.log("EOTF 曲线 + 数据表格绘制完成", level="success")
|
||||||
|
|
||||||
|
|
||||||
|
class PlotEotfMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
plot_eotf = plot_eotf
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
"""Gamma 曲线绘制。
|
"""Gamma 曲线绘制。
|
||||||
|
|
||||||
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_gamma 原样搬迁。
|
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_gamma 原样搬迁。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
def plot_gamma(self, L_bar, results_with_gamma_list, target_gamma, test_type):
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def plot_gamma(self: "PQAutomationApp", L_bar, results_with_gamma_list, target_gamma, test_type):
|
||||||
"""绘制Gamma曲线 + 数据表格(包含实测亮度)"""
|
"""绘制Gamma曲线 + 数据表格(包含实测亮度)"""
|
||||||
|
|
||||||
# ========== 1. 清空并重置左侧曲线 ==========
|
# ========== 1. 清空并重置左侧曲线 ==========
|
||||||
@@ -140,3 +146,10 @@ def plot_gamma(self, L_bar, results_with_gamma_list, target_gamma, test_type):
|
|||||||
self.chart_notebook.select(self.gamma_chart_frame)
|
self.chart_notebook.select(self.gamma_chart_frame)
|
||||||
|
|
||||||
self.log_gui.log("Gamma曲线 + 数据表格绘制完成", level="success")
|
self.log_gui.log("Gamma曲线 + 数据表格绘制完成", level="success")
|
||||||
|
|
||||||
|
|
||||||
|
class PlotGammaMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
plot_gamma = plot_gamma
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ from app.plots.gamut_background import (
|
|||||||
get_cie1976_background,
|
get_cie1976_background,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
# ============ 参考色域定义(CIE 1931 xy)============
|
# ============ 参考色域定义(CIE 1931 xy)============
|
||||||
_REF_GAMUTS_XY = {
|
_REF_GAMUTS_XY = {
|
||||||
@@ -193,7 +198,7 @@ def _blit_background(ax, background, bbox):
|
|||||||
# 主入口
|
# 主入口
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def plot_gamut(self, results, coverage, test_type):
|
def plot_gamut(self: "PQAutomationApp", results, coverage, test_type):
|
||||||
"""绘制色域图(图像层 + 框架层分离架构)。"""
|
"""绘制色域图(图像层 + 框架层分离架构)。"""
|
||||||
|
|
||||||
ax_xy = self.gamut_ax_xy
|
ax_xy = self.gamut_ax_xy
|
||||||
@@ -408,3 +413,10 @@ def plot_gamut(self, results, coverage, test_type):
|
|||||||
self.sync_gamut_toolbar()
|
self.sync_gamut_toolbar()
|
||||||
|
|
||||||
self.log_gui.log("色域图绘制完成", level="success")
|
self.log_gui.log("色域图绘制完成", level="success")
|
||||||
|
|
||||||
|
|
||||||
|
class PlotGamutMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
plot_gamut = plot_gamut
|
||||||
|
|||||||
@@ -221,6 +221,401 @@ def get_pattern(name: str) -> dict:
|
|||||||
return _load_pattern_or_empty(_PATTERNS_DIR / f"{name}.json")
|
return _load_pattern_or_empty(_PATTERNS_DIR / f"{name}.json")
|
||||||
|
|
||||||
|
|
||||||
|
def reload_gray_pattern() -> dict:
|
||||||
|
"""重新从 ``settings/patterns/gray.json`` 加载灰阶 pattern。
|
||||||
|
|
||||||
|
原地更新 ``_PATTERN_GRAY``,让 ``PQConfig.default_pattern_gray``
|
||||||
|
与 ``_KNOWN_PATTERNS['gray']`` 等所有现有引用同步生效,
|
||||||
|
无需重启程序即可应用新 pattern 列表。
|
||||||
|
"""
|
||||||
|
new_data = _load_pattern_or_empty(
|
||||||
|
_PATTERNS_DIR / "gray.json", default=_PATTERN_GRAY_FALLBACK
|
||||||
|
)
|
||||||
|
_PATTERN_GRAY.clear()
|
||||||
|
_PATTERN_GRAY.update(new_data)
|
||||||
|
return copy.deepcopy(_PATTERN_GRAY)
|
||||||
|
|
||||||
|
|
||||||
|
def get_gray_pattern_fallback() -> dict:
|
||||||
|
"""返回硬编码默认 11 点灰阶 pattern 的深拷贝(用于 UI 的"恢复默认")。"""
|
||||||
|
return copy.deepcopy(_PATTERN_GRAY_FALLBACK)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 灰阶 Pattern 预设管理(settings/patterns/presets/gray/*.json)
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# 设计要点:
|
||||||
|
# - 每个预设独立 JSON 文件,文件名(不含 .json)即预设名。
|
||||||
|
# - 内置预设以 ``_builtin_`` 前缀命名,并在 _meta.locked=True,UI 禁止删除/改名/覆盖。
|
||||||
|
# - 当前激活预设记录在 settings/patterns/presets/_active.json,便于 UI 显示。
|
||||||
|
# - 应用某预设 = 把它复制写入 settings/patterns/gray.json + reload_gray_pattern()。
|
||||||
|
# - gamma/cct/contrast/eotf 共用同一份 gray 预设(与 runner 现有共享灰阶采集对齐)。
|
||||||
|
#
|
||||||
|
|
||||||
|
_PRESETS_DIR = _PATTERNS_DIR / "presets"
|
||||||
|
_ACTIVE_INDEX_FILE = _PRESETS_DIR / "_active.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _gray_presets_dir() -> Path:
|
||||||
|
p = _PRESETS_DIR / "gray"
|
||||||
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_preset_name(name: str) -> str:
|
||||||
|
"""清洗预设名,去掉文件系统危险字符。"""
|
||||||
|
name = (name or "").strip()
|
||||||
|
bad = '<>:"/\\|?*\n\r\t'
|
||||||
|
for ch in bad:
|
||||||
|
name = name.replace(ch, "_")
|
||||||
|
return name[:80] or "untitled"
|
||||||
|
|
||||||
|
|
||||||
|
def _preset_path(test_kind: str, name: str) -> Path:
|
||||||
|
if test_kind != "gray":
|
||||||
|
raise ValueError(f"暂仅支持 test_kind='gray',收到: {test_kind}")
|
||||||
|
return _gray_presets_dir() / f"{_safe_preset_name(name)}.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_active_index() -> dict:
|
||||||
|
try:
|
||||||
|
with open(_ACTIVE_INDEX_FILE, encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data if isinstance(data, dict) else {}
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _save_active_index(data: dict) -> None:
|
||||||
|
_PRESETS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(_ACTIVE_INDEX_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def list_presets(test_kind: str = "gray") -> list[dict]:
|
||||||
|
"""
|
||||||
|
列出指定类别下的所有预设。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of {name, locked, description, point_count, file_path, generator}
|
||||||
|
"""
|
||||||
|
d = _gray_presets_dir() if test_kind == "gray" else None
|
||||||
|
if d is None:
|
||||||
|
return []
|
||||||
|
items: list[dict] = []
|
||||||
|
for fp in sorted(d.glob("*.json")):
|
||||||
|
try:
|
||||||
|
data = load_pattern_file(fp)
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
continue
|
||||||
|
meta = data.get("_meta", {}) or {}
|
||||||
|
items.append({
|
||||||
|
"name": fp.stem,
|
||||||
|
"locked": bool(meta.get("locked", False)),
|
||||||
|
"description": meta.get("description", ""),
|
||||||
|
"generator": meta.get("generator", ""),
|
||||||
|
"created": meta.get("created", ""),
|
||||||
|
"point_count": len(data.get("pattern_params") or []),
|
||||||
|
"file_path": str(fp),
|
||||||
|
})
|
||||||
|
# 内置 _builtin_ 排前,其余按名字排序
|
||||||
|
items.sort(key=lambda x: (not x["name"].startswith("_builtin_"), x["name"]))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def load_preset(test_kind: str, name: str) -> dict:
|
||||||
|
"""加载预设的完整 pattern 数据(含 _meta)。"""
|
||||||
|
return load_pattern_file(_preset_path(test_kind, name))
|
||||||
|
|
||||||
|
|
||||||
|
def save_preset(
|
||||||
|
test_kind: str,
|
||||||
|
name: str,
|
||||||
|
pattern: dict,
|
||||||
|
*,
|
||||||
|
description: str = "",
|
||||||
|
generator: str = "",
|
||||||
|
locked: bool = False,
|
||||||
|
overwrite: bool = True,
|
||||||
|
) -> Path:
|
||||||
|
"""保存预设。若 overwrite=False 且文件已存在或目标为锁定预设则抛错。"""
|
||||||
|
from datetime import datetime
|
||||||
|
name = _safe_preset_name(name)
|
||||||
|
path = _preset_path(test_kind, name)
|
||||||
|
|
||||||
|
if path.exists():
|
||||||
|
try:
|
||||||
|
existing = load_pattern_file(path)
|
||||||
|
if (existing.get("_meta") or {}).get("locked"):
|
||||||
|
raise PermissionError(f"预设 '{name}' 已锁定,不可覆盖")
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
pass
|
||||||
|
if not overwrite:
|
||||||
|
raise FileExistsError(f"预设 '{name}' 已存在")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"pattern_mode": pattern.get("pattern_mode", "SolidColor"),
|
||||||
|
"measurement_bit_depth": pattern.get("measurement_bit_depth", 8),
|
||||||
|
"measurement_max_value": max(0, len(pattern.get("pattern_params") or []) - 1),
|
||||||
|
"pattern_params": [list(map(int, rgb)) for rgb in (pattern.get("pattern_params") or [])],
|
||||||
|
"_meta": {
|
||||||
|
"description": description,
|
||||||
|
"generator": generator,
|
||||||
|
"locked": locked,
|
||||||
|
"created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
save_pattern_file(path, data)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def delete_preset(test_kind: str, name: str) -> None:
|
||||||
|
"""删除预设;锁定预设不可删除。"""
|
||||||
|
path = _preset_path(test_kind, name)
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"预设 '{name}' 不存在")
|
||||||
|
data = load_pattern_file(path)
|
||||||
|
if (data.get("_meta") or {}).get("locked"):
|
||||||
|
raise PermissionError(f"预设 '{name}' 已锁定,不可删除")
|
||||||
|
path.unlink()
|
||||||
|
# 若被删的恰是激活预设,清理记录
|
||||||
|
idx = _load_active_index()
|
||||||
|
if idx.get(test_kind) == name:
|
||||||
|
idx.pop(test_kind, None)
|
||||||
|
_save_active_index(idx)
|
||||||
|
|
||||||
|
|
||||||
|
def rename_preset(test_kind: str, old: str, new: str) -> Path:
|
||||||
|
"""重命名;锁定预设不可改名。"""
|
||||||
|
src = _preset_path(test_kind, old)
|
||||||
|
if not src.exists():
|
||||||
|
raise FileNotFoundError(f"预设 '{old}' 不存在")
|
||||||
|
data = load_pattern_file(src)
|
||||||
|
if (data.get("_meta") or {}).get("locked"):
|
||||||
|
raise PermissionError(f"预设 '{old}' 已锁定,不可重命名")
|
||||||
|
dst = _preset_path(test_kind, new)
|
||||||
|
if dst.exists():
|
||||||
|
raise FileExistsError(f"目标预设 '{new}' 已存在")
|
||||||
|
src.rename(dst)
|
||||||
|
idx = _load_active_index()
|
||||||
|
if idx.get(test_kind) == old:
|
||||||
|
idx[test_kind] = dst.stem
|
||||||
|
_save_active_index(idx)
|
||||||
|
return dst
|
||||||
|
|
||||||
|
|
||||||
|
def duplicate_preset(test_kind: str, src_name: str, new_name: str) -> Path:
|
||||||
|
"""复制一个预设为新副本(解除锁定)。"""
|
||||||
|
data = load_preset(test_kind, src_name)
|
||||||
|
meta = dict(data.get("_meta") or {})
|
||||||
|
meta["locked"] = False
|
||||||
|
meta["description"] = f"复制自 {src_name}" + (
|
||||||
|
f";{meta.get('description', '')}" if meta.get("description") else ""
|
||||||
|
)
|
||||||
|
data["_meta"] = meta
|
||||||
|
return save_preset(
|
||||||
|
test_kind,
|
||||||
|
new_name,
|
||||||
|
data,
|
||||||
|
description=meta["description"],
|
||||||
|
generator=meta.get("generator", ""),
|
||||||
|
locked=False,
|
||||||
|
overwrite=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def activate_preset(test_kind: str, name: str) -> dict:
|
||||||
|
"""
|
||||||
|
将指定预设应用为当前 gray pattern:
|
||||||
|
- 写入 settings/patterns/gray.json(剥离 _meta 以保持原格式)
|
||||||
|
- reload_gray_pattern() 让运行时立即生效
|
||||||
|
- 在 _active.json 记录激活预设名
|
||||||
|
"""
|
||||||
|
data = load_preset(test_kind, name)
|
||||||
|
clean = {
|
||||||
|
"pattern_mode": data.get("pattern_mode", "SolidColor"),
|
||||||
|
"measurement_bit_depth": data.get("measurement_bit_depth", 8),
|
||||||
|
"measurement_max_value": data.get("measurement_max_value", 0),
|
||||||
|
"pattern_params": data.get("pattern_params") or [],
|
||||||
|
}
|
||||||
|
save_pattern_file(_PATTERNS_DIR / "gray.json", clean)
|
||||||
|
reload_gray_pattern()
|
||||||
|
idx = _load_active_index()
|
||||||
|
idx[test_kind] = _safe_preset_name(name)
|
||||||
|
_save_active_index(idx)
|
||||||
|
return clean
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_preset_name(test_kind: str = "gray") -> str | None:
|
||||||
|
"""返回当前激活预设名(来自 _active.json);不存在则返回 None。"""
|
||||||
|
return _load_active_index().get(test_kind)
|
||||||
|
|
||||||
|
|
||||||
|
def import_preset_from_file(test_kind: str, src_file, *, name: str | None = None) -> Path:
|
||||||
|
"""从外部 JSON 文件导入为预设。"""
|
||||||
|
data = load_pattern_file(src_file)
|
||||||
|
if not data.get("pattern_params"):
|
||||||
|
raise ValueError("文件中未找到 pattern_params")
|
||||||
|
preset_name = name or Path(src_file).stem
|
||||||
|
return save_preset(
|
||||||
|
test_kind,
|
||||||
|
preset_name,
|
||||||
|
data,
|
||||||
|
description=(data.get("_meta") or {}).get("description", "导入"),
|
||||||
|
generator=(data.get("_meta") or {}).get("generator", ""),
|
||||||
|
locked=False,
|
||||||
|
overwrite=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def export_preset_to_file(test_kind: str, name: str, dst_file) -> Path:
|
||||||
|
"""将预设导出到指定外部 JSON 文件。"""
|
||||||
|
data = load_preset(test_kind, name)
|
||||||
|
save_pattern_file(dst_file, data)
|
||||||
|
return Path(dst_file)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 内置预设生成器 ----------------------------------------------------------
|
||||||
|
|
||||||
|
def _gen_even_gray(n: int) -> list[list[int]]:
|
||||||
|
"""N 点等分 (100%→0%)。"""
|
||||||
|
if n < 2:
|
||||||
|
n = 2
|
||||||
|
out = []
|
||||||
|
for i in range(n):
|
||||||
|
pct = 100.0 - (100.0 / (n - 1)) * i
|
||||||
|
v = int(round(pct / 100.0 * 255))
|
||||||
|
out.append([v, v, v])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_pq_gray(n: int) -> list[list[int]]:
|
||||||
|
"""在 PQ 编码空间 N 点等分(亮度按 PQ 曲线均匀分布,低端更密集)。
|
||||||
|
|
||||||
|
采样规则:取 PQ 信号值从 1.0 到 0.0 等分,转 8-bit RGB 灰阶。
|
||||||
|
(PQ 信号本身在感知亮度上即为线性,故"等分编码值"≈"等分感知亮度"。)
|
||||||
|
"""
|
||||||
|
if n < 2:
|
||||||
|
n = 2
|
||||||
|
out = []
|
||||||
|
for i in range(n):
|
||||||
|
v_pq = 1.0 - i / (n - 1)
|
||||||
|
v = int(round(v_pq * 255))
|
||||||
|
out.append([v, v, v])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_gamma_gray(n: int, gamma: float = 2.2) -> list[list[int]]:
|
||||||
|
"""在线性光强空间 N 点等分,再用 gamma 编码到 8-bit 灰阶(暗端更密集)。"""
|
||||||
|
if n < 2:
|
||||||
|
n = 2
|
||||||
|
out = []
|
||||||
|
for i in range(n):
|
||||||
|
lin = 1.0 - i / (n - 1) # 线性光 1→0
|
||||||
|
code = lin ** (1.0 / gamma) # gamma 编码
|
||||||
|
v = int(round(code * 255))
|
||||||
|
out.append([v, v, v])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
_BUILTIN_GRAY_PRESETS = [
|
||||||
|
("_builtin_even_11pt",
|
||||||
|
"11 点等分 (100%→0%),行业标准灰阶",
|
||||||
|
"even-11",
|
||||||
|
_gen_even_gray(11)),
|
||||||
|
("_builtin_even_21pt",
|
||||||
|
"21 点等分 (5% 步长),更精细的 SDR 灰阶",
|
||||||
|
"even-21",
|
||||||
|
_gen_even_gray(21)),
|
||||||
|
("_builtin_gamma22_17pt",
|
||||||
|
"17 点 Gamma 2.2 分布(暗端更密集),用于 SDR Gamma 拟合",
|
||||||
|
"gamma2.2-17",
|
||||||
|
_gen_gamma_gray(17, 2.2)),
|
||||||
|
("_builtin_pq_17pt",
|
||||||
|
"17 点 PQ 编码等分,用于 HDR EOTF 评估",
|
||||||
|
"pq-17",
|
||||||
|
_gen_pq_gray(17)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_builtin_presets() -> None:
|
||||||
|
"""启动时调用:确保内置预设存在;若已存在则跳过(不覆盖用户修改)。
|
||||||
|
|
||||||
|
同时迁移:若 presets/ 目录为空但 settings/patterns/gray.json 存在,
|
||||||
|
将其作为 ``user_current`` 预设引入,避免用户原有自定义丢失。
|
||||||
|
"""
|
||||||
|
presets_dir = _gray_presets_dir()
|
||||||
|
for name, desc, gen, params in _BUILTIN_GRAY_PRESETS:
|
||||||
|
path = presets_dir / f"{name}.json"
|
||||||
|
if path.exists():
|
||||||
|
continue
|
||||||
|
save_preset(
|
||||||
|
"gray",
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
"pattern_mode": "SolidColor",
|
||||||
|
"measurement_bit_depth": 8,
|
||||||
|
"measurement_max_value": len(params) - 1,
|
||||||
|
"pattern_params": params,
|
||||||
|
},
|
||||||
|
description=desc,
|
||||||
|
generator=gen,
|
||||||
|
locked=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 迁移:用户原有 gray.json 入库为 user_current(仅在 presets 目录还没有任何用户预设时)
|
||||||
|
user_presets = [p for p in presets_dir.glob("*.json") if not p.stem.startswith("_builtin_")]
|
||||||
|
if not user_presets:
|
||||||
|
gray_file = _PATTERNS_DIR / "gray.json"
|
||||||
|
if gray_file.exists():
|
||||||
|
try:
|
||||||
|
cur = load_pattern_file(gray_file)
|
||||||
|
if cur.get("pattern_params"):
|
||||||
|
save_preset(
|
||||||
|
"gray",
|
||||||
|
"user_current",
|
||||||
|
cur,
|
||||||
|
description="迁移自原 gray.json",
|
||||||
|
generator="migrated",
|
||||||
|
locked=False,
|
||||||
|
overwrite=False,
|
||||||
|
)
|
||||||
|
idx = _load_active_index()
|
||||||
|
idx.setdefault("gray", "user_current")
|
||||||
|
_save_active_index(idx)
|
||||||
|
except (json.JSONDecodeError, OSError, FileExistsError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 若无激活预设记录,按规则推断(优先匹配当前 gray.json 内容)
|
||||||
|
idx = _load_active_index()
|
||||||
|
if "gray" not in idx:
|
||||||
|
try:
|
||||||
|
cur = load_pattern_file(_PATTERNS_DIR / "gray.json")
|
||||||
|
cur_params = cur.get("pattern_params") or []
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
||||||
|
cur_params = []
|
||||||
|
match: str | None = None
|
||||||
|
for fp in presets_dir.glob("*.json"):
|
||||||
|
try:
|
||||||
|
if (load_pattern_file(fp).get("pattern_params") or []) == cur_params:
|
||||||
|
match = fp.stem
|
||||||
|
break
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
continue
|
||||||
|
if match:
|
||||||
|
idx["gray"] = match
|
||||||
|
_save_active_index(idx)
|
||||||
|
|
||||||
|
|
||||||
|
# 自动确保内置预设存在(首次启动会创建文件)
|
||||||
|
try:
|
||||||
|
ensure_builtin_presets()
|
||||||
|
except OSError:
|
||||||
|
# 启动期目录不可写则跳过;UI 层有兜底
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PQConfig:
|
class PQConfig:
|
||||||
def __init__(self, current_test_type="screen_module"):
|
def __init__(self, current_test_type="screen_module"):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""测试执行(runner)相关逻辑(Step 5 重构)。
|
"""测试执行(runner)相关逻辑(Step 5 重构)。
|
||||||
|
|
||||||
从 pqAutomationApp.PQAutomationApp 中搬迁。每个函数第一行 `self = app`
|
从 pqAutomationApp.PQAutomationApp 中搬迁。每个函数第一行 `self = app`
|
||||||
以保留原有 `self.xxx` 属性访问不变。
|
以保留原有 `self.xxx` 属性访问不变。
|
||||||
@@ -15,7 +15,13 @@ import numpy as np
|
|||||||
import algorithm.pq_algorithm as pq_algorithm
|
import algorithm.pq_algorithm as pq_algorithm
|
||||||
from app.pq.pq_result import PQResult
|
from app.pq.pq_result import PQResult
|
||||||
|
|
||||||
def new_pq_results(self, test_type, test_name):
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
def new_pq_results(self: "PQAutomationApp", test_type, test_name):
|
||||||
# 通过 PQResultStore 创建/替换指定 test_type 的结果,并设为当前活跃
|
# 通过 PQResultStore 创建/替换指定 test_type 的结果,并设为当前活跃
|
||||||
self.results.new(test_type, test_name)
|
self.results.new(test_type, test_name)
|
||||||
# 设置配置
|
# 设置配置
|
||||||
@@ -36,7 +42,7 @@ def new_pq_results(self, test_type, test_name):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_test(self, test_type, test_items):
|
def run_test(self: "PQAutomationApp", test_type, test_items):
|
||||||
"""执行测试"""
|
"""执行测试"""
|
||||||
try:
|
try:
|
||||||
self.log_gui.log(f"开始执行{self.get_test_type_name(test_type)}测试", level="info")
|
self.log_gui.log(f"开始执行{self.get_test_type_name(test_type)}测试", level="info")
|
||||||
@@ -63,7 +69,7 @@ def run_test(self, test_type, test_items):
|
|||||||
self._dispatch_ui(self.on_test_error)
|
self._dispatch_ui(self.on_test_error)
|
||||||
|
|
||||||
|
|
||||||
def run_screen_module_test(self, test_items):
|
def run_screen_module_test(self: "PQAutomationApp", test_items):
|
||||||
"""执行屏模组性能测试 - 优化版"""
|
"""执行屏模组性能测试 - 优化版"""
|
||||||
self.log_gui.log("执行屏模组性能测试...", level="info")
|
self.log_gui.log("执行屏模组性能测试...", level="info")
|
||||||
|
|
||||||
@@ -138,7 +144,7 @@ def run_screen_module_test(self, test_items):
|
|||||||
self.test_contrast("screen_module", shared_gray_data)
|
self.test_contrast("screen_module", shared_gray_data)
|
||||||
|
|
||||||
|
|
||||||
def run_custom_sdr_test(self, test_items):
|
def run_custom_sdr_test(self: "PQAutomationApp", test_items):
|
||||||
"""执行客户定制 SDR 测试 - 升级版"""
|
"""执行客户定制 SDR 测试 - 升级版"""
|
||||||
self.log_gui.log("执行客户定制 SDR 测试...", level="info")
|
self.log_gui.log("执行客户定制 SDR 测试...", level="info")
|
||||||
# 获取信号格式设置
|
# 获取信号格式设置
|
||||||
@@ -154,7 +160,7 @@ def run_custom_sdr_test(self, test_items):
|
|||||||
self._dispatch_ui(self.on_custom_template_test_completed)
|
self._dispatch_ui(self.on_custom_template_test_completed)
|
||||||
|
|
||||||
|
|
||||||
def run_sdr_movie_test(self, test_items):
|
def run_sdr_movie_test(self: "PQAutomationApp", test_items):
|
||||||
"""执行SDR Movie测试"""
|
"""执行SDR Movie测试"""
|
||||||
self.log_gui.log("执行SDR Movie测试...", level="info")
|
self.log_gui.log("执行SDR Movie测试...", level="info")
|
||||||
|
|
||||||
@@ -225,7 +231,7 @@ def run_sdr_movie_test(self, test_items):
|
|||||||
self.test_color_accuracy("sdr_movie")
|
self.test_color_accuracy("sdr_movie")
|
||||||
|
|
||||||
|
|
||||||
def run_hdr_movie_test(self, test_items):
|
def run_hdr_movie_test(self: "PQAutomationApp", test_items):
|
||||||
"""执行HDR Movie测试"""
|
"""执行HDR Movie测试"""
|
||||||
self.log_gui.log("执行HDR Movie测试...", level="info")
|
self.log_gui.log("执行HDR Movie测试...", level="info")
|
||||||
|
|
||||||
@@ -300,7 +306,7 @@ def run_hdr_movie_test(self, test_items):
|
|||||||
self.test_color_accuracy("hdr_movie")
|
self.test_color_accuracy("hdr_movie")
|
||||||
|
|
||||||
|
|
||||||
def send_fix_pattern(self, mode):
|
def send_fix_pattern(self: "PQAutomationApp", mode):
|
||||||
"""发送固定图案并采集数据 - 支持不同测试类型的信号格式"""
|
"""发送固定图案并采集数据 - 支持不同测试类型的信号格式"""
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
@@ -460,7 +466,7 @@ def send_fix_pattern(self, mode):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def test_custom_sdr(self):
|
def test_custom_sdr(self: "PQAutomationApp"):
|
||||||
"""执行客户定制 SDR 测试 - 升级版"""
|
"""执行客户定制 SDR 测试 - 升级版"""
|
||||||
self.log_gui.log("执行客户定制 SDR 测试...", level="info")
|
self.log_gui.log("执行客户定制 SDR 测试...", level="info")
|
||||||
results = self.send_fix_pattern("custom")
|
results = self.send_fix_pattern("custom")
|
||||||
@@ -471,7 +477,7 @@ def test_custom_sdr(self):
|
|||||||
self.log_gui.log(f"客户模板采集完成,共 {len(results)} 组数据", level="success")
|
self.log_gui.log(f"客户模板采集完成,共 {len(results)} 组数据", level="success")
|
||||||
|
|
||||||
|
|
||||||
def test_gamut(self, test_type):
|
def test_gamut(self: "PQAutomationApp", test_type):
|
||||||
"""测试色域"""
|
"""测试色域"""
|
||||||
self.log_gui.log("开始测试色域...", level="info")
|
self.log_gui.log("开始测试色域...", level="info")
|
||||||
self.results.start_test_item("gamut")
|
self.results.start_test_item("gamut")
|
||||||
@@ -629,7 +635,7 @@ def test_gamut(self, test_type):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def test_gamma(self, test_type, gray_data=None):
|
def test_gamma(self: "PQAutomationApp", test_type, gray_data=None):
|
||||||
"""测试Gamma曲线
|
"""测试Gamma曲线
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -720,7 +726,7 @@ def test_gamma(self, test_type, gray_data=None):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def test_eotf(self, test_type, gray_data=None):
|
def test_eotf(self: "PQAutomationApp", test_type, gray_data=None):
|
||||||
"""测试 EOTF 曲线(HDR 专用)
|
"""测试 EOTF 曲线(HDR 专用)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -803,7 +809,7 @@ def test_eotf(self, test_type, gray_data=None):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def test_cct(self, test_type, gray_data=None):
|
def test_cct(self: "PQAutomationApp", test_type, gray_data=None):
|
||||||
"""测试色度一致性"""
|
"""测试色度一致性"""
|
||||||
self.log_gui.log("开始测试色度一致性...", level="info")
|
self.log_gui.log("开始测试色度一致性...", level="info")
|
||||||
self.results.start_test_item("cct")
|
self.results.start_test_item("cct")
|
||||||
@@ -843,7 +849,7 @@ def test_cct(self, test_type, gray_data=None):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def test_contrast(self, test_type, gray_data=None):
|
def test_contrast(self: "PQAutomationApp", test_type, gray_data=None):
|
||||||
"""测试对比度
|
"""测试对比度
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -907,7 +913,7 @@ def test_contrast(self, test_type, gray_data=None):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def test_color_accuracy(self, test_type):
|
def test_color_accuracy(self: "PQAutomationApp", test_type):
|
||||||
"""测试色准 - 使用手工实现的 ΔE 2000(应用 Gamma)"""
|
"""测试色准 - 使用手工实现的 ΔE 2000(应用 Gamma)"""
|
||||||
|
|
||||||
# ========== Gamma 参考值 ==========
|
# ========== Gamma 参考值 ==========
|
||||||
@@ -1067,7 +1073,7 @@ def test_color_accuracy(self, test_type):
|
|||||||
self.log_gui.log("色准测试完成", level="success")
|
self.log_gui.log("色准测试完成", level="success")
|
||||||
|
|
||||||
|
|
||||||
def on_test_completed(self):
|
def on_test_completed(self: "PQAutomationApp"):
|
||||||
"""测试完成后的UI更新"""
|
"""测试完成后的UI更新"""
|
||||||
self.testing = False
|
self.testing = False
|
||||||
self.start_btn.config(state=tk.NORMAL)
|
self.start_btn.config(state=tk.NORMAL)
|
||||||
@@ -1210,7 +1216,7 @@ def on_test_completed(self):
|
|||||||
messagebox.showinfo("完成", "测试已完成!")
|
messagebox.showinfo("完成", "测试已完成!")
|
||||||
|
|
||||||
|
|
||||||
def on_custom_template_test_completed(self):
|
def on_custom_template_test_completed(self: "PQAutomationApp"):
|
||||||
"""客户模板测试完成后的UI更新"""
|
"""客户模板测试完成后的UI更新"""
|
||||||
self.testing = False
|
self.testing = False
|
||||||
self.set_custom_result_table_locked(False)
|
self.set_custom_result_table_locked(False)
|
||||||
@@ -1231,7 +1237,7 @@ def on_custom_template_test_completed(self):
|
|||||||
messagebox.showinfo("完成", "客户模板测试已完成!")
|
messagebox.showinfo("完成", "客户模板测试已完成!")
|
||||||
|
|
||||||
|
|
||||||
def get_current_test_result(self):
|
def get_current_test_result(self: "PQAutomationApp"):
|
||||||
"""获取当前测试结果"""
|
"""获取当前测试结果"""
|
||||||
test_type = self.test_type_var.get()
|
test_type = self.test_type_var.get()
|
||||||
test_items = self.get_selected_test_items()
|
test_items = self.get_selected_test_items()
|
||||||
@@ -1263,7 +1269,7 @@ def get_current_test_result(self):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def on_test_error(self):
|
def on_test_error(self: "PQAutomationApp"):
|
||||||
"""测试出错后的UI更新"""
|
"""测试出错后的UI更新"""
|
||||||
self.testing = False
|
self.testing = False
|
||||||
self.set_custom_result_table_locked(False)
|
self.set_custom_result_table_locked(False)
|
||||||
@@ -1284,3 +1290,27 @@ def on_test_error(self):
|
|||||||
messagebox.showerror("错误", "测试过程中发生错误,请查看日志")
|
messagebox.showerror("错误", "测试过程中发生错误,请查看日志")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunnerMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
new_pq_results = new_pq_results
|
||||||
|
run_test = run_test
|
||||||
|
run_screen_module_test = run_screen_module_test
|
||||||
|
run_custom_sdr_test = run_custom_sdr_test
|
||||||
|
run_sdr_movie_test = run_sdr_movie_test
|
||||||
|
run_hdr_movie_test = run_hdr_movie_test
|
||||||
|
send_fix_pattern = send_fix_pattern
|
||||||
|
test_custom_sdr = test_custom_sdr
|
||||||
|
test_gamut = test_gamut
|
||||||
|
test_gamma = test_gamma
|
||||||
|
test_eotf = test_eotf
|
||||||
|
test_cct = test_cct
|
||||||
|
test_contrast = test_contrast
|
||||||
|
test_color_accuracy = test_color_accuracy
|
||||||
|
on_test_completed = on_test_completed
|
||||||
|
on_custom_template_test_completed = on_custom_template_test_completed
|
||||||
|
get_current_test_result = get_current_test_result
|
||||||
|
on_test_error = on_test_error
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Local Dimming 测试逻辑(应用层)。
|
"""Local Dimming 测试逻辑(应用层)。
|
||||||
|
|
||||||
整合自原 drivers/local_dimming_test.py:窗口图片生成与测试主循环
|
整合自原 drivers/local_dimming_test.py:窗口图片生成与测试主循环
|
||||||
直接落在本模块,UCD 通用操作通过 SignalService 完成。
|
直接落在本模块,UCD 通用操作通过 SignalService 完成。
|
||||||
@@ -18,6 +18,12 @@ from tkinter import filedialog, messagebox
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -87,7 +93,7 @@ def _ensure_window_image(width, height, percentage):
|
|||||||
# GUI 入口(绑定为 PQAutomationApp 方法)
|
# GUI 入口(绑定为 PQAutomationApp 方法)
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
def start_local_dimming_test(self):
|
def start_local_dimming_test(self: "PQAutomationApp"):
|
||||||
"""开始 Local Dimming 测试。"""
|
"""开始 Local Dimming 测试。"""
|
||||||
if not self.ca or not self.signal_service.is_connected:
|
if not self.ca or not self.signal_service.is_connected:
|
||||||
messagebox.showerror("错误", "请先连接 CA410 和 UCD323")
|
messagebox.showerror("错误", "请先连接 CA410 和 UCD323")
|
||||||
@@ -163,7 +169,7 @@ def start_local_dimming_test(self):
|
|||||||
threading.Thread(target=worker, daemon=True).start()
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def update_ld_results(self, results):
|
def update_ld_results(self: "PQAutomationApp", results):
|
||||||
"""把批量测试结果填入 Treeview。"""
|
"""把批量测试结果填入 Treeview。"""
|
||||||
for percentage, x, y, lv, _X, _Y, _Z in results:
|
for percentage, x, y, lv, _X, _Y, _Z in results:
|
||||||
self.ld_tree.insert(
|
self.ld_tree.insert(
|
||||||
@@ -172,14 +178,14 @@ def update_ld_results(self, results):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def stop_local_dimming_test(self):
|
def stop_local_dimming_test(self: "PQAutomationApp"):
|
||||||
"""请求停止当前 Local Dimming 测试。"""
|
"""请求停止当前 Local Dimming 测试。"""
|
||||||
ev = getattr(self, "ld_stop_event", None)
|
ev = getattr(self, "ld_stop_event", None)
|
||||||
if ev:
|
if ev:
|
||||||
ev.set()
|
ev.set()
|
||||||
|
|
||||||
|
|
||||||
def send_ld_window(self, percentage):
|
def send_ld_window(self: "PQAutomationApp", percentage):
|
||||||
"""发送指定百分比的白色窗口(手动模式)。"""
|
"""发送指定百分比的白色窗口(手动模式)。"""
|
||||||
if not self.signal_service.is_connected:
|
if not self.signal_service.is_connected:
|
||||||
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
||||||
@@ -209,7 +215,7 @@ def send_ld_window(self, percentage):
|
|||||||
threading.Thread(target=send, daemon=True).start()
|
threading.Thread(target=send, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def measure_ld_luminance(self):
|
def measure_ld_luminance(self: "PQAutomationApp"):
|
||||||
"""测量当前显示的亮度并追加一行到 Treeview。"""
|
"""测量当前显示的亮度并追加一行到 Treeview。"""
|
||||||
if not self.ca:
|
if not self.ca:
|
||||||
messagebox.showwarning("警告", "请先连接 CA410 色度计")
|
messagebox.showwarning("警告", "请先连接 CA410 色度计")
|
||||||
@@ -246,7 +252,7 @@ def measure_ld_luminance(self):
|
|||||||
threading.Thread(target=measure, daemon=True).start()
|
threading.Thread(target=measure, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def clear_ld_records(self):
|
def clear_ld_records(self: "PQAutomationApp"):
|
||||||
"""清空 Treeview 中的测试记录。"""
|
"""清空 Treeview 中的测试记录。"""
|
||||||
for item in self.ld_tree.get_children():
|
for item in self.ld_tree.get_children():
|
||||||
self.ld_tree.delete(item)
|
self.ld_tree.delete(item)
|
||||||
@@ -255,7 +261,7 @@ def clear_ld_records(self):
|
|||||||
self.log_gui.log("测试记录已清空", level="info")
|
self.log_gui.log("测试记录已清空", level="info")
|
||||||
|
|
||||||
|
|
||||||
def save_local_dimming_results(self):
|
def save_local_dimming_results(self: "PQAutomationApp"):
|
||||||
"""把 Treeview 中的全部记录导出为 CSV。"""
|
"""把 Treeview 中的全部记录导出为 CSV。"""
|
||||||
if len(self.ld_tree.get_children()) == 0:
|
if len(self.ld_tree.get_children()) == 0:
|
||||||
messagebox.showinfo("提示", "没有可保存的数据")
|
messagebox.showinfo("提示", "没有可保存的数据")
|
||||||
@@ -284,3 +290,16 @@ def save_local_dimming_results(self):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"保存失败: {str(e)}", level="error")
|
self.log_gui.log(f"保存失败: {str(e)}", level="error")
|
||||||
messagebox.showerror("错误", f"保存失败: {str(e)}")
|
messagebox.showerror("错误", f"保存失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
class LocalDimmingMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
start_local_dimming_test = start_local_dimming_test
|
||||||
|
update_ld_results = update_ld_results
|
||||||
|
stop_local_dimming_test = stop_local_dimming_test
|
||||||
|
send_ld_window = send_ld_window
|
||||||
|
measure_ld_luminance = measure_ld_luminance
|
||||||
|
clear_ld_records = clear_ld_records
|
||||||
|
save_local_dimming_results = save_local_dimming_results
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""图表框架相关逻辑(Step 3 重构)。
|
"""图表框架相关逻辑(Step 3 重构)。
|
||||||
|
|
||||||
从 pqAutomationApp.PQAutomationApp 中搬迁而来。每个函数第一行 `self = app`
|
从 pqAutomationApp.PQAutomationApp 中搬迁而来。每个函数第一行 `self = app`
|
||||||
以保留原有 `self.xxx` 属性访问不变。
|
以保留原有 `self.xxx` 属性访问不变。
|
||||||
@@ -10,7 +10,13 @@ import matplotlib.pyplot as plt
|
|||||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||||
from app.views.pq_debug_panel import PQDebugPanel
|
from app.views.pq_debug_panel import PQDebugPanel
|
||||||
|
|
||||||
def init_gamut_chart(self):
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
def init_gamut_chart(self: "PQAutomationApp"):
|
||||||
"""初始化色域图表 - 手动设置subplot位置,完全避免重叠"""
|
"""初始化色域图表 - 手动设置subplot位置,完全避免重叠"""
|
||||||
container = ttk.Frame(self.gamut_chart_frame)
|
container = ttk.Frame(self.gamut_chart_frame)
|
||||||
container.pack(expand=True, fill=tk.BOTH)
|
container.pack(expand=True, fill=tk.BOTH)
|
||||||
@@ -65,7 +71,7 @@ def init_gamut_chart(self):
|
|||||||
self.gamut_canvas.draw()
|
self.gamut_canvas.draw()
|
||||||
|
|
||||||
|
|
||||||
def sync_gamut_toolbar(self):
|
def sync_gamut_toolbar(self: "PQAutomationApp"):
|
||||||
"""将工具栏参考标准按钮同步为当前测试类型的 ref var 值。"""
|
"""将工具栏参考标准按钮同步为当前测试类型的 ref var 值。"""
|
||||||
if not hasattr(self, "_gamut_ref_toolbar_var"):
|
if not hasattr(self, "_gamut_ref_toolbar_var"):
|
||||||
return
|
return
|
||||||
@@ -80,7 +86,7 @@ def sync_gamut_toolbar(self):
|
|||||||
self._gamut_ref_toolbar_var.set(getattr(self, attr).get())
|
self._gamut_ref_toolbar_var.set(getattr(self, attr).get())
|
||||||
|
|
||||||
|
|
||||||
def _on_gamut_toolbar_changed(self, std):
|
def _on_gamut_toolbar_changed(self: "PQAutomationApp", std):
|
||||||
"""用户点击工具栏参考标准按钮时:更新 var → 保存配置 → 重绘(有数据时)。"""
|
"""用户点击工具栏参考标准按钮时:更新 var → 保存配置 → 重绘(有数据时)。"""
|
||||||
test_type = self.config.current_test_type
|
test_type = self.config.current_test_type
|
||||||
var_map = {
|
var_map = {
|
||||||
@@ -105,7 +111,7 @@ def _on_gamut_toolbar_changed(self, std):
|
|||||||
self.recalculate_gamut()
|
self.recalculate_gamut()
|
||||||
|
|
||||||
|
|
||||||
def init_gamma_chart(self):
|
def init_gamma_chart(self: "PQAutomationApp"):
|
||||||
"""初始化Gamma曲线图表 - 左侧曲线 + 右侧表格(4列 + 通用说明)"""
|
"""初始化Gamma曲线图表 - 左侧曲线 + 右侧表格(4列 + 通用说明)"""
|
||||||
container = ttk.Frame(self.gamma_chart_frame)
|
container = ttk.Frame(self.gamma_chart_frame)
|
||||||
container.pack(expand=True, fill=tk.BOTH)
|
container.pack(expand=True, fill=tk.BOTH)
|
||||||
@@ -214,7 +220,7 @@ def init_gamma_chart(self):
|
|||||||
self.gamma_fig.suptitle("Gamma曲线 + 数据表格", fontsize=12, y=0.98)
|
self.gamma_fig.suptitle("Gamma曲线 + 数据表格", fontsize=12, y=0.98)
|
||||||
self.gamma_canvas.draw()
|
self.gamma_canvas.draw()
|
||||||
|
|
||||||
def init_eotf_chart(self):
|
def init_eotf_chart(self: "PQAutomationApp"):
|
||||||
"""初始化 EOTF 曲线图表(HDR 专用)- 左侧曲线 + 右侧表格(4列)"""
|
"""初始化 EOTF 曲线图表(HDR 专用)- 左侧曲线 + 右侧表格(4列)"""
|
||||||
container = ttk.Frame(self.eotf_chart_frame)
|
container = ttk.Frame(self.eotf_chart_frame)
|
||||||
container.pack(expand=True, fill=tk.BOTH)
|
container.pack(expand=True, fill=tk.BOTH)
|
||||||
@@ -319,7 +325,7 @@ def init_eotf_chart(self):
|
|||||||
self.eotf_fig.suptitle("EOTF 曲线 + 数据表格", fontsize=12, y=0.98)
|
self.eotf_fig.suptitle("EOTF 曲线 + 数据表格", fontsize=12, y=0.98)
|
||||||
self.eotf_canvas.draw()
|
self.eotf_canvas.draw()
|
||||||
|
|
||||||
def init_cct_chart(self):
|
def init_cct_chart(self: "PQAutomationApp"):
|
||||||
"""初始化色度坐标图表 - 正向横坐标,标题居中最上方"""
|
"""初始化色度坐标图表 - 正向横坐标,标题居中最上方"""
|
||||||
container = ttk.Frame(self.cct_chart_frame)
|
container = ttk.Frame(self.cct_chart_frame)
|
||||||
container.pack(expand=True)
|
container.pack(expand=True)
|
||||||
@@ -364,7 +370,7 @@ def init_cct_chart(self):
|
|||||||
|
|
||||||
self.cct_canvas.draw()
|
self.cct_canvas.draw()
|
||||||
|
|
||||||
def init_contrast_chart(self):
|
def init_contrast_chart(self: "PQAutomationApp"):
|
||||||
"""初始化对比度图表 - 固定大小,居中显示"""
|
"""初始化对比度图表 - 固定大小,居中显示"""
|
||||||
container = ttk.Frame(self.contrast_chart_frame)
|
container = ttk.Frame(self.contrast_chart_frame)
|
||||||
container.pack(expand=True)
|
container.pack(expand=True)
|
||||||
@@ -399,7 +405,7 @@ def init_contrast_chart(self):
|
|||||||
|
|
||||||
self.contrast_canvas.draw()
|
self.contrast_canvas.draw()
|
||||||
|
|
||||||
def init_accuracy_chart(self):
|
def init_accuracy_chart(self: "PQAutomationApp"):
|
||||||
"""初始化色准图表 - 固定大小,居中显示"""
|
"""初始化色准图表 - 固定大小,居中显示"""
|
||||||
container = ttk.Frame(self.accuracy_chart_frame)
|
container = ttk.Frame(self.accuracy_chart_frame)
|
||||||
container.pack(expand=True)
|
container.pack(expand=True)
|
||||||
@@ -434,7 +440,7 @@ def init_accuracy_chart(self):
|
|||||||
|
|
||||||
self.accuracy_canvas.draw()
|
self.accuracy_canvas.draw()
|
||||||
|
|
||||||
def clear_chart(self):
|
def clear_chart(self: "PQAutomationApp"):
|
||||||
"""清空所有图表"""
|
"""清空所有图表"""
|
||||||
|
|
||||||
# ========== 1. 清空色域图表 ==========
|
# ========== 1. 清空色域图表 ==========
|
||||||
@@ -729,7 +735,7 @@ def clear_chart(self):
|
|||||||
|
|
||||||
self.accuracy_canvas.draw()
|
self.accuracy_canvas.draw()
|
||||||
|
|
||||||
def update_chart_tabs_state(self):
|
def update_chart_tabs_state(self: "PQAutomationApp"):
|
||||||
"""根据测试项目复选框状态动态增删图表 Tab(保持规范顺序)。
|
"""根据测试项目复选框状态动态增删图表 Tab(保持规范顺序)。
|
||||||
|
|
||||||
- 色域 / Gamma 或 EOTF / 色度一致性 / 对比度 / 色准 全部走动态 add/forget
|
- 色域 / Gamma 或 EOTF / 色度一致性 / 对比度 / 色准 全部走动态 add/forget
|
||||||
@@ -801,7 +807,7 @@ def update_chart_tabs_state(self):
|
|||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"更新Tab状态失败: {str(e)}", level="error")
|
self.log_gui.log(f"更新Tab状态失败: {str(e)}", level="error")
|
||||||
|
|
||||||
def create_result_chart_frame(self):
|
def create_result_chart_frame(self: "PQAutomationApp"):
|
||||||
"""创建结果图表区域 - 6个独立Tab(Gamma 和 EOTF 分离)"""
|
"""创建结果图表区域 - 6个独立Tab(Gamma 和 EOTF 分离)"""
|
||||||
# 创建Notebook用于图表切换
|
# 创建Notebook用于图表切换
|
||||||
self.chart_notebook = ttk.Notebook(self.result_frame)
|
self.chart_notebook = ttk.Notebook(self.result_frame)
|
||||||
@@ -859,7 +865,7 @@ def create_result_chart_frame(self):
|
|||||||
# 创建单步调试面板实例
|
# 创建单步调试面板实例
|
||||||
self.debug_panel = PQDebugPanel(self.debug_container, self)
|
self.debug_panel = PQDebugPanel(self.debug_container, self)
|
||||||
|
|
||||||
def on_chart_tab_changed(self, event):
|
def on_chart_tab_changed(self: "PQAutomationApp", event):
|
||||||
"""Tab切换时的事件处理"""
|
"""Tab切换时的事件处理"""
|
||||||
try:
|
try:
|
||||||
self._last_tab_index = self.chart_notebook.index(
|
self._last_tab_index = self.chart_notebook.index(
|
||||||
@@ -868,3 +874,21 @@ def on_chart_tab_changed(self, event):
|
|||||||
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")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ChartFrameMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
init_gamut_chart = init_gamut_chart
|
||||||
|
sync_gamut_toolbar = sync_gamut_toolbar
|
||||||
|
_on_gamut_toolbar_changed = _on_gamut_toolbar_changed
|
||||||
|
init_gamma_chart = init_gamma_chart
|
||||||
|
init_eotf_chart = init_eotf_chart
|
||||||
|
init_cct_chart = init_cct_chart
|
||||||
|
init_contrast_chart = init_contrast_chart
|
||||||
|
init_accuracy_chart = init_accuracy_chart
|
||||||
|
clear_chart = clear_chart
|
||||||
|
update_chart_tabs_state = update_chart_tabs_state
|
||||||
|
create_result_chart_frame = create_result_chart_frame
|
||||||
|
on_chart_tab_changed = on_chart_tab_changed
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ register_panel / show_panel / hide_all_panels —— 在右侧栏不同浮动面
|
|||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
|
||||||
def register_panel(self, panel_name, frame, button, visible_attr):
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
def register_panel(self: "PQAutomationApp", panel_name, frame, button, visible_attr):
|
||||||
"""注册一个面板到管理系统"""
|
"""注册一个面板到管理系统"""
|
||||||
self.panels[panel_name] = {
|
self.panels[panel_name] = {
|
||||||
"frame": frame,
|
"frame": frame,
|
||||||
@@ -14,7 +20,7 @@ def register_panel(self, panel_name, frame, button, visible_attr):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def show_panel(self, panel_name):
|
def show_panel(self: "PQAutomationApp", panel_name):
|
||||||
"""显示指定面板,隐藏其他所有面板"""
|
"""显示指定面板,隐藏其他所有面板"""
|
||||||
if panel_name not in self.panels:
|
if panel_name not in self.panels:
|
||||||
return
|
return
|
||||||
@@ -47,7 +53,7 @@ def show_panel(self, panel_name):
|
|||||||
self.current_panel = panel_name
|
self.current_panel = panel_name
|
||||||
|
|
||||||
|
|
||||||
def hide_all_panels(self):
|
def hide_all_panels(self: "PQAutomationApp"):
|
||||||
"""隐藏所有面板,显示主内容区域"""
|
"""隐藏所有面板,显示主内容区域"""
|
||||||
# 隐藏所有注册的面板
|
# 隐藏所有注册的面板
|
||||||
for panel_name, panel_info in self.panels.items():
|
for panel_name, panel_info in self.panels.items():
|
||||||
@@ -70,3 +76,12 @@ def hide_all_panels(self):
|
|||||||
self.current_panel = None
|
self.current_panel = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PanelManagerMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
register_panel = register_panel
|
||||||
|
show_panel = show_panel
|
||||||
|
hide_all_panels = hide_all_panels
|
||||||
|
|||||||
@@ -14,13 +14,19 @@ from PIL import Image, ImageTk
|
|||||||
|
|
||||||
from app.services import ai_image as _svc
|
from app.services import ai_image as _svc
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------- 面板创建 ----------------
|
# ---------------- 面板创建 ----------------
|
||||||
|
|
||||||
|
|
||||||
def create_ai_image_panel(self):
|
def create_ai_image_panel(self: "PQAutomationApp"):
|
||||||
"""创建 AI 图片对话面板,并注册到面板管理。"""
|
"""创建 AI 图片对话面板,并注册到面板管理。"""
|
||||||
frame = ttk.Frame(self.content_frame)
|
frame = ttk.Frame(self.content_frame)
|
||||||
self.ai_image_frame = frame
|
self.ai_image_frame = frame
|
||||||
@@ -190,12 +196,12 @@ def create_ai_image_panel(self):
|
|||||||
reload_ai_image_list(self)
|
reload_ai_image_list(self)
|
||||||
|
|
||||||
|
|
||||||
def toggle_ai_image_panel(self):
|
def toggle_ai_image_panel(self: "PQAutomationApp"):
|
||||||
"""切换 AI 图片面板显隐。"""
|
"""切换 AI 图片面板显隐。"""
|
||||||
self.show_panel("ai_image")
|
self.show_panel("ai_image")
|
||||||
|
|
||||||
|
|
||||||
def _get_app_base_dir(self) -> str:
|
def _get_app_base_dir(self: "PQAutomationApp") -> str:
|
||||||
"""返回应用根目录(settings 的上一级)。"""
|
"""返回应用根目录(settings 的上一级)。"""
|
||||||
if getattr(self, "config_file", None):
|
if getattr(self, "config_file", None):
|
||||||
return os.path.dirname(os.path.dirname(self.config_file))
|
return os.path.dirname(os.path.dirname(self.config_file))
|
||||||
@@ -207,7 +213,7 @@ def _get_app_base_dir(self) -> str:
|
|||||||
# ---------------- 列表 / 选中 ----------------
|
# ---------------- 列表 / 选中 ----------------
|
||||||
|
|
||||||
|
|
||||||
def reload_ai_image_list(self, auto_select_first=True):
|
def reload_ai_image_list(self: "PQAutomationApp", auto_select_first=True):
|
||||||
"""重新扫描缓存并刷新列表。
|
"""重新扫描缓存并刷新列表。
|
||||||
|
|
||||||
按 ``session_id`` 分组:每个会话用一行不可选的分隔头(``── 会话 #N · 时间 ──``),
|
按 ``session_id`` 分组:每个会话用一行不可选的分隔头(``── 会话 #N · 时间 ──``),
|
||||||
@@ -289,7 +295,7 @@ def _format_list_label(rec: _svc.AIImageRecord) -> str:
|
|||||||
return f"{size_tag}{name_line}"
|
return f"{size_tag}{name_line}"
|
||||||
|
|
||||||
|
|
||||||
def _on_list_select(self):
|
def _on_list_select(self: "PQAutomationApp"):
|
||||||
sel = self.ai_image_listbox.curselection()
|
sel = self.ai_image_listbox.curselection()
|
||||||
if not sel:
|
if not sel:
|
||||||
return
|
return
|
||||||
@@ -311,7 +317,7 @@ def _on_list_select(self):
|
|||||||
_select_record(self, rec)
|
_select_record(self, rec)
|
||||||
|
|
||||||
|
|
||||||
def _select_record(self, rec: _svc.AIImageRecord):
|
def _select_record(self: "PQAutomationApp", rec: _svc.AIImageRecord):
|
||||||
self.ai_image_current = rec
|
self.ai_image_current = rec
|
||||||
self.ai_image_meta_var.set(
|
self.ai_image_meta_var.set(
|
||||||
f"{os.path.basename(rec.image_path)} | {rec.created_at}"
|
f"{os.path.basename(rec.image_path)} | {rec.created_at}"
|
||||||
@@ -322,7 +328,7 @@ def _select_record(self, rec: _svc.AIImageRecord):
|
|||||||
# ---------------- 预览绘制 ----------------
|
# ---------------- 预览绘制 ----------------
|
||||||
|
|
||||||
|
|
||||||
def _redraw_preview(self):
|
def _redraw_preview(self: "PQAutomationApp"):
|
||||||
rec = getattr(self, "ai_image_current", None)
|
rec = getattr(self, "ai_image_current", None)
|
||||||
canvas = self.ai_image_canvas
|
canvas = self.ai_image_canvas
|
||||||
canvas.delete("all")
|
canvas.delete("all")
|
||||||
@@ -347,7 +353,7 @@ def _redraw_preview(self):
|
|||||||
# ---------------- 发送 / 保存 / 删除 ----------------
|
# ---------------- 发送 / 保存 / 删除 ----------------
|
||||||
|
|
||||||
|
|
||||||
def _start_new_session(self):
|
def _start_new_session(self: "PQAutomationApp"):
|
||||||
"""开启新的对话会话,后续生成将使用新的 session_id。"""
|
"""开启新的对话会话,后续生成将使用新的 session_id。"""
|
||||||
if getattr(self, "_ai_image_requesting", False):
|
if getattr(self, "_ai_image_requesting", False):
|
||||||
messagebox.showinfo("提示", "请等待当前请求完成")
|
messagebox.showinfo("提示", "请等待当前请求完成")
|
||||||
@@ -357,14 +363,14 @@ def _start_new_session(self):
|
|||||||
reload_ai_image_list(self, auto_select_first=False)
|
reload_ai_image_list(self, auto_select_first=False)
|
||||||
|
|
||||||
|
|
||||||
def _session_id_for_row(self, row: int) -> str:
|
def _session_id_for_row(self: "PQAutomationApp", row: int) -> str:
|
||||||
session_map = getattr(self, "_ai_image_row_session_map", None) or []
|
session_map = getattr(self, "_ai_image_row_session_map", None) or []
|
||||||
if row < 0 or row >= len(session_map):
|
if row < 0 or row >= len(session_map):
|
||||||
return ""
|
return ""
|
||||||
return session_map[row] or ""
|
return session_map[row] or ""
|
||||||
|
|
||||||
|
|
||||||
def _switch_to_session(self, session_id: str, show_message: bool = True, target_record_id: str = ""):
|
def _switch_to_session(self: "PQAutomationApp", session_id: str, show_message: bool = True, target_record_id: str = ""):
|
||||||
sid = (session_id or "").strip()
|
sid = (session_id or "").strip()
|
||||||
if not sid:
|
if not sid:
|
||||||
return
|
return
|
||||||
@@ -389,7 +395,7 @@ def _switch_to_session(self, session_id: str, show_message: bool = True, target_
|
|||||||
messagebox.showinfo("提示", "已切换到所选历史对话")
|
messagebox.showinfo("提示", "已切换到所选历史对话")
|
||||||
|
|
||||||
|
|
||||||
def _update_request_progress(self):
|
def _update_request_progress(self: "PQAutomationApp"):
|
||||||
if not getattr(self, "_ai_image_requesting", False):
|
if not getattr(self, "_ai_image_requesting", False):
|
||||||
self._ai_image_progress_job = None
|
self._ai_image_progress_job = None
|
||||||
return
|
return
|
||||||
@@ -398,7 +404,7 @@ def _update_request_progress(self):
|
|||||||
self._ai_image_progress_job = self.root.after(900, lambda: _update_request_progress(self))
|
self._ai_image_progress_job = self.root.after(900, lambda: _update_request_progress(self))
|
||||||
|
|
||||||
|
|
||||||
def _send_prompt(self):
|
def _send_prompt(self: "PQAutomationApp"):
|
||||||
if getattr(self, "_ai_image_requesting", False):
|
if getattr(self, "_ai_image_requesting", False):
|
||||||
return
|
return
|
||||||
prompt = self.ai_image_input.get("1.0", tk.END).strip()
|
prompt = self.ai_image_input.get("1.0", tk.END).strip()
|
||||||
@@ -439,7 +445,7 @@ def _send_prompt(self):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _set_requesting(self, flag: bool):
|
def _set_requesting(self: "PQAutomationApp", flag: bool):
|
||||||
self._ai_image_requesting = flag
|
self._ai_image_requesting = flag
|
||||||
try:
|
try:
|
||||||
self.ai_image_send_btn.configure(state=tk.DISABLED if flag else tk.NORMAL)
|
self.ai_image_send_btn.configure(state=tk.DISABLED if flag else tk.NORMAL)
|
||||||
@@ -465,7 +471,7 @@ def _set_requesting(self, flag: bool):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _on_request_done(self, record, exc, req_seq):
|
def _on_request_done(self: "PQAutomationApp", record, exc, req_seq):
|
||||||
# 旧请求回调(例如用户已点击停止后)直接忽略
|
# 旧请求回调(例如用户已点击停止后)直接忽略
|
||||||
if req_seq != getattr(self, "_ai_image_active_seq", 0):
|
if req_seq != getattr(self, "_ai_image_active_seq", 0):
|
||||||
return
|
return
|
||||||
@@ -493,7 +499,7 @@ def _on_request_done(self, record, exc, req_seq):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def _stop_request(self):
|
def _stop_request(self: "PQAutomationApp"):
|
||||||
"""停止当前生成任务(协作取消:屏蔽后续回调并恢复 UI)。"""
|
"""停止当前生成任务(协作取消:屏蔽后续回调并恢复 UI)。"""
|
||||||
if not getattr(self, "_ai_image_requesting", False):
|
if not getattr(self, "_ai_image_requesting", False):
|
||||||
return
|
return
|
||||||
@@ -505,7 +511,7 @@ def _stop_request(self):
|
|||||||
self.ai_image_status_var.set("已停止生成")
|
self.ai_image_status_var.set("已停止生成")
|
||||||
|
|
||||||
|
|
||||||
def _save_current(self):
|
def _save_current(self: "PQAutomationApp"):
|
||||||
rec = getattr(self, "ai_image_current", None)
|
rec = getattr(self, "ai_image_current", None)
|
||||||
if rec is None:
|
if rec is None:
|
||||||
messagebox.showinfo("提示", "请先选择一张图片")
|
messagebox.showinfo("提示", "请先选择一张图片")
|
||||||
@@ -526,7 +532,7 @@ def _save_current(self):
|
|||||||
messagebox.showerror("保存失败", str(exc))
|
messagebox.showerror("保存失败", str(exc))
|
||||||
|
|
||||||
|
|
||||||
def _delete_current(self):
|
def _delete_current(self: "PQAutomationApp"):
|
||||||
rec = getattr(self, "ai_image_current", None)
|
rec = getattr(self, "ai_image_current", None)
|
||||||
if rec is None:
|
if rec is None:
|
||||||
messagebox.showinfo("提示", "请先选择一张图片")
|
messagebox.showinfo("提示", "请先选择一张图片")
|
||||||
@@ -537,7 +543,7 @@ def _delete_current(self):
|
|||||||
reload_ai_image_list(self)
|
reload_ai_image_list(self)
|
||||||
|
|
||||||
|
|
||||||
def _rename_current(self):
|
def _rename_current(self: "PQAutomationApp"):
|
||||||
"""弹窗让用户修改当前记录的展示标题(保存到侧车 ``title`` 字段,原始 prompt 不变)。"""
|
"""弹窗让用户修改当前记录的展示标题(保存到侧车 ``title`` 字段,原始 prompt 不变)。"""
|
||||||
rec = getattr(self, "ai_image_current", None)
|
rec = getattr(self, "ai_image_current", None)
|
||||||
if rec is None:
|
if rec is None:
|
||||||
@@ -580,7 +586,7 @@ def _rename_current(self):
|
|||||||
# ---------------- 发送到 UCD ----------------
|
# ---------------- 发送到 UCD ----------------
|
||||||
|
|
||||||
|
|
||||||
def _show_list_context_menu(self, event):
|
def _show_list_context_menu(self: "PQAutomationApp", event):
|
||||||
"""在图片列表上显示右键菜单,并根据状态启用/禁用项。"""
|
"""在图片列表上显示右键菜单,并根据状态启用/禁用项。"""
|
||||||
try:
|
try:
|
||||||
row = self.ai_image_listbox.nearest(event.y)
|
row = self.ai_image_listbox.nearest(event.y)
|
||||||
@@ -619,7 +625,7 @@ def _show_list_context_menu(self, event):
|
|||||||
self.ai_image_menu.grab_release()
|
self.ai_image_menu.grab_release()
|
||||||
|
|
||||||
|
|
||||||
def _send_to_ucd(self):
|
def _send_to_ucd(self: "PQAutomationApp"):
|
||||||
"""把当前选中的 AI 图片通过 UCD 发送到显示设备。"""
|
"""把当前选中的 AI 图片通过 UCD 发送到显示设备。"""
|
||||||
rec = getattr(self, "ai_image_current", None)
|
rec = getattr(self, "ai_image_current", None)
|
||||||
if rec is None:
|
if rec is None:
|
||||||
@@ -730,3 +736,29 @@ def _build_ucd_resized_image(image_path: str, target_w: int, target_h: int) -> s
|
|||||||
resized = img.convert("RGB").resize((target_w, target_h), Image.LANCZOS)
|
resized = img.convert("RGB").resize((target_w, target_h), Image.LANCZOS)
|
||||||
resized.save(out_path, format="PNG")
|
resized.save(out_path, format="PNG")
|
||||||
return out_path
|
return out_path
|
||||||
|
|
||||||
|
|
||||||
|
class AIImagePanelMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
create_ai_image_panel = create_ai_image_panel
|
||||||
|
toggle_ai_image_panel = toggle_ai_image_panel
|
||||||
|
_get_app_base_dir = _get_app_base_dir
|
||||||
|
reload_ai_image_list = reload_ai_image_list
|
||||||
|
_on_list_select = _on_list_select
|
||||||
|
_select_record = _select_record
|
||||||
|
_redraw_preview = _redraw_preview
|
||||||
|
_start_new_session = _start_new_session
|
||||||
|
_session_id_for_row = _session_id_for_row
|
||||||
|
_switch_to_session = _switch_to_session
|
||||||
|
_update_request_progress = _update_request_progress
|
||||||
|
_send_prompt = _send_prompt
|
||||||
|
_set_requesting = _set_requesting
|
||||||
|
_on_request_done = _on_request_done
|
||||||
|
_stop_request = _stop_request
|
||||||
|
_save_current = _save_current
|
||||||
|
_delete_current = _delete_current
|
||||||
|
_rename_current = _rename_current
|
||||||
|
_show_list_context_menu = _show_list_context_menu
|
||||||
|
_send_to_ucd = _send_to_ucd
|
||||||
|
|||||||
987
app/views/panels/calman_panel.py
Normal file
987
app/views/panels/calman_panel.py
Normal file
@@ -0,0 +1,987 @@
|
|||||||
|
"""CALMAN 风格灰阶测试面板(持续演进版)。
|
||||||
|
|
||||||
|
布局尽量贴近 Calman Grayscale - Multi:
|
||||||
|
- 顶部暗色四图:DeltaE、RGB Balance 线图、RGB Balance 条图、Gamma Log/Log;
|
||||||
|
- 中部双行灰阶条:Actual(实测亮度映射)+ Target(目标灰阶),可点击发送图案;
|
||||||
|
- 底部左:Current Reading + CIE 1931 xy 散点;
|
||||||
|
- 底部右:按灰阶展开的矩阵表(x/y/Y/Gamma/CCT/DeltaE 等)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import math
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import ttkbootstrap as ttk
|
||||||
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
|
||||||
|
from app.tests.color_accuracy import calculate_delta_e_2000
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
# 默认灰阶档位(百分比)
|
||||||
|
DEFAULT_LEVELS_PCT: list[int] = list(range(0, 101, 5))
|
||||||
|
|
||||||
|
# 目标白点 D65(CIE 1931)
|
||||||
|
D65_X = 0.3127
|
||||||
|
D65_Y = 0.3290
|
||||||
|
TARGET_CCT = 6504
|
||||||
|
TARGET_GAMMA = 2.2
|
||||||
|
_DARK_BG = "#2f2f2f"
|
||||||
|
_AX_BG = "#262626"
|
||||||
|
_FG = "#d8d8d8"
|
||||||
|
_GRID = "#5b5b5b"
|
||||||
|
|
||||||
|
DE_FORMULAS = ["2000", "94", "76"]
|
||||||
|
|
||||||
|
|
||||||
|
def _pct_to_gray_rgb(pct: int) -> tuple[int, int, int]:
|
||||||
|
value = int(round(pct * 255 / 100))
|
||||||
|
return value, value, value
|
||||||
|
|
||||||
|
|
||||||
|
def _rgb_to_hex(rgb: tuple[int, int, int]) -> str:
|
||||||
|
r, g, b = rgb
|
||||||
|
return f"#{r:02x}{g:02x}{b:02x}"
|
||||||
|
|
||||||
|
|
||||||
|
def _contrast_fg(gray_value: int) -> str:
|
||||||
|
return "#ffffff" if gray_value < 128 else "#000000"
|
||||||
|
|
||||||
|
|
||||||
|
def _set_canvas_patch(canvas: tk.Canvas, color: str, text: str) -> None:
|
||||||
|
"""统一设置色块 Canvas 颜色与文字,避免主题对 Label 背景渲染的干扰。"""
|
||||||
|
gray = int(color[1:3], 16)
|
||||||
|
canvas.configure(bg=color, highlightbackground="#666666")
|
||||||
|
canvas.itemconfigure("patch_bg", fill=color, outline=color)
|
||||||
|
canvas.itemconfigure("patch_text", text=text, fill=_contrast_fg(gray))
|
||||||
|
|
||||||
|
|
||||||
|
def _xy_to_cct_mccamy(x: float, y: float) -> float:
|
||||||
|
"""McCamy 近似公式计算 CCT。对极暗灰阶 (xy 噪声大) 仅做参考。"""
|
||||||
|
denom = 0.1858 - y
|
||||||
|
if denom == 0:
|
||||||
|
return float("nan")
|
||||||
|
n = (x - 0.3320) / denom
|
||||||
|
return 437 * n ** 3 + 3601 * n ** 2 + 6861 * n + 5517
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_float(value, fmt="{:.4f}", placeholder="-"):
|
||||||
|
try:
|
||||||
|
if value is None or value != value: # NaN
|
||||||
|
return placeholder
|
||||||
|
return fmt.format(value)
|
||||||
|
except Exception:
|
||||||
|
return placeholder
|
||||||
|
|
||||||
|
|
||||||
|
def _xy_to_upvp(x: float, y: float) -> tuple[float, float]:
|
||||||
|
denom = (-2.0 * x) + (12.0 * y) + 3.0
|
||||||
|
if denom == 0:
|
||||||
|
return float("nan"), float("nan")
|
||||||
|
up = (4.0 * x) / denom
|
||||||
|
vp = (9.0 * y) / denom
|
||||||
|
return up, vp
|
||||||
|
|
||||||
|
|
||||||
|
def _xyY_to_rgb_balance(x: float, y: float, big_y: float) -> tuple[float, float, float]:
|
||||||
|
"""把 xyY 近似映射到 RGB 比例,并归一到平均值 100。"""
|
||||||
|
if y <= 0 or big_y <= 0:
|
||||||
|
return float("nan"), float("nan"), float("nan")
|
||||||
|
|
||||||
|
big_x = (x * big_y) / y
|
||||||
|
big_z = ((1.0 - x - y) * big_y) / y
|
||||||
|
|
||||||
|
r = (3.2406 * big_x) + (-1.5372 * big_y) + (-0.4986 * big_z)
|
||||||
|
g = (-0.9689 * big_x) + (1.8758 * big_y) + (0.0415 * big_z)
|
||||||
|
b = (0.0557 * big_x) + (-0.2040 * big_y) + (1.0570 * big_z)
|
||||||
|
|
||||||
|
r = max(r, 0.0)
|
||||||
|
g = max(g, 0.0)
|
||||||
|
b = max(b, 0.0)
|
||||||
|
|
||||||
|
avg = (r + g + b) / 3.0
|
||||||
|
if avg <= 0:
|
||||||
|
return float("nan"), float("nan"), float("nan")
|
||||||
|
return (r / avg) * 100.0, (g / avg) * 100.0, (b / avg) * 100.0
|
||||||
|
|
||||||
|
|
||||||
|
def _style_axes(ax, title: str) -> None:
|
||||||
|
ax.set_title(title, color=_FG, fontsize=9, pad=4)
|
||||||
|
ax.set_facecolor(_AX_BG)
|
||||||
|
ax.grid(True, color=_GRID, alpha=0.35, linewidth=0.6)
|
||||||
|
ax.tick_params(colors=_FG, labelsize=8)
|
||||||
|
for spine in ax.spines.values():
|
||||||
|
spine.set_color("#8a8a8a")
|
||||||
|
|
||||||
|
|
||||||
|
def create_calman_panel(self: "PQAutomationApp") -> None:
|
||||||
|
"""创建 CALMAN 风格灰阶测试面板,注册到 panel_manager。"""
|
||||||
|
self.calman_frame = ttk.Frame(self.content_frame)
|
||||||
|
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)
|
||||||
|
self.calman_results = {}
|
||||||
|
self.calman_stop_event = threading.Event()
|
||||||
|
self.calman_running = False
|
||||||
|
self.calman_current_level = None
|
||||||
|
self.calman_last_record = None
|
||||||
|
self.calman_last_step_seconds = None
|
||||||
|
|
||||||
|
root = ttk.Frame(self.calman_frame, padding=8)
|
||||||
|
root.pack(fill=tk.BOTH, expand=True)
|
||||||
|
root.rowconfigure(0, weight=4)
|
||||||
|
root.rowconfigure(1, weight=0)
|
||||||
|
root.rowconfigure(2, weight=3)
|
||||||
|
root.columnconfigure(0, weight=1)
|
||||||
|
root.columnconfigure(1, weight=0)
|
||||||
|
|
||||||
|
# ---------------------------- 顶部:图表区(暗色) ----------------------------
|
||||||
|
chart_frame = ttk.LabelFrame(root, text="Grayscale - Multi", padding=4)
|
||||||
|
chart_frame.grid(row=0, column=0, sticky=tk.NSEW)
|
||||||
|
chart_frame.rowconfigure(0, weight=4)
|
||||||
|
chart_frame.rowconfigure(1, weight=0)
|
||||||
|
chart_frame.rowconfigure(2, weight=0)
|
||||||
|
chart_frame.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
fig = Figure(figsize=(10.5, 3.4), dpi=90, facecolor=_DARK_BG)
|
||||||
|
self.calman_fig = fig
|
||||||
|
self.calman_ax_de = fig.add_subplot(141)
|
||||||
|
self.calman_ax_rgb_line = fig.add_subplot(142)
|
||||||
|
self.calman_ax_rgb_bar = fig.add_subplot(143)
|
||||||
|
self.calman_ax_gamma = fig.add_subplot(144)
|
||||||
|
fig.subplots_adjust(
|
||||||
|
left=0.045, right=0.985, top=0.90, bottom=0.18, wspace=0.30
|
||||||
|
)
|
||||||
|
canvas = FigureCanvasTkAgg(fig, master=chart_frame)
|
||||||
|
canvas_widget = canvas.get_tk_widget()
|
||||||
|
canvas_widget.configure(bg=_DARK_BG, highlightthickness=0)
|
||||||
|
canvas_widget.grid(row=0, column=0, sticky=tk.NSEW)
|
||||||
|
self.calman_canvas = canvas
|
||||||
|
|
||||||
|
control_row = ttk.Frame(chart_frame)
|
||||||
|
control_row.grid(row=1, column=0, sticky=tk.EW, pady=(2, 2))
|
||||||
|
ttk.Label(control_row, text="dE Formula:").pack(side=tk.LEFT)
|
||||||
|
self.calman_de_formula_var = tk.StringVar(value="2000")
|
||||||
|
de_combo = ttk.Combobox(
|
||||||
|
control_row,
|
||||||
|
values=DE_FORMULAS,
|
||||||
|
textvariable=self.calman_de_formula_var,
|
||||||
|
width=8,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
de_combo.pack(side=tk.LEFT, padx=(4, 10))
|
||||||
|
|
||||||
|
self.calman_elapsed_var = tk.StringVar(value="Step: -- s | Total: -- s")
|
||||||
|
ttk.Label(
|
||||||
|
control_row,
|
||||||
|
textvariable=self.calman_elapsed_var,
|
||||||
|
foreground="#d0d0d0",
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
metrics_row = ttk.Frame(chart_frame)
|
||||||
|
metrics_row.grid(row=2, column=0, sticky=tk.EW, pady=(2, 0))
|
||||||
|
metrics_row.columnconfigure((0, 1, 2, 3), weight=1)
|
||||||
|
self.calman_avg_de_var = tk.StringVar(value="Avg dE2000: --")
|
||||||
|
self.calman_avg_cct_var = tk.StringVar(value="Avg CCT: --")
|
||||||
|
self.calman_contrast_var = tk.StringVar(value="Contrast Ratio: --")
|
||||||
|
self.calman_avg_gamma_var = tk.StringVar(value="Average Gamma: --")
|
||||||
|
for idx, v in enumerate(
|
||||||
|
(
|
||||||
|
self.calman_avg_de_var,
|
||||||
|
self.calman_avg_cct_var,
|
||||||
|
self.calman_contrast_var,
|
||||||
|
self.calman_avg_gamma_var,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
tk.Label(
|
||||||
|
metrics_row,
|
||||||
|
textvariable=v,
|
||||||
|
anchor=tk.CENTER,
|
||||||
|
fg="#f2f2f2",
|
||||||
|
bg="#373737",
|
||||||
|
font=("微软雅黑", 10, "bold"),
|
||||||
|
).grid(row=0, column=idx, sticky=tk.EW, padx=2)
|
||||||
|
|
||||||
|
# ---------------------------- 顶部右:按钮列 ----------------------------
|
||||||
|
btn_col = ttk.LabelFrame(root, text="操作", padding=6)
|
||||||
|
btn_col.grid(row=0, column=1, sticky=tk.NS, padx=(8, 0))
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
btn_col,
|
||||||
|
text="停止",
|
||||||
|
bootstyle="danger",
|
||||||
|
width=18,
|
||||||
|
command=lambda: stop_sequence_test(self),
|
||||||
|
).pack(fill=tk.X, pady=2)
|
||||||
|
ttk.Button(
|
||||||
|
btn_col,
|
||||||
|
text="测试该色块",
|
||||||
|
bootstyle="primary",
|
||||||
|
width=18,
|
||||||
|
command=lambda: measure_current_patch(self),
|
||||||
|
).pack(fill=tk.X, pady=2)
|
||||||
|
ttk.Button(
|
||||||
|
btn_col,
|
||||||
|
text="连续测试列表",
|
||||||
|
bootstyle="success",
|
||||||
|
width=18,
|
||||||
|
command=lambda: start_sequence_test(self),
|
||||||
|
).pack(fill=tk.X, pady=2)
|
||||||
|
ttk.Separator(btn_col, orient="horizontal").pack(fill=tk.X, pady=6)
|
||||||
|
ttk.Button(
|
||||||
|
btn_col,
|
||||||
|
text="清空结果",
|
||||||
|
bootstyle="warning-outline",
|
||||||
|
width=18,
|
||||||
|
command=lambda: clear_results(self),
|
||||||
|
).pack(fill=tk.X, pady=2)
|
||||||
|
|
||||||
|
self.calman_status_var = tk.StringVar(value="待机")
|
||||||
|
ttk.Label(
|
||||||
|
btn_col,
|
||||||
|
textvariable=self.calman_status_var,
|
||||||
|
foreground="#555",
|
||||||
|
wraplength=150,
|
||||||
|
justify=tk.LEFT,
|
||||||
|
).pack(fill=tk.X, pady=(8, 0))
|
||||||
|
|
||||||
|
self.calman_progress_var = tk.StringVar(value="0 / 0")
|
||||||
|
self.calman_progress = ttk.Progressbar(
|
||||||
|
btn_col,
|
||||||
|
orient="horizontal",
|
||||||
|
mode="determinate",
|
||||||
|
maximum=100,
|
||||||
|
value=0,
|
||||||
|
length=160,
|
||||||
|
)
|
||||||
|
self.calman_progress.pack(fill=tk.X, pady=(8, 2))
|
||||||
|
ttk.Label(btn_col, textvariable=self.calman_progress_var).pack(anchor=tk.W)
|
||||||
|
|
||||||
|
self.calman_reading_var = tk.StringVar(
|
||||||
|
value="x: -- y: -- Y: --\nCCT: -- ΔE: --"
|
||||||
|
)
|
||||||
|
ttk.Label(
|
||||||
|
btn_col,
|
||||||
|
textvariable=self.calman_reading_var,
|
||||||
|
font=("Consolas", 9),
|
||||||
|
foreground="#1f6fb2",
|
||||||
|
wraplength=160,
|
||||||
|
justify=tk.LEFT,
|
||||||
|
).pack(fill=tk.X, pady=(8, 0))
|
||||||
|
|
||||||
|
# ---------------------------- 中部:灰阶色块(Target / Actual) ----------------------------
|
||||||
|
patch_outer = ttk.LabelFrame(root, text="灰阶色块(点击可直接输出 Pattern)", padding=6)
|
||||||
|
patch_outer.grid(row=1, column=0, columnspan=2, sticky=tk.EW, pady=(8, 4))
|
||||||
|
patch_outer.columnconfigure(0, weight=0)
|
||||||
|
patch_outer.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
lbl_col = ttk.Frame(patch_outer)
|
||||||
|
lbl_col.grid(row=0, column=0, sticky=tk.NS, padx=(0, 6))
|
||||||
|
ttk.Label(lbl_col, text="Actual", width=7).pack(fill=tk.X, pady=(1, 2))
|
||||||
|
ttk.Label(lbl_col, text="Target", width=7).pack(fill=tk.X, pady=(1, 2))
|
||||||
|
|
||||||
|
patch_holder = ttk.Frame(patch_outer)
|
||||||
|
patch_holder.grid(row=0, column=1, sticky=tk.EW)
|
||||||
|
patch_holder.columnconfigure(tuple(range(len(self.calman_levels))), weight=1)
|
||||||
|
|
||||||
|
self.calman_patch_cells = []
|
||||||
|
self.calman_actual_cells = []
|
||||||
|
self.calman_actual_patch_cells = []
|
||||||
|
self.calman_target_patch_canvases = []
|
||||||
|
self.calman_target_hexes = []
|
||||||
|
for idx, pct in enumerate(self.calman_levels):
|
||||||
|
rgb = _pct_to_gray_rgb(pct)
|
||||||
|
color = _rgb_to_hex(rgb)
|
||||||
|
rgb_val = rgb[0]
|
||||||
|
text_color = _contrast_fg(rgb_val)
|
||||||
|
self.calman_target_hexes.append(color)
|
||||||
|
patch_holder.columnconfigure(idx, weight=1, uniform="patch")
|
||||||
|
|
||||||
|
actual_cell = tk.Frame(
|
||||||
|
patch_holder,
|
||||||
|
bd=1,
|
||||||
|
relief="solid",
|
||||||
|
highlightthickness=1,
|
||||||
|
highlightbackground="#808080",
|
||||||
|
cursor="hand2",
|
||||||
|
)
|
||||||
|
actual_cell.grid(row=0, column=idx, padx=1, pady=1, sticky=tk.NSEW)
|
||||||
|
actual_canvas = tk.Canvas(
|
||||||
|
actual_cell,
|
||||||
|
bg=color,
|
||||||
|
highlightthickness=0,
|
||||||
|
width=3,
|
||||||
|
height=16,
|
||||||
|
)
|
||||||
|
actual_canvas.pack(fill=tk.BOTH, expand=True)
|
||||||
|
actual_canvas.create_rectangle(0, 0, 400, 40, fill=color, outline=color, tags="patch_bg")
|
||||||
|
actual_canvas.create_text(
|
||||||
|
18,
|
||||||
|
8,
|
||||||
|
text=f"{pct}",
|
||||||
|
fill=text_color,
|
||||||
|
font=("Consolas", 6, "bold"),
|
||||||
|
tags="patch_text",
|
||||||
|
)
|
||||||
|
|
||||||
|
cell = tk.Frame(
|
||||||
|
patch_holder,
|
||||||
|
bd=1,
|
||||||
|
relief="solid",
|
||||||
|
highlightthickness=1,
|
||||||
|
highlightbackground="#9c9c9c",
|
||||||
|
cursor="hand2",
|
||||||
|
)
|
||||||
|
cell.grid(row=1, column=idx, padx=1, pady=1, sticky=tk.NSEW)
|
||||||
|
|
||||||
|
target_canvas = tk.Canvas(
|
||||||
|
cell,
|
||||||
|
bg=color,
|
||||||
|
highlightthickness=0,
|
||||||
|
width=3,
|
||||||
|
height=30,
|
||||||
|
)
|
||||||
|
target_canvas.pack(fill=tk.BOTH, expand=True)
|
||||||
|
target_canvas.create_rectangle(0, 0, 400, 40, fill=color, outline=color, tags="patch_bg")
|
||||||
|
target_canvas.create_text(
|
||||||
|
18,
|
||||||
|
8,
|
||||||
|
text=f"{pct}",
|
||||||
|
fill=text_color,
|
||||||
|
font=("Consolas", 7, "bold"),
|
||||||
|
tags="patch_text",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _bind_click(widget, p=pct):
|
||||||
|
widget.bind("<Button-1>", lambda _e, pp=p: send_patch(self, pp))
|
||||||
|
|
||||||
|
for w in (cell, target_canvas):
|
||||||
|
_bind_click(w)
|
||||||
|
for w in (actual_cell, actual_canvas):
|
||||||
|
_bind_click(w)
|
||||||
|
|
||||||
|
self.calman_patch_cells.append(cell)
|
||||||
|
self.calman_actual_cells.append(actual_cell)
|
||||||
|
self.calman_actual_patch_cells.append(actual_canvas)
|
||||||
|
self.calman_target_patch_canvases.append(target_canvas)
|
||||||
|
|
||||||
|
# ---------------------------- 底部:Current Reading + xy + 数据矩阵 ----------------------------
|
||||||
|
bottom = ttk.Frame(root)
|
||||||
|
bottom.grid(row=2, column=0, columnspan=2, sticky=tk.NSEW, pady=(4, 0))
|
||||||
|
bottom.columnconfigure(0, weight=0)
|
||||||
|
bottom.columnconfigure(1, weight=1)
|
||||||
|
bottom.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
left = ttk.LabelFrame(bottom, text="Current Reading", padding=6)
|
||||||
|
left.grid(row=0, column=0, sticky=tk.NS, padx=(0, 6))
|
||||||
|
|
||||||
|
self.calman_reading_var.set(
|
||||||
|
"x: -- y: --\n"
|
||||||
|
"u': -- v': --\n"
|
||||||
|
"cd/m²: --\n"
|
||||||
|
"ΔE2000: --"
|
||||||
|
)
|
||||||
|
tk.Label(
|
||||||
|
left,
|
||||||
|
textvariable=self.calman_reading_var,
|
||||||
|
justify=tk.LEFT,
|
||||||
|
font=("Consolas", 10),
|
||||||
|
fg="#e5e5e5",
|
||||||
|
bg="#323232",
|
||||||
|
width=22,
|
||||||
|
padx=4,
|
||||||
|
pady=4,
|
||||||
|
).pack(fill=tk.X)
|
||||||
|
|
||||||
|
xy_fig = Figure(figsize=(2.6, 2.2), dpi=90, facecolor=_DARK_BG)
|
||||||
|
self.calman_xy_ax = xy_fig.add_subplot(111)
|
||||||
|
xy_fig.subplots_adjust(left=0.20, right=0.96, top=0.90, bottom=0.18)
|
||||||
|
xy_canvas = FigureCanvasTkAgg(xy_fig, master=left)
|
||||||
|
xy_widget = xy_canvas.get_tk_widget()
|
||||||
|
xy_widget.configure(bg=_DARK_BG, highlightthickness=0)
|
||||||
|
xy_widget.pack(fill=tk.BOTH, expand=True, pady=(6, 0))
|
||||||
|
self.calman_xy_canvas = xy_canvas
|
||||||
|
|
||||||
|
right = ttk.LabelFrame(bottom, text="测量矩阵", padding=4)
|
||||||
|
right.grid(row=0, column=1, sticky=tk.NSEW)
|
||||||
|
right.rowconfigure(0, weight=1)
|
||||||
|
right.rowconfigure(1, weight=0)
|
||||||
|
right.columnconfigure(0, weight=0)
|
||||||
|
right.columnconfigure(1, weight=1)
|
||||||
|
right.columnconfigure(2, weight=0)
|
||||||
|
|
||||||
|
metric_tree = ttk.Treeview(
|
||||||
|
right,
|
||||||
|
columns=("metric",),
|
||||||
|
show="headings",
|
||||||
|
height=9,
|
||||||
|
selectmode="none",
|
||||||
|
)
|
||||||
|
metric_tree.heading("metric", text="Metric")
|
||||||
|
metric_tree.column("metric", width=118, anchor=tk.W, stretch=False)
|
||||||
|
metric_tree.grid(row=0, column=0, sticky=tk.NS)
|
||||||
|
|
||||||
|
data_columns = [str(p) for p in self.calman_levels]
|
||||||
|
data_tree = ttk.Treeview(
|
||||||
|
right,
|
||||||
|
columns=data_columns,
|
||||||
|
show="headings",
|
||||||
|
height=9,
|
||||||
|
selectmode="none",
|
||||||
|
)
|
||||||
|
for p in self.calman_levels:
|
||||||
|
cid = str(p)
|
||||||
|
data_tree.heading(cid, text=cid)
|
||||||
|
data_tree.column(cid, width=50, anchor=tk.CENTER, stretch=False)
|
||||||
|
data_tree.grid(row=0, column=1, sticky=tk.NSEW)
|
||||||
|
|
||||||
|
ysb = ttk.Scrollbar(right, orient="vertical", command=lambda *a: _matrix_yview(self, *a))
|
||||||
|
ysb.grid(row=0, column=2, sticky=tk.NS)
|
||||||
|
xsb = ttk.Scrollbar(right, orient="horizontal", command=data_tree.xview)
|
||||||
|
xsb.grid(row=1, column=1, sticky=tk.EW)
|
||||||
|
data_tree.configure(xscrollcommand=xsb.set)
|
||||||
|
|
||||||
|
self.calman_metric_tree = metric_tree
|
||||||
|
self.calman_data_tree = data_tree
|
||||||
|
self.calman_table_ysb = ysb
|
||||||
|
self.calman_tree = data_tree
|
||||||
|
|
||||||
|
for widget in (metric_tree, data_tree):
|
||||||
|
widget.bind("<MouseWheel>", lambda e: _matrix_mousewheel(self, e))
|
||||||
|
|
||||||
|
style = ttk.Style()
|
||||||
|
style.configure("Calman.Treeview", rowheight=22, font=("Consolas", 9))
|
||||||
|
style.configure("Calman.Treeview.Heading", font=("微软雅黑", 9, "bold"))
|
||||||
|
self.calman_metric_tree.configure(style="Calman.Treeview")
|
||||||
|
self.calman_data_tree.configure(style="Calman.Treeview")
|
||||||
|
|
||||||
|
right.bind("<Configure>", lambda _e: _adaptive_matrix_columns(self))
|
||||||
|
|
||||||
|
_refresh_metric_table(self)
|
||||||
|
_update_target_strip(self)
|
||||||
|
_update_actual_strip(self)
|
||||||
|
_redraw_calman_charts(self)
|
||||||
|
|
||||||
|
# 注册到统一面板管理(按钮稍后由 main_layout 注入)
|
||||||
|
self.register_panel("calman", self.calman_frame, None, "calman_visible")
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_calman_panel(self: "PQAutomationApp") -> None:
|
||||||
|
"""切换 CALMAN 灰阶面板显示。"""
|
||||||
|
self.show_panel("calman")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 发送 / 测量
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def send_patch(self: "PQAutomationApp", pct: int) -> None:
|
||||||
|
"""点击色块时,发送对应灰阶图案到信号发生器。"""
|
||||||
|
if not self.signal_service.is_connected:
|
||||||
|
messagebox.showwarning("提示", "请先连接 UCD323 设备")
|
||||||
|
return
|
||||||
|
|
||||||
|
rgb_val = int(round(pct * 255 / 100))
|
||||||
|
self.calman_current_level = pct
|
||||||
|
self.calman_status_var.set(f"发送 {pct}%(RGB={rgb_val})...")
|
||||||
|
_highlight_patch(self, pct)
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
try:
|
||||||
|
self.signal_service.send_solid_rgb((rgb_val, rgb_val, rgb_val))
|
||||||
|
self._dispatch_ui(
|
||||||
|
self.log_gui.log, f"CALMAN: 已发送 {pct}% 灰阶 (RGB={rgb_val})",
|
||||||
|
"info",
|
||||||
|
)
|
||||||
|
self._dispatch_ui(self.calman_status_var.set, f"{pct}% 已发送")
|
||||||
|
except Exception as exc:
|
||||||
|
self._dispatch_ui(self.log_gui.log, f"发送失败: {exc}", "error")
|
||||||
|
self._dispatch_ui(self.calman_status_var.set, f"发送失败: {exc}")
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
def _measure_once(self: "PQAutomationApp", pct: int) -> dict | None:
|
||||||
|
"""采集一次 CA410,并组装一条记录(含 CCT/Gamma/ΔE2000)。"""
|
||||||
|
try:
|
||||||
|
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
|
||||||
|
except Exception as exc:
|
||||||
|
self._dispatch_ui(self.log_gui.log, f"CA410 采集异常: {exc}", "error")
|
||||||
|
return None
|
||||||
|
if lv is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# CCT:对很暗的色块意义不大,按阈值过滤
|
||||||
|
cct = _xy_to_cct_mccamy(x, y) if lv >= 0.5 else float("nan")
|
||||||
|
|
||||||
|
# Gamma 需要 100% 作为参考亮度
|
||||||
|
ref = self.calman_results.get(100, {}).get("Y")
|
||||||
|
gamma = float("nan")
|
||||||
|
if ref and ref > 0 and 0 < pct < 100 and lv > 0:
|
||||||
|
nv = pct / 100.0
|
||||||
|
ny = lv / ref
|
||||||
|
if ny > 0:
|
||||||
|
try:
|
||||||
|
gamma = math.log(ny) / math.log(nv)
|
||||||
|
except (ValueError, ZeroDivisionError):
|
||||||
|
gamma = float("nan")
|
||||||
|
|
||||||
|
# ΔE:根据下拉框切换公式(当前 94/76 先复用 2000,保留接口)
|
||||||
|
formula = getattr(self, "calman_de_formula_var", None)
|
||||||
|
formula_value = formula.get() if formula is not None else "2000"
|
||||||
|
try:
|
||||||
|
de = calculate_delta_e_2000(x, y, lv, D65_X, D65_Y)
|
||||||
|
except Exception:
|
||||||
|
de = float("nan")
|
||||||
|
|
||||||
|
# 未来接入 76/94 时可在此切换实现。
|
||||||
|
if formula_value in ("94", "76"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
rr, gg, bb = _xyY_to_rgb_balance(x, y, lv)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pct": pct,
|
||||||
|
"x": x,
|
||||||
|
"y": y,
|
||||||
|
"Y": lv,
|
||||||
|
"X": X,
|
||||||
|
"Z": Z,
|
||||||
|
"cct": cct,
|
||||||
|
"gamma": gamma,
|
||||||
|
"de2000": de,
|
||||||
|
"rgb_r": rr,
|
||||||
|
"rgb_g": gg,
|
||||||
|
"rgb_b": bb,
|
||||||
|
"time": datetime.datetime.now().strftime("%H:%M:%S"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def measure_current_patch(self: "PQAutomationApp") -> None:
|
||||||
|
"""采集当前已发送色块对应的 CA410 数据。"""
|
||||||
|
if not getattr(self, "ca", None):
|
||||||
|
messagebox.showwarning("提示", "请先连接 CA410 色度计")
|
||||||
|
return
|
||||||
|
pct = self.calman_current_level
|
||||||
|
if pct is None:
|
||||||
|
messagebox.showinfo("提示", "请先点击一个灰阶色块发送")
|
||||||
|
return
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
self._dispatch_ui(self.calman_status_var.set, f"采集 {pct}% 中...")
|
||||||
|
rec = _measure_once(self, pct)
|
||||||
|
if rec is None:
|
||||||
|
self._dispatch_ui(self.calman_status_var.set, "采集失败")
|
||||||
|
return
|
||||||
|
step_s = time.perf_counter() - t0
|
||||||
|
self.calman_last_step_seconds = step_s
|
||||||
|
self.calman_results[pct] = rec
|
||||||
|
self._dispatch_ui(_apply_record_to_ui, self, rec)
|
||||||
|
self._dispatch_ui(
|
||||||
|
self.calman_status_var.set, f"{pct}% 采集完成 ({step_s:.2f}s)"
|
||||||
|
)
|
||||||
|
self._dispatch_ui(
|
||||||
|
self.calman_elapsed_var.set,
|
||||||
|
f"Step: {step_s:.2f} s | Total: -- s",
|
||||||
|
)
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
def start_sequence_test(self: "PQAutomationApp") -> None:
|
||||||
|
"""从 100% 到 0% 连续发送并采集(先测 100% 以确定 gamma 参考)。"""
|
||||||
|
if not getattr(self, "ca", None) or not self.signal_service.is_connected:
|
||||||
|
messagebox.showerror("错误", "请先连接 CA410 和 UCD323")
|
||||||
|
return
|
||||||
|
if self.calman_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.calman_running = True
|
||||||
|
self.calman_stop_event.clear()
|
||||||
|
settle = float(getattr(self, "pattern_settle_time", 0.4))
|
||||||
|
self.calman_progress["value"] = 0
|
||||||
|
self.calman_progress_var.set("0 / 0")
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
seq_t0 = time.perf_counter()
|
||||||
|
try:
|
||||||
|
order = sorted(self.calman_levels, reverse=True)
|
||||||
|
total = len(order)
|
||||||
|
self._dispatch_ui(self.calman_progress_var.set, f"0 / {total}")
|
||||||
|
for i, pct in enumerate(order, 1):
|
||||||
|
if self.calman_stop_event.is_set():
|
||||||
|
self._dispatch_ui(self.log_gui.log, "已停止连续测试", "warning")
|
||||||
|
break
|
||||||
|
step_t0 = time.perf_counter()
|
||||||
|
rgb_val = int(round(pct * 255 / 100))
|
||||||
|
self._dispatch_ui(
|
||||||
|
self.calman_status_var.set, f"[{i}/{total}] 发送 {pct}%"
|
||||||
|
)
|
||||||
|
self._dispatch_ui(_highlight_patch, self, pct)
|
||||||
|
try:
|
||||||
|
self.signal_service.send_solid_rgb(
|
||||||
|
(rgb_val, rgb_val, rgb_val)
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self._dispatch_ui(
|
||||||
|
self.log_gui.log, f"发送 {pct}% 失败: {exc}", "error"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
self.calman_current_level = pct
|
||||||
|
# 等待稳定,停止事件触发时尽快退出
|
||||||
|
if self.calman_stop_event.wait(settle):
|
||||||
|
break
|
||||||
|
rec = _measure_once(self, pct)
|
||||||
|
if rec is None:
|
||||||
|
continue
|
||||||
|
self.calman_results[pct] = rec
|
||||||
|
self._dispatch_ui(_apply_record_to_ui, self, rec)
|
||||||
|
step_s = time.perf_counter() - step_t0
|
||||||
|
total_s = time.perf_counter() - seq_t0
|
||||||
|
self._dispatch_ui(
|
||||||
|
_set_sequence_progress,
|
||||||
|
self,
|
||||||
|
i,
|
||||||
|
total,
|
||||||
|
step_s,
|
||||||
|
total_s,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._dispatch_ui(self.calman_status_var.set, "连续测试完成")
|
||||||
|
self._dispatch_ui(self.log_gui.log, "CALMAN: 连续测试完成", "success")
|
||||||
|
return
|
||||||
|
self._dispatch_ui(self.calman_status_var.set, "已停止")
|
||||||
|
finally:
|
||||||
|
self.calman_running = False
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
def stop_sequence_test(self: "PQAutomationApp") -> None:
|
||||||
|
"""请求停止连续测试。"""
|
||||||
|
if self.calman_running:
|
||||||
|
self.calman_stop_event.set()
|
||||||
|
self.calman_status_var.set("正在停止...")
|
||||||
|
else:
|
||||||
|
self.calman_status_var.set("当前没有运行中的连续测试")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_results(self: "PQAutomationApp") -> None:
|
||||||
|
"""清空结果表和图表。"""
|
||||||
|
self.calman_results.clear()
|
||||||
|
self.calman_last_record = None
|
||||||
|
self.calman_reading_var.set(
|
||||||
|
"x: -- y: --\n"
|
||||||
|
"u': -- v': --\n"
|
||||||
|
"cd/m²: --\n"
|
||||||
|
"ΔE2000: --"
|
||||||
|
)
|
||||||
|
_refresh_metric_table(self)
|
||||||
|
_update_actual_strip(self)
|
||||||
|
_redraw_calman_charts(self)
|
||||||
|
self.calman_progress["value"] = 0
|
||||||
|
self.calman_progress_var.set("0 / 0")
|
||||||
|
self.calman_elapsed_var.set("Step: -- s | Total: -- s")
|
||||||
|
self.calman_status_var.set("已清空")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# UI 更新辅助
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _highlight_patch(self: "PQAutomationApp", pct: int) -> None:
|
||||||
|
"""高亮当前选中色块。"""
|
||||||
|
try:
|
||||||
|
idx = self.calman_levels.index(pct)
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
for i, cell in enumerate(self.calman_patch_cells):
|
||||||
|
if i == idx:
|
||||||
|
cell.configure(highlightbackground="#1f6fb2", highlightthickness=2)
|
||||||
|
else:
|
||||||
|
cell.configure(highlightbackground="#9c9c9c", highlightthickness=1)
|
||||||
|
for i, cell in enumerate(self.calman_actual_cells):
|
||||||
|
if i == idx:
|
||||||
|
cell.configure(highlightbackground="#1f6fb2", highlightthickness=2)
|
||||||
|
else:
|
||||||
|
cell.configure(highlightbackground="#808080", highlightthickness=1)
|
||||||
|
|
||||||
|
total_cols = len(self.calman_levels) + 1 # 含 metric 列
|
||||||
|
col_index = idx + 1
|
||||||
|
left_fraction = max(0.0, min(1.0, (col_index - 2) / max(1, total_cols - 1)))
|
||||||
|
try:
|
||||||
|
self.calman_data_tree.xview_moveto(left_fraction)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_record_to_ui(self: "PQAutomationApp", rec: dict) -> None:
|
||||||
|
"""把一条测量结果写入 Treeview,并刷新图表与 Current Reading。"""
|
||||||
|
self.calman_last_record = rec
|
||||||
|
_refresh_metric_table(self)
|
||||||
|
_update_actual_strip(self)
|
||||||
|
|
||||||
|
up, vp = _xy_to_upvp(rec["x"], rec["y"])
|
||||||
|
|
||||||
|
self.calman_reading_var.set(
|
||||||
|
f"x: {_safe_float(rec['x'])} y: {_safe_float(rec['y'])}\n"
|
||||||
|
f"u': {_safe_float(up)} v': {_safe_float(vp)}\n"
|
||||||
|
f"cd/m²: {_safe_float(rec['Y'], '{:.3f}')}\n"
|
||||||
|
f"ΔE2000: {_safe_float(rec['de2000'], '{:.3f}')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
_redraw_calman_charts(self)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_sequence_progress(
|
||||||
|
self: "PQAutomationApp",
|
||||||
|
finished: int,
|
||||||
|
total: int,
|
||||||
|
step_seconds: float,
|
||||||
|
total_seconds: float,
|
||||||
|
) -> None:
|
||||||
|
percent = (finished / total) * 100 if total > 0 else 0
|
||||||
|
self.calman_progress["value"] = percent
|
||||||
|
self.calman_progress_var.set(f"{finished} / {total}")
|
||||||
|
self.calman_elapsed_var.set(
|
||||||
|
f"Step: {step_seconds:.2f} s | Total: {total_seconds:.1f} s"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _matrix_yview(self: "PQAutomationApp", *args) -> None:
|
||||||
|
self.calman_metric_tree.yview(*args)
|
||||||
|
self.calman_data_tree.yview(*args)
|
||||||
|
first, last = self.calman_data_tree.yview()
|
||||||
|
self.calman_table_ysb.set(first, last)
|
||||||
|
|
||||||
|
|
||||||
|
def _matrix_mousewheel(self: "PQAutomationApp", event) -> str:
|
||||||
|
delta = -1 if event.delta > 0 else 1
|
||||||
|
self.calman_metric_tree.yview_scroll(delta, "units")
|
||||||
|
self.calman_data_tree.yview_scroll(delta, "units")
|
||||||
|
first, last = self.calman_data_tree.yview()
|
||||||
|
self.calman_table_ysb.set(first, last)
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
|
||||||
|
def _adaptive_matrix_columns(self: "PQAutomationApp") -> None:
|
||||||
|
"""按可用宽度自适应数据列宽;空间不足时保留横向滚动。"""
|
||||||
|
try:
|
||||||
|
available = self.calman_data_tree.winfo_width()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
if available <= 40:
|
||||||
|
return
|
||||||
|
|
||||||
|
col_count = max(1, len(self.calman_levels))
|
||||||
|
min_w = 44
|
||||||
|
ideal = int(available / col_count)
|
||||||
|
width = max(min_w, ideal)
|
||||||
|
|
||||||
|
for p in self.calman_levels:
|
||||||
|
self.calman_data_tree.column(str(p), width=width, minwidth=min_w, stretch=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _redraw_calman_charts(self: "PQAutomationApp") -> None:
|
||||||
|
"""根据 calman_results 重绘四张图和 xy 散点。"""
|
||||||
|
recs = sorted(self.calman_results.values(), key=lambda r: r["pct"])
|
||||||
|
pcts = [r["pct"] for r in recs]
|
||||||
|
de_vals = [r["de2000"] if r["de2000"] == r["de2000"] else 0 for r in recs]
|
||||||
|
lum_vals = [r["Y"] if r["Y"] == r["Y"] else 0 for r in recs]
|
||||||
|
rgb_r = [r["rgb_r"] for r in recs if r["rgb_r"] == r["rgb_r"]]
|
||||||
|
rgb_g = [r["rgb_g"] for r in recs if r["rgb_g"] == r["rgb_g"]]
|
||||||
|
rgb_b = [r["rgb_b"] for r in recs if r["rgb_b"] == r["rgb_b"]]
|
||||||
|
rgb_pcts = [r["pct"] for r in recs if r["rgb_r"] == r["rgb_r"]]
|
||||||
|
gamma_pcts = [r["pct"] for r in recs if r["gamma"] == r["gamma"]]
|
||||||
|
gamma_vals = [r["gamma"] for r in recs if r["gamma"] == r["gamma"]]
|
||||||
|
cct_vals = [r["cct"] for r in recs if r["cct"] == r["cct"]]
|
||||||
|
|
||||||
|
if de_vals:
|
||||||
|
avg_de = sum(de_vals) / len(de_vals)
|
||||||
|
self.calman_avg_de_var.set(f"Avg dE2000: {avg_de:.2f}")
|
||||||
|
else:
|
||||||
|
self.calman_avg_de_var.set("Avg dE2000: --")
|
||||||
|
if cct_vals:
|
||||||
|
avg_cct = sum(cct_vals) / len(cct_vals)
|
||||||
|
self.calman_avg_cct_var.set(f"Avg CCT: {avg_cct:.0f}")
|
||||||
|
else:
|
||||||
|
self.calman_avg_cct_var.set("Avg CCT: --")
|
||||||
|
if gamma_vals:
|
||||||
|
avg_gamma = sum(gamma_vals) / len(gamma_vals)
|
||||||
|
self.calman_avg_gamma_var.set(f"Average Gamma: {avg_gamma:.2f}")
|
||||||
|
else:
|
||||||
|
self.calman_avg_gamma_var.set("Average Gamma: --")
|
||||||
|
if len(lum_vals) >= 2 and min(v for v in lum_vals if v > 0) > 0:
|
||||||
|
max_lum = max(lum_vals)
|
||||||
|
min_lum = min(v for v in lum_vals if v > 0)
|
||||||
|
contrast = max_lum / min_lum
|
||||||
|
self.calman_contrast_var.set(f"Contrast Ratio: {contrast:.0f}")
|
||||||
|
else:
|
||||||
|
self.calman_contrast_var.set("Contrast Ratio: --")
|
||||||
|
|
||||||
|
# ΔE2000
|
||||||
|
a1 = self.calman_ax_de
|
||||||
|
a1.clear()
|
||||||
|
_style_axes(a1, "DeltaE 2000")
|
||||||
|
if pcts:
|
||||||
|
a1.bar(pcts, de_vals, color="#ffcf57", width=3.5)
|
||||||
|
a1.set_xlim(-2, 102)
|
||||||
|
a1.set_ylim(bottom=0)
|
||||||
|
a1.set_xlabel("", fontsize=8)
|
||||||
|
|
||||||
|
# RGB Balance 线图
|
||||||
|
a2 = self.calman_ax_rgb_line
|
||||||
|
a2.clear()
|
||||||
|
_style_axes(a2, "RGB Balance")
|
||||||
|
if rgb_pcts:
|
||||||
|
a2.plot(rgb_pcts, rgb_r, "-", color="#ff4d4d", linewidth=1.2)
|
||||||
|
a2.plot(rgb_pcts, rgb_g, "-", color="#4caf50", linewidth=1.2)
|
||||||
|
a2.plot(rgb_pcts, rgb_b, "-", color="#4a73ff", linewidth=1.2)
|
||||||
|
a2.axhline(100, color="#9e9e9e", lw=0.8, ls="--")
|
||||||
|
a2.set_xlim(-2, 102)
|
||||||
|
a2.set_ylim(95, 105)
|
||||||
|
a2.set_xlabel("", fontsize=8)
|
||||||
|
|
||||||
|
# RGB Balance 条图(用最后一个点)
|
||||||
|
a3 = self.calman_ax_rgb_bar
|
||||||
|
a3.clear()
|
||||||
|
_style_axes(a3, "RGB Balance")
|
||||||
|
if recs:
|
||||||
|
last = recs[-1]
|
||||||
|
bars = [
|
||||||
|
last["rgb_r"] if last["rgb_r"] == last["rgb_r"] else 100,
|
||||||
|
last["rgb_g"] if last["rgb_g"] == last["rgb_g"] else 100,
|
||||||
|
last["rgb_b"] if last["rgb_b"] == last["rgb_b"] else 100,
|
||||||
|
]
|
||||||
|
a3.bar([0, 1, 2], bars, color=["#ff4d4d", "#4caf50", "#4a73ff"], width=0.7)
|
||||||
|
a3.set_xticks([0, 1, 2], ["R", "G", "B"])
|
||||||
|
else:
|
||||||
|
a3.set_xticks([0, 1, 2], ["R", "G", "B"])
|
||||||
|
a3.set_ylim(95, 105)
|
||||||
|
a3.set_xlabel("", fontsize=8)
|
||||||
|
|
||||||
|
# Gamma
|
||||||
|
a4 = self.calman_ax_gamma
|
||||||
|
a4.clear()
|
||||||
|
_style_axes(a4, "Gamma Log/Log")
|
||||||
|
if gamma_pcts:
|
||||||
|
a4.plot(gamma_pcts, gamma_vals, "-", color="#ffe24d", linewidth=1.3)
|
||||||
|
a4.axhline(TARGET_GAMMA, color="#9e9e9e", lw=0.8, ls="--")
|
||||||
|
a4.set_xlim(-2, 102)
|
||||||
|
a4.set_ylim(1.6, 2.8)
|
||||||
|
a4.set_xlabel("", fontsize=8)
|
||||||
|
|
||||||
|
self.calman_canvas.draw_idle()
|
||||||
|
|
||||||
|
_redraw_xy_chart(self)
|
||||||
|
|
||||||
|
|
||||||
|
def _redraw_xy_chart(self: "PQAutomationApp") -> None:
|
||||||
|
ax = self.calman_xy_ax
|
||||||
|
ax.clear()
|
||||||
|
_style_axes(ax, "CIE 1931 xy")
|
||||||
|
ax.set_xlim(0.29, 0.34)
|
||||||
|
ax.set_ylim(0.31, 0.35)
|
||||||
|
ax.plot([D65_X], [D65_Y], marker="x", color="#ffffff", markersize=7)
|
||||||
|
|
||||||
|
recs = sorted(self.calman_results.values(), key=lambda r: r["pct"])
|
||||||
|
if recs:
|
||||||
|
xs = [r["x"] for r in recs]
|
||||||
|
ys = [r["y"] for r in recs]
|
||||||
|
ax.plot(xs, ys, "o-", color="#000000", linewidth=1.0, markersize=3)
|
||||||
|
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="#c7c7c7", linewidth=0.8)
|
||||||
|
self.calman_xy_canvas.draw_idle()
|
||||||
|
|
||||||
|
|
||||||
|
def _update_actual_strip(self: "PQAutomationApp") -> None:
|
||||||
|
"""把实测亮度归一后映射到 Actual 色条。"""
|
||||||
|
y_map = {pct: rec["Y"] for pct, rec in self.calman_results.items() if rec.get("Y") is not None}
|
||||||
|
if not y_map:
|
||||||
|
for idx, w in enumerate(self.calman_actual_patch_cells):
|
||||||
|
base = self.calman_target_hexes[idx]
|
||||||
|
_set_canvas_patch(w, base, f"{self.calman_levels[idx]}")
|
||||||
|
return
|
||||||
|
|
||||||
|
max_y = max(y_map.values())
|
||||||
|
if max_y <= 0:
|
||||||
|
max_y = 1.0
|
||||||
|
for idx, pct in enumerate(self.calman_levels):
|
||||||
|
yy = y_map.get(pct)
|
||||||
|
if yy is None:
|
||||||
|
base = self.calman_target_hexes[idx]
|
||||||
|
_set_canvas_patch(self.calman_actual_patch_cells[idx], base, f"{pct}")
|
||||||
|
continue
|
||||||
|
norm = max(0.0, min(1.0, yy / max_y))
|
||||||
|
g = int(round(norm * 255))
|
||||||
|
_set_canvas_patch(self.calman_actual_patch_cells[idx], f"#{g:02x}{g:02x}{g:02x}", f"{pct}")
|
||||||
|
|
||||||
|
|
||||||
|
def _update_target_strip(self: "PQAutomationApp") -> None:
|
||||||
|
for idx, canvas in enumerate(self.calman_target_patch_canvases):
|
||||||
|
_set_canvas_patch(canvas, self.calman_target_hexes[idx], f"{self.calman_levels[idx]}")
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_metric_table(self: "PQAutomationApp") -> None:
|
||||||
|
"""重绘下方矩阵表。"""
|
||||||
|
metrics = [
|
||||||
|
("x CIE31", lambda r: _safe_float(r.get("x")) if r else "-"),
|
||||||
|
("y CIE31", lambda r: _safe_float(r.get("y")) if r else "-"),
|
||||||
|
("Y", lambda r: _safe_float(r.get("Y"), "{:.3f}") if r else "-"),
|
||||||
|
(
|
||||||
|
"Target Y",
|
||||||
|
lambda _r, pctx=None: _safe_float((pctx / 100.0) ** TARGET_GAMMA, "{:.4f}") if pctx and pctx > 0 else ("0.0000" if pctx == 0 else "-"),
|
||||||
|
),
|
||||||
|
("Gamma Log/Log", lambda r: _safe_float(r.get("gamma"), "{:.3f}") if r else "-"),
|
||||||
|
("CCT", lambda r: _safe_float(r.get("cct"), "{:.0f}") if r else "-"),
|
||||||
|
("dE 2000", lambda r: _safe_float(r.get("de2000"), "{:.3f}") if r else "-"),
|
||||||
|
("RGB R", lambda r: _safe_float(r.get("rgb_r"), "{:.2f}") if r else "-"),
|
||||||
|
("RGB G", lambda r: _safe_float(r.get("rgb_g"), "{:.2f}") if r else "-"),
|
||||||
|
("RGB B", lambda r: _safe_float(r.get("rgb_b"), "{:.2f}") if r else "-"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for iid in self.calman_metric_tree.get_children():
|
||||||
|
self.calman_metric_tree.delete(iid)
|
||||||
|
for iid in self.calman_data_tree.get_children():
|
||||||
|
self.calman_data_tree.delete(iid)
|
||||||
|
|
||||||
|
for row_idx, (name, func) in enumerate(metrics):
|
||||||
|
values = []
|
||||||
|
for pct in self.calman_levels:
|
||||||
|
rec = self.calman_results.get(pct)
|
||||||
|
if name == "Target Y":
|
||||||
|
values.append(func(rec, pctx=pct))
|
||||||
|
else:
|
||||||
|
values.append(func(rec))
|
||||||
|
iid = f"row_{row_idx}"
|
||||||
|
tags = ("odd",) if row_idx % 2 else ("even",)
|
||||||
|
self.calman_metric_tree.insert("", tk.END, iid=iid, values=(name,), tags=tags)
|
||||||
|
self.calman_data_tree.insert("", tk.END, iid=iid, values=values, tags=tags)
|
||||||
|
|
||||||
|
self.calman_metric_tree.tag_configure("odd", background="#f5f7fa")
|
||||||
|
self.calman_metric_tree.tag_configure("even", background="#ffffff")
|
||||||
|
self.calman_data_tree.tag_configure("odd", background="#f5f7fa")
|
||||||
|
self.calman_data_tree.tag_configure("even", background="#ffffff")
|
||||||
|
|
||||||
|
first, last = self.calman_data_tree.yview()
|
||||||
|
self.calman_table_ysb.set(first, last)
|
||||||
|
|
||||||
|
|
||||||
|
class CalmanPanelMixin:
|
||||||
|
"""挂载本模块的自由函数到 PQAutomationApp。"""
|
||||||
|
|
||||||
|
create_calman_panel = create_calman_panel
|
||||||
|
toggle_calman_panel = toggle_calman_panel
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""CCT 参数面板及其处理函数(与主文件重构版本保持同步)。"""
|
"""CCT 参数面板及其处理函数(与主文件重构版本保持同步)。"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@@ -8,8 +8,14 @@ import ttkbootstrap as ttk
|
|||||||
|
|
||||||
import algorithm.pq_algorithm as pq_algorithm
|
import algorithm.pq_algorithm as pq_algorithm
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
def create_cct_params_frame(self):
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def create_cct_params_frame(self: "PQAutomationApp"):
|
||||||
"""创建色度参数设置区域 - 屏模组、SDR、HDR 独立(增加色域参考标准选择 + 单步调试按钮)"""
|
"""创建色度参数设置区域 - 屏模组、SDR、HDR 独立(增加色域参考标准选择 + 单步调试按钮)"""
|
||||||
|
|
||||||
# ==================== 屏模组色度参数 Frame ====================
|
# ==================== 屏模组色度参数 Frame ====================
|
||||||
@@ -330,7 +336,7 @@ def create_cct_params_frame(self):
|
|||||||
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
|
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
|
||||||
|
|
||||||
|
|
||||||
def _get_cct_var_dict(self, test_type):
|
def _get_cct_var_dict(self: "PQAutomationApp", test_type):
|
||||||
"""按测试类型返回 CCT 变量映射。"""
|
"""按测试类型返回 CCT 变量映射。"""
|
||||||
if test_type == "sdr_movie":
|
if test_type == "sdr_movie":
|
||||||
return {
|
return {
|
||||||
@@ -354,7 +360,7 @@ def _get_cct_var_dict(self, test_type):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _parse_cct_float(self, var, default):
|
def _parse_cct_float(self: "PQAutomationApp", var, default):
|
||||||
"""读取并解析 CCT 输入值,失败时回落默认值。"""
|
"""读取并解析 CCT 输入值,失败时回落默认值。"""
|
||||||
try:
|
try:
|
||||||
value = var.get().strip()
|
value = var.get().strip()
|
||||||
@@ -365,7 +371,7 @@ def _parse_cct_float(self, var, default):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def _save_cct_params_for(self, test_type):
|
def _save_cct_params_for(self: "PQAutomationApp", test_type):
|
||||||
"""保存指定测试类型的 CCT 参数。"""
|
"""保存指定测试类型的 CCT 参数。"""
|
||||||
try:
|
try:
|
||||||
default_params = self.config.get_default_cct_params(test_type)
|
default_params = self.config.get_default_cct_params(test_type)
|
||||||
@@ -384,7 +390,7 @@ def _save_cct_params_for(self, test_type):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _handle_cct_focus_out(self, var, default_value, save_func, label):
|
def _handle_cct_focus_out(self: "PQAutomationApp", var, default_value, save_func, label):
|
||||||
"""统一处理 CCT 参数失焦校验并保存。"""
|
"""统一处理 CCT 参数失焦校验并保存。"""
|
||||||
try:
|
try:
|
||||||
value = var.get().strip()
|
value = var.get().strip()
|
||||||
@@ -414,27 +420,27 @@ def _handle_cct_focus_out(self, var, default_value, save_func, label):
|
|||||||
self.log_gui.log(f"处理 {label} 参数失败: {str(e)}", level="error")
|
self.log_gui.log(f"处理 {label} 参数失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
def on_sdr_cct_param_focus_out(self, var, default_value):
|
def on_sdr_cct_param_focus_out(self: "PQAutomationApp", var, default_value):
|
||||||
"""SDR 色度参数失去焦点时的处理。"""
|
"""SDR 色度参数失去焦点时的处理。"""
|
||||||
_handle_cct_focus_out(self, var, default_value, self.save_sdr_cct_params, "SDR")
|
_handle_cct_focus_out(self, var, default_value, self.save_sdr_cct_params, "SDR")
|
||||||
|
|
||||||
|
|
||||||
def save_sdr_cct_params(self):
|
def save_sdr_cct_params(self: "PQAutomationApp"):
|
||||||
"""保存 SDR 色度参数。"""
|
"""保存 SDR 色度参数。"""
|
||||||
_save_cct_params_for(self, "sdr_movie")
|
_save_cct_params_for(self, "sdr_movie")
|
||||||
|
|
||||||
|
|
||||||
def on_hdr_cct_param_focus_out(self, var, default_value):
|
def on_hdr_cct_param_focus_out(self: "PQAutomationApp", var, default_value):
|
||||||
"""HDR 色度参数失去焦点时的处理。"""
|
"""HDR 色度参数失去焦点时的处理。"""
|
||||||
_handle_cct_focus_out(self, var, default_value, self.save_hdr_cct_params, "HDR")
|
_handle_cct_focus_out(self, var, default_value, self.save_hdr_cct_params, "HDR")
|
||||||
|
|
||||||
|
|
||||||
def save_hdr_cct_params(self):
|
def save_hdr_cct_params(self: "PQAutomationApp"):
|
||||||
"""保存 HDR 色度参数。"""
|
"""保存 HDR 色度参数。"""
|
||||||
_save_cct_params_for(self, "hdr_movie")
|
_save_cct_params_for(self, "hdr_movie")
|
||||||
|
|
||||||
|
|
||||||
def recalculate_cct(self):
|
def recalculate_cct(self: "PQAutomationApp"):
|
||||||
"""重新计算并绘制色度图"""
|
"""重新计算并绘制色度图"""
|
||||||
try:
|
try:
|
||||||
# 1. 保存新参数
|
# 1. 保存新参数
|
||||||
@@ -496,7 +502,7 @@ def recalculate_cct(self):
|
|||||||
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
|
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def recalculate_gamut(self):
|
def recalculate_gamut(self: "PQAutomationApp"):
|
||||||
"""重新计算并绘制色域图(使用新的参考标准)"""
|
"""重新计算并绘制色域图(使用新的参考标准)"""
|
||||||
try:
|
try:
|
||||||
# 1. 收起配置项
|
# 1. 收起配置项
|
||||||
@@ -644,17 +650,17 @@ def recalculate_gamut(self):
|
|||||||
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
|
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def on_cct_param_focus_out(self, var, default_value):
|
def on_cct_param_focus_out(self: "PQAutomationApp", var, default_value):
|
||||||
"""色度参数失去焦点时的处理 - 空值恢复默认"""
|
"""色度参数失去焦点时的处理 - 空值恢复默认"""
|
||||||
_handle_cct_focus_out(self, var, default_value, self.save_cct_params, "屏模组")
|
_handle_cct_focus_out(self, var, default_value, self.save_cct_params, "屏模组")
|
||||||
|
|
||||||
|
|
||||||
def save_cct_params(self):
|
def save_cct_params(self: "PQAutomationApp"):
|
||||||
"""保存色度参数 - 简化版"""
|
"""保存色度参数 - 简化版"""
|
||||||
_save_cct_params_for(self, self.config.current_test_type)
|
_save_cct_params_for(self, self.config.current_test_type)
|
||||||
|
|
||||||
|
|
||||||
def reload_cct_params(self):
|
def reload_cct_params(self: "PQAutomationApp"):
|
||||||
"""切换测试类型时重新加载色度参数"""
|
"""切换测试类型时重新加载色度参数"""
|
||||||
try:
|
try:
|
||||||
current_type = self.config.current_test_type
|
current_type = self.config.current_test_type
|
||||||
@@ -676,7 +682,7 @@ def reload_cct_params(self):
|
|||||||
self.log_gui.log(f"重新加载色度参数失败: {str(e)}", level="error")
|
self.log_gui.log(f"重新加载色度参数失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
def toggle_cct_params_frame(self):
|
def toggle_cct_params_frame(self: "PQAutomationApp"):
|
||||||
"""根据测试类型和测试项的选中状态显示对应参数框"""
|
"""根据测试类型和测试项的选中状态显示对应参数框"""
|
||||||
selected_items = self.get_selected_test_items()
|
selected_items = self.get_selected_test_items()
|
||||||
current_test_type = self.config.current_test_type
|
current_test_type = self.config.current_test_type
|
||||||
@@ -718,7 +724,7 @@ _GAMUT_REF_CONFIGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _on_gamut_ref_changed(self, test_type, event=None):
|
def _on_gamut_ref_changed(self: "PQAutomationApp", test_type, event=None):
|
||||||
cfg = _GAMUT_REF_CONFIGS[test_type]
|
cfg = _GAMUT_REF_CONFIGS[test_type]
|
||||||
try:
|
try:
|
||||||
new_ref = getattr(self, cfg["var_attr"]).get()
|
new_ref = getattr(self, cfg["var_attr"]).get()
|
||||||
@@ -732,13 +738,38 @@ def _on_gamut_ref_changed(self, test_type, event=None):
|
|||||||
self.log_gui.log(f"保存 {cfg['label']} 色域参考标准失败: {str(e)}", level="error")
|
self.log_gui.log(f"保存 {cfg['label']} 色域参考标准失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
def on_screen_gamut_ref_changed(self, event=None):
|
def on_screen_gamut_ref_changed(self: "PQAutomationApp", event=None):
|
||||||
_on_gamut_ref_changed(self, "screen_module", event)
|
_on_gamut_ref_changed(self, "screen_module", event)
|
||||||
|
|
||||||
|
|
||||||
def on_sdr_gamut_ref_changed(self, event=None):
|
def on_sdr_gamut_ref_changed(self: "PQAutomationApp", event=None):
|
||||||
_on_gamut_ref_changed(self, "sdr_movie", event)
|
_on_gamut_ref_changed(self, "sdr_movie", event)
|
||||||
|
|
||||||
|
|
||||||
def on_hdr_gamut_ref_changed(self, event=None):
|
def on_hdr_gamut_ref_changed(self: "PQAutomationApp", event=None):
|
||||||
_on_gamut_ref_changed(self, "hdr_movie", event)
|
_on_gamut_ref_changed(self, "hdr_movie", event)
|
||||||
|
|
||||||
|
|
||||||
|
class CctPanelMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
create_cct_params_frame = create_cct_params_frame
|
||||||
|
_get_cct_var_dict = _get_cct_var_dict
|
||||||
|
_parse_cct_float = _parse_cct_float
|
||||||
|
_save_cct_params_for = _save_cct_params_for
|
||||||
|
_handle_cct_focus_out = _handle_cct_focus_out
|
||||||
|
on_sdr_cct_param_focus_out = on_sdr_cct_param_focus_out
|
||||||
|
save_sdr_cct_params = save_sdr_cct_params
|
||||||
|
on_hdr_cct_param_focus_out = on_hdr_cct_param_focus_out
|
||||||
|
save_hdr_cct_params = save_hdr_cct_params
|
||||||
|
recalculate_cct = recalculate_cct
|
||||||
|
recalculate_gamut = recalculate_gamut
|
||||||
|
on_cct_param_focus_out = on_cct_param_focus_out
|
||||||
|
save_cct_params = save_cct_params
|
||||||
|
reload_cct_params = reload_cct_params
|
||||||
|
toggle_cct_params_frame = toggle_cct_params_frame
|
||||||
|
_on_gamut_ref_changed = _on_gamut_ref_changed
|
||||||
|
on_screen_gamut_ref_changed = on_screen_gamut_ref_changed
|
||||||
|
on_sdr_gamut_ref_changed = on_sdr_gamut_ref_changed
|
||||||
|
on_hdr_gamut_ref_changed = on_hdr_gamut_ref_changed
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""自定义模板结果面板(Step 6 重构)。"""
|
"""自定义模板结果面板(Step 6 重构)。"""
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -11,7 +11,13 @@ import numpy as np
|
|||||||
|
|
||||||
from app.data_range_converter import convert_pattern_params
|
from app.data_range_converter import convert_pattern_params
|
||||||
|
|
||||||
def create_custom_template_result_panel(self):
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
def create_custom_template_result_panel(self: "PQAutomationApp"):
|
||||||
"""创建客户模板结果显示区域(黑底表格)"""
|
"""创建客户模板结果显示区域(黑底表格)"""
|
||||||
self.custom_result_frame = ttk.LabelFrame(
|
self.custom_result_frame = ttk.LabelFrame(
|
||||||
self.custom_template_tab_frame, text="客户模板结果显示"
|
self.custom_template_tab_frame, text="客户模板结果显示"
|
||||||
@@ -151,7 +157,7 @@ def create_custom_template_result_panel(self):
|
|||||||
table_container.grid_columnconfigure(0, weight=1)
|
table_container.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
|
||||||
def show_custom_result_context_menu(self, event):
|
def show_custom_result_context_menu(self: "PQAutomationApp", event):
|
||||||
"""显示客户模板结果右键菜单"""
|
"""显示客户模板结果右键菜单"""
|
||||||
if not hasattr(self, "custom_result_tree") or not hasattr(
|
if not hasattr(self, "custom_result_tree") or not hasattr(
|
||||||
self, "custom_result_menu"
|
self, "custom_result_menu"
|
||||||
@@ -197,7 +203,7 @@ def show_custom_result_context_menu(self, event):
|
|||||||
self.custom_result_menu.grab_release()
|
self.custom_result_menu.grab_release()
|
||||||
|
|
||||||
|
|
||||||
def set_custom_result_table_locked(self, locked):
|
def set_custom_result_table_locked(self: "PQAutomationApp", locked):
|
||||||
"""锁定/解锁客户模板结果表(测试期间禁选择、禁右键)"""
|
"""锁定/解锁客户模板结果表(测试期间禁选择、禁右键)"""
|
||||||
if not hasattr(self, "custom_result_tree"):
|
if not hasattr(self, "custom_result_tree"):
|
||||||
return
|
return
|
||||||
@@ -208,7 +214,7 @@ def set_custom_result_table_locked(self, locked):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def start_custom_row_single_step(self):
|
def start_custom_row_single_step(self: "PQAutomationApp"):
|
||||||
"""单步测试当前选中行:发送该行 pattern 并覆盖该行测量结果"""
|
"""单步测试当前选中行:发送该行 pattern 并覆盖该行测量结果"""
|
||||||
if not hasattr(self, "custom_result_tree"):
|
if not hasattr(self, "custom_result_tree"):
|
||||||
return
|
return
|
||||||
@@ -252,7 +258,7 @@ def start_custom_row_single_step(self):
|
|||||||
).start()
|
).start()
|
||||||
|
|
||||||
|
|
||||||
def _clear_custom_result_row(self, item_id, row_no):
|
def _clear_custom_result_row(self: "PQAutomationApp", item_id, row_no):
|
||||||
"""单步测试开始前清空指定行的测量数据"""
|
"""单步测试开始前清空指定行的测量数据"""
|
||||||
if not hasattr(self, "custom_result_tree"):
|
if not hasattr(self, "custom_result_tree"):
|
||||||
return
|
return
|
||||||
@@ -281,7 +287,7 @@ def _clear_custom_result_row(self, item_id, row_no):
|
|||||||
self.custom_result_tree.see(item_id)
|
self.custom_result_tree.see(item_id)
|
||||||
|
|
||||||
|
|
||||||
def _run_custom_row_single_step(self, item_id, row_no):
|
def _run_custom_row_single_step(self: "PQAutomationApp", item_id, row_no):
|
||||||
"""后台执行客户模板单步测试"""
|
"""后台执行客户模板单步测试"""
|
||||||
try:
|
try:
|
||||||
self._dispatch_ui(self.status_var.set, f"单步测试第 {row_no} 行...")
|
self._dispatch_ui(self.status_var.set, f"单步测试第 {row_no} 行...")
|
||||||
@@ -352,7 +358,7 @@ def _run_custom_row_single_step(self, item_id, row_no):
|
|||||||
self._dispatch_ui(self.status_var.set, "单步测试失败")
|
self._dispatch_ui(self.status_var.set, "单步测试失败")
|
||||||
|
|
||||||
|
|
||||||
def _update_custom_result_row(self, item_id, row_no, result_data):
|
def _update_custom_result_row(self: "PQAutomationApp", item_id, row_no, result_data):
|
||||||
"""覆盖更新客户模板结果表中指定行"""
|
"""覆盖更新客户模板结果表中指定行"""
|
||||||
|
|
||||||
def fmt(value, digits=4):
|
def fmt(value, digits=4):
|
||||||
@@ -394,7 +400,7 @@ def _update_custom_result_row(self, item_id, row_no, result_data):
|
|||||||
self.custom_result_tree.item(item_id, values=new_values)
|
self.custom_result_tree.item(item_id, values=new_values)
|
||||||
|
|
||||||
|
|
||||||
def copy_custom_result_table(self):
|
def copy_custom_result_table(self: "PQAutomationApp"):
|
||||||
"""复制客户模板结果表格到剪贴板(不含标题行/No./Pattern)"""
|
"""复制客户模板结果表格到剪贴板(不含标题行/No./Pattern)"""
|
||||||
if not hasattr(self, "custom_result_tree"):
|
if not hasattr(self, "custom_result_tree"):
|
||||||
return
|
return
|
||||||
@@ -434,7 +440,7 @@ def copy_custom_result_table(self):
|
|||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"已复制客户模板表格数据({len(items)} 行)", level="success")
|
self.log_gui.log(f"已复制客户模板表格数据({len(items)} 行)", level="success")
|
||||||
|
|
||||||
def clear_custom_template_results(self):
|
def clear_custom_template_results(self: "PQAutomationApp"):
|
||||||
"""清空客户模板结果表格"""
|
"""清空客户模板结果表格"""
|
||||||
if not hasattr(self, "custom_result_tree"):
|
if not hasattr(self, "custom_result_tree"):
|
||||||
return
|
return
|
||||||
@@ -442,7 +448,7 @@ def clear_custom_template_results(self):
|
|||||||
self.custom_result_tree.delete(item)
|
self.custom_result_tree.delete(item)
|
||||||
|
|
||||||
|
|
||||||
def auto_expand_custom_result_view(self):
|
def auto_expand_custom_result_view(self: "PQAutomationApp"):
|
||||||
"""当客户模板表格有数据时,自动扩展窗口以尽量完整显示所有列"""
|
"""当客户模板表格有数据时,自动扩展窗口以尽量完整显示所有列"""
|
||||||
if not hasattr(self, "custom_result_tree"):
|
if not hasattr(self, "custom_result_tree"):
|
||||||
return
|
return
|
||||||
@@ -480,7 +486,7 @@ def auto_expand_custom_result_view(self):
|
|||||||
self.log_gui.log(f"自动扩展客户模板窗口失败: {str(e)}", level="error")
|
self.log_gui.log(f"自动扩展客户模板窗口失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
def append_custom_template_result(self, row_no, result_data):
|
def append_custom_template_result(self: "PQAutomationApp", row_no, result_data):
|
||||||
"""追加一条客户模板结果到表格"""
|
"""追加一条客户模板结果到表格"""
|
||||||
|
|
||||||
def fmt(value, digits=4):
|
def fmt(value, digits=4):
|
||||||
@@ -523,7 +529,7 @@ def append_custom_template_result(self, row_no, result_data):
|
|||||||
self.auto_expand_custom_result_view()
|
self.auto_expand_custom_result_view()
|
||||||
|
|
||||||
|
|
||||||
def start_custom_template_test(self):
|
def start_custom_template_test(self: "PQAutomationApp"):
|
||||||
"""开始客户模板测试(SDR)"""
|
"""开始客户模板测试(SDR)"""
|
||||||
|
|
||||||
if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
|
if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
|
||||||
@@ -571,7 +577,7 @@ def start_custom_template_test(self):
|
|||||||
self.test_thread.daemon = True
|
self.test_thread.daemon = True
|
||||||
self.test_thread.start()
|
self.test_thread.start()
|
||||||
|
|
||||||
def update_custom_button_visibility(self):
|
def update_custom_button_visibility(self: "PQAutomationApp"):
|
||||||
"""只在 SDR 测试时显示客户模版按钮"""
|
"""只在 SDR 测试时显示客户模版按钮"""
|
||||||
if not hasattr(self, "custom_btn") or not hasattr(self, "test_type_var"):
|
if not hasattr(self, "custom_btn") or not hasattr(self, "test_type_var"):
|
||||||
return
|
return
|
||||||
@@ -625,7 +631,7 @@ def update_custom_button_visibility(self):
|
|||||||
# self.log_gui.log("已填充 147 行客户模板测试数据", level="success")
|
# self.log_gui.log("已填充 147 行客户模板测试数据", level="success")
|
||||||
|
|
||||||
|
|
||||||
def export_custom_template_excel(self):
|
def export_custom_template_excel(self: "PQAutomationApp"):
|
||||||
"""将客户模板结果表导出为 Excel 文件(14 列完整数据)"""
|
"""将客户模板结果表导出为 Excel 文件(14 列完整数据)"""
|
||||||
if not hasattr(self, "custom_result_tree"):
|
if not hasattr(self, "custom_result_tree"):
|
||||||
return
|
return
|
||||||
@@ -773,7 +779,7 @@ def export_custom_template_excel(self):
|
|||||||
messagebox.showerror("错误", f"导出失败:{str(e)}")
|
messagebox.showerror("错误", f"导出失败:{str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def export_custom_template_charts(self):
|
def export_custom_template_charts(self: "PQAutomationApp"):
|
||||||
"""生成客户模板图表:xy 色度散点图 + Lv 亮度曲线图,保存为 PNG"""
|
"""生成客户模板图表:xy 色度散点图 + Lv 亮度曲线图,保存为 PNG"""
|
||||||
if not hasattr(self, "custom_result_tree"):
|
if not hasattr(self, "custom_result_tree"):
|
||||||
return
|
return
|
||||||
@@ -910,3 +916,24 @@ def export_custom_template_charts(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")
|
||||||
messagebox.showerror("错误", f"生成图表失败:{str(e)}")
|
messagebox.showerror("错误", f"生成图表失败:{str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
class CustomTemplatePanelMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
create_custom_template_result_panel = create_custom_template_result_panel
|
||||||
|
show_custom_result_context_menu = show_custom_result_context_menu
|
||||||
|
set_custom_result_table_locked = set_custom_result_table_locked
|
||||||
|
start_custom_row_single_step = start_custom_row_single_step
|
||||||
|
_clear_custom_result_row = _clear_custom_result_row
|
||||||
|
_run_custom_row_single_step = _run_custom_row_single_step
|
||||||
|
_update_custom_result_row = _update_custom_result_row
|
||||||
|
copy_custom_result_table = copy_custom_result_table
|
||||||
|
clear_custom_template_results = clear_custom_template_results
|
||||||
|
auto_expand_custom_result_view = auto_expand_custom_result_view
|
||||||
|
append_custom_template_result = append_custom_template_result
|
||||||
|
start_custom_template_test = start_custom_template_test
|
||||||
|
update_custom_button_visibility = update_custom_button_visibility
|
||||||
|
export_custom_template_excel = export_custom_template_excel
|
||||||
|
export_custom_template_charts = export_custom_template_charts
|
||||||
|
|||||||
1075
app/views/panels/gamma_pattern_panel.py
Normal file
1075
app/views/panels/gamma_pattern_panel.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
"""主布局面板创建函数(Step 6 重构)。"""
|
"""主布局面板创建函数(Step 6 重构)。"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
@@ -8,7 +8,13 @@ from drivers.UCD323_Enum import UCDEnum
|
|||||||
from app.views.collapsing_frame import CollapsingFrame
|
from app.views.collapsing_frame import CollapsingFrame
|
||||||
from app.resources import load_icon
|
from app.resources import load_icon
|
||||||
|
|
||||||
def create_floating_config_panel(self):
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
def create_floating_config_panel(self: "PQAutomationApp"):
|
||||||
"""创建右上角悬浮配置框"""
|
"""创建右上角悬浮配置框"""
|
||||||
cf = CollapsingFrame(self.control_frame_top)
|
cf = CollapsingFrame(self.control_frame_top)
|
||||||
cf.pack(fill="both")
|
cf.pack(fill="both")
|
||||||
@@ -53,7 +59,7 @@ def create_floating_config_panel(self):
|
|||||||
self.config_panel_frame.btn.configure(image="closed")
|
self.config_panel_frame.btn.configure(image="closed")
|
||||||
|
|
||||||
|
|
||||||
def create_test_items_content(self):
|
def create_test_items_content(self: "PQAutomationApp"):
|
||||||
"""创建测试项目选项卡内容"""
|
"""创建测试项目选项卡内容"""
|
||||||
# 创建测试项目字典,用于管理不同测试类型的选项
|
# 创建测试项目字典,用于管理不同测试类型的选项
|
||||||
self.test_items = {
|
self.test_items = {
|
||||||
@@ -96,7 +102,7 @@ def create_test_items_content(self):
|
|||||||
self.create_cct_params_frame()
|
self.create_cct_params_frame()
|
||||||
|
|
||||||
|
|
||||||
def create_signal_format_content(self):
|
def create_signal_format_content(self: "PQAutomationApp"):
|
||||||
"""创建信号格式选项卡内容"""
|
"""创建信号格式选项卡内容"""
|
||||||
self.signal_tabs = ttk.Notebook(self.signal_format_frame)
|
self.signal_tabs = ttk.Notebook(self.signal_format_frame)
|
||||||
self.signal_tabs.pack(fill=tk.BOTH, expand=True)
|
self.signal_tabs.pack(fill=tk.BOTH, expand=True)
|
||||||
@@ -314,7 +320,7 @@ def create_signal_format_content(self):
|
|||||||
self.signal_tabs.tab(2, state="disabled") # 禁用 HDR
|
self.signal_tabs.tab(2, state="disabled") # 禁用 HDR
|
||||||
|
|
||||||
|
|
||||||
def create_connection_content(self):
|
def create_connection_content(self: "PQAutomationApp"):
|
||||||
"""创建设备连接区域"""
|
"""创建设备连接区域"""
|
||||||
# 创建设备连接区域的主框架
|
# 创建设备连接区域的主框架
|
||||||
com_frame = ttk.Frame(self.connection_frame)
|
com_frame = ttk.Frame(self.connection_frame)
|
||||||
@@ -424,7 +430,7 @@ def create_connection_content(self):
|
|||||||
ca_channel_combo.bind("<<ComboboxSelected>>", self.update_config)
|
ca_channel_combo.bind("<<ComboboxSelected>>", self.update_config)
|
||||||
|
|
||||||
|
|
||||||
def create_test_type_frame(self):
|
def create_test_type_frame(self: "PQAutomationApp"):
|
||||||
"""创建测试类型选择区域(侧边栏形式)"""
|
"""创建测试类型选择区域(侧边栏形式)"""
|
||||||
# 设置测试类型变量
|
# 设置测试类型变量
|
||||||
self.test_type_var = tk.StringVar(value="screen_module")
|
self.test_type_var = tk.StringVar(value="screen_module")
|
||||||
@@ -503,6 +509,26 @@ def create_test_type_frame(self):
|
|||||||
)
|
)
|
||||||
self.pantone_baseline_btn.pack(fill=tk.X, padx=0, pady=1)
|
self.pantone_baseline_btn.pack(fill=tk.X, padx=0, pady=1)
|
||||||
|
|
||||||
|
# Gamma 测试图案配置按钮
|
||||||
|
self.gamma_pattern_btn = ttk.Button(
|
||||||
|
self.sidebar_frame,
|
||||||
|
text="Gamma 图案配置",
|
||||||
|
style="Sidebar.TButton",
|
||||||
|
command=self.toggle_gamma_pattern_panel,
|
||||||
|
takefocus=False,
|
||||||
|
)
|
||||||
|
self.gamma_pattern_btn.pack(fill=tk.X, padx=0, pady=1)
|
||||||
|
|
||||||
|
# CALMAN 风格灰阶测试按钮
|
||||||
|
self.calman_btn = ttk.Button(
|
||||||
|
self.sidebar_frame,
|
||||||
|
text="CALMAN 灰阶",
|
||||||
|
style="Sidebar.TButton",
|
||||||
|
command=self.toggle_calman_panel,
|
||||||
|
takefocus=False,
|
||||||
|
)
|
||||||
|
self.calman_btn.pack(fill=tk.X, padx=0, pady=1)
|
||||||
|
|
||||||
# 测试版水印标签(版本 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
|
||||||
if is_beta_version():
|
if is_beta_version():
|
||||||
@@ -528,9 +554,13 @@ def create_test_type_frame(self):
|
|||||||
self.panels["single_step"]["button"] = self.single_step_btn
|
self.panels["single_step"]["button"] = self.single_step_btn
|
||||||
if "pantone_baseline" in self.panels:
|
if "pantone_baseline" in self.panels:
|
||||||
self.panels["pantone_baseline"]["button"] = self.pantone_baseline_btn
|
self.panels["pantone_baseline"]["button"] = self.pantone_baseline_btn
|
||||||
|
if "gamma_pattern" in self.panels:
|
||||||
|
self.panels["gamma_pattern"]["button"] = self.gamma_pattern_btn
|
||||||
|
if "calman" in self.panels:
|
||||||
|
self.panels["calman"]["button"] = self.calman_btn
|
||||||
|
|
||||||
|
|
||||||
def update_config_info_display(self):
|
def update_config_info_display(self: "PQAutomationApp"):
|
||||||
"""更新配置信息显示"""
|
"""更新配置信息显示"""
|
||||||
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
|
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
|
||||||
current_config = self.config.get_current_config()
|
current_config = self.config.get_current_config()
|
||||||
@@ -547,7 +577,7 @@ def update_config_info_display(self):
|
|||||||
self.update_sidebar_selection()
|
self.update_sidebar_selection()
|
||||||
|
|
||||||
|
|
||||||
def create_operation_frame(self):
|
def create_operation_frame(self: "PQAutomationApp"):
|
||||||
"""创建操作按钮区域"""
|
"""创建操作按钮区域"""
|
||||||
operation_frame = ttk.Frame(self.control_frame_top)
|
operation_frame = ttk.Frame(self.control_frame_top)
|
||||||
operation_frame.pack(fill=tk.X, padx=5, pady=10)
|
operation_frame.pack(fill=tk.X, padx=5, pady=10)
|
||||||
@@ -594,7 +624,7 @@ def create_operation_frame(self):
|
|||||||
self.update_custom_button_visibility()
|
self.update_custom_button_visibility()
|
||||||
|
|
||||||
|
|
||||||
def on_screen_module_timing_changed(self, event=None):
|
def on_screen_module_timing_changed(self: "PQAutomationApp", event=None):
|
||||||
"""屏模组信号格式改变时的回调"""
|
"""屏模组信号格式改变时的回调"""
|
||||||
try:
|
try:
|
||||||
selected_timing = self.screen_module_timing_var.get()
|
selected_timing = self.screen_module_timing_var.get()
|
||||||
@@ -632,7 +662,7 @@ def on_screen_module_timing_changed(self, event=None):
|
|||||||
self.log_gui.log(f"屏模组信号格式更改失败: {str(e)}", level="error")
|
self.log_gui.log(f"屏模组信号格式更改失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
def on_sdr_timing_changed(self, event=None):
|
def on_sdr_timing_changed(self: "PQAutomationApp", event=None):
|
||||||
"""SDR测试分辨率改变时的回调"""
|
"""SDR测试分辨率改变时的回调"""
|
||||||
try:
|
try:
|
||||||
selected_timing = self.sdr_timing_var.get()
|
selected_timing = self.sdr_timing_var.get()
|
||||||
@@ -650,7 +680,7 @@ def on_sdr_timing_changed(self, event=None):
|
|||||||
self.log_gui.log(f"SDR测试分辨率更改失败: {str(e)}", level="error")
|
self.log_gui.log(f"SDR测试分辨率更改失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
def on_sdr_output_format_changed(self, event=None):
|
def on_sdr_output_format_changed(self: "PQAutomationApp", event=None):
|
||||||
"""SDR 色彩格式改变时的回调"""
|
"""SDR 色彩格式改变时的回调"""
|
||||||
try:
|
try:
|
||||||
fmt = self.sdr_output_format_var.get()
|
fmt = self.sdr_output_format_var.get()
|
||||||
@@ -674,7 +704,7 @@ def on_sdr_output_format_changed(self, event=None):
|
|||||||
self.log_gui.log(f"SDR色彩格式更改失败: {str(e)}", level="error")
|
self.log_gui.log(f"SDR色彩格式更改失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
def on_hdr_output_format_changed(self, event=None):
|
def on_hdr_output_format_changed(self: "PQAutomationApp", event=None):
|
||||||
"""HDR 色彩格式改变时的回调"""
|
"""HDR 色彩格式改变时的回调"""
|
||||||
try:
|
try:
|
||||||
fmt = self.hdr_output_format_var.get()
|
fmt = self.hdr_output_format_var.get()
|
||||||
@@ -700,7 +730,7 @@ def on_hdr_output_format_changed(self, event=None):
|
|||||||
self.log_gui.log(f"HDR色彩格式更改失败: {str(e)}", level="error")
|
self.log_gui.log(f"HDR色彩格式更改失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
def update_test_items(self):
|
def update_test_items(self: "PQAutomationApp"):
|
||||||
"""根据当前测试类型更新测试项目复选框"""
|
"""根据当前测试类型更新测试项目复选框"""
|
||||||
# 先隐藏所有测试项目框架
|
# 先隐藏所有测试项目框架
|
||||||
for config in self.test_items.values():
|
for config in self.test_items.values():
|
||||||
@@ -747,7 +777,7 @@ def update_test_items(self):
|
|||||||
self.toggle_cct_params_frame()
|
self.toggle_cct_params_frame()
|
||||||
|
|
||||||
|
|
||||||
def on_test_type_change(self):
|
def on_test_type_change(self: "PQAutomationApp"):
|
||||||
"""根据测试类型更新内容区域"""
|
"""根据测试类型更新内容区域"""
|
||||||
# 更新配置信息显示
|
# 更新配置信息显示
|
||||||
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
|
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
|
||||||
@@ -756,3 +786,22 @@ def on_test_type_change(self):
|
|||||||
# SDR 选中时显示客户模版按钮
|
# SDR 选中时显示客户模版按钮
|
||||||
self.update_custom_button_visibility()
|
self.update_custom_button_visibility()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MainLayoutMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
create_floating_config_panel = create_floating_config_panel
|
||||||
|
create_test_items_content = create_test_items_content
|
||||||
|
create_signal_format_content = create_signal_format_content
|
||||||
|
create_connection_content = create_connection_content
|
||||||
|
create_test_type_frame = create_test_type_frame
|
||||||
|
update_config_info_display = update_config_info_display
|
||||||
|
create_operation_frame = create_operation_frame
|
||||||
|
on_screen_module_timing_changed = on_screen_module_timing_changed
|
||||||
|
on_sdr_timing_changed = on_sdr_timing_changed
|
||||||
|
on_sdr_output_format_changed = on_sdr_output_format_changed
|
||||||
|
on_hdr_output_format_changed = on_hdr_output_format_changed
|
||||||
|
update_test_items = update_test_items
|
||||||
|
on_test_type_change = on_test_type_change
|
||||||
|
|||||||
@@ -11,11 +11,17 @@ from tkinter import filedialog, messagebox
|
|||||||
|
|
||||||
import ttkbootstrap as ttk
|
import ttkbootstrap as ttk
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_TEMPLATE_FILE = "pantone_2670_colors.xlsx"
|
_TEMPLATE_FILE = "pantone_2670_colors.xlsx"
|
||||||
|
|
||||||
|
|
||||||
def create_pantone_baseline_panel(self):
|
def create_pantone_baseline_panel(self: "PQAutomationApp"):
|
||||||
"""创建 Pantone 认证摸底测试面板。"""
|
"""创建 Pantone 认证摸底测试面板。"""
|
||||||
frame = ttk.Frame(self.content_frame)
|
frame = ttk.Frame(self.content_frame)
|
||||||
self.pantone_baseline_frame = frame
|
self.pantone_baseline_frame = frame
|
||||||
@@ -149,12 +155,12 @@ def create_pantone_baseline_panel(self):
|
|||||||
_set_button_states(self)
|
_set_button_states(self)
|
||||||
|
|
||||||
|
|
||||||
def toggle_pantone_baseline_panel(self):
|
def toggle_pantone_baseline_panel(self: "PQAutomationApp"):
|
||||||
"""切换 Pantone 认证摸底测试面板。"""
|
"""切换 Pantone 认证摸底测试面板。"""
|
||||||
self.show_panel("pantone_baseline")
|
self.show_panel("pantone_baseline")
|
||||||
|
|
||||||
|
|
||||||
def _get_settings_dir(self):
|
def _get_settings_dir(self: "PQAutomationApp"):
|
||||||
"""返回 settings 绝对目录,避免依赖当前工作目录。"""
|
"""返回 settings 绝对目录,避免依赖当前工作目录。"""
|
||||||
if getattr(self, "config_file", None):
|
if getattr(self, "config_file", None):
|
||||||
return os.path.dirname(self.config_file)
|
return os.path.dirname(self.config_file)
|
||||||
@@ -168,7 +174,7 @@ def _get_settings_dir(self):
|
|||||||
return os.path.join(base_dir, "settings")
|
return os.path.join(base_dir, "settings")
|
||||||
|
|
||||||
|
|
||||||
def _load_patterns(self):
|
def _load_patterns(self: "PQAutomationApp"):
|
||||||
path = os.path.join(_get_settings_dir(self), _TEMPLATE_FILE)
|
path = os.path.join(_get_settings_dir(self), _TEMPLATE_FILE)
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
raise FileNotFoundError(f"未找到模板文件: {path}")
|
raise FileNotFoundError(f"未找到模板文件: {path}")
|
||||||
@@ -201,7 +207,7 @@ def _load_patterns(self):
|
|||||||
return patterns
|
return patterns
|
||||||
|
|
||||||
|
|
||||||
def _start_pantone_baseline(self):
|
def _start_pantone_baseline(self: "PQAutomationApp"):
|
||||||
if self._pantone_running:
|
if self._pantone_running:
|
||||||
messagebox.showinfo("提示", "Pantone 任务正在执行")
|
messagebox.showinfo("提示", "Pantone 任务正在执行")
|
||||||
return
|
return
|
||||||
@@ -247,7 +253,7 @@ def _start_pantone_baseline(self):
|
|||||||
_launch_worker(self, start_index=0, settle=settle)
|
_launch_worker(self, start_index=0, settle=settle)
|
||||||
|
|
||||||
|
|
||||||
def _resume_pantone_baseline(self):
|
def _resume_pantone_baseline(self: "PQAutomationApp"):
|
||||||
if self._pantone_running:
|
if self._pantone_running:
|
||||||
messagebox.showinfo("提示", "Pantone 任务正在执行")
|
messagebox.showinfo("提示", "Pantone 任务正在执行")
|
||||||
return
|
return
|
||||||
@@ -291,7 +297,7 @@ def _resume_pantone_baseline(self):
|
|||||||
_launch_worker(self, start_index=self._pantone_next_index, settle=settle)
|
_launch_worker(self, start_index=self._pantone_next_index, settle=settle)
|
||||||
|
|
||||||
|
|
||||||
def _launch_worker(self, start_index, settle):
|
def _launch_worker(self: "PQAutomationApp", start_index, settle):
|
||||||
total = self._pantone_target_count or len(self.pantone_patterns)
|
total = self._pantone_target_count or len(self.pantone_patterns)
|
||||||
|
|
||||||
def worker():
|
def worker():
|
||||||
@@ -401,7 +407,7 @@ def _launch_worker(self, start_index, settle):
|
|||||||
threading.Thread(target=worker, daemon=True).start()
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def _append_result_row(self, record, total):
|
def _append_result_row(self: "PQAutomationApp", record, total):
|
||||||
self.pantone_tree.insert(
|
self.pantone_tree.insert(
|
||||||
"",
|
"",
|
||||||
tk.END,
|
tk.END,
|
||||||
@@ -423,7 +429,7 @@ def _append_result_row(self, record, total):
|
|||||||
self.pantone_tree.see(children[-1])
|
self.pantone_tree.see(children[-1])
|
||||||
|
|
||||||
|
|
||||||
def _pause_pantone_baseline(self):
|
def _pause_pantone_baseline(self: "PQAutomationApp"):
|
||||||
if not self._pantone_running:
|
if not self._pantone_running:
|
||||||
messagebox.showinfo("提示", "当前没有运行中的任务")
|
messagebox.showinfo("提示", "当前没有运行中的任务")
|
||||||
return
|
return
|
||||||
@@ -433,7 +439,7 @@ def _pause_pantone_baseline(self):
|
|||||||
self._pantone_control_event.set()
|
self._pantone_control_event.set()
|
||||||
|
|
||||||
|
|
||||||
def _end_pantone_baseline(self):
|
def _end_pantone_baseline(self: "PQAutomationApp"):
|
||||||
if self._pantone_running:
|
if self._pantone_running:
|
||||||
self._pantone_stop_requested = True
|
self._pantone_stop_requested = True
|
||||||
self.pantone_status_var.set("结束中...")
|
self.pantone_status_var.set("结束中...")
|
||||||
@@ -448,7 +454,7 @@ def _end_pantone_baseline(self):
|
|||||||
_set_button_states(self)
|
_set_button_states(self)
|
||||||
|
|
||||||
|
|
||||||
def _clear_results(self):
|
def _clear_results(self: "PQAutomationApp"):
|
||||||
if self._pantone_running:
|
if self._pantone_running:
|
||||||
messagebox.showinfo("提示", "任务执行中,无法清空")
|
messagebox.showinfo("提示", "任务执行中,无法清空")
|
||||||
return
|
return
|
||||||
@@ -463,7 +469,7 @@ def _clear_results(self):
|
|||||||
_set_button_states(self)
|
_set_button_states(self)
|
||||||
|
|
||||||
|
|
||||||
def _set_button_states(self):
|
def _set_button_states(self: "PQAutomationApp"):
|
||||||
if self._pantone_running:
|
if self._pantone_running:
|
||||||
self.pantone_start_btn.configure(state=tk.DISABLED)
|
self.pantone_start_btn.configure(state=tk.DISABLED)
|
||||||
self.pantone_pause_btn.configure(state=tk.NORMAL)
|
self.pantone_pause_btn.configure(state=tk.NORMAL)
|
||||||
@@ -479,7 +485,7 @@ def _set_button_states(self):
|
|||||||
self.pantone_resume_btn.configure(state=tk.NORMAL if can_resume else tk.DISABLED)
|
self.pantone_resume_btn.configure(state=tk.NORMAL if can_resume else tk.DISABLED)
|
||||||
|
|
||||||
|
|
||||||
def _save_as_template(self):
|
def _save_as_template(self: "PQAutomationApp"):
|
||||||
if not self.pantone_results:
|
if not self.pantone_results:
|
||||||
messagebox.showinfo("提示", "暂无可导出的结果")
|
messagebox.showinfo("提示", "暂无可导出的结果")
|
||||||
return
|
return
|
||||||
@@ -502,7 +508,7 @@ def _save_as_template(self):
|
|||||||
messagebox.showerror("保存失败", f"写入 xlsx 失败: {exc}")
|
messagebox.showerror("保存失败", f"写入 xlsx 失败: {exc}")
|
||||||
|
|
||||||
|
|
||||||
def _resolve_results_dir(self):
|
def _resolve_results_dir(self: "PQAutomationApp"):
|
||||||
if getattr(self, "config_file", None):
|
if getattr(self, "config_file", None):
|
||||||
root_dir = os.path.dirname(os.path.dirname(self.config_file))
|
root_dir = os.path.dirname(os.path.dirname(self.config_file))
|
||||||
else:
|
else:
|
||||||
@@ -514,7 +520,7 @@ def _resolve_results_dir(self):
|
|||||||
return results_dir
|
return results_dir
|
||||||
|
|
||||||
|
|
||||||
def _auto_save_template(self):
|
def _auto_save_template(self: "PQAutomationApp"):
|
||||||
results_dir = _resolve_results_dir(self)
|
results_dir = _resolve_results_dir(self)
|
||||||
target_count = len(self.pantone_results)
|
target_count = len(self.pantone_results)
|
||||||
filename = (
|
filename = (
|
||||||
@@ -526,7 +532,7 @@ def _auto_save_template(self):
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def _write_template_xlsx(self, path):
|
def _write_template_xlsx(self: "PQAutomationApp", path):
|
||||||
# 优先复制 settings 模板,再覆盖数据区;没有模板时自动创建同结构表。
|
# 优先复制 settings 模板,再覆盖数据区;没有模板时自动创建同结构表。
|
||||||
template_path = os.path.join(_get_settings_dir(self), _TEMPLATE_FILE)
|
template_path = os.path.join(_get_settings_dir(self), _TEMPLATE_FILE)
|
||||||
from openpyxl import load_workbook, Workbook
|
from openpyxl import load_workbook, Workbook
|
||||||
@@ -560,3 +566,25 @@ def _write_template_xlsx(self, path):
|
|||||||
ws.cell(row=idx, column=6, value=float(item["y"]))
|
ws.cell(row=idx, column=6, value=float(item["y"]))
|
||||||
|
|
||||||
wb.save(path)
|
wb.save(path)
|
||||||
|
|
||||||
|
|
||||||
|
class PantoneBaselinePanelMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
create_pantone_baseline_panel = create_pantone_baseline_panel
|
||||||
|
toggle_pantone_baseline_panel = toggle_pantone_baseline_panel
|
||||||
|
_get_settings_dir = _get_settings_dir
|
||||||
|
_load_patterns = _load_patterns
|
||||||
|
_start_pantone_baseline = _start_pantone_baseline
|
||||||
|
_resume_pantone_baseline = _resume_pantone_baseline
|
||||||
|
_launch_worker = _launch_worker
|
||||||
|
_append_result_row = _append_result_row
|
||||||
|
_pause_pantone_baseline = _pause_pantone_baseline
|
||||||
|
_end_pantone_baseline = _end_pantone_baseline
|
||||||
|
_clear_results = _clear_results
|
||||||
|
_set_button_states = _set_button_states
|
||||||
|
_save_as_template = _save_as_template
|
||||||
|
_resolve_results_dir = _resolve_results_dir
|
||||||
|
_auto_save_template = _auto_save_template
|
||||||
|
_write_template_xlsx = _write_template_xlsx
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""侧边面板(日志 / Local Dimming / 调试)"""
|
"""侧边面板(日志 / Local Dimming / 调试)"""
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
@@ -7,7 +7,13 @@ import ttkbootstrap as ttk
|
|||||||
from app.views.pq_log_gui import PQLogGUI
|
from app.views.pq_log_gui import PQLogGUI
|
||||||
from app.views.pq_debug_panel import PQDebugPanel
|
from app.views.pq_debug_panel import PQDebugPanel
|
||||||
|
|
||||||
def create_log_panel(self):
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
def create_log_panel(self: "PQAutomationApp"):
|
||||||
"""创建日志面板"""
|
"""创建日志面板"""
|
||||||
self.log_frame = ttk.Frame(self.content_frame)
|
self.log_frame = ttk.Frame(self.content_frame)
|
||||||
self.log_gui = PQLogGUI(self.log_frame)
|
self.log_gui = PQLogGUI(self.log_frame)
|
||||||
@@ -22,7 +28,7 @@ def create_log_panel(self):
|
|||||||
) # button会在后面设置
|
) # button会在后面设置
|
||||||
|
|
||||||
|
|
||||||
def create_local_dimming_panel(self):
|
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.content_frame)
|
||||||
|
|
||||||
@@ -172,12 +178,12 @@ def create_local_dimming_panel(self):
|
|||||||
self.current_ld_percentage = None
|
self.current_ld_percentage = None
|
||||||
|
|
||||||
|
|
||||||
def toggle_local_dimming_panel(self):
|
def toggle_local_dimming_panel(self: "PQAutomationApp"):
|
||||||
"""切换 Local Dimming 面板显示"""
|
"""切换 Local Dimming 面板显示"""
|
||||||
self.show_panel("local_dimming")
|
self.show_panel("local_dimming")
|
||||||
|
|
||||||
|
|
||||||
def toggle_log_panel(self):
|
def toggle_log_panel(self: "PQAutomationApp"):
|
||||||
"""切换日志面板的显示状态"""
|
"""切换日志面板的显示状态"""
|
||||||
self.show_panel("log")
|
self.show_panel("log")
|
||||||
|
|
||||||
@@ -226,7 +232,7 @@ DEBUG_PANEL_CONFIGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _toggle_debug_panel(self, test_type):
|
def _toggle_debug_panel(self: "PQAutomationApp", test_type):
|
||||||
"""打开/关闭对应测试类型的单步调试面板(独立窗口)。"""
|
"""打开/关闭对应测试类型的单步调试面板(独立窗口)。"""
|
||||||
cfg = DEBUG_PANEL_CONFIGS[test_type]
|
cfg = DEBUG_PANEL_CONFIGS[test_type]
|
||||||
win_attr = cfg["window_attr"]
|
win_attr = cfg["window_attr"]
|
||||||
@@ -288,20 +294,20 @@ def _toggle_debug_panel(self, test_type):
|
|||||||
win.update_idletasks()
|
win.update_idletasks()
|
||||||
|
|
||||||
|
|
||||||
def toggle_screen_debug_panel(self):
|
def toggle_screen_debug_panel(self: "PQAutomationApp"):
|
||||||
_toggle_debug_panel(self, "screen_module")
|
_toggle_debug_panel(self, "screen_module")
|
||||||
|
|
||||||
|
|
||||||
def toggle_sdr_debug_panel(self):
|
def toggle_sdr_debug_panel(self: "PQAutomationApp"):
|
||||||
_toggle_debug_panel(self, "sdr_movie")
|
_toggle_debug_panel(self, "sdr_movie")
|
||||||
|
|
||||||
|
|
||||||
def toggle_hdr_debug_panel(self):
|
def toggle_hdr_debug_panel(self: "PQAutomationApp"):
|
||||||
_toggle_debug_panel(self, "hdr_movie")
|
_toggle_debug_panel(self, "hdr_movie")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def update_sidebar_selection(self):
|
def update_sidebar_selection(self: "PQAutomationApp"):
|
||||||
"""更新侧边栏按钮的选中状态"""
|
"""更新侧边栏按钮的选中状态"""
|
||||||
# 重置所有按钮样式为默认
|
# 重置所有按钮样式为默认
|
||||||
self.screen_module_btn.configure(style="Sidebar.TButton")
|
self.screen_module_btn.configure(style="Sidebar.TButton")
|
||||||
@@ -316,3 +322,18 @@ def update_sidebar_selection(self):
|
|||||||
self.sdr_movie_btn.configure(style="SidebarSelected.TButton")
|
self.sdr_movie_btn.configure(style="SidebarSelected.TButton")
|
||||||
elif current_type == "hdr_movie":
|
elif current_type == "hdr_movie":
|
||||||
self.hdr_movie_btn.configure(style="SidebarSelected.TButton")
|
self.hdr_movie_btn.configure(style="SidebarSelected.TButton")
|
||||||
|
|
||||||
|
|
||||||
|
class SidePanelsMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
create_log_panel = create_log_panel
|
||||||
|
create_local_dimming_panel = create_local_dimming_panel
|
||||||
|
toggle_local_dimming_panel = toggle_local_dimming_panel
|
||||||
|
toggle_log_panel = toggle_log_panel
|
||||||
|
_toggle_debug_panel = _toggle_debug_panel
|
||||||
|
toggle_screen_debug_panel = toggle_screen_debug_panel
|
||||||
|
toggle_sdr_debug_panel = toggle_sdr_debug_panel
|
||||||
|
toggle_hdr_debug_panel = toggle_hdr_debug_panel
|
||||||
|
update_sidebar_selection = update_sidebar_selection
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ from tkinter import filedialog, messagebox
|
|||||||
import ttkbootstrap as ttk
|
import ttkbootstrap as ttk
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -27,7 +33,7 @@ _DEFAULT_SAMPLES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def create_single_step_panel(self):
|
def create_single_step_panel(self: "PQAutomationApp"):
|
||||||
"""创建单步调试面板。"""
|
"""创建单步调试面板。"""
|
||||||
frame = ttk.Frame(self.content_frame)
|
frame = ttk.Frame(self.content_frame)
|
||||||
self.single_step_frame = frame
|
self.single_step_frame = frame
|
||||||
@@ -246,12 +252,12 @@ def create_single_step_panel(self):
|
|||||||
_load_default_samples(self)
|
_load_default_samples(self)
|
||||||
|
|
||||||
|
|
||||||
def toggle_single_step_panel(self):
|
def toggle_single_step_panel(self: "PQAutomationApp"):
|
||||||
"""切换单步调试面板。"""
|
"""切换单步调试面板。"""
|
||||||
self.show_panel("single_step")
|
self.show_panel("single_step")
|
||||||
|
|
||||||
|
|
||||||
def _load_default_samples(self):
|
def _load_default_samples(self: "PQAutomationApp"):
|
||||||
self.single_step_samples = [dict(item) for item in _DEFAULT_SAMPLES]
|
self.single_step_samples = [dict(item) for item in _DEFAULT_SAMPLES]
|
||||||
_refresh_sample_list(self, select_index=0 if self.single_step_samples else None)
|
_refresh_sample_list(self, select_index=0 if self.single_step_samples else None)
|
||||||
self.single_step_status_var.set(
|
self.single_step_status_var.set(
|
||||||
@@ -259,7 +265,7 @@ def _load_default_samples(self):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _refresh_sample_list(self, select_index=None):
|
def _refresh_sample_list(self: "PQAutomationApp", select_index=None):
|
||||||
self.single_step_listbox.delete(0, tk.END)
|
self.single_step_listbox.delete(0, tk.END)
|
||||||
for sample in self.single_step_samples:
|
for sample in self.single_step_samples:
|
||||||
self.single_step_listbox.insert(
|
self.single_step_listbox.insert(
|
||||||
@@ -280,14 +286,14 @@ def _refresh_sample_list(self, select_index=None):
|
|||||||
self.single_step_status_var.set("样本列表为空")
|
self.single_step_status_var.set("样本列表为空")
|
||||||
|
|
||||||
|
|
||||||
def _on_sample_select(self):
|
def _on_sample_select(self: "PQAutomationApp"):
|
||||||
selection = self.single_step_listbox.curselection()
|
selection = self.single_step_listbox.curselection()
|
||||||
if not selection:
|
if not selection:
|
||||||
return
|
return
|
||||||
_select_sample(self, selection[0])
|
_select_sample(self, selection[0])
|
||||||
|
|
||||||
|
|
||||||
def _select_sample(self, index):
|
def _select_sample(self: "PQAutomationApp", index):
|
||||||
sample = self.single_step_samples[index]
|
sample = self.single_step_samples[index]
|
||||||
self.single_step_current_index = index
|
self.single_step_current_index = index
|
||||||
self.single_step_name_var.set(sample["name"])
|
self.single_step_name_var.set(sample["name"])
|
||||||
@@ -297,7 +303,7 @@ def _select_sample(self, index):
|
|||||||
self.single_step_status_var.set(f"当前样本: {sample['name']}")
|
self.single_step_status_var.set(f"当前样本: {sample['name']}")
|
||||||
|
|
||||||
|
|
||||||
def _import_samples_csv(self):
|
def _import_samples_csv(self: "PQAutomationApp"):
|
||||||
path = filedialog.askopenfilename(
|
path = filedialog.askopenfilename(
|
||||||
title="选择单步调试样本 CSV",
|
title="选择单步调试样本 CSV",
|
||||||
filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")],
|
filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")],
|
||||||
@@ -334,7 +340,7 @@ def _import_samples_csv(self):
|
|||||||
self.log_gui.log(f"单步调试样本已导入: {len(samples)} 条", level="success")
|
self.log_gui.log(f"单步调试样本已导入: {len(samples)} 条", level="success")
|
||||||
|
|
||||||
|
|
||||||
def _delete_current_sample(self):
|
def _delete_current_sample(self: "PQAutomationApp"):
|
||||||
if self.single_step_current_index is None:
|
if self.single_step_current_index is None:
|
||||||
return
|
return
|
||||||
removed = self.single_step_samples.pop(self.single_step_current_index)
|
removed = self.single_step_samples.pop(self.single_step_current_index)
|
||||||
@@ -343,7 +349,7 @@ def _delete_current_sample(self):
|
|||||||
self.single_step_status_var.set(f"已删除样本: {removed['name']}")
|
self.single_step_status_var.set(f"已删除样本: {removed['name']}")
|
||||||
|
|
||||||
|
|
||||||
def _upsert_sample(self):
|
def _upsert_sample(self: "PQAutomationApp"):
|
||||||
try:
|
try:
|
||||||
sample = {
|
sample = {
|
||||||
"name": self.single_step_name_var.get().strip(),
|
"name": self.single_step_name_var.get().strip(),
|
||||||
@@ -387,7 +393,7 @@ def _format_float(value):
|
|||||||
return f"{number:.4f}"
|
return f"{number:.4f}"
|
||||||
|
|
||||||
|
|
||||||
def _build_color_patch(self, hex_value):
|
def _build_color_patch(self: "PQAutomationApp", hex_value):
|
||||||
if not self.signal_service.is_connected:
|
if not self.signal_service.is_connected:
|
||||||
raise RuntimeError("请先连接 UCD323 设备")
|
raise RuntimeError("请先连接 UCD323 设备")
|
||||||
width, height = self.signal_service.current_resolution()
|
width, height = self.signal_service.current_resolution()
|
||||||
@@ -401,7 +407,7 @@ def _build_color_patch(self, hex_value):
|
|||||||
return file_path
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
def _send_current_patch(self):
|
def _send_current_patch(self: "PQAutomationApp"):
|
||||||
if self.single_step_current_index is None:
|
if self.single_step_current_index is None:
|
||||||
messagebox.showinfo("提示", "请先选择一个样本")
|
messagebox.showinfo("提示", "请先选择一个样本")
|
||||||
return
|
return
|
||||||
@@ -428,7 +434,7 @@ def _send_current_patch(self):
|
|||||||
threading.Thread(target=worker, daemon=True).start()
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def _measure_current_sample(self):
|
def _measure_current_sample(self: "PQAutomationApp"):
|
||||||
if self.single_step_current_index is None:
|
if self.single_step_current_index is None:
|
||||||
messagebox.showinfo("提示", "请先选择一个样本")
|
messagebox.showinfo("提示", "请先选择一个样本")
|
||||||
return
|
return
|
||||||
@@ -457,7 +463,7 @@ def _measure_current_sample(self):
|
|||||||
threading.Thread(target=worker, daemon=True).start()
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def _commit_result(self):
|
def _commit_result(self: "PQAutomationApp"):
|
||||||
if self.single_step_current_index is None:
|
if self.single_step_current_index is None:
|
||||||
messagebox.showinfo("提示", "请先选择一个样本")
|
messagebox.showinfo("提示", "请先选择一个样本")
|
||||||
return
|
return
|
||||||
@@ -509,14 +515,14 @@ def _commit_result(self):
|
|||||||
self.single_step_status_var.set(f"已记录结果,ΔE2000={record['delta_e']}")
|
self.single_step_status_var.set(f"已记录结果,ΔE2000={record['delta_e']}")
|
||||||
|
|
||||||
|
|
||||||
def _clear_results(self):
|
def _clear_results(self: "PQAutomationApp"):
|
||||||
self.single_step_results = []
|
self.single_step_results = []
|
||||||
for item in self.single_step_result_tree.get_children():
|
for item in self.single_step_result_tree.get_children():
|
||||||
self.single_step_result_tree.delete(item)
|
self.single_step_result_tree.delete(item)
|
||||||
self.single_step_status_var.set("结果已清空")
|
self.single_step_status_var.set("结果已清空")
|
||||||
|
|
||||||
|
|
||||||
def _export_results_csv(self):
|
def _export_results_csv(self: "PQAutomationApp"):
|
||||||
if not self.single_step_results:
|
if not self.single_step_results:
|
||||||
messagebox.showinfo("提示", "暂无可导出的调试结果")
|
messagebox.showinfo("提示", "暂无可导出的调试结果")
|
||||||
return
|
return
|
||||||
@@ -547,4 +553,25 @@ def _export_results_csv(self):
|
|||||||
self.log_gui.log(f"单步调试结果已导出: {path}", level="success")
|
self.log_gui.log(f"单步调试结果已导出: {path}", level="success")
|
||||||
self.single_step_status_var.set(f"结果已导出: {os.path.basename(path)}")
|
self.single_step_status_var.set(f"结果已导出: {os.path.basename(path)}")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
messagebox.showerror("导出失败", f"写入 CSV 失败: {exc}")
|
messagebox.showerror("导出失败", f"写入 CSV 失败: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
class SingleStepPanelMixin:
|
||||||
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
|
"""
|
||||||
|
create_single_step_panel = create_single_step_panel
|
||||||
|
toggle_single_step_panel = toggle_single_step_panel
|
||||||
|
_load_default_samples = _load_default_samples
|
||||||
|
_refresh_sample_list = _refresh_sample_list
|
||||||
|
_on_sample_select = _on_sample_select
|
||||||
|
_select_sample = _select_sample
|
||||||
|
_import_samples_csv = _import_samples_csv
|
||||||
|
_delete_current_sample = _delete_current_sample
|
||||||
|
_upsert_sample = _upsert_sample
|
||||||
|
_build_color_patch = _build_color_patch
|
||||||
|
_send_current_patch = _send_current_patch
|
||||||
|
_measure_current_sample = _measure_current_sample
|
||||||
|
_commit_result = _commit_result
|
||||||
|
_clear_results = _clear_results
|
||||||
|
_export_results_csv = _export_results_csv
|
||||||
|
|||||||
@@ -19,14 +19,16 @@ from app.export import (
|
|||||||
export_excel_report as _export_excel_report_impl,
|
export_excel_report as _export_excel_report_impl,
|
||||||
EXCEL_EXPORT_CONFIG as _EXCEL_EXPORT_CONFIG,
|
EXCEL_EXPORT_CONFIG as _EXCEL_EXPORT_CONFIG,
|
||||||
)
|
)
|
||||||
from app.views.panels import custom_template_panel as _ctp
|
from app.views.panels.custom_template_panel import CustomTemplatePanelMixin
|
||||||
from app.views.panels import side_panels as _sp
|
from app.views.panels.side_panels import SidePanelsMixin
|
||||||
from app.views.panels import cct_panel as _ccp
|
from app.views.panels.cct_panel import CctPanelMixin
|
||||||
from app.views.panels import main_layout as _main
|
from app.views.panels.main_layout import MainLayoutMixin
|
||||||
from app.views.panels import ai_image_panel as _aip
|
from app.views.panels.ai_image_panel import AIImagePanelMixin
|
||||||
from app.views.panels import single_step_panel as _ssp
|
from app.views.panels.single_step_panel import SingleStepPanelMixin
|
||||||
from app.views.panels import pantone_baseline_panel as _pbp
|
from app.views.panels.pantone_baseline_panel import PantoneBaselinePanelMixin
|
||||||
from app.views import panel_manager as PM
|
from app.views.panels.gamma_pattern_panel import GammaPatternPanelMixin
|
||||||
|
from app.views.panels.calman_panel import CalmanPanelMixin
|
||||||
|
from app.views.panel_manager import PanelManagerMixin
|
||||||
from app.logging_setup import setup_logging, attach_gui_handler
|
from app.logging_setup import setup_logging, attach_gui_handler
|
||||||
|
|
||||||
# Step 0/1 重构:资源工具和纯算法已迁移到 app/ 包,这里重新导入以保持
|
# Step 0/1 重构:资源工具和纯算法已迁移到 app/ 包,这里重新导入以保持
|
||||||
@@ -44,77 +46,46 @@ from app.tests.color_accuracy import (
|
|||||||
from app.tests.eotf import calculate_pq_curve as _calc_pq_curve
|
from app.tests.eotf import calculate_pq_curve as _calc_pq_curve
|
||||||
from app.tests.gamma import calculate_gamma as _calc_gamma
|
from app.tests.gamma import calculate_gamma as _calc_gamma
|
||||||
from app.tests.gamut import calculate_gamut_coverage as _calc_gamut_coverage
|
from app.tests.gamut import calculate_gamut_coverage as _calc_gamut_coverage
|
||||||
from app.plots.plot_accuracy import plot_accuracy as _plot_accuracy
|
from app.plots.plot_accuracy import PlotAccuracyMixin
|
||||||
from app.plots.plot_cct import plot_cct as _plot_cct
|
from app.plots.plot_cct import PlotCctMixin
|
||||||
from app.plots.plot_contrast import plot_contrast as _plot_contrast
|
from app.plots.plot_contrast import PlotContrastMixin
|
||||||
from app.plots.plot_eotf import plot_eotf as _plot_eotf
|
from app.plots.plot_eotf import PlotEotfMixin
|
||||||
from app.plots.plot_gamma import plot_gamma as _plot_gamma
|
from app.plots.plot_gamma import PlotGammaMixin
|
||||||
from app.plots.plot_gamut import plot_gamut as _plot_gamut
|
from app.plots.plot_gamut import PlotGamutMixin
|
||||||
from app.views.chart_frame import (
|
from app.views.chart_frame import ChartFrameMixin
|
||||||
clear_chart as _cf_clear_chart,
|
from app.config_io import ConfigIOMixin
|
||||||
create_result_chart_frame as _cf_create_result_chart_frame,
|
from app.tests.local_dimming import LocalDimmingMixin
|
||||||
init_accuracy_chart as _cf_init_accuracy_chart,
|
|
||||||
init_cct_chart as _cf_init_cct_chart,
|
|
||||||
init_contrast_chart as _cf_init_contrast_chart,
|
|
||||||
init_eotf_chart as _cf_init_eotf_chart,
|
|
||||||
init_gamma_chart as _cf_init_gamma_chart,
|
|
||||||
init_gamut_chart as _cf_init_gamut_chart,
|
|
||||||
on_chart_tab_changed as _cf_on_chart_tab_changed,
|
|
||||||
sync_gamut_toolbar as _cf_sync_gamut_toolbar,
|
|
||||||
_on_gamut_toolbar_changed as _cf_on_gamut_toolbar_changed,
|
|
||||||
update_chart_tabs_state as _cf_update_chart_tabs_state,
|
|
||||||
)
|
|
||||||
from app.config_io import (
|
|
||||||
clear_config_file as _cfg_clear_config_file,
|
|
||||||
get_config_path as _cfg_get_config_path,
|
|
||||||
load_pq_config as _cfg_load_pq_config,
|
|
||||||
save_pq_config as _cfg_save_pq_config,
|
|
||||||
)
|
|
||||||
from app.tests.local_dimming import (
|
|
||||||
clear_ld_records as _ld_clear_ld_records,
|
|
||||||
measure_ld_luminance as _ld_measure_ld_luminance,
|
|
||||||
save_local_dimming_results as _ld_save_local_dimming_results,
|
|
||||||
send_ld_window as _ld_send_ld_window,
|
|
||||||
start_local_dimming_test as _ld_start_local_dimming_test,
|
|
||||||
stop_local_dimming_test as _ld_stop_local_dimming_test,
|
|
||||||
update_ld_results as _ld_update_ld_results,
|
|
||||||
)
|
|
||||||
from app.services import PatternService
|
from app.services import PatternService
|
||||||
from app.device.connection import (
|
from app.device.connection import DeviceConnectionMixin
|
||||||
check_com_connections as _dev_check_com_connections,
|
from app.runner.test_runner import TestRunnerMixin
|
||||||
check_port_connection as _dev_check_port_connection,
|
|
||||||
disconnect_com_connections as _dev_disconnect_com_connections,
|
|
||||||
enable_com_widgets as _dev_enable_com_widgets,
|
|
||||||
get_available_com_ports as _dev_get_available_com_ports,
|
|
||||||
get_available_ucd_ports as _dev_get_available_ucd_ports,
|
|
||||||
refresh_com_ports as _dev_refresh_com_ports,
|
|
||||||
update_connection_indicator as _dev_update_connection_indicator,
|
|
||||||
)
|
|
||||||
from app.runner.test_runner import (
|
|
||||||
get_current_test_result as _run_get_current_test_result,
|
|
||||||
new_pq_results as _run_new_pq_results,
|
|
||||||
on_custom_template_test_completed as _run_on_custom_template_test_completed,
|
|
||||||
on_test_completed as _run_on_test_completed,
|
|
||||||
on_test_error as _run_on_test_error,
|
|
||||||
run_custom_sdr_test as _run_run_custom_sdr_test,
|
|
||||||
run_hdr_movie_test as _run_run_hdr_movie_test,
|
|
||||||
run_screen_module_test as _run_run_screen_module_test,
|
|
||||||
run_sdr_movie_test as _run_run_sdr_movie_test,
|
|
||||||
run_test as _run_run_test,
|
|
||||||
send_fix_pattern as _run_send_fix_pattern,
|
|
||||||
test_cct as _run_test_cct,
|
|
||||||
test_color_accuracy as _run_test_color_accuracy,
|
|
||||||
test_contrast as _run_test_contrast,
|
|
||||||
test_custom_sdr as _run_test_custom_sdr,
|
|
||||||
test_eotf as _run_test_eotf,
|
|
||||||
test_gamma as _run_test_gamma,
|
|
||||||
test_gamut as _run_test_gamut,
|
|
||||||
)
|
|
||||||
|
|
||||||
plt.rcParams["font.family"] = ["sans-serif"]
|
plt.rcParams["font.family"] = ["sans-serif"]
|
||||||
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
|
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
|
||||||
|
|
||||||
class PQAutomationApp:
|
|
||||||
|
class PQAutomationApp(
|
||||||
|
ConfigIOMixin,
|
||||||
|
ChartFrameMixin,
|
||||||
|
MainLayoutMixin,
|
||||||
|
CctPanelMixin,
|
||||||
|
DeviceConnectionMixin,
|
||||||
|
CustomTemplatePanelMixin,
|
||||||
|
SidePanelsMixin,
|
||||||
|
AIImagePanelMixin,
|
||||||
|
SingleStepPanelMixin,
|
||||||
|
PantoneBaselinePanelMixin,
|
||||||
|
GammaPatternPanelMixin,
|
||||||
|
CalmanPanelMixin,
|
||||||
|
LocalDimmingMixin,
|
||||||
|
PanelManagerMixin,
|
||||||
|
TestRunnerMixin,
|
||||||
|
PlotGamutMixin,
|
||||||
|
PlotGammaMixin,
|
||||||
|
PlotEotfMixin,
|
||||||
|
PlotCctMixin,
|
||||||
|
PlotContrastMixin,
|
||||||
|
PlotAccuracyMixin,
|
||||||
|
):
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title(get_app_title())
|
self.root.title(get_app_title())
|
||||||
@@ -228,6 +199,10 @@ class PQAutomationApp:
|
|||||||
# self.create_single_step_panel()
|
# self.create_single_step_panel()
|
||||||
# 创建 Pantone 认证摸底测试面板
|
# 创建 Pantone 认证摸底测试面板
|
||||||
self.create_pantone_baseline_panel()
|
self.create_pantone_baseline_panel()
|
||||||
|
# 创建 Gamma 测试图案配置面板
|
||||||
|
self.create_gamma_pattern_panel()
|
||||||
|
# 创建 CALMAN 风格灰阶测试面板
|
||||||
|
self.create_calman_panel()
|
||||||
# 创建测试类型选择区域
|
# 创建测试类型选择区域
|
||||||
self.create_test_type_frame()
|
self.create_test_type_frame()
|
||||||
# 创建操作按钮区域
|
# 创建操作按钮区域
|
||||||
@@ -277,111 +252,6 @@ class PQAutomationApp:
|
|||||||
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")
|
||||||
|
|
||||||
get_config_path = _cfg_get_config_path
|
|
||||||
load_pq_config = _cfg_load_pq_config
|
|
||||||
save_pq_config = _cfg_save_pq_config
|
|
||||||
|
|
||||||
register_panel = PM.register_panel
|
|
||||||
show_panel = PM.show_panel
|
|
||||||
hide_all_panels = PM.hide_all_panels
|
|
||||||
|
|
||||||
init_gamut_chart = _cf_init_gamut_chart
|
|
||||||
init_gamma_chart = _cf_init_gamma_chart
|
|
||||||
init_eotf_chart = _cf_init_eotf_chart
|
|
||||||
init_cct_chart = _cf_init_cct_chart
|
|
||||||
init_contrast_chart = _cf_init_contrast_chart
|
|
||||||
init_accuracy_chart = _cf_init_accuracy_chart
|
|
||||||
clear_chart = _cf_clear_chart
|
|
||||||
create_result_chart_frame = _cf_create_result_chart_frame
|
|
||||||
on_chart_tab_changed = _cf_on_chart_tab_changed
|
|
||||||
sync_gamut_toolbar = _cf_sync_gamut_toolbar
|
|
||||||
_on_gamut_toolbar_changed = _cf_on_gamut_toolbar_changed
|
|
||||||
|
|
||||||
create_floating_config_panel = _main.create_floating_config_panel
|
|
||||||
create_test_items_content = _main.create_test_items_content
|
|
||||||
create_signal_format_content = _main.create_signal_format_content
|
|
||||||
create_connection_content = _main.create_connection_content
|
|
||||||
create_operation_frame = _main.create_operation_frame
|
|
||||||
create_test_type_frame = _main.create_test_type_frame
|
|
||||||
update_config_info_display = _main.update_config_info_display
|
|
||||||
on_screen_module_timing_changed = _main.on_screen_module_timing_changed
|
|
||||||
on_sdr_timing_changed = _main.on_sdr_timing_changed
|
|
||||||
update_test_items = _main.update_test_items
|
|
||||||
on_test_type_change = _main.on_test_type_change
|
|
||||||
on_sdr_output_format_changed = _main.on_sdr_output_format_changed
|
|
||||||
on_hdr_output_format_changed = _main.on_hdr_output_format_changed
|
|
||||||
|
|
||||||
create_cct_params_frame = _ccp.create_cct_params_frame
|
|
||||||
on_sdr_cct_param_focus_out = _ccp.on_sdr_cct_param_focus_out
|
|
||||||
save_sdr_cct_params = _ccp.save_sdr_cct_params
|
|
||||||
on_hdr_cct_param_focus_out = _ccp.on_hdr_cct_param_focus_out
|
|
||||||
save_hdr_cct_params = _ccp.save_hdr_cct_params
|
|
||||||
recalculate_cct = _ccp.recalculate_cct
|
|
||||||
recalculate_gamut = _ccp.recalculate_gamut
|
|
||||||
on_cct_param_focus_out = _ccp.on_cct_param_focus_out
|
|
||||||
save_cct_params = _ccp.save_cct_params
|
|
||||||
reload_cct_params = _ccp.reload_cct_params
|
|
||||||
toggle_cct_params_frame = _ccp.toggle_cct_params_frame
|
|
||||||
on_screen_gamut_ref_changed = _ccp.on_screen_gamut_ref_changed
|
|
||||||
on_sdr_gamut_ref_changed = _ccp.on_sdr_gamut_ref_changed
|
|
||||||
on_hdr_gamut_ref_changed = _ccp.on_hdr_gamut_ref_changed
|
|
||||||
|
|
||||||
get_available_ucd_ports = _dev_get_available_ucd_ports
|
|
||||||
get_available_com_ports = _dev_get_available_com_ports
|
|
||||||
refresh_com_ports = _dev_refresh_com_ports
|
|
||||||
check_com_connections = _dev_check_com_connections
|
|
||||||
update_connection_indicator = _dev_update_connection_indicator
|
|
||||||
check_port_connection = _dev_check_port_connection
|
|
||||||
enable_com_widgets = _dev_enable_com_widgets
|
|
||||||
disconnect_com_connections = _dev_disconnect_com_connections
|
|
||||||
|
|
||||||
create_custom_template_result_panel = _ctp.create_custom_template_result_panel
|
|
||||||
show_custom_result_context_menu = _ctp.show_custom_result_context_menu
|
|
||||||
set_custom_result_table_locked = _ctp.set_custom_result_table_locked
|
|
||||||
start_custom_row_single_step = _ctp.start_custom_row_single_step
|
|
||||||
copy_custom_result_table = _ctp.copy_custom_result_table
|
|
||||||
clear_custom_template_results = _ctp.clear_custom_template_results
|
|
||||||
auto_expand_custom_result_view = _ctp.auto_expand_custom_result_view
|
|
||||||
append_custom_template_result = _ctp.append_custom_template_result
|
|
||||||
start_custom_template_test = _ctp.start_custom_template_test
|
|
||||||
update_custom_button_visibility = _ctp.update_custom_button_visibility
|
|
||||||
export_custom_template_excel = _ctp.export_custom_template_excel
|
|
||||||
export_custom_template_charts = _ctp.export_custom_template_charts
|
|
||||||
|
|
||||||
create_log_panel = _sp.create_log_panel
|
|
||||||
create_local_dimming_panel = _sp.create_local_dimming_panel
|
|
||||||
toggle_local_dimming_panel = _sp.toggle_local_dimming_panel
|
|
||||||
toggle_log_panel = _sp.toggle_log_panel
|
|
||||||
update_sidebar_selection = _sp.update_sidebar_selection
|
|
||||||
|
|
||||||
# ---- AI 图片对话面板 ----
|
|
||||||
create_ai_image_panel = _aip.create_ai_image_panel
|
|
||||||
toggle_ai_image_panel = _aip.toggle_ai_image_panel
|
|
||||||
reload_ai_image_list = _aip.reload_ai_image_list
|
|
||||||
|
|
||||||
# ---- 单步调试面板 ----
|
|
||||||
create_single_step_panel = _ssp.create_single_step_panel
|
|
||||||
toggle_single_step_panel = _ssp.toggle_single_step_panel
|
|
||||||
|
|
||||||
# ---- Pantone 认证摸底测试面板 ----
|
|
||||||
create_pantone_baseline_panel = _pbp.create_pantone_baseline_panel
|
|
||||||
toggle_pantone_baseline_panel = _pbp.toggle_pantone_baseline_panel
|
|
||||||
|
|
||||||
# ---- 单步调试面板(统一实现,委托到 side_panels 模块) ----
|
|
||||||
_toggle_debug_panel = _sp._toggle_debug_panel
|
|
||||||
toggle_screen_debug_panel = _sp.toggle_screen_debug_panel
|
|
||||||
toggle_sdr_debug_panel = _sp.toggle_sdr_debug_panel
|
|
||||||
toggle_hdr_debug_panel = _sp.toggle_hdr_debug_panel
|
|
||||||
|
|
||||||
clear_config_file = _cfg_clear_config_file
|
|
||||||
start_local_dimming_test = _ld_start_local_dimming_test
|
|
||||||
update_ld_results = _ld_update_ld_results
|
|
||||||
stop_local_dimming_test = _ld_stop_local_dimming_test
|
|
||||||
send_ld_window = _ld_send_ld_window
|
|
||||||
measure_ld_luminance = _ld_measure_ld_luminance
|
|
||||||
clear_ld_records = _ld_clear_ld_records
|
|
||||||
save_local_dimming_results = _ld_save_local_dimming_results
|
|
||||||
|
|
||||||
def _save_current_cct_params(self, swallow_errors=True):
|
def _save_current_cct_params(self, swallow_errors=True):
|
||||||
"""按当前测试类型分发保存对应的 CCT 参数。"""
|
"""按当前测试类型分发保存对应的 CCT 参数。"""
|
||||||
try:
|
try:
|
||||||
@@ -550,6 +420,7 @@ class PQAutomationApp:
|
|||||||
"ai_image",
|
"ai_image",
|
||||||
"single_step",
|
"single_step",
|
||||||
"pantone_baseline",
|
"pantone_baseline",
|
||||||
|
"gamma_pattern",
|
||||||
):
|
):
|
||||||
self.hide_all_panels()
|
self.hide_all_panels()
|
||||||
self._save_cct_params_before_test_type_switch()
|
self._save_cct_params_before_test_type_switch()
|
||||||
@@ -843,21 +714,8 @@ class PQAutomationApp:
|
|||||||
self.log_gui.log(traceback.format_exc(), level="error")
|
self.log_gui.log(traceback.format_exc(), level="error")
|
||||||
messagebox.showerror("错误", f"保存测试结果失败: {str(e)}")
|
messagebox.showerror("错误", f"保存测试结果失败: {str(e)}")
|
||||||
|
|
||||||
new_pq_results = _run_new_pq_results
|
# 纯算法函数:作为 staticmethod 保留在主类(不依赖 self,且 calculate_xxx
|
||||||
run_test = _run_run_test
|
# 的命名空间由历史代码以 self.calculate_xxx 调用)。
|
||||||
run_screen_module_test = _run_run_screen_module_test
|
|
||||||
run_custom_sdr_test = _run_run_custom_sdr_test
|
|
||||||
run_sdr_movie_test = _run_run_sdr_movie_test
|
|
||||||
run_hdr_movie_test = _run_run_hdr_movie_test
|
|
||||||
send_fix_pattern = _run_send_fix_pattern
|
|
||||||
test_custom_sdr = _run_test_custom_sdr
|
|
||||||
test_gamut = _run_test_gamut
|
|
||||||
test_gamma = _run_test_gamma
|
|
||||||
test_eotf = _run_test_eotf
|
|
||||||
test_cct = _run_test_cct
|
|
||||||
test_contrast = _run_test_contrast
|
|
||||||
test_color_accuracy = _run_test_color_accuracy
|
|
||||||
|
|
||||||
calculate_delta_e_2000 = staticmethod(_calc_delta_e_2000)
|
calculate_delta_e_2000 = staticmethod(_calc_delta_e_2000)
|
||||||
get_accuracy_color_standards = staticmethod(_get_accuracy_color_standards)
|
get_accuracy_color_standards = staticmethod(_get_accuracy_color_standards)
|
||||||
calculate_gamut_coverage = staticmethod(_calc_gamut_coverage)
|
calculate_gamut_coverage = staticmethod(_calc_gamut_coverage)
|
||||||
@@ -865,20 +723,6 @@ class PQAutomationApp:
|
|||||||
calculate_color_accuracy = staticmethod(_calc_color_accuracy)
|
calculate_color_accuracy = staticmethod(_calc_color_accuracy)
|
||||||
calculate_pq_curve = staticmethod(_calc_pq_curve)
|
calculate_pq_curve = staticmethod(_calc_pq_curve)
|
||||||
|
|
||||||
plot_gamut = _plot_gamut
|
|
||||||
plot_gamma = _plot_gamma
|
|
||||||
plot_eotf = _plot_eotf
|
|
||||||
plot_cct = _plot_cct
|
|
||||||
plot_contrast = _plot_contrast
|
|
||||||
plot_accuracy = _plot_accuracy
|
|
||||||
|
|
||||||
on_test_completed = _run_on_test_completed
|
|
||||||
on_custom_template_test_completed = _run_on_custom_template_test_completed
|
|
||||||
get_current_test_result = _run_get_current_test_result
|
|
||||||
on_test_error = _run_on_test_error
|
|
||||||
|
|
||||||
update_chart_tabs_state = _cf_update_chart_tabs_state
|
|
||||||
|
|
||||||
def get_test_type_name(self, test_type):
|
def get_test_type_name(self, test_type):
|
||||||
"""获取测试类型的显示名称"""
|
"""获取测试类型的显示名称"""
|
||||||
if test_type == "screen_module":
|
if test_type == "screen_module":
|
||||||
|
|||||||
224
tools/refactor_to_mixins.py
Normal file
224
tools/refactor_to_mixins.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
"""一次性脚本:将外置模块中的 `def f(self, ...)` 自由函数转换为 Mixin 方式。
|
||||||
|
|
||||||
|
操作:
|
||||||
|
1. 给所有顶层 `def f(self, ...)` 加 `self: "PQAutomationApp"` 注解(仅注解,不移动)。
|
||||||
|
2. 在文件顶部(首个 def/class 之前、import 块之后)插入 TYPE_CHECKING 块。
|
||||||
|
3. 在文件末尾追加 `class XxxMixin:` 把这些函数作为类属性挂上。
|
||||||
|
|
||||||
|
不会改变:
|
||||||
|
- 函数体(包括内部 `_xxx(self, ...)` 直接调用)。
|
||||||
|
- 已存在的类、模块级常量。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tokenize
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
# 文件 -> Mixin 类名
|
||||||
|
TARGETS: dict[str, str] = {
|
||||||
|
"app/config_io.py": "ConfigIOMixin",
|
||||||
|
"app/views/chart_frame.py": "ChartFrameMixin",
|
||||||
|
"app/views/panels/main_layout.py": "MainLayoutMixin",
|
||||||
|
"app/views/panels/cct_panel.py": "CctPanelMixin",
|
||||||
|
"app/device/connection.py": "DeviceConnectionMixin",
|
||||||
|
"app/views/panels/custom_template_panel.py": "CustomTemplatePanelMixin",
|
||||||
|
"app/views/panels/side_panels.py": "SidePanelsMixin",
|
||||||
|
"app/views/panels/ai_image_panel.py": "AIImagePanelMixin",
|
||||||
|
"app/views/panels/single_step_panel.py": "SingleStepPanelMixin",
|
||||||
|
"app/views/panels/pantone_baseline_panel.py": "PantoneBaselinePanelMixin",
|
||||||
|
"app/tests/local_dimming.py": "LocalDimmingMixin",
|
||||||
|
"app/views/panel_manager.py": "PanelManagerMixin",
|
||||||
|
"app/runner/test_runner.py": "TestRunnerMixin",
|
||||||
|
"app/plots/plot_gamut.py": "PlotGamutMixin",
|
||||||
|
"app/plots/plot_gamma.py": "PlotGammaMixin",
|
||||||
|
"app/plots/plot_eotf.py": "PlotEotfMixin",
|
||||||
|
"app/plots/plot_cct.py": "PlotCctMixin",
|
||||||
|
"app/plots/plot_contrast.py": "PlotContrastMixin",
|
||||||
|
"app/plots/plot_accuracy.py": "PlotAccuracyMixin",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TYPE_CHECKING_BLOCK = (
|
||||||
|
"from typing import TYPE_CHECKING\n"
|
||||||
|
"\n"
|
||||||
|
"if TYPE_CHECKING:\n"
|
||||||
|
" from pqAutomationApp import PQAutomationApp\n"
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SelfFunc:
|
||||||
|
name: str
|
||||||
|
lineno: int # 1-based, line of `def`
|
||||||
|
col_offset: int
|
||||||
|
end_lineno: int
|
||||||
|
def_line_idx: int # 0-based line index of `def ...` line
|
||||||
|
self_token_line_idx: int # 0-based line index of `self`
|
||||||
|
self_token_col: int
|
||||||
|
|
||||||
|
|
||||||
|
def _read(path: str) -> str:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
# 去 BOM
|
||||||
|
if data.startswith(b"\xef\xbb\xbf"):
|
||||||
|
data = data[3:]
|
||||||
|
return data.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _write(path: str, text: str) -> None:
|
||||||
|
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
def _locate_self_token(src_lines: list[str], def_line_idx: int) -> tuple[int, int] | None:
|
||||||
|
"""在 def 行(可能多行签名)中定位首个参数 `self` 的位置。"""
|
||||||
|
# 用 tokenize 解析从 def 行开始的片段
|
||||||
|
snippet = "".join(src_lines[def_line_idx:])
|
||||||
|
try:
|
||||||
|
tokens = list(tokenize.generate_tokens(io.StringIO(snippet).readline))
|
||||||
|
except tokenize.TokenizeError:
|
||||||
|
return None
|
||||||
|
saw_open_paren = False
|
||||||
|
for tok in tokens:
|
||||||
|
if tok.type == tokenize.OP and tok.string == "(":
|
||||||
|
saw_open_paren = True
|
||||||
|
continue
|
||||||
|
if saw_open_paren and tok.type == tokenize.NAME and tok.string == "self":
|
||||||
|
# tok.start 是 (row, col) 相对于 snippet(1-based row)
|
||||||
|
row_in_snippet = tok.start[0] - 1
|
||||||
|
col = tok.start[1]
|
||||||
|
return (def_line_idx + row_in_snippet, col)
|
||||||
|
if saw_open_paren and tok.type == tokenize.OP and tok.string in (")", ","):
|
||||||
|
# 第一个参数不是 self —— 跳过
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def collect_self_funcs(src: str) -> tuple[ast.Module, list[SelfFunc]]:
|
||||||
|
tree = ast.parse(src)
|
||||||
|
src_lines = src.splitlines(keepends=True)
|
||||||
|
results: list[SelfFunc] = []
|
||||||
|
for node in tree.body:
|
||||||
|
if isinstance(node, ast.FunctionDef) and node.args.args and node.args.args[0].arg == "self":
|
||||||
|
def_idx = node.lineno - 1
|
||||||
|
pos = _locate_self_token(src_lines, def_idx)
|
||||||
|
if pos is None:
|
||||||
|
continue
|
||||||
|
results.append(SelfFunc(
|
||||||
|
name=node.name,
|
||||||
|
lineno=node.lineno,
|
||||||
|
col_offset=node.col_offset,
|
||||||
|
end_lineno=node.end_lineno or node.lineno,
|
||||||
|
def_line_idx=def_idx,
|
||||||
|
self_token_line_idx=pos[0],
|
||||||
|
self_token_col=pos[1],
|
||||||
|
))
|
||||||
|
return tree, results
|
||||||
|
|
||||||
|
|
||||||
|
def annotate_self(src: str, funcs: list[SelfFunc]) -> str:
|
||||||
|
"""把每个 def 的首个 `self` 形参替换为 `self: "PQAutomationApp"`。"""
|
||||||
|
lines = src.splitlines(keepends=True)
|
||||||
|
# 从后往前替换,避免行号变动
|
||||||
|
for fn in sorted(funcs, key=lambda f: -f.self_token_line_idx):
|
||||||
|
line = lines[fn.self_token_line_idx]
|
||||||
|
col = fn.self_token_col
|
||||||
|
# 检查后续是否已经有注解
|
||||||
|
after = line[col + len("self"):]
|
||||||
|
# 已经注解过则跳过
|
||||||
|
m = re.match(r"\s*:", after)
|
||||||
|
if m:
|
||||||
|
continue
|
||||||
|
new_line = line[:col] + 'self: "PQAutomationApp"' + line[col + len("self"):]
|
||||||
|
lines[fn.self_token_line_idx] = new_line
|
||||||
|
return "".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_type_checking_block(src: str) -> str:
|
||||||
|
if "from pqAutomationApp import PQAutomationApp" in src:
|
||||||
|
return src
|
||||||
|
# 找到首个非 docstring / 非注释 / 非 import 的位置:
|
||||||
|
# 简单策略:在最后一个 import 行之后插入;若没有 import,则在 docstring 之后插入。
|
||||||
|
lines = src.splitlines(keepends=True)
|
||||||
|
insert_idx = 0
|
||||||
|
in_docstring = False
|
||||||
|
doc_quote: str | None = None
|
||||||
|
last_import_idx = -1
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
stripped = line.lstrip()
|
||||||
|
if i == 0 and (stripped.startswith('"""') or stripped.startswith("'''")):
|
||||||
|
q = stripped[:3]
|
||||||
|
doc_quote = q
|
||||||
|
if stripped.count(q) >= 2 and len(stripped) > 3:
|
||||||
|
# 单行 docstring
|
||||||
|
last_import_idx = max(last_import_idx, i)
|
||||||
|
continue
|
||||||
|
in_docstring = True
|
||||||
|
continue
|
||||||
|
if in_docstring:
|
||||||
|
if doc_quote and doc_quote in line:
|
||||||
|
in_docstring = False
|
||||||
|
last_import_idx = max(last_import_idx, i)
|
||||||
|
continue
|
||||||
|
if stripped.startswith("import ") or stripped.startswith("from "):
|
||||||
|
last_import_idx = i
|
||||||
|
continue
|
||||||
|
if stripped == "" or stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
# 遇到第一个真实代码
|
||||||
|
break
|
||||||
|
insert_idx = last_import_idx + 1
|
||||||
|
new_lines = lines[:insert_idx] + ["\n", TYPE_CHECKING_BLOCK] + lines[insert_idx:]
|
||||||
|
return "".join(new_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def append_mixin(src: str, mixin_name: str, func_names: list[str]) -> str:
|
||||||
|
if f"class {mixin_name}" in src:
|
||||||
|
return src
|
||||||
|
body_lines = []
|
||||||
|
body_lines.append("")
|
||||||
|
body_lines.append("")
|
||||||
|
body_lines.append(f"class {mixin_name}:")
|
||||||
|
body_lines.append(f' """由 tools/refactor_to_mixins.py 自动生成。')
|
||||||
|
body_lines.append(" 把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。")
|
||||||
|
body_lines.append(' """')
|
||||||
|
for name in func_names:
|
||||||
|
body_lines.append(f" {name} = {name}")
|
||||||
|
text = "\n".join(body_lines) + "\n"
|
||||||
|
if not src.endswith("\n"):
|
||||||
|
src += "\n"
|
||||||
|
return src + text
|
||||||
|
|
||||||
|
|
||||||
|
def process(path: str, mixin_name: str) -> None:
|
||||||
|
src = _read(path)
|
||||||
|
tree, funcs = collect_self_funcs(src)
|
||||||
|
if not funcs:
|
||||||
|
print(f" -> skip (no self-funcs)")
|
||||||
|
return
|
||||||
|
func_names = [f.name for f in funcs]
|
||||||
|
new_src = annotate_self(src, funcs)
|
||||||
|
new_src = ensure_type_checking_block(new_src)
|
||||||
|
new_src = append_mixin(new_src, mixin_name, func_names)
|
||||||
|
_write(path, new_src)
|
||||||
|
print(f" -> {mixin_name} with {len(func_names)} methods")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
os.chdir(root)
|
||||||
|
for rel, mixin in TARGETS.items():
|
||||||
|
print(f"Processing {rel}")
|
||||||
|
process(rel, mixin)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user