2026-05-28 10:20:17 +08:00
|
|
|
|
"""现代化 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
|
2026-05-28 10:50:52 +08:00
|
|
|
|
info = theme.info
|
|
|
|
|
|
dark = theme.dark
|
2026-05-28 10:20:17 +08:00
|
|
|
|
border = theme.border
|
|
|
|
|
|
inputbg = theme.inputbg
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-05-28 10:50:52 +08:00
|
|
|
|
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)
|
2026-05-29 08:32:21 +08:00
|
|
|
|
# 侧栏背景在浅色主题下也偏深,文字颜色需按侧栏亮度自适应,避免“黑字不明显”。
|
|
|
|
|
|
sidebar_fg = "#F4F8FD" if _is_dark(sidebar_bg) else _mix(fg, bg, 0.05)
|
|
|
|
|
|
sidebar_muted = _mix(sidebar_fg, sidebar_bg, 0.45)
|
2026-05-28 10:20:17 +08:00
|
|
|
|
|
|
|
|
|
|
# ---------------- 卡片 ----------------
|
|
|
|
|
|
style.configure(
|
|
|
|
|
|
"Card.TFrame",
|
|
|
|
|
|
background=card_bg,
|
|
|
|
|
|
bordercolor=card_border,
|
|
|
|
|
|
relief="solid",
|
|
|
|
|
|
borderwidth=1,
|
|
|
|
|
|
)
|
|
|
|
|
|
style.configure(
|
|
|
|
|
|
"CardTitle.TLabel",
|
|
|
|
|
|
background=card_bg,
|
2026-05-28 10:50:52 +08:00
|
|
|
|
foreground=fg,
|
|
|
|
|
|
font=("Segoe UI", 10, "bold"),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
style.configure(
|
|
|
|
|
|
"CardBody.TLabel",
|
|
|
|
|
|
background=card_bg,
|
|
|
|
|
|
foreground=fg,
|
2026-05-28 10:50:52 +08:00
|
|
|
|
font=("Segoe UI", 9),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
style.configure(
|
|
|
|
|
|
"CardIcon.TLabel",
|
|
|
|
|
|
background=card_bg,
|
2026-05-28 10:50:52 +08:00
|
|
|
|
foreground=info if dark_theme else primary,
|
|
|
|
|
|
font=("Segoe UI", 9, "bold"),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 内嵌于 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,
|
2026-05-28 10:50:52 +08:00
|
|
|
|
font=("Segoe UI", 10, "bold"),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
style.configure(
|
|
|
|
|
|
"ConfigHeaderHover.TLabel",
|
|
|
|
|
|
background=header_hover_bg,
|
|
|
|
|
|
foreground=header_fg,
|
2026-05-28 10:50:52 +08:00
|
|
|
|
font=("Segoe UI", 10, "bold"),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
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,
|
2026-05-28 10:50:52 +08:00
|
|
|
|
font=("Segoe UI", 9),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- 顶部工具条 ----------------
|
|
|
|
|
|
style.configure("Toolbar.TFrame", background=bg, borderwidth=0)
|
|
|
|
|
|
# 工具条上的次要按钮(清理配置等)
|
|
|
|
|
|
style.configure(
|
|
|
|
|
|
"ToolbarMuted.TButton",
|
2026-05-28 10:50:52 +08:00
|
|
|
|
font=("Segoe UI", 9),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
padding=(10, 5),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- 区段标题(侧栏 / 卡片外) ----------------
|
|
|
|
|
|
style.configure(
|
|
|
|
|
|
"SectionTitle.TLabel",
|
|
|
|
|
|
background=bg,
|
|
|
|
|
|
foreground=_mix(fg, bg, 0.45),
|
2026-05-28 10:50:52 +08:00
|
|
|
|
font=("Segoe UI", 8, "bold"),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
)
|
2026-05-28 10:50:52 +08:00
|
|
|
|
style.configure("Sidebar.TFrame", background=sidebar_bg, borderwidth=0)
|
2026-05-28 10:20:17 +08:00
|
|
|
|
# 侧栏内的小区段标题(侧栏背景是 primary)
|
|
|
|
|
|
style.configure(
|
|
|
|
|
|
"SidebarSection.TLabel",
|
2026-05-28 10:50:52 +08:00
|
|
|
|
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),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- 结果区无边框标题行 ----------------
|
|
|
|
|
|
style.configure("ResultHeader.TFrame", background=bg, borderwidth=0)
|
|
|
|
|
|
style.configure(
|
|
|
|
|
|
"ResultHeader.TLabel",
|
|
|
|
|
|
background=bg,
|
|
|
|
|
|
foreground=fg,
|
2026-05-28 10:50:52 +08:00
|
|
|
|
font=("Segoe UI", 11, "bold"),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- 状态栏 ----------------
|
|
|
|
|
|
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,
|
2026-05-28 10:50:52 +08:00
|
|
|
|
font=("Segoe UI", 9),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
padding=(10, 4),
|
|
|
|
|
|
)
|
|
|
|
|
|
style.configure(
|
|
|
|
|
|
"StatusBarAccent.TLabel",
|
|
|
|
|
|
background=statusbar_bg,
|
2026-05-28 10:50:52 +08:00
|
|
|
|
foreground=info if dark_theme else primary,
|
|
|
|
|
|
font=("Segoe UI", 9, "bold"),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
padding=(10, 4),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- Sidebar 按钮(保留兼容名) ----------------
|
|
|
|
|
|
style.configure(
|
|
|
|
|
|
"Sidebar.TButton",
|
2026-05-28 10:50:52 +08:00
|
|
|
|
background=sidebar_bg,
|
|
|
|
|
|
foreground=sidebar_fg,
|
|
|
|
|
|
font=("Segoe UI", 10),
|
|
|
|
|
|
padding=(18, 9),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
borderwidth=0,
|
|
|
|
|
|
anchor="w",
|
|
|
|
|
|
)
|
|
|
|
|
|
style.map(
|
|
|
|
|
|
"Sidebar.TButton",
|
|
|
|
|
|
background=[
|
2026-05-28 10:50:52 +08:00
|
|
|
|
("active", sidebar_hover),
|
|
|
|
|
|
("pressed", sidebar_selected),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
],
|
2026-05-29 08:32:21 +08:00
|
|
|
|
foreground=[("active", "#ffffff" if _is_dark(sidebar_hover) else sidebar_fg)],
|
2026-05-28 10:20:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
style.configure(
|
|
|
|
|
|
"SidebarSelected.TButton",
|
2026-05-28 10:50:52 +08:00
|
|
|
|
background=sidebar_selected,
|
2026-05-28 10:20:17 +08:00
|
|
|
|
foreground="#ffffff",
|
2026-05-28 10:50:52 +08:00
|
|
|
|
font=("Segoe UI Semibold", 10),
|
|
|
|
|
|
padding=(18, 9),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
borderwidth=0,
|
|
|
|
|
|
anchor="w",
|
|
|
|
|
|
)
|
|
|
|
|
|
style.map(
|
|
|
|
|
|
"SidebarSelected.TButton",
|
|
|
|
|
|
background=[
|
2026-05-28 10:50:52 +08:00
|
|
|
|
("active", _mix(sidebar_selected, "#ffffff", 0.06) if dark_theme else _mix(sidebar_selected, "#000000", 0.06)),
|
|
|
|
|
|
("pressed", _mix(sidebar_selected, "#000000", 0.08)),
|
2026-05-28 10:20:17 +08:00
|
|
|
|
],
|
|
|
|
|
|
)
|