屏模组添加colorinfo设置、修改配置项UI样式
This commit is contained in:
@@ -87,6 +87,7 @@ _DEFAULT_TEST_TYPES = {
|
||||
"name": "屏模组性能测试",
|
||||
"test_items": ["gamut", "gamma", "cct", "contrast"],
|
||||
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||
"data_range": "Full",
|
||||
"color_format": "RGB",
|
||||
"bpc": 8,
|
||||
"colorimetry": "sRGB",
|
||||
@@ -96,6 +97,7 @@ _DEFAULT_TEST_TYPES = {
|
||||
"name": "SDR Movie测试",
|
||||
"test_items": ["gamut", "gamma", "cct", "contrast", "accuracy"],
|
||||
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||
"data_range": "Full",
|
||||
"color_format": "RGB",
|
||||
"bpc": 8,
|
||||
"colorimetry": "sRGB",
|
||||
@@ -105,6 +107,7 @@ _DEFAULT_TEST_TYPES = {
|
||||
"name": "HDR Movie测试",
|
||||
"test_items": ["gamut", "eotf", "cct", "contrast", "accuracy"],
|
||||
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||
"data_range": "Full",
|
||||
"color_format": "RGB",
|
||||
"bpc": 8,
|
||||
"colorimetry": "sRGB",
|
||||
@@ -695,7 +698,16 @@ class PQConfig:
|
||||
def from_dict(self, config_dict):
|
||||
"""从字典加载配置"""
|
||||
self.current_test_type = config_dict.get("current_test_type", "screen_module")
|
||||
self.current_test_types = config_dict.get("test_types", self.current_test_types)
|
||||
|
||||
# 以默认模板为底,叠加历史配置,保证新字段(如 data_range)在旧配置下也有值。
|
||||
loaded_test_types = config_dict.get("test_types", {})
|
||||
merged_test_types = copy.deepcopy(_DEFAULT_TEST_TYPES)
|
||||
if isinstance(loaded_test_types, dict):
|
||||
for test_type, loaded_cfg in loaded_test_types.items():
|
||||
if test_type in merged_test_types and isinstance(loaded_cfg, dict):
|
||||
merged_test_types[test_type].update(loaded_cfg)
|
||||
self.current_test_types = merged_test_types
|
||||
|
||||
self.device_config = config_dict.get("device_config", self.device_config)
|
||||
self.custom_pattern = config_dict.get("custom_pattern", self.custom_pattern)
|
||||
|
||||
|
||||
@@ -45,11 +45,7 @@ def load_icon(png_path):
|
||||
|
||||
|
||||
def backgroud_style_set():
|
||||
style = ttk.Style()
|
||||
# 移除背景色设置,使用默认背景色
|
||||
style.configure(
|
||||
"SidebarSelected.TButton",
|
||||
# anchor="w",
|
||||
padding=10,
|
||||
background="#005470",
|
||||
)
|
||||
style = ttk.Style() # noqa: F841 - 保持原副作用:确保 Style 实例化
|
||||
# 现代化样式集中注册(Card / ConfigHeader / Toolbar / StatusBar / Sidebar 等)
|
||||
from app.views.modern_styles import apply_modern_styles
|
||||
apply_modern_styles()
|
||||
|
||||
@@ -30,15 +30,52 @@ class PatternService:
|
||||
source_params = self._get_source_pattern_params(mode)
|
||||
|
||||
if test_type == "screen_module":
|
||||
screen_cfg = self.app.config.current_test_types.get("screen_module", {})
|
||||
color_space = (
|
||||
self.app.screen_module_color_space_var.get()
|
||||
if hasattr(self.app, "screen_module_color_space_var")
|
||||
else screen_cfg.get("colorimetry", "sRGB")
|
||||
)
|
||||
data_range = (
|
||||
self.app.screen_module_data_range_var.get()
|
||||
if hasattr(self.app, "screen_module_data_range_var")
|
||||
else screen_cfg.get("data_range", "Full")
|
||||
)
|
||||
bit_depth = (
|
||||
self.app.screen_module_bit_depth_var.get()
|
||||
if hasattr(self.app, "screen_module_bit_depth_var")
|
||||
else f"{int(screen_cfg.get('bpc', 8))}bit"
|
||||
)
|
||||
output_format = (
|
||||
self.app.screen_module_output_format_var.get()
|
||||
if hasattr(self.app, "screen_module_output_format_var")
|
||||
else screen_cfg.get("color_format", "RGB")
|
||||
)
|
||||
|
||||
if log_details:
|
||||
self._log("=" * 50, "separator")
|
||||
self._log("设置屏模组信号格式:", "info")
|
||||
self._log("=" * 50, "separator")
|
||||
self._log(
|
||||
f" Timing: {self.app.config.current_test_types[test_type]['timing']}",
|
||||
"info",
|
||||
)
|
||||
for label, value in [
|
||||
("色彩空间", color_space),
|
||||
("色彩格式", output_format),
|
||||
("数据范围", data_range),
|
||||
("编码位深", bit_depth),
|
||||
("Timing", self.app.config.current_test_types[test_type]["timing"]),
|
||||
]:
|
||||
self._log(f" {label}: {value}", "info")
|
||||
self.app.signal_service.apply_config(active_config)
|
||||
success = self.app.signal_service.update_signal_format(
|
||||
color_space=color_space,
|
||||
data_range=data_range,
|
||||
bit_depth=bit_depth,
|
||||
output_format=output_format,
|
||||
)
|
||||
if log_details:
|
||||
self._log(
|
||||
f"屏模组信号格式设置{'成功' if success else '失败'}",
|
||||
"success" if success else "error",
|
||||
)
|
||||
|
||||
elif test_type == "sdr_movie":
|
||||
data_range = self.app.sdr_data_range_var.get()
|
||||
|
||||
@@ -1,104 +1,149 @@
|
||||
import ttkbootstrap as ttk
|
||||
"""现代化的可折叠面板(取代 v1 的图标按钮版本)。
|
||||
|
||||
升级要点(保留 ``add(child, title=...)`` 旧签名兼容):
|
||||
- header 整条可点击切换展开/收起;
|
||||
- 使用 Unicode chevron (▾/▸),无需 PNG 资源;
|
||||
- 新增 ``preview_textvariable``:折叠时在 header 显示当前配置摘要;
|
||||
- 新增 ``header_actions``:在 header 右侧注入自定义按钮(如顶部工具条)。
|
||||
"""
|
||||
|
||||
import tkinter
|
||||
from tkinter import ttk
|
||||
from pathlib import Path
|
||||
from ttkbootstrap import Style
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def get_resource_path(relative_path):
|
||||
"""
|
||||
获取资源文件的绝对路径(兼容开发环境和打包后)
|
||||
|
||||
Args:
|
||||
relative_path: 相对路径,如 "assets/icons8_double_up_24px.png"
|
||||
|
||||
Returns:
|
||||
str: 资源文件的绝对路径
|
||||
"""
|
||||
try:
|
||||
# PyInstaller 打包后的临时文件夹路径
|
||||
base_path = sys._MEIPASS
|
||||
except AttributeError:
|
||||
# 开发环境:使用项目根目录
|
||||
# 当前文件: app/views/collapsing_frame.py
|
||||
# 项目根目录: app/views 的祖父目录
|
||||
current_file = os.path.abspath(__file__)
|
||||
views_dir = os.path.dirname(current_file)
|
||||
app_dir = os.path.dirname(views_dir)
|
||||
base_path = os.path.dirname(app_dir)
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
|
||||
class CollapsingFrame(ttk.Frame):
|
||||
"""
|
||||
A collapsible frame widget that opens and closes with a button click.
|
||||
"""
|
||||
"""A modern collapsible frame widget."""
|
||||
|
||||
CHEVRON_OPEN = "\u25be" # ▾
|
||||
CHEVRON_CLOSED = "\u25b8" # ▸
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.cumulative_rows = 0
|
||||
p = Path(__file__).parent
|
||||
self.images = [
|
||||
tkinter.PhotoImage(
|
||||
name="open", file=get_resource_path("assets/icons8_double_up_24px.png")
|
||||
),
|
||||
tkinter.PhotoImage(
|
||||
name="closed",
|
||||
file=get_resource_path("assets/icons8_double_right_24px.png"),
|
||||
),
|
||||
]
|
||||
# 兼容旧代码可能引用 self.images
|
||||
self.images: list = []
|
||||
|
||||
def add(self, child, title="", style="primary.TButton", **kwargs):
|
||||
"""Add a child to the collapsible frame
|
||||
# ------------------------------------------------------------------
|
||||
# 公共 API
|
||||
# ------------------------------------------------------------------
|
||||
def add(
|
||||
self,
|
||||
child,
|
||||
title: str = "",
|
||||
style: str = "primary.TButton", # 兼容旧签名(不再使用)
|
||||
preview_textvariable=None,
|
||||
header_actions=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""添加一个子区段到折叠面板。
|
||||
|
||||
:param ttk.Frame child: the child frame to add to the widget
|
||||
:param str title: the title appearing on the collapsible section header
|
||||
:param str style: the ttk style to apply to the collapsible section header
|
||||
:param child: 必须是一个 ttk.Frame;
|
||||
:param title: 标题文本;
|
||||
:param preview_textvariable: 折叠时显示在 header 上的状态摘要 StringVar;
|
||||
:param header_actions: 回调 ``fn(actions_frame)``,可在 header 右侧添加按钮。
|
||||
"""
|
||||
if child.winfo_class() != "TFrame": # must be a frame
|
||||
if child.winfo_class() != "TFrame":
|
||||
return
|
||||
style_color = style.split(".")[0]
|
||||
frm = ttk.Frame(self, style=f"{style_color}.TFrame")
|
||||
frm.grid(row=self.cumulative_rows, column=0, sticky="ew")
|
||||
|
||||
# header title
|
||||
lbl = ttk.Label(frm, text=title, style=f"{style_color}.Inverse.TLabel")
|
||||
if kwargs.get("textvariable"):
|
||||
lbl.configure(textvariable=kwargs.get("textvariable"))
|
||||
lbl.pack(side="left", fill="both", padx=10)
|
||||
header = ttk.Frame(self, style="ConfigHeader.TFrame", padding=(12, 6))
|
||||
header.grid(row=self.cumulative_rows, column=0, sticky="ew")
|
||||
header.columnconfigure(1, weight=1)
|
||||
|
||||
# header toggle button
|
||||
btn = ttk.Button(
|
||||
frm,
|
||||
image="open",
|
||||
style=style,
|
||||
command=lambda c=child: self._toggle_open_close(child),
|
||||
# 左:chevron + 标题
|
||||
title_box = ttk.Frame(header, style="ConfigHeader.TFrame")
|
||||
title_box.grid(row=0, column=0, sticky="w")
|
||||
|
||||
chevron = ttk.Label(
|
||||
title_box, text=self.CHEVRON_OPEN, style="ConfigChevron.TLabel"
|
||||
)
|
||||
btn.pack(side="right")
|
||||
chevron.pack(side="left", padx=(0, 8))
|
||||
|
||||
title_lbl = ttk.Label(title_box, text=title, style="ConfigHeader.TLabel")
|
||||
if kwargs.get("textvariable"):
|
||||
title_lbl.configure(textvariable=kwargs.get("textvariable"))
|
||||
title_lbl.pack(side="left")
|
||||
|
||||
# 中:折叠状态预览
|
||||
preview_lbl = None
|
||||
if preview_textvariable is not None:
|
||||
preview_lbl = ttk.Label(
|
||||
header,
|
||||
textvariable=preview_textvariable,
|
||||
style="ConfigPreview.TLabel",
|
||||
)
|
||||
preview_lbl.grid(row=0, column=1, sticky="w", padx=(16, 8))
|
||||
|
||||
# 右:actions(如顶部工具条按钮)
|
||||
actions_frame = ttk.Frame(header, style="ConfigHeader.TFrame")
|
||||
actions_frame.grid(row=0, column=2, sticky="e")
|
||||
if callable(header_actions):
|
||||
try:
|
||||
header_actions(actions_frame)
|
||||
except Exception:
|
||||
# 注入失败不应影响整体折叠面板渲染
|
||||
pass
|
||||
|
||||
# 整条 header 点击切换
|
||||
clickable = [header, title_box, chevron, title_lbl]
|
||||
if preview_lbl is not None:
|
||||
clickable.append(preview_lbl)
|
||||
for w in clickable:
|
||||
w.bind(
|
||||
"<Button-1>",
|
||||
lambda _e, c=child: self._toggle_open_close(c),
|
||||
)
|
||||
try:
|
||||
w.configure(cursor="hand2")
|
||||
except tkinter.TclError:
|
||||
pass
|
||||
|
||||
child._chevron = chevron
|
||||
child._header = header
|
||||
child._preview_lbl = preview_lbl
|
||||
# 兼容旧代码 child.btn.invoke() / child.btn.configure(image=...)
|
||||
child.btn = _HeaderToggleProxy(self, child, chevron)
|
||||
|
||||
# assign toggle button to child so that it's accesible when toggling (need to change image)
|
||||
child.btn = btn
|
||||
child.grid(row=self.cumulative_rows + 1, column=0, sticky="news")
|
||||
|
||||
# increment the row assignment
|
||||
self.cumulative_rows += 2
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 内部实现
|
||||
# ------------------------------------------------------------------
|
||||
def _toggle_open_close(self, child):
|
||||
"""
|
||||
Open or close the section and change the toggle button image accordingly
|
||||
|
||||
:param ttk.Frame child: the child element to add or remove from grid manager
|
||||
"""
|
||||
if child.winfo_viewable():
|
||||
child.grid_remove()
|
||||
child.btn.configure(image="closed")
|
||||
try:
|
||||
child._chevron.configure(text=self.CHEVRON_CLOSED)
|
||||
except (AttributeError, tkinter.TclError):
|
||||
pass
|
||||
else:
|
||||
child.grid()
|
||||
child.btn.configure(image="open")
|
||||
try:
|
||||
child._chevron.configure(text=self.CHEVRON_OPEN)
|
||||
except (AttributeError, tkinter.TclError):
|
||||
pass
|
||||
|
||||
|
||||
class _HeaderToggleProxy:
|
||||
"""兼容旧代码:``child.btn.invoke()`` / ``child.btn.configure(image=...)``。"""
|
||||
|
||||
def __init__(self, owner: "CollapsingFrame", child, chevron):
|
||||
self._owner = owner
|
||||
self._child = child
|
||||
self._chevron = chevron
|
||||
|
||||
def invoke(self):
|
||||
self._owner._toggle_open_close(self._child)
|
||||
|
||||
def configure(self, **kwargs):
|
||||
image = kwargs.get("image")
|
||||
if image == "closed":
|
||||
self._chevron.configure(text=CollapsingFrame.CHEVRON_CLOSED)
|
||||
elif image == "open":
|
||||
self._chevron.configure(text=CollapsingFrame.CHEVRON_OPEN)
|
||||
|
||||
config = configure
|
||||
|
||||
|
||||
# class Application(tkinter.Tk):
|
||||
|
||||
219
app/views/modern_styles.py
Normal file
219
app/views/modern_styles.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""现代化 UI 样式注册(跟随 ttkbootstrap 当前主题)。
|
||||
|
||||
由 backgroud_style_set() 调用一次。这里集中定义"配置项卡片化"、
|
||||
"现代化标题栏"、"工具条"、"状态栏" 等所需的所有 ttk Style,
|
||||
保持主题切换时颜色自动跟随。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ttkbootstrap as ttk
|
||||
|
||||
|
||||
def _hex_to_rgb(h: str) -> tuple[int, int, int]:
|
||||
h = h.lstrip("#")
|
||||
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
||||
|
||||
|
||||
def _rgb_to_hex(r: int, g: int, b: int) -> str:
|
||||
return f"#{r:02x}{g:02x}{b:02x}"
|
||||
|
||||
|
||||
def _mix(c1: str, c2: str, ratio: float) -> str:
|
||||
"""按 ratio (0~1) 将 c1 与 c2 线性混合。"""
|
||||
r1, g1, b1 = _hex_to_rgb(c1)
|
||||
r2, g2, b2 = _hex_to_rgb(c2)
|
||||
return _rgb_to_hex(
|
||||
int(r1 * (1 - ratio) + r2 * ratio),
|
||||
int(g1 * (1 - ratio) + g2 * ratio),
|
||||
int(b1 * (1 - ratio) + b2 * ratio),
|
||||
)
|
||||
|
||||
|
||||
def _is_dark(color: str) -> bool:
|
||||
r, g, b = _hex_to_rgb(color)
|
||||
# ITU-R BT.601 亮度
|
||||
return (r * 299 + g * 587 + b * 114) / 1000 < 128
|
||||
|
||||
|
||||
def apply_modern_styles() -> None:
|
||||
"""注册或刷新现代化样式集。可在主题切换后再次调用。"""
|
||||
style = ttk.Style()
|
||||
theme = style.colors # ttkbootstrap.style.Colors
|
||||
|
||||
bg = theme.bg # 主背景
|
||||
fg = theme.fg # 主前景
|
||||
primary = theme.primary
|
||||
secondary = theme.secondary
|
||||
border = theme.border
|
||||
inputbg = theme.inputbg
|
||||
selectbg = theme.selectbg
|
||||
selectfg = theme.selectfg
|
||||
|
||||
dark_theme = _is_dark(bg)
|
||||
|
||||
# 卡片背景:在主背景上轻微偏移,营造层级感
|
||||
card_bg = _mix(bg, "#ffffff", 0.04) if dark_theme else _mix(bg, "#000000", 0.025)
|
||||
card_border = _mix(bg, fg, 0.18) if dark_theme else _mix(bg, "#000000", 0.10)
|
||||
# 配置项 header 用 secondary 主题色
|
||||
header_bg = secondary
|
||||
header_fg = "#ffffff" if _is_dark(secondary) else "#1a1a1a"
|
||||
header_hover_bg = _mix(secondary, "#ffffff", 0.08) if _is_dark(secondary) else _mix(secondary, "#000000", 0.08)
|
||||
|
||||
preview_fg = _mix(header_fg, header_bg, 0.35)
|
||||
|
||||
# ---------------- 卡片 ----------------
|
||||
style.configure(
|
||||
"Card.TFrame",
|
||||
background=card_bg,
|
||||
bordercolor=card_border,
|
||||
relief="solid",
|
||||
borderwidth=1,
|
||||
)
|
||||
style.configure(
|
||||
"CardTitle.TLabel",
|
||||
background=card_bg,
|
||||
foreground=primary,
|
||||
font=("微软雅黑", 10, "bold"),
|
||||
)
|
||||
style.configure(
|
||||
"CardBody.TLabel",
|
||||
background=card_bg,
|
||||
foreground=fg,
|
||||
font=("微软雅黑", 9),
|
||||
)
|
||||
style.configure(
|
||||
"CardIcon.TLabel",
|
||||
background=card_bg,
|
||||
foreground=primary,
|
||||
font=("Segoe UI Emoji", 14),
|
||||
)
|
||||
|
||||
# 内嵌于 Card 的容器(与 Card.TFrame 同背景,无边框)
|
||||
style.configure("CardInner.TFrame", background=card_bg, borderwidth=0)
|
||||
|
||||
# ---------------- 配置项 Header ----------------
|
||||
style.configure(
|
||||
"ConfigHeader.TFrame",
|
||||
background=header_bg,
|
||||
borderwidth=0,
|
||||
)
|
||||
style.configure(
|
||||
"ConfigHeaderHover.TFrame",
|
||||
background=header_hover_bg,
|
||||
borderwidth=0,
|
||||
)
|
||||
style.configure(
|
||||
"ConfigHeader.TLabel",
|
||||
background=header_bg,
|
||||
foreground=header_fg,
|
||||
font=("微软雅黑", 10, "bold"),
|
||||
)
|
||||
style.configure(
|
||||
"ConfigHeaderHover.TLabel",
|
||||
background=header_hover_bg,
|
||||
foreground=header_fg,
|
||||
font=("微软雅黑", 10, "bold"),
|
||||
)
|
||||
style.configure(
|
||||
"ConfigChevron.TLabel",
|
||||
background=header_bg,
|
||||
foreground=header_fg,
|
||||
font=("Segoe UI Symbol", 12, "bold"),
|
||||
)
|
||||
style.configure(
|
||||
"ConfigPreview.TLabel",
|
||||
background=header_bg,
|
||||
foreground=preview_fg,
|
||||
font=("微软雅黑", 9),
|
||||
)
|
||||
|
||||
# ---------------- 顶部工具条 ----------------
|
||||
style.configure("Toolbar.TFrame", background=bg, borderwidth=0)
|
||||
# 工具条上的次要按钮(清理配置等)
|
||||
style.configure(
|
||||
"ToolbarMuted.TButton",
|
||||
font=("微软雅黑", 9),
|
||||
padding=(10, 5),
|
||||
)
|
||||
|
||||
# ---------------- 区段标题(侧栏 / 卡片外) ----------------
|
||||
style.configure(
|
||||
"SectionTitle.TLabel",
|
||||
background=bg,
|
||||
foreground=_mix(fg, bg, 0.45),
|
||||
font=("微软雅黑", 8, "bold"),
|
||||
)
|
||||
# 侧栏内的小区段标题(侧栏背景是 primary)
|
||||
style.configure(
|
||||
"SidebarSection.TLabel",
|
||||
background=primary,
|
||||
foreground=_mix("#ffffff", primary, 0.35),
|
||||
font=("微软雅黑", 8, "bold"),
|
||||
)
|
||||
|
||||
# ---------------- 结果区无边框标题行 ----------------
|
||||
style.configure("ResultHeader.TFrame", background=bg, borderwidth=0)
|
||||
style.configure(
|
||||
"ResultHeader.TLabel",
|
||||
background=bg,
|
||||
foreground=fg,
|
||||
font=("微软雅黑", 11, "bold"),
|
||||
)
|
||||
|
||||
# ---------------- 状态栏 ----------------
|
||||
statusbar_bg = _mix(bg, "#000000", 0.06) if not dark_theme else _mix(bg, "#ffffff", 0.06)
|
||||
statusbar_fg = _mix(fg, bg, 0.15)
|
||||
style.configure(
|
||||
"StatusBar.TFrame",
|
||||
background=statusbar_bg,
|
||||
borderwidth=0,
|
||||
)
|
||||
style.configure(
|
||||
"StatusBar.TLabel",
|
||||
background=statusbar_bg,
|
||||
foreground=statusbar_fg,
|
||||
font=("微软雅黑", 9),
|
||||
padding=(10, 4),
|
||||
)
|
||||
style.configure(
|
||||
"StatusBarAccent.TLabel",
|
||||
background=statusbar_bg,
|
||||
foreground=primary,
|
||||
font=("微软雅黑", 9, "bold"),
|
||||
padding=(10, 4),
|
||||
)
|
||||
|
||||
# ---------------- Sidebar 按钮(保留兼容名) ----------------
|
||||
style.configure(
|
||||
"Sidebar.TButton",
|
||||
background=primary,
|
||||
foreground="#ffffff" if _is_dark(primary) else "#1a1a1a",
|
||||
font=("微软雅黑", 10),
|
||||
padding=(12, 10),
|
||||
borderwidth=0,
|
||||
anchor="w",
|
||||
)
|
||||
style.map(
|
||||
"Sidebar.TButton",
|
||||
background=[
|
||||
("active", _mix(primary, "#ffffff", 0.10)),
|
||||
("pressed", _mix(primary, "#000000", 0.10)),
|
||||
],
|
||||
)
|
||||
style.configure(
|
||||
"SidebarSelected.TButton",
|
||||
background=_mix(primary, "#000000", 0.18),
|
||||
foreground="#ffffff",
|
||||
font=("微软雅黑", 10, "bold"),
|
||||
padding=(12, 10),
|
||||
borderwidth=0,
|
||||
anchor="w",
|
||||
)
|
||||
style.map(
|
||||
"SidebarSelected.TButton",
|
||||
background=[
|
||||
("active", _mix(primary, "#000000", 0.10)),
|
||||
("pressed", _mix(primary, "#000000", 0.25)),
|
||||
],
|
||||
)
|
||||
@@ -14,50 +14,129 @@ if TYPE_CHECKING:
|
||||
from pqAutomationApp import PQAutomationApp
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 内部工具:现代化卡片容器
|
||||
# ---------------------------------------------------------------------------
|
||||
def _make_card(parent, icon: str, title: str) -> ttk.Frame:
|
||||
"""构造一个"卡片"容器(圆角描边 + 顶部图标/标题)。
|
||||
|
||||
返回值是 outer card frame;通过 ``outer._body`` 拿到内部 body Frame,
|
||||
旧代码里习惯把控件挂在 ``self.connection_frame`` 等"卡片本身"上,所以
|
||||
保留 outer 作为对外别名,但实际控件父级换成 body 以避开边框 padding。
|
||||
"""
|
||||
outer = ttk.Frame(parent, style="Card.TFrame", padding=12)
|
||||
header = ttk.Frame(outer, style="CardInner.TFrame")
|
||||
header.pack(fill="x", pady=(0, 8))
|
||||
ttk.Label(header, text=icon, style="CardIcon.TLabel").pack(side="left", padx=(0, 8))
|
||||
ttk.Label(header, text=title, style="CardTitle.TLabel").pack(side="left")
|
||||
body = ttk.Frame(outer, style="CardInner.TFrame")
|
||||
body.pack(fill="both", expand=True)
|
||||
outer._body = body # type: ignore[attr-defined]
|
||||
return outer
|
||||
|
||||
|
||||
def create_floating_config_panel(self: "PQAutomationApp"):
|
||||
"""创建右上角悬浮配置框"""
|
||||
"""创建顶部"配置项"现代化折叠面板。
|
||||
|
||||
布局变化(vs 旧版):
|
||||
- 用 Unicode chevron + 整条 header 可点击折叠/展开;
|
||||
- header 上额外显示折叠状态预览(``config_preview_var``);
|
||||
- header 右侧承载常驻操作工具条(开始/停止/保存 等),不再放中部;
|
||||
- 内部三个区段从 LabelFrame 改为统一的 Card 样式。
|
||||
"""
|
||||
cf = CollapsingFrame(self.control_frame_top)
|
||||
cf.pack(fill="both")
|
||||
# 创建悬浮框主容器
|
||||
self._config_collapsing = cf
|
||||
|
||||
# 配置项主体容器(卡片宿主)
|
||||
self.config_panel_frame = ttk.Frame(cf)
|
||||
cf.add(self.config_panel_frame, title="配置项")
|
||||
|
||||
# 创建一个统一的frame来替代选项卡控件
|
||||
# 折叠预览:呈现"测试类型 · 已选测试项"
|
||||
self.config_preview_var = tk.StringVar(value="")
|
||||
|
||||
# header 右侧工具条占位 —— create_operation_frame 之后向这里挂按钮
|
||||
self.toolbar_actions_frame: ttk.Frame | None = None
|
||||
|
||||
def _header_actions(parent: ttk.Frame):
|
||||
# 暴露给 create_operation_frame 使用
|
||||
self.toolbar_actions_frame = parent
|
||||
|
||||
cf.add(
|
||||
self.config_panel_frame,
|
||||
title="配置项",
|
||||
preview_textvariable=self.config_preview_var,
|
||||
header_actions=_header_actions,
|
||||
)
|
||||
|
||||
# 卡片三栏
|
||||
self.config_content_frame = ttk.Frame(self.config_panel_frame)
|
||||
self.config_content_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
self.config_content_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
|
||||
|
||||
# 创建一个横向排列的Frame
|
||||
config_row_frame = ttk.Frame(self.config_content_frame)
|
||||
config_row_frame.pack(fill=tk.X, expand=False, padx=5, pady=5)
|
||||
config_row_frame.pack(fill=tk.X, expand=False)
|
||||
|
||||
# 创建连接内容区域
|
||||
self.connection_frame = ttk.LabelFrame(config_row_frame, text="设备连接")
|
||||
self.connection_frame.pack(
|
||||
side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5
|
||||
)
|
||||
# 设备连接 卡片
|
||||
connection_card = _make_card(config_row_frame, icon="\U0001F4E1", title="设备连接") # 📡
|
||||
connection_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6))
|
||||
self.connection_frame = connection_card._body # type: ignore[attr-defined]
|
||||
|
||||
# 创建测试项目区域
|
||||
self.test_items_frame = ttk.LabelFrame(config_row_frame, text="测试项目")
|
||||
self.test_items_frame.pack(
|
||||
side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5
|
||||
)
|
||||
# 测试项目 卡片
|
||||
test_items_card = _make_card(config_row_frame, icon="\u2714", title="测试项目") # ✔
|
||||
test_items_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=6)
|
||||
self.test_items_frame = test_items_card._body # type: ignore[attr-defined]
|
||||
|
||||
# 创建信号格式区域
|
||||
self.signal_format_frame = ttk.LabelFrame(config_row_frame, text="信号格式")
|
||||
self.signal_format_frame.pack(
|
||||
side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5
|
||||
)
|
||||
# 信号格式 卡片
|
||||
signal_card = _make_card(config_row_frame, icon="\u2699", title="信号格式") # ⚙
|
||||
signal_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(6, 0))
|
||||
self.signal_format_frame = signal_card._body # type: ignore[attr-defined]
|
||||
|
||||
# 创建连接内容
|
||||
# 创建卡片内部内容(沿用旧函数,父级已是 body Frame)
|
||||
self.create_connection_content()
|
||||
# 创建测试项目内容
|
||||
self.create_test_items_content()
|
||||
# 创建信号格式内容
|
||||
self.create_signal_format_content()
|
||||
|
||||
# 默认收起 —— 与旧版行为保持一致
|
||||
self.config_panel_frame.grid_remove()
|
||||
self.config_panel_frame.btn.configure(image="closed")
|
||||
|
||||
# 初始化预览文本
|
||||
refresh_config_preview(self)
|
||||
|
||||
|
||||
def refresh_config_preview(self: "PQAutomationApp") -> None:
|
||||
"""根据当前测试类型 + 已选测试项刷新折叠预览。"""
|
||||
if not hasattr(self, "config_preview_var"):
|
||||
return
|
||||
type_labels = {
|
||||
"screen_module": "屏模组",
|
||||
"sdr_movie": "SDR Movie",
|
||||
"hdr_movie": "HDR Movie",
|
||||
}
|
||||
current_type = getattr(self.config, "current_test_type", "")
|
||||
type_label = type_labels.get(current_type, "")
|
||||
|
||||
item_labels = []
|
||||
if current_type and hasattr(self, "test_items"):
|
||||
info = self.test_items.get(current_type, {})
|
||||
label_map = {code: name for name, code in info.get("items", [])}
|
||||
if hasattr(self, "test_vars"):
|
||||
for code, var in self.test_vars.items():
|
||||
try:
|
||||
if var.get():
|
||||
item_labels.append(label_map.get(code, code))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
parts = []
|
||||
if type_label:
|
||||
parts.append(f"[{type_label}]")
|
||||
if item_labels:
|
||||
# 限制宽度,避免顶部条过挤
|
||||
shown = item_labels[:5]
|
||||
suffix = " \u2026" if len(item_labels) > 5 else ""
|
||||
parts.append(" \u00b7 ".join(shown) + suffix)
|
||||
self.config_preview_var.set(" ".join(parts))
|
||||
|
||||
|
||||
def create_test_items_content(self: "PQAutomationApp"):
|
||||
"""创建测试项目选项卡内容"""
|
||||
@@ -109,13 +188,17 @@ def create_signal_format_content(self: "PQAutomationApp"):
|
||||
|
||||
# ==================== 屏模组格式设置 ====================
|
||||
self.screen_module_signal_frame = ttk.Frame(self.signal_tabs)
|
||||
self.screen_module_signal_frame.grid_columnconfigure(0, weight=1)
|
||||
self.screen_module_signal_frame.grid_columnconfigure(0, weight=0)
|
||||
self.screen_module_signal_frame.grid_columnconfigure(1, weight=1)
|
||||
self.signal_tabs.add(self.screen_module_signal_frame, text="屏模组测试")
|
||||
|
||||
screen_cfg = self.config.current_test_types.get("screen_module", {})
|
||||
|
||||
self.screen_module_timing_var = tk.StringVar(
|
||||
value=self.config.current_test_types[self.config.current_test_type][
|
||||
"timing"
|
||||
]
|
||||
value=screen_cfg.get("timing", "DMT 1920x 1080 @ 60Hz")
|
||||
)
|
||||
ttk.Label(self.screen_module_signal_frame, text="分辨率:").grid(
|
||||
row=0, column=0, sticky=tk.W, padx=5, pady=2
|
||||
)
|
||||
screen_module_timing_combo = ttk.Combobox(
|
||||
self.screen_module_signal_frame,
|
||||
@@ -126,7 +209,83 @@ def create_signal_format_content(self: "PQAutomationApp"):
|
||||
screen_module_timing_combo.bind(
|
||||
"<<ComboboxSelected>>", self.on_screen_module_timing_changed
|
||||
)
|
||||
screen_module_timing_combo.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
||||
screen_module_timing_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
|
||||
|
||||
ttk.Label(self.screen_module_signal_frame, text="色彩空间:").grid(
|
||||
row=1, column=0, sticky=tk.W, padx=5, pady=2
|
||||
)
|
||||
self.screen_module_color_space_var = tk.StringVar(
|
||||
value=screen_cfg.get("colorimetry", "sRGB")
|
||||
)
|
||||
screen_module_color_space_combo = ttk.Combobox(
|
||||
self.screen_module_signal_frame,
|
||||
textvariable=self.screen_module_color_space_var,
|
||||
values=["sRGB", "BT.709", "BT.601", "BT.2020", "DCI-P3"],
|
||||
width=10,
|
||||
state="readonly",
|
||||
)
|
||||
screen_module_color_space_combo.bind(
|
||||
"<<ComboboxSelected>>", self.on_screen_module_signal_format_changed
|
||||
)
|
||||
screen_module_color_space_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
|
||||
|
||||
ttk.Label(self.screen_module_signal_frame, text="数据范围:").grid(
|
||||
row=2, column=0, sticky=tk.W, padx=5, pady=2
|
||||
)
|
||||
self.screen_module_data_range_var = tk.StringVar(
|
||||
value=screen_cfg.get("data_range", UCDEnum.SignalFormat.DataRange.FULL)
|
||||
)
|
||||
screen_module_data_range_combo = ttk.Combobox(
|
||||
self.screen_module_signal_frame,
|
||||
textvariable=self.screen_module_data_range_var,
|
||||
values=UCDEnum.SignalFormat.DataRange.get_list(),
|
||||
width=10,
|
||||
state="readonly",
|
||||
)
|
||||
screen_module_data_range_combo.bind(
|
||||
"<<ComboboxSelected>>", self.on_screen_module_signal_format_changed
|
||||
)
|
||||
screen_module_data_range_combo.grid(row=2, column=1, sticky=tk.W, padx=5, pady=2)
|
||||
|
||||
default_screen_bpc = int(screen_cfg.get("bpc", 8))
|
||||
default_screen_bit_depth = (
|
||||
f"{default_screen_bpc}bit"
|
||||
if f"{default_screen_bpc}bit" in UCDEnum.SignalFormat.BitDepth.get_list()
|
||||
else UCDEnum.SignalFormat.BitDepth.BIT_8
|
||||
)
|
||||
ttk.Label(self.screen_module_signal_frame, text="编码位深:").grid(
|
||||
row=3, column=0, sticky=tk.W, padx=5, pady=2
|
||||
)
|
||||
self.screen_module_bit_depth_var = tk.StringVar(value=default_screen_bit_depth)
|
||||
screen_module_bit_depth_combo = ttk.Combobox(
|
||||
self.screen_module_signal_frame,
|
||||
textvariable=self.screen_module_bit_depth_var,
|
||||
values=UCDEnum.SignalFormat.BitDepth.get_list(),
|
||||
width=10,
|
||||
state="readonly",
|
||||
)
|
||||
screen_module_bit_depth_combo.bind(
|
||||
"<<ComboboxSelected>>", self.on_screen_module_signal_format_changed
|
||||
)
|
||||
screen_module_bit_depth_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
|
||||
|
||||
ttk.Label(self.screen_module_signal_frame, text="色彩格式:").grid(
|
||||
row=4, column=0, sticky=tk.W, padx=5, pady=2
|
||||
)
|
||||
self.screen_module_output_format_var = tk.StringVar(
|
||||
value=screen_cfg.get("color_format", UCDEnum.SignalFormat.OutputFormat.RGB)
|
||||
)
|
||||
screen_module_output_format_combo = ttk.Combobox(
|
||||
self.screen_module_signal_frame,
|
||||
textvariable=self.screen_module_output_format_var,
|
||||
values=UCDEnum.SignalFormat.OutputFormat.get_list(),
|
||||
width=10,
|
||||
state="readonly",
|
||||
)
|
||||
screen_module_output_format_combo.bind(
|
||||
"<<ComboboxSelected>>", self.on_screen_module_signal_format_changed
|
||||
)
|
||||
screen_module_output_format_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
|
||||
|
||||
# ==================== SDR信号格式设置 ====================
|
||||
self.sdr_signal_frame = ttk.Frame(self.signal_tabs)
|
||||
@@ -578,57 +737,87 @@ def update_config_info_display(self: "PQAutomationApp"):
|
||||
|
||||
|
||||
def create_operation_frame(self: "PQAutomationApp"):
|
||||
"""创建操作按钮区域"""
|
||||
operation_frame = ttk.Frame(self.control_frame_top)
|
||||
operation_frame.pack(fill=tk.X, padx=5, pady=10)
|
||||
"""创建操作按钮区域。
|
||||
|
||||
新布局:按钮挂到配置项 header 右侧常驻工具条 ``self.toolbar_actions_frame``。
|
||||
若该容器尚未创建(极端情况下顺序异常),回退到原 control_frame_top 位置,
|
||||
确保按钮始终可见、调用方代码不破。
|
||||
"""
|
||||
parent = getattr(self, "toolbar_actions_frame", None)
|
||||
fallback = parent is None
|
||||
if fallback:
|
||||
parent = ttk.Frame(self.control_frame_top)
|
||||
parent.pack(fill=tk.X, padx=5, pady=8)
|
||||
|
||||
# 用 bootstyle 取代旧 style="xxx.TButton",跟随主题;
|
||||
# 给按钮统一加 padding,触摸友好。
|
||||
btn_pad = dict(padx=4, pady=0)
|
||||
|
||||
self.start_btn = ttk.Button(
|
||||
operation_frame,
|
||||
text="开始测试",
|
||||
parent,
|
||||
text="\u25b6 开始测试",
|
||||
command=self.start_test,
|
||||
style="success.TButton",
|
||||
bootstyle="success",
|
||||
padding=(12, 6),
|
||||
takefocus=False,
|
||||
)
|
||||
self.start_btn.pack(side=tk.LEFT, padx=5)
|
||||
self.start_btn.pack(side=tk.LEFT, **btn_pad)
|
||||
|
||||
self.simulate_btn = ttk.Button(
|
||||
operation_frame,
|
||||
parent,
|
||||
text="模拟测试",
|
||||
command=self.run_simulation_test,
|
||||
style="warning.TButton",
|
||||
bootstyle="warning-outline",
|
||||
padding=(12, 6),
|
||||
takefocus=False,
|
||||
)
|
||||
self.simulate_btn.pack(side=tk.LEFT, padx=5)
|
||||
self.simulate_btn.pack(side=tk.LEFT, **btn_pad)
|
||||
|
||||
self.stop_btn = ttk.Button(
|
||||
operation_frame,
|
||||
text="停止测试",
|
||||
parent,
|
||||
text="\u25a0 停止",
|
||||
command=self.stop_test,
|
||||
style="danger.TButton",
|
||||
bootstyle="danger",
|
||||
padding=(12, 6),
|
||||
state=tk.DISABLED,
|
||||
takefocus=False,
|
||||
)
|
||||
self.stop_btn.pack(side=tk.LEFT, padx=5)
|
||||
self.stop_btn.pack(side=tk.LEFT, **btn_pad)
|
||||
|
||||
# 分隔
|
||||
ttk.Separator(parent, orient="vertical").pack(side=tk.LEFT, fill="y", padx=8, pady=4)
|
||||
|
||||
self.save_btn = ttk.Button(
|
||||
operation_frame,
|
||||
parent,
|
||||
text="保存结果",
|
||||
command=self.save_results,
|
||||
bootstyle="info-outline",
|
||||
padding=(12, 6),
|
||||
state=tk.DISABLED,
|
||||
takefocus=False,
|
||||
)
|
||||
self.save_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.clear_config_btn = ttk.Button(
|
||||
operation_frame,
|
||||
text="清理配置",
|
||||
command=self.clear_config_file,
|
||||
)
|
||||
self.clear_config_btn.pack(side=tk.LEFT, padx=5)
|
||||
self.save_btn.pack(side=tk.LEFT, **btn_pad)
|
||||
|
||||
self.custom_btn = ttk.Button(
|
||||
operation_frame,
|
||||
parent,
|
||||
text="客户模版",
|
||||
command=self.start_custom_template_test,
|
||||
style="info.TButton",
|
||||
bootstyle="info",
|
||||
padding=(12, 6),
|
||||
takefocus=False,
|
||||
)
|
||||
self.custom_btn.pack(side=tk.LEFT, padx=5)
|
||||
self.custom_btn.pack(side=tk.LEFT, **btn_pad)
|
||||
|
||||
self.clear_config_btn = ttk.Button(
|
||||
parent,
|
||||
text="清理配置",
|
||||
command=self.clear_config_file,
|
||||
bootstyle="secondary-outline",
|
||||
padding=(10, 6),
|
||||
takefocus=False,
|
||||
)
|
||||
self.clear_config_btn.pack(side=tk.LEFT, **btn_pad)
|
||||
|
||||
self.update_custom_button_visibility()
|
||||
|
||||
|
||||
@@ -656,8 +845,10 @@ def on_screen_module_timing_changed(self: "PQAutomationApp", event=None):
|
||||
if refresh_rate >= 120:
|
||||
self.log_gui.log(" ℹ️ 检测到高刷新率", level="info")
|
||||
|
||||
# 更新配置
|
||||
self.config.set_current_timing(selected_timing)
|
||||
# 更新屏模组配置(独立于 current_test_type)
|
||||
self.config.current_test_types.setdefault("screen_module", {})[
|
||||
"timing"
|
||||
] = selected_timing
|
||||
|
||||
# 如果正在测试,提示用户
|
||||
if self.testing:
|
||||
@@ -670,6 +861,50 @@ def on_screen_module_timing_changed(self: "PQAutomationApp", event=None):
|
||||
self.log_gui.log(f"屏模组信号格式更改失败: {str(e)}", level="error")
|
||||
|
||||
|
||||
def on_screen_module_signal_format_changed(self: "PQAutomationApp", event=None):
|
||||
"""屏模组 ColorInfo 相关选项变更回调。"""
|
||||
try:
|
||||
color_space = self.screen_module_color_space_var.get()
|
||||
data_range = self.screen_module_data_range_var.get()
|
||||
bit_depth = self.screen_module_bit_depth_var.get()
|
||||
output_format = self.screen_module_output_format_var.get()
|
||||
|
||||
screen_cfg = self.config.current_test_types.setdefault("screen_module", {})
|
||||
screen_cfg["colorimetry"] = color_space
|
||||
screen_cfg["color_format"] = output_format
|
||||
screen_cfg["bpc"] = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
|
||||
screen_cfg["data_range"] = data_range
|
||||
|
||||
self.log_gui.log(
|
||||
(
|
||||
"屏模组信号格式已更新: "
|
||||
f"色彩空间={color_space}, 数据范围={data_range}, "
|
||||
f"位深={bit_depth}, 色彩格式={output_format}"
|
||||
),
|
||||
level="info",
|
||||
)
|
||||
|
||||
if self.testing:
|
||||
self.log_gui.log("警告: 测试进行中,格式更改将在下次测试时生效", level="error")
|
||||
self.save_pq_config()
|
||||
return
|
||||
|
||||
if getattr(self.ucd, "status", False):
|
||||
ok = self.signal_service.update_signal_format(
|
||||
color_space=color_space,
|
||||
data_range=data_range,
|
||||
bit_depth=bit_depth,
|
||||
output_format=output_format,
|
||||
)
|
||||
if not ok:
|
||||
self.log_gui.log("屏模组信号格式应用到UCD失败", level="error")
|
||||
|
||||
self.save_pq_config()
|
||||
|
||||
except Exception as e:
|
||||
self.log_gui.log(f"屏模组信号格式更改失败: {str(e)}", level="error")
|
||||
|
||||
|
||||
def on_sdr_timing_changed(self: "PQAutomationApp", event=None):
|
||||
"""SDR测试分辨率改变时的回调"""
|
||||
try:
|
||||
@@ -784,6 +1019,9 @@ def update_test_items(self: "PQAutomationApp"):
|
||||
if hasattr(self, "cct_params_frame"):
|
||||
self.toggle_cct_params_frame()
|
||||
|
||||
# 同步刷新 header 折叠预览
|
||||
refresh_config_preview(self)
|
||||
|
||||
|
||||
def on_test_type_change(self: "PQAutomationApp"):
|
||||
"""根据测试类型更新内容区域"""
|
||||
@@ -801,6 +1039,7 @@ class MainLayoutMixin:
|
||||
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||
"""
|
||||
create_floating_config_panel = create_floating_config_panel
|
||||
refresh_config_preview = refresh_config_preview
|
||||
create_test_items_content = create_test_items_content
|
||||
create_signal_format_content = create_signal_format_content
|
||||
create_connection_content = create_connection_content
|
||||
@@ -808,6 +1047,7 @@ class MainLayoutMixin:
|
||||
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_screen_module_signal_format_changed = on_screen_module_signal_format_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
|
||||
|
||||
Reference in New Issue
Block a user