添加深色模式

This commit is contained in:
xinzhu.yin
2026-05-28 10:50:52 +08:00
parent f8f2d471e5
commit cf724d60d7
4 changed files with 278 additions and 114 deletions

View File

@@ -45,10 +45,10 @@ def apply_modern_styles() -> None:
fg = theme.fg # 主前景
primary = theme.primary
secondary = theme.secondary
info = theme.info
dark = theme.dark
border = theme.border
inputbg = theme.inputbg
selectbg = theme.selectbg
selectfg = theme.selectfg
dark_theme = _is_dark(bg)
@@ -61,6 +61,11 @@ def apply_modern_styles() -> None:
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)
sidebar_bg = _mix(dark, bg, 0.18) if dark_theme else _mix(primary, "#000000", 0.10)
sidebar_hover = _mix(sidebar_bg, "#ffffff", 0.07) if dark_theme else _mix(sidebar_bg, "#000000", 0.06)
sidebar_selected = _mix(sidebar_bg, "#ffffff", 0.14) if dark_theme else _mix(sidebar_bg, "#000000", 0.10)
sidebar_fg = _mix(fg, bg, 0.05)
sidebar_muted = _mix(fg, sidebar_bg, 0.45)
# ---------------- 卡片 ----------------
style.configure(
@@ -73,20 +78,20 @@ def apply_modern_styles() -> None:
style.configure(
"CardTitle.TLabel",
background=card_bg,
foreground=primary,
font=("微软雅黑", 10, "bold"),
foreground=fg,
font=("Segoe UI", 10, "bold"),
)
style.configure(
"CardBody.TLabel",
background=card_bg,
foreground=fg,
font=("微软雅黑", 9),
font=("Segoe UI", 9),
)
style.configure(
"CardIcon.TLabel",
background=card_bg,
foreground=primary,
font=("Segoe UI Emoji", 14),
foreground=info if dark_theme else primary,
font=("Segoe UI", 9, "bold"),
)
# 内嵌于 Card 的容器(与 Card.TFrame 同背景,无边框)
@@ -107,13 +112,13 @@ def apply_modern_styles() -> None:
"ConfigHeader.TLabel",
background=header_bg,
foreground=header_fg,
font=("微软雅黑", 10, "bold"),
font=("Segoe UI", 10, "bold"),
)
style.configure(
"ConfigHeaderHover.TLabel",
background=header_hover_bg,
foreground=header_fg,
font=("微软雅黑", 10, "bold"),
font=("Segoe UI", 10, "bold"),
)
style.configure(
"ConfigChevron.TLabel",
@@ -125,7 +130,7 @@ def apply_modern_styles() -> None:
"ConfigPreview.TLabel",
background=header_bg,
foreground=preview_fg,
font=("微软雅黑", 9),
font=("Segoe UI", 9),
)
# ---------------- 顶部工具条 ----------------
@@ -133,7 +138,7 @@ def apply_modern_styles() -> None:
# 工具条上的次要按钮(清理配置等)
style.configure(
"ToolbarMuted.TButton",
font=("微软雅黑", 9),
font=("Segoe UI", 9),
padding=(10, 5),
)
@@ -142,14 +147,28 @@ def apply_modern_styles() -> None:
"SectionTitle.TLabel",
background=bg,
foreground=_mix(fg, bg, 0.45),
font=("微软雅黑", 8, "bold"),
font=("Segoe UI", 8, "bold"),
)
style.configure("Sidebar.TFrame", background=sidebar_bg, borderwidth=0)
# 侧栏内的小区段标题(侧栏背景是 primary
style.configure(
"SidebarSection.TLabel",
background=primary,
foreground=_mix("#ffffff", primary, 0.35),
font=("微软雅黑", 8, "bold"),
background=sidebar_bg,
foreground=sidebar_muted,
font=("Segoe UI", 8, "bold"),
)
# 侧栏顶部品牌区
brand_bg = _mix(sidebar_bg, "#ffffff", 0.05) if dark_theme else _mix(sidebar_bg, "#000000", 0.05)
style.configure(
"SidebarBrand.TFrame",
background=brand_bg,
borderwidth=0,
)
style.configure(
"SidebarBrand.TLabel",
background=brand_bg,
foreground="#ffffff",
font=("Segoe UI Semibold", 12),
)
# ---------------- 结果区无边框标题行 ----------------
@@ -158,7 +177,7 @@ def apply_modern_styles() -> None:
"ResultHeader.TLabel",
background=bg,
foreground=fg,
font=("微软雅黑", 11, "bold"),
font=("Segoe UI", 11, "bold"),
)
# ---------------- 状态栏 ----------------
@@ -173,47 +192,48 @@ def apply_modern_styles() -> None:
"StatusBar.TLabel",
background=statusbar_bg,
foreground=statusbar_fg,
font=("微软雅黑", 9),
font=("Segoe UI", 9),
padding=(10, 4),
)
style.configure(
"StatusBarAccent.TLabel",
background=statusbar_bg,
foreground=primary,
font=("微软雅黑", 9, "bold"),
foreground=info if dark_theme else primary,
font=("Segoe UI", 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),
background=sidebar_bg,
foreground=sidebar_fg,
font=("Segoe UI", 10),
padding=(18, 9),
borderwidth=0,
anchor="w",
)
style.map(
"Sidebar.TButton",
background=[
("active", _mix(primary, "#ffffff", 0.10)),
("pressed", _mix(primary, "#000000", 0.10)),
("active", sidebar_hover),
("pressed", sidebar_selected),
],
foreground=[("active", "#ffffff" if dark_theme else sidebar_fg)],
)
style.configure(
"SidebarSelected.TButton",
background=_mix(primary, "#000000", 0.18),
background=sidebar_selected,
foreground="#ffffff",
font=("微软雅黑", 10, "bold"),
padding=(12, 10),
font=("Segoe UI Semibold", 10),
padding=(18, 9),
borderwidth=0,
anchor="w",
)
style.map(
"SidebarSelected.TButton",
background=[
("active", _mix(primary, "#000000", 0.10)),
("pressed", _mix(primary, "#000000", 0.25)),
("active", _mix(sidebar_selected, "#ffffff", 0.06) if dark_theme else _mix(sidebar_selected, "#000000", 0.06)),
("pressed", _mix(sidebar_selected, "#000000", 0.08)),
],
)

