feat(ui): add about window

Signed-off-by: 晨叶梦春 <wuziqian211@126.com>
This commit is contained in:
晨叶梦春
2026-08-04 02:53:09 +08:00
parent f57a81d678
commit 2c5dd1c942
11 changed files with 190 additions and 24 deletions
+2 -2
View File
@@ -9,10 +9,10 @@ Copyright (c) Microsoft Corporation.
Licensed under the MIT License.
Source:
https://github.com/microsoft/fluentui-emoji
<https://github.com/microsoft/fluentui-emoji>
Modifications:
None
The full license text is included in:
licenses/Microsoft-Fluent-Emoji.txt
[licenses/Microsoft-Fluent-Emoji.txt](./licenses/Microsoft-Fluent-Emoji.txt)
+2 -2
View File
@@ -9,8 +9,8 @@
| `logo.icns` | macOS `.app` 应用包图标 | `tchMaterial-parser.spec` 中的 `BUNDLE(icon=...)` |
| `icon.ico` | Windows 可执行文件图标 | `tchMaterial-parser.spec` 中的 `EXE(icon=...)` |
| `window_icon.png` | 程序运行时的窗口图标(窗口左上角、Alt+Tab,以及主界面标题旁) | `src/tchmaterial_parser/app.py` 直接读取,`tchMaterial-parser.spec` 负责打包 |
| `sun_3d.png`、`crescent_moon_3d.png`、`last_quarter_moon_3d.png`(来自 [microsoft/fluentui-emoji](https://github.com/microsoft/fluentui-emoji)) | 程序右上角选择主题图标 | `src/tchmaterial_parser/images.py` 直接读取,`tchMaterial-parser.spec` 负责打包 |
| `sun_flat.svg`、`crescent_moon_flat.svg`、`last_quarter_moon_flat.svg`(来自 [microsoft/fluentui-emoji](https://github.com/microsoft/fluentui-emoji)) | 矢量图 | 仅供设计使用,不参与构建 |
| `last_quarter_moon_3d.png`、`sun_3d.png`、`crescent_moon_3d.png`、`information_3d.png`(来自 [microsoft/fluentui-emoji](https://github.com/microsoft/fluentui-emoji)) | 程序右上角选择主题、关于图标 | `src/tchmaterial_parser/images.py` 直接读取,`tchMaterial-parser.spec` 负责打包 |
| `last_quarter_moon_flat.svg`、`sun_flat.svg`、`crescent_moon_flat.svg`、`information_flat.svg`(来自 [microsoft/fluentui-emoji](https://github.com/microsoft/fluentui-emoji)) | 矢量图 | 仅供设计使用,不参与构建 |
## 更换图标时的操作
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#00A6ED"/>
<path d="M16 9.97119C17.0925 9.97119 17.9781 9.08554 17.9781 7.99304C17.9781 6.90054 17.0925 6.01489 16 6.01489C14.9075 6.01489 14.0219 6.90054 14.0219 7.99304C14.0219 9.08554 14.9075 9.97119 16 9.97119Z" fill="white"/>
<path d="M17.6102 13.7179C17.6102 12.7514 16.8267 11.9679 15.8602 11.9679C14.8937 11.9679 14.1102 12.7514 14.1102 13.7179L14.1102 24.3084C14.1102 25.2749 14.8937 26.0584 15.8602 26.0584C16.8267 26.0584 17.6102 25.2749 17.6102 24.3084L17.6102 13.7179Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 655 B

+15 -5
View File
@@ -10,9 +10,10 @@ from PIL import Image, ImageTk
from . import __version__
from .catalog import ResourceHelper
from .config import load_access_token, load_config, save_config
from .images import make_theme_icon_image, render_system_emoji
from .images import make_icon_image, render_system_emoji
from .platform_utils import ctypes, os_name, print_error, resource_path, win32api, win32con, win32gui, win32print
from .ui import download_panel, runtime, theme
from .ui.about_window import show_about_window
from .ui.resource_tree import build_resource_tree
from .ui.runtime import scaled
from .ui.token_window import show_access_token_window
@@ -130,7 +131,7 @@ def main() -> None: # 程序入口:初始化界面并进入主循环
container_frame = ttk.Frame(root, padding=(scaled(24), scaled(18))) # 通过内边距在窗口四周留白
container_frame.pack(expand=True, fill="both")
# 顶部:左侧为图标与标题,右侧为主题切换按钮
# 顶部:左侧为图标与标题,右侧为关于、主题切换按钮
header_frame = ttk.Frame(container_frame)
header_frame.pack(fill="x")
@@ -154,7 +155,7 @@ def main() -> None: # 程序入口:初始化界面并进入主循环
def update_theme_button() -> None: # 更新按钮文字与图标,使其表示点击后将切换到的主题
if theme.switched_theme not in theme_icons:
icon_size = max(scaled(16), 16)
theme_icons[theme.switched_theme] = ImageTk.PhotoImage(make_theme_icon_image(theme.switched_theme, icon_size))
theme_icons[theme.switched_theme] = ImageTk.PhotoImage(make_icon_image(theme.switched_theme, icon_size))
theme_btn.config(
text=" 浅色" if theme.switched_theme == "light" else " 深色" if theme.switched_theme == "dark" else " 跟随系统",
image=theme_icons[theme.switched_theme],
@@ -167,8 +168,17 @@ def main() -> None: # 程序入口:初始化界面并进入主循环
theme.apply_theme(target_theme)
update_theme_button()
theme_btn = ttk.Button(header_frame, style="Toolbutton", command=switch_theme)
theme_btn.pack(side="right", anchor="n")
header_actions = ttk.Frame(header_frame)
header_actions.pack(side="right", anchor="n")
# 关于按钮
about_image = ImageTk.PhotoImage(make_icon_image("about", max(scaled(16), 16)))
about_btn = ttk.Button(header_actions, text=" 关于", image=about_image, compound="left", style="Toolbutton", command=show_about_window)
about_btn.pack(side="left", padx=(0, scaled(8)))
# 切换主题按钮
theme_btn = ttk.Button(header_actions, style="Toolbutton", command=switch_theme)
theme_btn.pack(side="left")
update_theme_button()
# 功能说明
+4 -5
View File
@@ -93,14 +93,13 @@ def render_system_emoji(symbol: str, icon_size: int) -> Image.Image | None: #
continue
return None
def make_theme_icon_image(target_theme: Literal["system", "light", "dark"], icon_size: int) -> Image.Image: # 优先使用系统 Emoji 的原始字形,无法渲染时使用图片
symbol = "☀️" if target_theme == "light" else "🌙" if target_theme == "dark" else "🌗"
emoji_icon = render_system_emoji(symbol, icon_size)
def make_icon_image(icon_name: Literal["system", "light", "dark", "about"], icon_size: int) -> Image.Image: # 优先使用系统 Emoji 的原始字形,无法渲染时使用图片
icon_mapping = { "system": ("🌗", "last_quarter_moon_3d.png"), "light": ("☀️", "sun_3d.png"), "dark": ("🌙", "crescent_moon_3d.png"), "about": ("ℹ️", "information_3d.png") }
emoji_icon = render_system_emoji(icon_mapping[icon_name][0], icon_size)
if emoji_icon is not None:
return emoji_icon
icon_mapping = { "system": "last_quarter_moon_3d.png", "light": "sun_3d.png", "dark": "crescent_moon_3d.png" }
with Image.open(resource_path("assets", icon_mapping[target_theme])) as icon:
with Image.open(resource_path("assets", icon_mapping[icon_name][1])) as icon:
icon_image = icon.copy()
icon_image.thumbnail((icon_size, icon_size), Image.Resampling.LANCZOS)
return icon_image
+146
View File
@@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
# 关于窗口:展示程序、作者、项目地址与第三方许可证信息
import tkinter as tk
from tkinter import messagebox, ttk
import webbrowser
from PIL import Image, ImageTk
from .. import __version__
from ..platform_utils import resource_path
from . import runtime, theme
from .runtime import scaled
from .widgets import center_window, make_card
PROJECT_URL = "https://github.com/happycola233/tchMaterial-parser"
LICENSE_URL = "https://github.com/happycola233/tchMaterial-parser/blob/main/LICENSE"
FLUENT_EMOJI_LICENSE_URL = "https://github.com/microsoft/fluentui-emoji/blob/main/LICENSE"
def open_url(url: str, parent: tk.Toplevel) -> None: # 使用系统默认浏览器打开链接,并在失败时向用户给出明确提示
try:
if not webbrowser.open_new_tab(url):
messagebox.showerror("无法打开链接", f"请复制链接后在浏览器中打开:\n{url}", parent=parent)
except Exception as e:
messagebox.showerror("无法打开链接", f"请复制链接后在浏览器中打开:\n{url}\n\n{e}", parent=parent)
def make_link(parent: tk.Widget, text: str, url: str, window: tk.Toplevel) -> ttk.Label: # 创建可通过鼠标或键盘打开的链接标签
link = ttk.Label(parent, text=text, style="AboutLink.TLabel", cursor="hand2", takefocus=True)
link.bind("<Button-1>", lambda _event: open_url(url, window)) # 鼠标左键
link.bind("<Return>", lambda _event: open_url(url, window)) # 回车键
link.bind("<space>", lambda _event: open_url(url, window)) # 空格键
return link
def show_about_window() -> None: # 打开关于窗口
about_window = tk.Toplevel(runtime.root)
about_window.title("关于")
about_window.resizable(False, False)
about_window.focus()
about_window.transient(runtime.root)
about_window.bind("<Escape>", lambda _event: about_window.destroy())
# 卡片内的标签需要使用与卡片一致的底色;链接通过下划线和强调色表明可点击
style = ttk.Style(about_window)
style.configure(
"AboutCard.TLabel",
background=theme.current_colors["surface"],
foreground=theme.current_colors["fg"],
)
style.configure(
"AboutCardMuted.TLabel",
font="AppCaptionFont",
background=theme.current_colors["surface"],
foreground=theme.current_colors["muted"],
)
style.configure(
"AboutCardStrong.TLabel",
font="AppStrongFont",
background=theme.current_colors["surface"],
foreground=theme.current_colors["fg"],
)
style.configure(
"AboutLink.TLabel",
font="AppLinkFont",
background=theme.current_colors["surface"],
foreground="#3973e6" if theme.current_theme == "light" else "#75a7ff",
)
frame = ttk.Frame(about_window, padding=scaled(24))
frame.pack(fill="both", expand=True)
# 程序图标与名称
header = ttk.Frame(frame)
header.pack(fill="x")
with Image.open(resource_path("assets", "window_icon.png")) as icon:
icon_image = icon.copy()
icon_image.thumbnail((scaled(64), scaled(64)), Image.Resampling.LANCZOS)
icon_photo = ImageTk.PhotoImage(icon_image)
icon_label = ttk.Label(header, image=icon_photo)
icon_label.pack(side="left", padx=(0, scaled(16)))
setattr(icon_label, "_image_ref", icon_photo)
title_frame = ttk.Frame(header)
title_frame.pack(side="left", fill="x", expand=True)
ttk.Label(title_frame, text= "国家中小学智慧教育平台 资源下载工具", style="Title.TLabel").pack(anchor="w")
ttk.Label(title_frame, text=f"版本 {__version__}", style="Caption.TLabel").pack(anchor="w", pady=(scaled(3), 0))
# 作者与项目地址
info_card = make_card(frame, padding=(scaled(16), scaled(12)))
info_card.pack(fill="x", pady=(scaled(18), 0))
info_card.columnconfigure(1, weight=1)
ttk.Label(info_card, text="作者", style="AboutCardMuted.TLabel").grid(
row=0, column=0, sticky="nw", padx=(0, scaled(16))
)
ttk.Label(info_card, text="肥宅水水呀、晨叶梦春 及其他贡献者", style="AboutCard.TLabel").grid(
row=0, column=1, sticky="nw"
)
ttk.Label(info_card, text="仓库", style="AboutCardMuted.TLabel").grid(
row=1, column=0, sticky="nw", padx=(0, scaled(16)), pady=(scaled(8), 0)
)
project_link = make_link(info_card, "happycola233/tchMaterial-parser", PROJECT_URL, about_window)
project_link.grid(row=1, column=1, sticky="nw", pady=(scaled(8), 0))
ttk.Label(info_card, text="许可证", style="AboutCardMuted.TLabel").grid(
row=2, column=0, sticky="nw", padx=(0, scaled(16)), pady=(scaled(8), 0)
)
license_link = make_link(info_card, "MIT License", LICENSE_URL, about_window)
license_link.grid(row=2, column=1, sticky="nw", pady=(scaled(8), 0))
# 第三方资源与许可证说明
license_card = make_card(frame, padding=(scaled(16), scaled(12)))
license_card.pack(fill="x", pady=(scaled(12), 0))
ttk.Label(license_card, text="第三方许可证", style="AboutCardStrong.TLabel").pack(anchor="w")
ttk.Label(
license_card,
text="本软件使用了 Microsoft Fluent Emoji 的部分图像资源,依据 MIT 许可证授权使用。\n相关图像资源及其版权归 Microsoft 所有。",
style="AboutCard.TLabel",
justify="left",
wraplength=scaled(600),
).pack(anchor="w", pady=(scaled(6), 0))
license_link = make_link(license_card, "查看 Microsoft Fluent Emoji 许可证 ↗", FLUENT_EMOJI_LICENSE_URL, about_window)
license_link.pack(anchor="w", pady=(scaled(8), 0))
# 声明
notice_card = make_card(frame, padding=(scaled(16), scaled(12)))
notice_card.pack(fill="x", pady=(scaled(12), 0))
ttk.Label(notice_card, text="声明", style="AboutCardStrong.TLabel").pack(anchor="w")
ttk.Label(
notice_card,
text="本软件采用 MIT 许可证开源,官方版本永久免费。\n谨防第三方付费倒卖、捆绑软件或冒充官方等行为;任何再分发行为均须保留原有版权及许可证声明。",
style="AboutCard.TLabel",
justify="left",
wraplength=scaled(600),
).pack(anchor="w", pady=(scaled(6), 0))
button_frame = ttk.Frame(frame)
button_frame.pack(fill="x", pady=(scaled(16), 0))
ttk.Button(
button_frame, text="关闭", style=theme.ACCENT_BUTTON_STYLE, command=about_window.destroy
).pack(side="right")
center_window(about_window, runtime.root)
theme.apply_titlebar_theme(about_window)
about_window.lift()
+1 -1
View File
@@ -121,7 +121,7 @@ def download_file(url: str, save_path: str, chapters: list[dict] | None = None)
ui_call(progress_label.config, text=f"{format_bytes(all_downloaded_size)}/{format_bytes(all_total_size)} ({download_progress:.2f}%) 已下载 {downloaded_number}/{total_number}") # 更新标签以显示当前下载进度
if current_state["total_size"] > 0 and current_state["downloaded_size"] != current_state["total_size"]: # 文件下载不完整
current_state["failed_reason"] = f"文件下载不完整,需下载 {current_state["total_size"]} 字节,实际下载 {current_state["downloaded_size"]} 字节"
current_state["failed_reason"] = f"文件下载不完整,需下载 {current_state['total_size']} 字节,实际下载 {current_state['downloaded_size']} 字节"
current_state["downloaded_size"], current_state["total_size"] = 0, 0
current_state["finished"] = True
try:
+11 -6
View File
@@ -27,12 +27,17 @@ THEME_COLORS = {
ACCENT_BUTTON_STYLE = "Accent.TButton"
SWITCH_STYLE = "Switch.TCheckbutton"
# 本程序使用的命名字体,格式为 字体名称: (基准字号(像素), 是否加粗)
APP_FONTS = { "AppCaptionFont": (12, False), "AppBodyFont": (14, False), "AppStrongFont": (14, True), "AppTitleFont": (20, True) }
# 本程序使用的命名字体,格式为 字体名称: (基准字号(像素), 是否加粗, 是否添加下划线)
APP_FONTS = {
"AppCaptionFont": (12, False, False), "AppBodyFont": (14, False, False), "AppStrongFont": (14, True, False),
"AppTitleFont": (20, True, False), "AppLinkFont": (12, False, True)
}
# sv-ttk 内置的命名字体,需要一并改为中文字体(其默认字体不含中文字形),基准字号与 sv-ttk 原始取值保持一致
SV_FONTS = {
"SunValleyCaptionFont": (12, False), "SunValleyBodyFont": (14, False), "SunValleyBodyStrongFont": (14, True), "SunValleyBodyLargeFont": (18, False),
"SunValleySubtitleFont": (20, True), "SunValleyTitleFont": (28, True), "SunValleyTitleLargeFont": (40, True), "SunValleyDisplayFont": (68, True),
"SunValleyCaptionFont": (12, False, False), "SunValleyBodyFont": (14, False, False),
"SunValleyBodyStrongFont": (14, True, False), "SunValleyBodyLargeFont": (18, False, False),
"SunValleySubtitleFont": (20, True, False), "SunValleyTitleFont": (28, True, False),
"SunValleyTitleLargeFont": (40, True, False), "SunValleyDisplayFont": (68, True, False),
}
def bind_font_family(family: str) -> None: # 由 app.py 在选定界面字体后写入,供 setup_fonts() 使用
@@ -57,9 +62,9 @@ def pick_ui_font_family() -> str: # 选择一个合适的字体
def setup_fonts() -> None: # 创建(或更新)所有命名字体,使其使用中文字体并跟随缩放因子
# 此处直接调用 Tcl 命令而不使用 tkinter.font.Font,因为后者创建的字体会随 Python 对象被垃圾回收而一并删除
existing_fonts: tuple[str, ...] = runtime.root.tk.splitlist(runtime.root.tk.call("font", "names"))
for name, (size, bold) in { **APP_FONTS, **SV_FONTS }.items():
for name, (size, bold, underline) in { **APP_FONTS, **SV_FONTS }.items():
# 字号取负值表示以像素为单位,从而避开 tk scaling 的二次缩放,与 sv-ttk 的取值方式保持一致
options = ("-family", ui_font_family, "-size", -scaled(size), "-weight", "bold" if bold else "normal")
options = ("-family", ui_font_family, "-size", -scaled(size), "-weight", "bold" if bold else "normal", "-underline", 1 if underline else 0)
runtime.root.tk.call("font", "configure" if name in existing_fonts else "create", name, *options)
def detect_system_theme() -> Literal["light", "dark"]: # 获取系统当前使用的是浅色还是深色模式
+2 -2
View File
@@ -17,6 +17,7 @@ def show_access_token_window() -> None: # 打开输入 Access Token 的窗口
# 让窗口自动根据控件自适应尺寸;如需最小尺寸可用 token_window.minsize(...)
token_window.focus() # 自动获得焦点
token_window.grab_set() # 阻止主窗口操作
token_window.transient(runtime.root) # 使窗口依赖于主窗口
token_window.bind("<Escape>", lambda event: token_window.destroy()) # 绑定 Esc 键关闭窗口
# 设置一个 Frame 用于留白,使布局更美观
@@ -64,9 +65,8 @@ def show_access_token_window() -> None: # 打开输入 Access Token 的窗口
help_win.title("获取 Access Token 方法")
help_win.resizable(False, False) # 禁止调整窗口大小
help_win.focus() # 自动获得焦点
help_win.grab_set() # 阻止主窗口操作
help_win.transient(token_window) # 使窗口依赖于主窗口
help_win.bind("<Escape>", lambda event: help_win.destroy()) # 绑定 Esc 键关闭窗口
help_win.bind("<Destroy>", lambda event: token_window.grab_set())
help_frame = ttk.Frame(help_win, padding=scaled(20))
help_frame.pack(fill="both", expand=True)
+2 -1
View File
@@ -9,9 +9,10 @@ is_mac = sys.platform.startswith('darwin')
# 下面 4 个图标文件是程序运行时读取的自有资源,目标目录保持为 assets/,与源码中的相对路径一致
data_files = collect_data_files('sv_ttk') + [
('assets/window_icon.png', 'assets'),
('assets/last_quarter_moon_3d.png', 'assets'),
('assets/sun_3d.png', 'assets'),
('assets/crescent_moon_3d.png', 'assets'),
('assets/last_quarter_moon_3d.png', 'assets'),
('assets/information_3d.png', 'assets'),
]
a = Analysis(