433 lines
14 KiB
Python
433 lines
14 KiB
Python
"""现代化 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 _contrast_text(color: str, *, dark_text: str, light_text: str) -> str:
|
||
return dark_text if _is_dark(color) else light_text
|
||
|
||
|
||
def get_theme_palette() -> dict[str, str]:
|
||
"""返回当前主题的语义色板,供 ttk / tk 自定义控件共用。"""
|
||
style = ttk.Style()
|
||
theme = style.colors
|
||
|
||
bg = theme.bg
|
||
fg = theme.fg
|
||
primary = theme.primary
|
||
secondary = theme.secondary
|
||
success = theme.success
|
||
info = theme.info
|
||
warning = theme.warning
|
||
danger = theme.danger
|
||
dark = theme.dark
|
||
border = theme.border
|
||
inputbg = theme.inputbg
|
||
inputfg = getattr(theme, "inputfg", fg)
|
||
|
||
dark_theme = _is_dark(bg)
|
||
select_bg = getattr(theme, "selectbg", _mix(primary, bg, 0.30 if dark_theme else 0.12))
|
||
select_fg = getattr(theme, "selectfg", "#ffffff" if _is_dark(select_bg) else fg)
|
||
|
||
if dark_theme:
|
||
card_bg = _mix(bg, "#ffffff", 0.04)
|
||
card_border = _mix(bg, fg, 0.18)
|
||
header_fg = _contrast_text(
|
||
"#444A51",
|
||
dark_text="#ffffff",
|
||
light_text="#1a1a1a",
|
||
)
|
||
sidebar_bg = _mix(dark, bg, 0.18)
|
||
sidebar_hover = _mix(sidebar_bg, "#ffffff", 0.07)
|
||
sidebar_selected = _mix(sidebar_bg, "#ffffff", 0.14)
|
||
sidebar_fg = _mix(fg, "#ffffff", 0.04)
|
||
sidebar_muted = _mix(sidebar_fg, sidebar_bg, 0.45)
|
||
muted_fg = _mix(fg, bg, 0.32)
|
||
disabled_fg = _mix(fg, bg, 0.42)
|
||
disabled_bg = _mix(inputbg, bg, 0.18)
|
||
disabled_border = _mix(border, fg, 0.22)
|
||
readonly_bg = _mix(inputbg, "#ffffff", 0.06)
|
||
success_fg = _mix(success, "#ffffff", 0.08)
|
||
warning_fg = _mix(warning, "#ffffff", 0.06)
|
||
info_fg = _mix(info, "#ffffff", 0.06)
|
||
statusbar_bg = _mix(bg, "#ffffff", 0.06)
|
||
tooltip_bg = _mix(inputbg, bg, 0.08)
|
||
tooltip_fg = inputfg
|
||
tooltip_border = _mix(border, fg, 0.20)
|
||
surface_alt_bg = _mix(card_bg, "#ffffff", 0.05)
|
||
surface_hover_bg = _mix(card_bg, "#ffffff", 0.09)
|
||
badge_bg = _mix(danger, bg, 0.12)
|
||
badge_fg = "#ffffff"
|
||
focus = _mix(primary, "#ffffff", 0.18)
|
||
config_bg = _mix("#444A51", bg, 0.30)
|
||
else:
|
||
card_bg = inputbg
|
||
card_border = border
|
||
header_fg = bg
|
||
config_bg = _mix(primary, bg, 0.25)
|
||
sidebar_bg = _mix(primary, bg, 0.82)
|
||
sidebar_hover = _mix(primary, bg, 0.72)
|
||
sidebar_selected = primary
|
||
sidebar_fg = fg
|
||
sidebar_muted = _mix(fg, sidebar_bg, 0.35)
|
||
muted_fg = _mix(fg, bg, 0.38)
|
||
disabled_fg = _mix(fg, bg, 0.55)
|
||
disabled_bg = _mix(bg, border, 0.18)
|
||
disabled_border = _mix(border, bg, 0.18)
|
||
readonly_bg = _mix(inputbg, primary, 0.04)
|
||
success_fg = success
|
||
warning_fg = _mix(warning, fg, 0.18)
|
||
info_fg = info
|
||
statusbar_bg = _mix(bg, dark, 0.04)
|
||
tooltip_bg = inputbg
|
||
tooltip_fg = inputfg
|
||
tooltip_border = border
|
||
surface_alt_bg = _mix(bg, dark, 0.03)
|
||
surface_hover_bg = _mix(bg, dark, 0.05)
|
||
badge_bg = danger
|
||
badge_fg = "#ffffff"
|
||
focus = _mix(primary, bg, 0.20)
|
||
|
||
return {
|
||
"bg": bg,
|
||
"fg": fg,
|
||
"primary": primary,
|
||
"secondary": secondary,
|
||
"success": success,
|
||
"info": info,
|
||
"warning": warning,
|
||
"danger": danger,
|
||
"border": border,
|
||
"input_bg": inputbg,
|
||
"input_fg": inputfg,
|
||
"select_bg": select_bg,
|
||
"select_fg": select_fg,
|
||
"card_bg": card_bg,
|
||
"card_border": card_border,
|
||
"header_fg": header_fg,
|
||
"sidebar_bg": sidebar_bg,
|
||
"sidebar_hover": sidebar_hover,
|
||
"sidebar_selected": sidebar_selected,
|
||
"sidebar_fg": sidebar_fg,
|
||
"sidebar_muted": sidebar_muted,
|
||
"muted_fg": muted_fg,
|
||
"disabled_fg": disabled_fg,
|
||
"disabled_bg": disabled_bg,
|
||
"disabled_border": disabled_border,
|
||
"readonly_bg": readonly_bg,
|
||
"success_fg": success_fg,
|
||
"warning_fg": warning_fg,
|
||
"info_fg": info_fg,
|
||
"statusbar_bg": statusbar_bg,
|
||
"tooltip_bg": tooltip_bg,
|
||
"tooltip_fg": tooltip_fg,
|
||
"tooltip_border": tooltip_border,
|
||
"surface_alt_bg": surface_alt_bg,
|
||
"surface_hover_bg": surface_hover_bg,
|
||
"badge_bg": badge_bg,
|
||
"badge_fg": badge_fg,
|
||
"focus": focus,
|
||
"config_bg": config_bg,
|
||
}
|
||
|
||
|
||
def apply_listbox_theme(widget) -> None:
|
||
"""将 tk.Listbox 颜色同步到当前主题。"""
|
||
palette = get_theme_palette()
|
||
widget.configure(
|
||
background=palette["input_bg"],
|
||
foreground=palette["input_fg"],
|
||
highlightbackground=palette["border"],
|
||
highlightcolor=palette["focus"],
|
||
selectbackground=palette["select_bg"],
|
||
selectforeground=palette["select_fg"],
|
||
disabledforeground=palette["disabled_fg"],
|
||
)
|
||
|
||
|
||
def apply_tooltip_theme(toplevel, label) -> None:
|
||
"""将 tooltip 的 tk.Toplevel / Label 同步到当前主题。"""
|
||
palette = get_theme_palette()
|
||
toplevel.configure(background=palette["tooltip_border"])
|
||
label.configure(
|
||
bg=palette["tooltip_bg"],
|
||
fg=palette["tooltip_fg"],
|
||
highlightbackground=palette["tooltip_border"],
|
||
)
|
||
|
||
|
||
def apply_modern_styles() -> None:
|
||
"""注册或刷新现代化样式集。可在主题切换后再次调用。"""
|
||
style = ttk.Style()
|
||
palette = get_theme_palette()
|
||
|
||
bg = palette["bg"]
|
||
fg = palette["fg"]
|
||
primary = palette["primary"]
|
||
secondary = palette["secondary"]
|
||
info = palette["info"]
|
||
card_bg = palette["card_bg"]
|
||
card_border = palette["card_border"]
|
||
header_bg = palette["config_bg"]
|
||
header_fg = palette["header_fg"]
|
||
dark_theme = _is_dark(bg)
|
||
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 = palette["sidebar_bg"]
|
||
sidebar_hover = palette["sidebar_hover"]
|
||
sidebar_selected = palette["sidebar_selected"]
|
||
sidebar_fg = palette["sidebar_fg"]
|
||
sidebar_muted = palette["sidebar_muted"]
|
||
muted_fg = palette["muted_fg"]
|
||
disabled_fg = palette["disabled_fg"]
|
||
disabled_bg = palette["disabled_bg"]
|
||
disabled_border = palette["disabled_border"]
|
||
readonly_bg = palette["readonly_bg"]
|
||
success_fg = palette["success_fg"]
|
||
warning_fg = palette["warning_fg"]
|
||
|
||
# ---------------- 卡片 ----------------
|
||
style.configure(
|
||
"Card.TFrame",
|
||
background=card_bg,
|
||
bordercolor=card_border,
|
||
relief="solid",
|
||
borderwidth=1,
|
||
)
|
||
style.configure(
|
||
"CardTitle.TLabel",
|
||
background=card_bg,
|
||
foreground=fg,
|
||
font=("Segoe UI", 10, "bold"),
|
||
)
|
||
style.configure(
|
||
"CardBody.TLabel",
|
||
background=card_bg,
|
||
foreground=fg,
|
||
font=("Segoe UI", 9),
|
||
)
|
||
style.configure(
|
||
"CardIcon.TLabel",
|
||
background=card_bg,
|
||
foreground=info if dark_theme else primary,
|
||
font=("Segoe UI", 9, "bold"),
|
||
)
|
||
|
||
# 内嵌于 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=("Segoe UI", 10, "bold"),
|
||
)
|
||
style.configure(
|
||
"ConfigHeaderHover.TLabel",
|
||
background=header_hover_bg,
|
||
foreground=header_fg,
|
||
font=("Segoe UI", 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=("Segoe UI", 9),
|
||
)
|
||
|
||
# ---------------- 通用文字语义 ----------------
|
||
style.configure("Muted.TLabel", background=bg, foreground=muted_fg)
|
||
style.configure("SuccessState.TLabel", background=bg, foreground=success_fg)
|
||
style.configure("WarningState.TLabel", background=bg, foreground=warning_fg)
|
||
style.configure("InfoState.TLabel", background=bg, foreground=palette["info_fg"])
|
||
|
||
# ---------------- 顶部工具条 ----------------
|
||
style.configure("Toolbar.TFrame", background=bg, borderwidth=0)
|
||
# 工具条上的次要按钮(清理配置等)
|
||
style.configure(
|
||
"ToolbarMuted.TButton",
|
||
font=("Segoe UI", 9),
|
||
padding=(10, 5),
|
||
)
|
||
|
||
# ---------------- 区段标题(侧栏 / 卡片外) ----------------
|
||
style.configure(
|
||
"SectionTitle.TLabel",
|
||
background=bg,
|
||
foreground=_mix(fg, bg, 0.45),
|
||
font=("Segoe UI", 8, "bold"),
|
||
)
|
||
style.configure("Sidebar.TFrame", background=sidebar_bg, borderwidth=0)
|
||
# 侧栏内的小区段标题(侧栏背景是 primary)
|
||
style.configure(
|
||
"SidebarSection.TLabel",
|
||
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=palette["badge_fg"],
|
||
font=("Segoe UI Semibold", 12),
|
||
)
|
||
style.configure(
|
||
"SidebarBadge.TLabel",
|
||
background=palette["badge_bg"],
|
||
foreground=palette["badge_fg"],
|
||
font=("微软雅黑", 8, "bold"),
|
||
anchor="center",
|
||
padding=(6, 2),
|
||
)
|
||
|
||
# ---------------- 结果区无边框标题行 ----------------
|
||
style.configure("ResultHeader.TFrame", background=bg, borderwidth=0)
|
||
style.configure(
|
||
"ResultHeader.TLabel",
|
||
background=bg,
|
||
foreground=fg,
|
||
font=("Segoe UI", 11, "bold"),
|
||
)
|
||
|
||
# ---------------- 状态栏 ----------------
|
||
statusbar_bg = palette["statusbar_bg"]
|
||
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=("Segoe UI", 9),
|
||
padding=(10, 4),
|
||
)
|
||
style.configure(
|
||
"StatusBarAccent.TLabel",
|
||
background=statusbar_bg,
|
||
foreground=info if dark_theme else primary,
|
||
font=("Segoe UI", 9, "bold"),
|
||
padding=(10, 4),
|
||
)
|
||
|
||
# ---------------- 深色禁用态 / 只读态增强 ----------------
|
||
style.map(
|
||
"TLabel",
|
||
foreground=[("disabled", disabled_fg)],
|
||
)
|
||
style.map(
|
||
"TButton",
|
||
foreground=[("disabled", disabled_fg)],
|
||
background=[("disabled", disabled_bg)],
|
||
bordercolor=[("disabled", disabled_border)],
|
||
darkcolor=[("disabled", disabled_bg)],
|
||
lightcolor=[("disabled", disabled_bg)],
|
||
)
|
||
style.map(
|
||
"TEntry",
|
||
foreground=[("disabled", disabled_fg)],
|
||
fieldbackground=[("disabled", disabled_bg), ("readonly", readonly_bg)],
|
||
bordercolor=[("disabled", disabled_border), ("readonly", disabled_border)],
|
||
)
|
||
style.map(
|
||
"TCombobox",
|
||
foreground=[("disabled", disabled_fg), ("readonly", fg)],
|
||
fieldbackground=[("disabled", disabled_bg), ("readonly", readonly_bg)],
|
||
bordercolor=[("disabled", disabled_border), ("readonly", disabled_border)],
|
||
arrowcolor=[("disabled", disabled_fg), ("readonly", muted_fg)],
|
||
)
|
||
|
||
# ---------------- Sidebar 按钮(保留兼容名) ----------------
|
||
style.configure(
|
||
"Sidebar.TButton",
|
||
background=sidebar_bg,
|
||
foreground=sidebar_fg,
|
||
font=("Segoe UI", 10),
|
||
padding=(18, 9),
|
||
borderwidth=0,
|
||
anchor="w",
|
||
)
|
||
style.map(
|
||
"Sidebar.TButton",
|
||
background=[
|
||
("active", sidebar_hover),
|
||
("pressed", sidebar_selected),
|
||
],
|
||
foreground=[("active", "#ffffff" if _is_dark(sidebar_hover) else sidebar_fg)],
|
||
)
|
||
style.configure(
|
||
"SidebarSelected.TButton",
|
||
background=sidebar_selected,
|
||
foreground=_contrast_text(sidebar_selected, dark_text=palette["badge_fg"], light_text=sidebar_fg),
|
||
font=("Segoe UI Semibold", 10),
|
||
padding=(18, 9),
|
||
borderwidth=0,
|
||
anchor="w",
|
||
)
|
||
style.map(
|
||
"SidebarSelected.TButton",
|
||
background=[
|
||
("active", _mix(sidebar_selected, "#ffffff", 0.06) if dark_theme else _mix(sidebar_selected, "#000000", 0.06)),
|
||
("pressed", _mix(sidebar_selected, "#000000", 0.08)),
|
||
],
|
||
)
|