View File

@@ -76,17 +76,17 @@ def create_floating_config_panel(self: "PQAutomationApp"):
config_row_frame.pack(fill=tk.X, expand=False)
# 设备连接 卡片
connection_card = _make_card(config_row_frame, icon="\U0001F4E1", title="设备连接") # 📡
connection_card = _make_card(config_row_frame, icon="01", 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]
# 测试项目 卡片
test_items_card = _make_card(config_row_frame, icon="\u2714", title="测试项目") # ✔
test_items_card = _make_card(config_row_frame, icon="02", 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]
# 信号格式 卡片
signal_card = _make_card(config_row_frame, icon="\u2699", title="信号格式") # ⚙
signal_card = _make_card(config_row_frame, icon="03", 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]
@@ -590,15 +590,37 @@ def create_connection_content(self: "PQAutomationApp"):
def create_test_type_frame(self: "PQAutomationApp"):
"""创建测试类型选择区域(侧边栏形式)"""
"""创建测试类型选择区域(侧边栏形式)
新版v3改进
- 深灰分层背景,接近 Calman 的侧栏密度;
- 纯文字按钮,不使用 emoji
- 用更克制的字号 / 间距做层级区分;
- 不再使用 padding=10 硬覆盖(交给 Sidebar.TButton 样式统一管理)。
"""
# 设置测试类型变量
self.test_type_var = tk.StringVar(value="screen_module")
# 创建测试类型按钮并放置在侧边栏
# ---------- 顶部品牌区 ----------
brand = ttk.Frame(self.sidebar_frame, style="SidebarBrand.TFrame")
brand.pack(fill=tk.X, pady=(2, 10))
ttk.Label(
brand,
text="PQ AUTOMATION",
style="SidebarBrand.TLabel",
).pack(side=tk.LEFT, padx=16, pady=11)
# ---------- 分组:测试类型 ----------
ttk.Label(
self.sidebar_frame,
text="测试类型",
style="SidebarSection.TLabel",
).pack(fill=tk.X, padx=16, pady=(2, 6), anchor="w")
test_types = [
("屏模组性能测试", "screen_module"),
("SDR Movie测试", "sdr_movie"),
("HDR Movie测试", "hdr_movie"),
("SDR Movie", "sdr_movie"),
("HDR Movie", "hdr_movie"),
]
for text, type_value in test_types:
@@ -606,87 +628,37 @@ def create_test_type_frame(self: "PQAutomationApp"):
master=self.sidebar_frame,
text=text,
style="Sidebar.TButton",
padding=10,
command=lambda v=type_value: self.change_test_type(v),
takefocus=False,
)
btn.pack(fill=tk.X, padx=0, pady=1)
# 保存按钮引用以便后续更新样式
setattr(self, f"{type_value}_btn", btn)
# 添加分隔线
ttk.Separator(self.sidebar_frame, orient="horizontal").pack(
fill=tk.X, padx=10, pady=10
)
# 只保留日志按钮
self.log_btn = ttk.Button(
# ---------- 分组:工具面板 ----------
ttk.Label(
self.sidebar_frame,
text="测试日志",
text="工具面板",
style="SidebarSection.TLabel",
).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w")
panel_buttons = [
("log_btn", "测试日志", self.toggle_log_panel),
("local_dimming_btn", "Local Dimming", self.toggle_local_dimming_panel),
("ai_image_btn", "AI 图片", self.toggle_ai_image_panel),
("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel),
("gamma_pattern_btn", "Gamma 图案", self.toggle_gamma_pattern_panel),
("calman_btn", "CALMAN 灰阶", self.toggle_calman_panel),
]
for attr, text, cmd in panel_buttons:
btn = ttk.Button(
self.sidebar_frame,
text=text,
style="Sidebar.TButton",
command=self.toggle_log_panel,
command=cmd,
takefocus=False,
)
self.log_btn.pack(fill=tk.X, padx=0, pady=1)
# Local Dimming 测试按钮
self.local_dimming_btn = ttk.Button(
self.sidebar_frame,
text="Local Dimming",
style="Sidebar.TButton",
command=self.toggle_local_dimming_panel,
takefocus=False,
)
self.local_dimming_btn.pack(fill=tk.X, padx=0, pady=1)
# AI 图片对话按钮
self.ai_image_btn = ttk.Button(
self.sidebar_frame,
text="AI 图片",
style="Sidebar.TButton",
command=self.toggle_ai_image_panel,
takefocus=False,
)
self.ai_image_btn.pack(fill=tk.X, padx=0, pady=1)
# self.single_step_btn = ttk.Button(
# self.sidebar_frame,
# text="单步调试",
# style="Sidebar.TButton",
# command=self.toggle_single_step_panel,
# takefocus=False,
# )
# self.single_step_btn.pack(fill=tk.X, padx=0, pady=1)
self.pantone_baseline_btn = ttk.Button(
self.sidebar_frame,
text="Pantone认证摸底测试",
style="Sidebar.TButton",
command=self.toggle_pantone_baseline_panel,
takefocus=False,
)
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)
btn.pack(fill=tk.X, padx=0, pady=1)
setattr(self, attr, btn)
# 测试版水印标签(版本 x.x.0.0 时显示)
from app_version import is_beta_version, APP_VERSION
@@ -701,6 +673,17 @@ def create_test_type_frame(self: "PQAutomationApp"):
)
beta_lbl.pack(fill=tk.X, side=tk.BOTTOM, padx=4, pady=(6, 4))
# ---------- 主题切换(底部固定) ----------
self.theme_toggle_btn = ttk.Button(
self.sidebar_frame,
text="切换深色模式",
style="Sidebar.TButton",
command=self._on_toggle_theme,
takefocus=False,
)
self.theme_toggle_btn.pack(fill=tk.X, padx=0, pady=(0, 2), side=tk.BOTTOM)
_refresh_theme_toggle_label(self)
# 注册面板按钮
if hasattr(self, "panels"):
if "log" in self.panels:
@@ -710,7 +693,7 @@ def create_test_type_frame(self: "PQAutomationApp"):
if "ai_image" in self.panels:
self.panels["ai_image"]["button"] = self.ai_image_btn
if "single_step" in self.panels:
self.panels["single_step"]["button"] = self.single_step_btn
self.panels["single_step"]["button"] = getattr(self, "single_step_btn", None)
if "pantone_baseline" in self.panels:
self.panels["pantone_baseline"]["button"] = self.pantone_baseline_btn
if "gamma_pattern" in self.panels:
@@ -719,6 +702,30 @@ def create_test_type_frame(self: "PQAutomationApp"):
self.panels["calman"]["button"] = self.calman_btn
def _refresh_theme_toggle_label(self: "PQAutomationApp") -> None:
"""根据当前主题刷新切换按钮的文字。"""
from app.views.theme_manager import is_dark
if not hasattr(self, "theme_toggle_btn"):
return
if is_dark():
self.theme_toggle_btn.configure(text="切换浅色模式")
else:
self.theme_toggle_btn.configure(text="切换深色模式")
def _on_toggle_theme(self: "PQAutomationApp") -> None:
"""切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。"""
from app.views.theme_manager import toggle_theme
toggle_theme()
_refresh_theme_toggle_label(self)
# 同步刷新侧栏选中态(高亮样式跟随新色板)
if hasattr(self, "update_sidebar_selection"):
try:
self.update_sidebar_selection()
except Exception:
pass
def update_config_info_display(self: "PQAutomationApp"):
"""更新配置信息显示"""
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
@@ -1046,6 +1053,7 @@ class MainLayoutMixin:
create_test_type_frame = create_test_type_frame
update_config_info_display = update_config_info_display
create_operation_frame = create_operation_frame
_on_toggle_theme = _on_toggle_theme
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

134
app/views/theme_manager.py Normal file
View File

@@ -0,0 +1,134 @@
"""主题管理:注册 Calman 风格深色主题 + 提供运行时切换。
主题在启动时通过 ``apply_initial_theme(root_style)`` 注入到 ttkbootstrap
当前选择持久化到 ``settings/ui_preferences.json``。运行时调用
``toggle_theme(root_style)`` / ``set_theme(root_style, name)`` 可即时切换,
并自动重新调用 ``apply_modern_styles()`` 让自定义样式跟上新色板。
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Optional
from ttkbootstrap.style import Style, ThemeDefinition
from app.views.modern_styles import apply_modern_styles
_PREFS_PATH = Path("settings/ui_preferences.json")
# 浅色主题:沿用旧的 yeti首发布兼容
LIGHT_THEME = "yeti"
# 深色主题:自定义 Calman 风格
DARK_THEME = "calman_dark"
# ----------------------------------------------------------------------
# Calman 风格深色主题色板(参考实测截图取色)
# ----------------------------------------------------------------------
_CALMAN_DARK_COLORS = {
"primary": "#343A41", # 主色改为炭灰,避免大面积亮蓝
"secondary": "#444A51", # 中性深灰(用于 header / 分组背景)
"success": "#4FB960",
"info": "#6FAFCC", # 降低饱和度,只做少量点缀
"warning": "#F2A93B",
"danger": "#E0524A",
"light": "#BFC6CE", # 高亮文本
"dark": "#0D1014", # 最深背景(侧栏底色)
"bg": "#1B1F24", # 主窗口背景
"fg": "#E4E8EE", # 主文本颜色
"selectbg": "#5A6169",
"selectfg": "#E4E8EE",
"border": "#2A2F36",
"inputfg": "#E4E8EE",
"inputbg": "#24292F",
"active": "#2A2F36",
}
def register_themes() -> None:
"""把自定义深色主题注册到 ttkbootstrap可重复调用幂等"""
style = Style()
if DARK_THEME in style.theme_names():
return
theme_def = ThemeDefinition(
name=DARK_THEME,
themetype="dark",
colors=_CALMAN_DARK_COLORS,
)
style.register_theme(theme_def)
# ----------------------------------------------------------------------
# 偏好持久化
# ----------------------------------------------------------------------
def _read_prefs() -> dict:
try:
return json.loads(_PREFS_PATH.read_text(encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError):
return {}
def _write_prefs(data: dict) -> None:
try:
_PREFS_PATH.parent.mkdir(parents=True, exist_ok=True)
_PREFS_PATH.write_text(
json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8"
)
except OSError:
# 写入失败不应影响 UI
pass
def get_saved_theme() -> Optional[str]:
return _read_prefs().get("theme")
def save_theme(name: str) -> None:
prefs = _read_prefs()
prefs["theme"] = name
_write_prefs(prefs)
# ----------------------------------------------------------------------
# 主题应用 / 切换
# ----------------------------------------------------------------------
def apply_initial_theme() -> str:
"""启动时调用:注册主题 + 加载偏好 + 切到对应主题。
返回最终生效的主题名。
"""
register_themes()
name = get_saved_theme() or LIGHT_THEME
style = Style()
if name not in style.theme_names():
name = LIGHT_THEME
style.theme_use(name)
apply_modern_styles()
return name
def set_theme(name: str) -> str:
"""切换到指定主题,持久化偏好,并刷新自定义样式。"""
register_themes()
style = Style()
if name not in style.theme_names():
name = LIGHT_THEME
style.theme_use(name)
apply_modern_styles()
save_theme(name)
return name
def toggle_theme() -> str:
"""在浅 / 深之间切换。返回新主题名。"""
style = Style()
current = style.theme.name
target = DARK_THEME if current != DARK_THEME else LIGHT_THEME
return set_theme(target)
def is_dark() -> bool:
return Style().theme.name == DARK_THEME

View File

@@ -149,12 +149,12 @@ class PQAutomationApp(
self.log_visible = False
# 创建左侧面板
self.left_frame = ttk.Frame(self.main_frame, width=180)
self.left_frame = ttk.Frame(self.main_frame, width=208)
self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=0, pady=5)
self.left_frame.pack_propagate(False)
# 创建左侧导航栏
self.sidebar_frame = ttk.Frame(self.left_frame, bootstyle="primary")
self.sidebar_frame = ttk.Frame(self.left_frame, style="Sidebar.TFrame")
self.sidebar_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=5)
# self.sidebar_frame.pack_propagate(False)
@@ -824,8 +824,10 @@ class PQAutomationApp(
def main():
try:
setup_logging()
# root = tk.Tk()
# 先以浅色主题启动 Window再根据用户偏好含自定义 Calman 深色主题)切换
root = ttk.Window(themename="yeti")
from app.views.theme_manager import apply_initial_theme
apply_initial_theme()
app = PQAutomationApp(root)
# GUI 创建完成后,把 logging 记录同步到日志面板
if hasattr(app, "log_gui"):