Update
This commit is contained in:
82
ui/utilities/__init__.py
Normal file
82
ui/utilities/__init__.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
UI Utilities Package
|
||||
-------------------
|
||||
UI工具包,包含对话框、图标管理、工具函数等
|
||||
"""
|
||||
|
||||
from .custom_dialogs import (
|
||||
MessageDialog,
|
||||
InputDialog,
|
||||
show_info,
|
||||
show_warning,
|
||||
show_error,
|
||||
ask_yes_no,
|
||||
ask_string
|
||||
)
|
||||
|
||||
from .base_dialog import BaseDialog
|
||||
|
||||
# 图标工具
|
||||
from .icon_utils import (
|
||||
get_icons_dir,
|
||||
get_icon_path,
|
||||
set_window_icon,
|
||||
setup_dialog_icon
|
||||
)
|
||||
|
||||
# 窗口工具
|
||||
from .window_utils import (
|
||||
set_dark_title_bar,
|
||||
setup_dialog_window
|
||||
)
|
||||
|
||||
# 颜色工具
|
||||
from .color_utils import (
|
||||
darken_color,
|
||||
lighten_color,
|
||||
hex_to_rgb,
|
||||
rgb_to_hex
|
||||
)
|
||||
|
||||
# 向后兼容已迁移到具体模块,不再需要utils.py
|
||||
|
||||
from .icon_manager import IconManager
|
||||
from .window_manager import WindowManager
|
||||
from .ui_helpers import UIHelpers
|
||||
|
||||
__all__ = [
|
||||
# 对话框类
|
||||
'MessageDialog',
|
||||
'InputDialog',
|
||||
'BaseDialog',
|
||||
|
||||
# 对话框便捷函数
|
||||
'show_info',
|
||||
'show_warning',
|
||||
'show_error',
|
||||
'ask_yes_no',
|
||||
'ask_string',
|
||||
|
||||
# 图标工具函数
|
||||
'get_icons_dir',
|
||||
'get_icon_path',
|
||||
'set_window_icon',
|
||||
'setup_dialog_icon',
|
||||
|
||||
# 窗口工具函数
|
||||
'set_dark_title_bar',
|
||||
'setup_dialog_window',
|
||||
|
||||
# 颜色工具函数
|
||||
'darken_color',
|
||||
'lighten_color',
|
||||
'hex_to_rgb',
|
||||
'rgb_to_hex',
|
||||
|
||||
# 管理器类
|
||||
'IconManager',
|
||||
'WindowManager',
|
||||
'UIHelpers'
|
||||
]
|
||||
67
ui/utilities/base_dialog.py
Normal file
67
ui/utilities/base_dialog.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
对话框基类
|
||||
"""
|
||||
import tkinter as tk
|
||||
from .icon_utils import get_icon_path
|
||||
from config.constants import BG_COLOR_DARK
|
||||
import os
|
||||
|
||||
|
||||
class BaseDialog(tk.Toplevel):
|
||||
"""对话框基类,提供通用的初始化和居中功能"""
|
||||
|
||||
def __init__(self, parent, title: str, width: int, height: int):
|
||||
"""
|
||||
初始化对话框
|
||||
|
||||
Args:
|
||||
parent: 父窗口
|
||||
title: 窗口标题
|
||||
width: 窗口宽度
|
||||
height: 窗口高度
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
# 先隐藏窗口
|
||||
self.withdraw()
|
||||
|
||||
# 设置窗口属性
|
||||
self.title(title)
|
||||
self.geometry(f"{width}x{height}")
|
||||
self.resizable(False, False)
|
||||
|
||||
# 设置图标
|
||||
icon_path = get_icon_path()
|
||||
if os.path.exists(icon_path):
|
||||
self.iconbitmap(icon_path)
|
||||
|
||||
# 设置深色主题背景
|
||||
self.configure(bg=BG_COLOR_DARK)
|
||||
|
||||
# 设置为模态窗口
|
||||
self.transient(parent)
|
||||
|
||||
# 居中显示
|
||||
self._center_window()
|
||||
|
||||
def _center_window(self):
|
||||
"""将窗口居中显示"""
|
||||
self.update_idletasks()
|
||||
|
||||
screen_width = self.winfo_screenwidth()
|
||||
screen_height = self.winfo_screenheight()
|
||||
window_width = self.winfo_width()
|
||||
window_height = self.winfo_height()
|
||||
|
||||
x = (screen_width - window_width) // 2
|
||||
y = (screen_height - window_height) // 2
|
||||
|
||||
self.geometry(f"+{x}+{y}")
|
||||
|
||||
def show(self):
|
||||
"""显示对话框"""
|
||||
self.deiconify()
|
||||
self.grab_set()
|
||||
self.wait_window()
|
||||
87
ui/utilities/color_utils.py
Normal file
87
ui/utilities/color_utils.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
颜色工具模块
|
||||
负责颜色处理和转换
|
||||
"""
|
||||
|
||||
|
||||
def darken_color(hex_color: str, factor: float = 0.2) -> str:
|
||||
"""使颜色变暗
|
||||
|
||||
Args:
|
||||
hex_color: 十六进制颜色代码
|
||||
factor: 变暗系数,0-1 之间
|
||||
|
||||
Returns:
|
||||
变暗后的十六进制颜色代码
|
||||
"""
|
||||
# 去除#前缀
|
||||
hex_color = hex_color.lstrip('#')
|
||||
|
||||
# 转换为RGB
|
||||
r = int(hex_color[0:2], 16)
|
||||
g = int(hex_color[2:4], 16)
|
||||
b = int(hex_color[4:6], 16)
|
||||
|
||||
# 降低亮度
|
||||
r = max(0, int(r * (1 - factor)))
|
||||
g = max(0, int(g * (1 - factor)))
|
||||
b = max(0, int(b * (1 - factor)))
|
||||
|
||||
# 转回十六进制
|
||||
return f'#{r:02x}{g:02x}{b:02x}'
|
||||
|
||||
|
||||
def lighten_color(hex_color: str, factor: float = 0.2) -> str:
|
||||
"""使颜色变亮
|
||||
|
||||
Args:
|
||||
hex_color: 十六进制颜色代码
|
||||
factor: 变亮系数,0-1 之间
|
||||
|
||||
Returns:
|
||||
变亮后的十六进制颜色代码
|
||||
"""
|
||||
# 去除#前缀
|
||||
hex_color = hex_color.lstrip('#')
|
||||
|
||||
# 转换为RGB
|
||||
r = int(hex_color[0:2], 16)
|
||||
g = int(hex_color[2:4], 16)
|
||||
b = int(hex_color[4:6], 16)
|
||||
|
||||
# 提高亮度
|
||||
r = min(255, int(r + (255 - r) * factor))
|
||||
g = min(255, int(g + (255 - g) * factor))
|
||||
b = min(255, int(b + (255 - b) * factor))
|
||||
|
||||
# 转回十六进制
|
||||
return f'#{r:02x}{g:02x}{b:02x}'
|
||||
|
||||
|
||||
def hex_to_rgb(hex_color: str) -> tuple:
|
||||
"""将十六进制颜色转换为RGB元组
|
||||
|
||||
Args:
|
||||
hex_color: 十六进制颜色代码
|
||||
|
||||
Returns:
|
||||
RGB颜色元组 (r, g, b)
|
||||
"""
|
||||
hex_color = hex_color.lstrip('#')
|
||||
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
|
||||
|
||||
def rgb_to_hex(r: int, g: int, b: int) -> str:
|
||||
"""将RGB颜色转换为十六进制
|
||||
|
||||
Args:
|
||||
r: 红色分量 (0-255)
|
||||
g: 绿色分量 (0-255)
|
||||
b: 蓝色分量 (0-255)
|
||||
|
||||
Returns:
|
||||
十六进制颜色代码
|
||||
"""
|
||||
return f'#{r:02x}{g:02x}{b:02x}'
|
||||
296
ui/utilities/custom_dialogs.py
Normal file
296
ui/utilities/custom_dialogs.py
Normal file
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
自定义对话框
|
||||
提供与应用主题统一的消息对话框
|
||||
"""
|
||||
import customtkinter as ctk
|
||||
from typing import Optional
|
||||
from .base_dialog import BaseDialog
|
||||
from config.constants import (
|
||||
BG_COLOR_DARK,
|
||||
BUTTON_WIDTH_MEDIUM,
|
||||
BUTTON_HEIGHT_SMALL,
|
||||
FONT_SIZE_TINY,
|
||||
FONT_SIZE_MEDIUM,
|
||||
FONT_SIZE_LARGE
|
||||
)
|
||||
|
||||
|
||||
class MessageDialog(BaseDialog):
|
||||
"""自定义消息对话框"""
|
||||
|
||||
def __init__(self, parent, title: str, message: str, dialog_type: str = "info", **kwargs):
|
||||
"""
|
||||
初始化消息对话框
|
||||
|
||||
Args:
|
||||
parent: 父窗口
|
||||
title: 对话框标题
|
||||
message: 消息内容
|
||||
dialog_type: 对话框类型 (info, warning, error, question)
|
||||
"""
|
||||
super().__init__(parent, title, 400, 200)
|
||||
|
||||
self.result = None
|
||||
self.dialog_type = dialog_type
|
||||
|
||||
# 创建界面
|
||||
self._create_widgets(message)
|
||||
|
||||
# 显示对话框
|
||||
self.show()
|
||||
|
||||
def _create_widgets(self, message: str):
|
||||
"""创建界面组件"""
|
||||
# 主容器
|
||||
main_frame = ctk.CTkFrame(self, fg_color=BG_COLOR_DARK)
|
||||
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
||||
|
||||
# 图标和消息容器
|
||||
content_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
|
||||
content_frame.pack(fill="both", expand=True)
|
||||
|
||||
# 图标
|
||||
icon_text = self._get_icon_text()
|
||||
icon_color = self._get_icon_color()
|
||||
|
||||
icon_label = ctk.CTkLabel(
|
||||
content_frame,
|
||||
text=icon_text,
|
||||
font=ctk.CTkFont(size=40),
|
||||
text_color=icon_color,
|
||||
width=60
|
||||
)
|
||||
icon_label.pack(side="left", padx=(0, 15))
|
||||
|
||||
# 消息文本
|
||||
message_label = ctk.CTkLabel(
|
||||
content_frame,
|
||||
text=message,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_LARGE),
|
||||
wraplength=280,
|
||||
justify="left"
|
||||
)
|
||||
message_label.pack(side="left", fill="both", expand=True)
|
||||
|
||||
# 按钮容器
|
||||
button_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
|
||||
button_frame.pack(side="bottom", pady=(20, 0))
|
||||
|
||||
# 根据对话框类型创建按钮
|
||||
if self.dialog_type == "question":
|
||||
# 是/否按钮
|
||||
no_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="否",
|
||||
command=self._on_no,
|
||||
width=BUTTON_WIDTH_MEDIUM,
|
||||
height=BUTTON_HEIGHT_SMALL,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_MEDIUM),
|
||||
fg_color="#666666",
|
||||
hover_color="#555555"
|
||||
)
|
||||
no_btn.pack(side="left", padx=5)
|
||||
|
||||
yes_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="是",
|
||||
command=self._on_yes,
|
||||
width=BUTTON_WIDTH_MEDIUM,
|
||||
height=BUTTON_HEIGHT_SMALL,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_MEDIUM)
|
||||
)
|
||||
yes_btn.pack(side="left", padx=5)
|
||||
yes_btn.focus_set()
|
||||
else:
|
||||
# 确定按钮
|
||||
ok_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="确定",
|
||||
command=self._on_ok,
|
||||
width=BUTTON_WIDTH_MEDIUM,
|
||||
height=BUTTON_HEIGHT_SMALL,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_MEDIUM)
|
||||
)
|
||||
ok_btn.pack()
|
||||
ok_btn.focus_set()
|
||||
|
||||
# 绑定 Enter 键
|
||||
self.bind("<Return>", lambda e: self._on_ok() if self.dialog_type != "question" else self._on_yes())
|
||||
self.bind("<Escape>", lambda e: self._on_no() if self.dialog_type == "question" else self._on_ok())
|
||||
|
||||
def _get_icon_text(self) -> str:
|
||||
"""获取图标文本"""
|
||||
icons = {
|
||||
"info": "i",
|
||||
"warning": "!",
|
||||
"error": "X",
|
||||
"question": "?"
|
||||
}
|
||||
return icons.get(self.dialog_type, "i")
|
||||
|
||||
def _get_icon_color(self) -> str:
|
||||
"""获取图标颜色"""
|
||||
colors = {
|
||||
"info": "#3584e4",
|
||||
"warning": "#ff9800",
|
||||
"error": "#f44336",
|
||||
"question": "#3584e4"
|
||||
}
|
||||
return colors.get(self.dialog_type, "#3584e4")
|
||||
|
||||
def destroy(self):
|
||||
"""销毁对话框前解除事件绑定"""
|
||||
try:
|
||||
self.unbind("<Return>")
|
||||
self.unbind("<Escape>")
|
||||
except Exception:
|
||||
pass
|
||||
return super().destroy()
|
||||
|
||||
def _on_ok(self):
|
||||
"""确定按钮点击"""
|
||||
self.result = True
|
||||
self.destroy()
|
||||
|
||||
def _on_yes(self):
|
||||
"""是按钮点击"""
|
||||
self.result = True
|
||||
self.destroy()
|
||||
|
||||
def _on_no(self):
|
||||
"""否按钮点击"""
|
||||
self.result = False
|
||||
self.destroy()
|
||||
|
||||
|
||||
class InputDialog(BaseDialog):
|
||||
"""自定义输入对话框"""
|
||||
|
||||
def __init__(self, parent, title: str, prompt: str, initial_value: str = "", **kwargs):
|
||||
"""
|
||||
初始化输入对话框
|
||||
|
||||
Args:
|
||||
parent: 父窗口
|
||||
title: 对话框标题
|
||||
prompt: 提示文本
|
||||
initial_value: 初始值
|
||||
"""
|
||||
super().__init__(parent, title, 400, 180)
|
||||
|
||||
self.result = None
|
||||
|
||||
# 创建界面
|
||||
self._create_widgets(prompt, initial_value)
|
||||
|
||||
# 显示对话框
|
||||
self.show()
|
||||
|
||||
def _create_widgets(self, prompt: str, initial_value: str):
|
||||
"""创建界面组件"""
|
||||
# 主容器
|
||||
main_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
||||
|
||||
# 提示文本
|
||||
prompt_label = ctk.CTkLabel(
|
||||
main_frame,
|
||||
text=prompt,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_LARGE),
|
||||
wraplength=360,
|
||||
justify="left"
|
||||
)
|
||||
prompt_label.pack(pady=(0, 15))
|
||||
|
||||
# 输入框
|
||||
self.entry = ctk.CTkEntry(
|
||||
main_frame,
|
||||
height=BUTTON_HEIGHT_SMALL,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_MEDIUM)
|
||||
)
|
||||
self.entry.pack(fill="x", pady=(0, 20))
|
||||
self.entry.insert(0, initial_value)
|
||||
self.entry.select_range(0, "end")
|
||||
self.entry.focus_set()
|
||||
|
||||
# 按钮容器
|
||||
button_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
|
||||
button_frame.pack(side="bottom")
|
||||
|
||||
# 取消按钮
|
||||
cancel_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="取消",
|
||||
command=self._on_cancel,
|
||||
width=BUTTON_WIDTH_MEDIUM,
|
||||
height=BUTTON_HEIGHT_SMALL,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_MEDIUM),
|
||||
fg_color="#666666",
|
||||
hover_color="#555555"
|
||||
)
|
||||
cancel_btn.pack(side="left", padx=5)
|
||||
|
||||
# 确定按钮
|
||||
ok_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="确定",
|
||||
command=self._on_ok,
|
||||
width=BUTTON_WIDTH_MEDIUM,
|
||||
height=BUTTON_HEIGHT_SMALL,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_MEDIUM)
|
||||
)
|
||||
ok_btn.pack(side="left", padx=5)
|
||||
|
||||
# 绑定键盘事件
|
||||
self.entry.bind("<Return>", lambda e: self._on_ok())
|
||||
self.bind("<Escape>", lambda e: self._on_cancel())
|
||||
|
||||
def destroy(self):
|
||||
"""销毁对话框前解除事件绑定"""
|
||||
try:
|
||||
if hasattr(self, 'entry'):
|
||||
self.entry.unbind("<Return>")
|
||||
self.unbind("<Escape>")
|
||||
except Exception:
|
||||
pass
|
||||
return super().destroy()
|
||||
|
||||
def _on_ok(self):
|
||||
"""确定按钮点击"""
|
||||
self.result = self.entry.get().strip()
|
||||
self.destroy()
|
||||
|
||||
def _on_cancel(self):
|
||||
"""取消按钮点击"""
|
||||
self.result = None
|
||||
self.destroy()
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def show_info(parent, title: str, message: str):
|
||||
"""显示信息对话框"""
|
||||
MessageDialog(parent, title, message, "info")
|
||||
|
||||
|
||||
def show_warning(parent, title: str, message: str):
|
||||
"""显示警告对话框"""
|
||||
MessageDialog(parent, title, message, "warning")
|
||||
|
||||
|
||||
def show_error(parent, title: str, message: str):
|
||||
"""显示错误对话框"""
|
||||
MessageDialog(parent, title, message, "error")
|
||||
|
||||
|
||||
def ask_yes_no(parent, title: str, message: str) -> bool:
|
||||
"""显示是/否对话框"""
|
||||
dialog = MessageDialog(parent, title, message, "question")
|
||||
return dialog.result if dialog.result is not None else False
|
||||
|
||||
|
||||
def ask_string(parent, title: str, prompt: str, initial_value: str = "") -> Optional[str]:
|
||||
"""显示输入对话框"""
|
||||
dialog = InputDialog(parent, title, prompt, initial_value)
|
||||
return dialog.result
|
||||
202
ui/utilities/icon_manager.py
Normal file
202
ui/utilities/icon_manager.py
Normal file
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
图标管理器
|
||||
负责图标的加载、缓存和管理
|
||||
"""
|
||||
import os
|
||||
import glob
|
||||
import customtkinter as ctk
|
||||
from PIL import Image
|
||||
from functools import lru_cache
|
||||
from collections import OrderedDict
|
||||
from config.constants import APP_ICON_MAPPING
|
||||
|
||||
|
||||
class IconManager:
|
||||
"""图标管理器,负责图标的加载、缓存和管理"""
|
||||
|
||||
def __init__(self, icons_dir: str, icon_size: int):
|
||||
"""
|
||||
初始化图标管理器
|
||||
|
||||
Args:
|
||||
icons_dir: 图标目录路径
|
||||
icon_size: 图标大小
|
||||
"""
|
||||
self.icons_dir = icons_dir
|
||||
self.icon_size = icon_size
|
||||
# 使用有界缓存控制内存占用
|
||||
self._max_cache_size = 128
|
||||
self.cache: OrderedDict[str, ctk.CTkImage] = OrderedDict()
|
||||
|
||||
def get_app_icon(self, app_path: str, config_manager) -> ctk.CTkImage:
|
||||
"""
|
||||
获取应用图标
|
||||
|
||||
Args:
|
||||
app_path: 应用路径
|
||||
config_manager: 配置管理器
|
||||
|
||||
Returns:
|
||||
CTkImage 对象,如果失败则返回 None
|
||||
"""
|
||||
# 检查缓存
|
||||
if app_path in self.cache:
|
||||
self.cache.move_to_end(app_path)
|
||||
return self.cache[app_path]
|
||||
|
||||
try:
|
||||
# 查找图标路径
|
||||
icon_path = self._find_icon_path(app_path, config_manager)
|
||||
if not icon_path:
|
||||
return self._get_fallback_icon(app_path)
|
||||
|
||||
# 创建并缓存图标
|
||||
ctk_image = self._create_ctk_icon(icon_path)
|
||||
self.cache[app_path] = ctk_image
|
||||
# 控制缓存大小
|
||||
if len(self.cache) > self._max_cache_size:
|
||||
self.cache.popitem(last=False)
|
||||
return ctk_image
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to load icon ({app_path}): {e}")
|
||||
return self._get_fallback_icon(app_path)
|
||||
|
||||
def _find_icon_path(self, app_path: str, config_manager) -> str:
|
||||
"""
|
||||
查找应用图标路径
|
||||
|
||||
查找优先级:
|
||||
1. 自定义图标
|
||||
2. 预设图标映射
|
||||
3. 应用名称匹配
|
||||
4. 默认图标
|
||||
5. 任意可用图标
|
||||
|
||||
Args:
|
||||
app_path: 应用路径
|
||||
config_manager: 配置管理器
|
||||
|
||||
Returns:
|
||||
图标文件路径,如果未找到则返回 None
|
||||
"""
|
||||
app_name = os.path.splitext(os.path.basename(app_path))[0]
|
||||
|
||||
# 1. 检查自定义图标
|
||||
custom_icon = config_manager.get_app_icon(app_path)
|
||||
if custom_icon and os.path.exists(custom_icon):
|
||||
return custom_icon
|
||||
|
||||
# 2. 匹配预设图标
|
||||
app_name_lower = app_name.lower()
|
||||
for key, icon_name in APP_ICON_MAPPING.items():
|
||||
if key in app_name_lower:
|
||||
icon_path = os.path.join(self.icons_dir, f"{icon_name}.png")
|
||||
if os.path.exists(icon_path):
|
||||
return icon_path
|
||||
|
||||
# 3. 使用应用名称
|
||||
icon_path = os.path.join(self.icons_dir, f"{app_name}.png")
|
||||
if os.path.exists(icon_path):
|
||||
return icon_path
|
||||
|
||||
# 4. 使用默认图标
|
||||
default_icon = os.path.join(self.icons_dir, "NexusLauncher.ico")
|
||||
if os.path.exists(default_icon):
|
||||
return default_icon
|
||||
|
||||
# 5. 使用任意可用图标
|
||||
icons = glob.glob(os.path.join(self.icons_dir, "*.png"))
|
||||
icons.extend(glob.glob(os.path.join(self.icons_dir, "*.ico")))
|
||||
return icons[0] if icons else None
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def _load_pil_image(self, icon_path: str) -> Image.Image:
|
||||
"""
|
||||
加载 PIL 图像(带 LRU 缓存)
|
||||
|
||||
使用 LRU 缓存可以避免重复加载相同的图标文件,
|
||||
提升性能并减少磁盘 I/O
|
||||
|
||||
Args:
|
||||
icon_path: 图标文件路径
|
||||
|
||||
Returns:
|
||||
PIL Image 对象
|
||||
"""
|
||||
return Image.open(icon_path)
|
||||
|
||||
def _create_ctk_icon(self, icon_path: str) -> ctk.CTkImage:
|
||||
"""
|
||||
创建 CTk 图标对象
|
||||
|
||||
Args:
|
||||
icon_path: 图标文件路径
|
||||
|
||||
Returns:
|
||||
CTkImage 对象
|
||||
"""
|
||||
pil_image = self._load_pil_image(icon_path)
|
||||
icon_display_size = int(self.icon_size * 0.6)
|
||||
return ctk.CTkImage(
|
||||
light_image=pil_image,
|
||||
dark_image=pil_image,
|
||||
size=(icon_display_size, icon_display_size)
|
||||
)
|
||||
|
||||
def _get_fallback_icon(self, app_path: str) -> ctk.CTkImage:
|
||||
"""
|
||||
获取降级图标
|
||||
|
||||
当无法加载指定图标时,尝试使用任意可用图标
|
||||
|
||||
Args:
|
||||
app_path: 应用路径
|
||||
|
||||
Returns:
|
||||
CTkImage 对象,如果失败则返回 None
|
||||
"""
|
||||
try:
|
||||
icons = glob.glob(os.path.join(self.icons_dir, "*.png"))
|
||||
if icons:
|
||||
ctk_image = self._create_ctk_icon(icons[0])
|
||||
self.cache[app_path] = ctk_image
|
||||
if len(self.cache) > self._max_cache_size:
|
||||
self.cache.popitem(last=False)
|
||||
return ctk_image
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def clear_cache(self):
|
||||
"""清空所有缓存"""
|
||||
self.cache.clear()
|
||||
self._load_pil_image.cache_clear()
|
||||
|
||||
def update_icon_size(self, new_size: int):
|
||||
"""
|
||||
更新图标大小
|
||||
|
||||
更新图标大小后会清空缓存,
|
||||
下次获取图标时会使用新的尺寸重新创建
|
||||
|
||||
Args:
|
||||
new_size: 新的图标大小
|
||||
"""
|
||||
self.icon_size = new_size
|
||||
self.clear_cache()
|
||||
|
||||
def get_cache_info(self) -> dict:
|
||||
"""
|
||||
获取缓存信息
|
||||
|
||||
Returns:
|
||||
包含缓存统计信息的字典
|
||||
"""
|
||||
return {
|
||||
'ctk_cache_size': len(self.cache),
|
||||
'pil_cache_info': self._load_pil_image.cache_info()._asdict()
|
||||
}
|
||||
91
ui/utilities/icon_utils.py
Normal file
91
ui/utilities/icon_utils.py
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
图标工具模块
|
||||
负责图标路径获取和窗口图标设置
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def get_icons_dir() -> str:
|
||||
"""获取 icons 目录的绝对路径
|
||||
|
||||
Returns:
|
||||
icons 目录的绝对路径
|
||||
"""
|
||||
# 获取当前文件所在目录(ui/utilities/)
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 往上两级到项目根目录
|
||||
project_root = os.path.dirname(os.path.dirname(current_dir))
|
||||
|
||||
# 拼接 icons 文件夹
|
||||
icons_dir = os.path.join(project_root, "icons")
|
||||
|
||||
# 如果是打包后的应用,使用 sys._MEIPASS
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包后的应用
|
||||
icons_dir = os.path.join(sys._MEIPASS, "icons")
|
||||
|
||||
return icons_dir
|
||||
|
||||
|
||||
def get_icon_path(icon_name: str = "NexusLauncher.ico") -> str:
|
||||
"""获取图标文件的绝对路径
|
||||
|
||||
Args:
|
||||
icon_name: 图标文件名,默认为 "NexusLauncher.ico"
|
||||
|
||||
Returns:
|
||||
图标文件的绝对路径
|
||||
"""
|
||||
icons_dir = get_icons_dir()
|
||||
icon_path = os.path.join(icons_dir, icon_name)
|
||||
return icon_path
|
||||
|
||||
|
||||
def set_window_icon(window, icon_name: str = "NexusLauncher.ico"):
|
||||
"""为窗口设置图标
|
||||
|
||||
Args:
|
||||
window: Tkinter 窗口对象
|
||||
icon_name: 图标文件名,默认为 "NexusLauncher.ico"
|
||||
"""
|
||||
icon_path = get_icon_path(icon_name)
|
||||
|
||||
if os.path.exists(icon_path):
|
||||
try:
|
||||
window.iconbitmap(icon_path)
|
||||
window.wm_iconbitmap(icon_path)
|
||||
except Exception as e:
|
||||
print(f"Failed to set window icon: {e}")
|
||||
else:
|
||||
print(f"Icon file not found: {icon_path}")
|
||||
|
||||
|
||||
def setup_dialog_icon(dialog, icon_name: str = "NexusLauncher.ico"):
|
||||
"""为对话框设置图标(包含延迟设置以确保生效)
|
||||
|
||||
Args:
|
||||
dialog: 对话框窗口对象
|
||||
icon_name: 图标文件名,默认为 "NexusLauncher.ico"
|
||||
"""
|
||||
icon_path = get_icon_path(icon_name)
|
||||
|
||||
if not os.path.exists(icon_path):
|
||||
return
|
||||
|
||||
def set_icon():
|
||||
try:
|
||||
dialog.iconbitmap(icon_path)
|
||||
dialog.wm_iconbitmap(icon_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 立即设置
|
||||
set_icon()
|
||||
|
||||
# 延迟设置(确保生效)
|
||||
dialog.after(50, set_icon)
|
||||
dialog.after(200, set_icon)
|
||||
126
ui/utilities/ui_helpers.py
Normal file
126
ui/utilities/ui_helpers.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
UI辅助工具
|
||||
负责标签按钮、下拉框等UI组件的辅助功能
|
||||
"""
|
||||
import customtkinter as ctk
|
||||
from config.constants import COLOR_TRANSPARENT
|
||||
|
||||
|
||||
class UIHelpers:
|
||||
"""UI辅助工具类,提供各种UI组件的辅助功能"""
|
||||
|
||||
@staticmethod
|
||||
def adjust_tab_button_width(tabview):
|
||||
"""调整标签按钮宽度,让它们平分整个宽度"""
|
||||
try:
|
||||
# 获取tabview的宽度
|
||||
tabview_width = tabview.winfo_width()
|
||||
|
||||
if tabview_width > 1:
|
||||
# 获取标签按钮
|
||||
segmented_button = tabview._segmented_button
|
||||
buttons_dict = segmented_button._buttons_dict
|
||||
|
||||
# 计算每个按钮的宽度(平分)
|
||||
button_count = len(buttons_dict)
|
||||
if button_count > 0:
|
||||
button_width = tabview_width // button_count
|
||||
|
||||
# 设置每个按钮的宽度
|
||||
for button in buttons_dict.values():
|
||||
try:
|
||||
button.configure(width=button_width)
|
||||
except:
|
||||
pass
|
||||
|
||||
print(f"Adjusted tab button width: {button_width}px (total width: {tabview_width}px)")
|
||||
except Exception as e:
|
||||
print(f"Failed to adjust tab button width: {e}")
|
||||
|
||||
@staticmethod
|
||||
def fix_dropdown_width(combo_box):
|
||||
"""修复下拉菜单宽度,使其与下拉框一致"""
|
||||
try:
|
||||
# 获取下拉框的实际宽度
|
||||
combo_width = combo_box.winfo_width()
|
||||
|
||||
if combo_width > 1:
|
||||
# 方法1: 直接设置 CTkComboBox 的内部属性
|
||||
if hasattr(combo_box, '_dropdown_menu'):
|
||||
dropdown_menu = combo_box._dropdown_menu
|
||||
|
||||
if dropdown_menu is not None:
|
||||
# 设置下拉菜单的宽度
|
||||
try:
|
||||
dropdown_menu.configure(width=combo_width)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 尝试设置内部 frame 的宽度
|
||||
try:
|
||||
if hasattr(dropdown_menu, 'winfo_children'):
|
||||
for child in dropdown_menu.winfo_children():
|
||||
child.configure(width=combo_width)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 方法2: 重写 _open_dropdown_menu 方法
|
||||
if not hasattr(combo_box, '_original_open_dropdown'):
|
||||
combo_box._original_open_dropdown = combo_box._open_dropdown_menu
|
||||
|
||||
def custom_open_dropdown():
|
||||
combo_box._original_open_dropdown()
|
||||
# 延迟设置宽度
|
||||
if hasattr(combo_box, 'after'):
|
||||
combo_box.after(1, lambda: UIHelpers._set_dropdown_width(combo_box, combo_width))
|
||||
|
||||
combo_box._open_dropdown_menu = custom_open_dropdown
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to set dropdown menu width: {e}")
|
||||
|
||||
@staticmethod
|
||||
def _set_dropdown_width(combo_box, width):
|
||||
"""设置下拉菜单宽度的辅助方法"""
|
||||
try:
|
||||
if hasattr(combo_box, '_dropdown_menu') and combo_box._dropdown_menu is not None:
|
||||
dropdown = combo_box._dropdown_menu
|
||||
dropdown.configure(width=width)
|
||||
|
||||
# 遍历所有子组件并设置宽度
|
||||
for widget in dropdown.winfo_children():
|
||||
try:
|
||||
widget.configure(width=width)
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def configure_tab_transparency(project_tab, task_tab):
|
||||
"""配置标签页透明度"""
|
||||
try:
|
||||
# 尝试将标签页背景设置为透明,让圆角容器可见
|
||||
project_tab.configure(fg_color=COLOR_TRANSPARENT)
|
||||
task_tab.configure(fg_color=COLOR_TRANSPARENT)
|
||||
print("[OK] Set tab background to transparent")
|
||||
except Exception as e:
|
||||
print(f"Failed to set tab background transparent: {e}")
|
||||
|
||||
@staticmethod
|
||||
def hide_scrollbar(scrollable_frame):
|
||||
"""隐藏 CTkScrollableFrame 的滚动条(仍保持滚动功能)"""
|
||||
try:
|
||||
scrollbar = getattr(scrollable_frame, "_scrollbar", None)
|
||||
if scrollbar:
|
||||
bgcolor = scrollable_frame.cget("fg_color") if hasattr(scrollable_frame, "cget") else COLOR_TRANSPARENT
|
||||
scrollbar.configure(fg_color=COLOR_TRANSPARENT, button_color=bgcolor, button_hover_color=bgcolor)
|
||||
scrollbar.configure(width=0)
|
||||
try:
|
||||
scrollbar.grid_remove()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
327
ui/utilities/window_manager.py
Normal file
327
ui/utilities/window_manager.py
Normal file
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
窗口管理器
|
||||
负责窗口定位、托盘图标、日志窗口等通用窗口功能
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import ctypes
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from PIL import Image
|
||||
import pystray
|
||||
from pystray import MenuItem as item
|
||||
import customtkinter as ctk
|
||||
from config.constants import CONSOLE_WINDOW_SIZE
|
||||
from .icon_utils import get_icon_path
|
||||
|
||||
|
||||
class ConsoleRedirector:
|
||||
"""重定向 stdout 到控制台窗口"""
|
||||
|
||||
def __init__(self, log_callback, original_stdout):
|
||||
self.log_callback = log_callback
|
||||
self.original_stdout = original_stdout
|
||||
self.buffer = ""
|
||||
|
||||
def write(self, message):
|
||||
"""写入消息到控制台窗口和原始 stdout"""
|
||||
# 同时输出到原始 stdout(命令行)和控制台窗口
|
||||
if self.original_stdout:
|
||||
self.original_stdout.write(message)
|
||||
self.original_stdout.flush()
|
||||
|
||||
# 输出到控制台窗口
|
||||
if message and message.strip(): # 只记录非空消息
|
||||
self.log_callback(message.rstrip())
|
||||
|
||||
def flush(self):
|
||||
"""刷新缓冲区"""
|
||||
if self.original_stdout:
|
||||
self.original_stdout.flush()
|
||||
|
||||
|
||||
class WindowManager:
|
||||
"""窗口管理器,处理窗口定位、托盘图标、日志窗口等功能"""
|
||||
|
||||
def __init__(self, main_window, config_manager):
|
||||
"""
|
||||
初始化窗口管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例
|
||||
config_manager: 配置管理器
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.config_manager = config_manager
|
||||
|
||||
# 托盘图标相关
|
||||
self.tray_icon = None
|
||||
self.is_quitting = False
|
||||
|
||||
# 日志窗口相关
|
||||
self.console_visible = False
|
||||
self.console_window = None
|
||||
self.log_text = None
|
||||
|
||||
# stdout 重定向
|
||||
self.original_stdout = sys.stdout
|
||||
self.console_redirector = None
|
||||
|
||||
# 图标路径
|
||||
self.icon_path = get_icon_path()
|
||||
|
||||
def setup_window_appid(self):
|
||||
"""设置Windows AppUserModelID,确保任务栏图标正确显示"""
|
||||
try:
|
||||
myappid = 'NexusLauncher.App.1.0'
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
||||
except:
|
||||
pass
|
||||
|
||||
def position_window_bottom_right(self, width, height):
|
||||
"""将窗口定位到屏幕右下角(任务栏上方)"""
|
||||
# 先设置窗口大小
|
||||
self.main_window.geometry(f"{width}x{height}")
|
||||
|
||||
# 更新窗口以获取准确的尺寸
|
||||
self.main_window.update_idletasks()
|
||||
|
||||
# 获取屏幕尺寸
|
||||
screen_width = self.main_window.winfo_screenwidth()
|
||||
screen_height = self.main_window.winfo_screenheight()
|
||||
|
||||
# 计算右下角位置(留出任务栏空间,确保不重叠)
|
||||
taskbar_height = 80 # 任务栏高度 + 额外间距,确保不重叠
|
||||
x = screen_width - width - 15 # 右边距15px
|
||||
y = screen_height - height - taskbar_height # 底部留出足够空间
|
||||
|
||||
# 设置窗口位置
|
||||
self.main_window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
def set_window_icon(self):
|
||||
"""设置窗口图标"""
|
||||
if os.path.exists(self.icon_path):
|
||||
self.main_window.iconbitmap(self.icon_path)
|
||||
|
||||
def setup_tray_icon(self):
|
||||
"""设置系统托盘图标"""
|
||||
try:
|
||||
# 加载图标
|
||||
if os.path.exists(self.icon_path):
|
||||
icon_image = Image.open(self.icon_path)
|
||||
else:
|
||||
# 如果图标不存在,创建一个简单的默认图标
|
||||
icon_image = Image.new('RGB', (64, 64), color='blue')
|
||||
|
||||
# 创建托盘菜单
|
||||
console_text = '隐藏日志' if self.console_visible else '显示日志'
|
||||
menu = pystray.Menu(
|
||||
item('显示主窗口', self._show_window, default=True),
|
||||
item('设置', self._show_settings),
|
||||
item(console_text, self._toggle_console),
|
||||
pystray.Menu.SEPARATOR,
|
||||
item('退出', self._quit_app)
|
||||
)
|
||||
|
||||
# 创建托盘图标
|
||||
self.tray_icon = pystray.Icon(
|
||||
"NexusLauncher",
|
||||
icon_image,
|
||||
"NexusLauncher",
|
||||
menu
|
||||
)
|
||||
|
||||
# 在单独的线程中运行托盘图标
|
||||
tray_thread = threading.Thread(target=self.tray_icon.run, daemon=True)
|
||||
tray_thread.start()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to create tray icon: {e}")
|
||||
|
||||
def create_log_window(self):
|
||||
"""创建自定义日志窗口"""
|
||||
self.console_window = ctk.CTkToplevel(self.main_window)
|
||||
self.console_window.title("NexusLauncher - 控制台")
|
||||
self.console_window.geometry(CONSOLE_WINDOW_SIZE)
|
||||
|
||||
# 设置图标 - 使用持续监控方式
|
||||
if os.path.exists(self.icon_path):
|
||||
self.console_window.iconbitmap(self.icon_path)
|
||||
# 持续设置图标,防止被 CustomTkinter 覆盖
|
||||
self._keep_console_icon_alive()
|
||||
|
||||
# 创建文本框显示日志
|
||||
self.log_text = ctk.CTkTextbox(
|
||||
self.console_window,
|
||||
wrap="word",
|
||||
font=ctk.CTkFont(family="Consolas", size=12)
|
||||
)
|
||||
self.log_text.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
# 添加欢迎信息
|
||||
self.log_text.insert("1.0", "NexusLauncher 控制台\n")
|
||||
self.log_text.insert("end", "=" * 50 + "\n")
|
||||
self.log_text.insert("end", "实时显示应用调试信息\n")
|
||||
self.log_text.insert("end", "关闭此窗口不会退出应用\n")
|
||||
self.log_text.insert("end", "=" * 50 + "\n\n")
|
||||
|
||||
# 绑定关闭事件 - 只隐藏不退出
|
||||
self.console_window.protocol("WM_DELETE_WINDOW", self._on_log_window_close)
|
||||
|
||||
# 重定向 stdout 到控制台窗口
|
||||
self._redirect_stdout()
|
||||
|
||||
def show_console(self):
|
||||
"""显示日志窗口"""
|
||||
if not self.console_window or not self.console_window.winfo_exists():
|
||||
self.create_log_window()
|
||||
else:
|
||||
self.console_window.deiconify()
|
||||
self.console_window.lift()
|
||||
self.console_visible = True
|
||||
# 更新托盘菜单
|
||||
if self.tray_icon:
|
||||
self._update_tray_menu()
|
||||
self.log_with_timestamp("[VIEW] 日志窗口已显示")
|
||||
|
||||
def hide_console(self):
|
||||
"""隐藏日志窗口"""
|
||||
if self.console_window and self.console_window.winfo_exists():
|
||||
self.console_window.withdraw()
|
||||
self.console_visible = False
|
||||
# 更新托盘菜单
|
||||
if self.tray_icon:
|
||||
self._update_tray_menu()
|
||||
print("[VIEW] Console window hidden")
|
||||
|
||||
def log(self, message: str):
|
||||
"""记录日志到控制台窗口(不带时间戳,用于 print 重定向)"""
|
||||
if self.log_text:
|
||||
try:
|
||||
self.log_text.insert("end", f"{message}\n")
|
||||
self.log_text.see("end") # 自动滚动到最新日志
|
||||
except:
|
||||
pass
|
||||
|
||||
def log_with_timestamp(self, message: str):
|
||||
"""记录带时间戳的日志到控制台窗口(用于重要事件)"""
|
||||
if self.log_text:
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
self.log_text.insert("end", f"[{timestamp}] {message}\n")
|
||||
self.log_text.see("end") # 自动滚动到最新日志
|
||||
except:
|
||||
pass
|
||||
|
||||
def _redirect_stdout(self):
|
||||
"""重定向 stdout 到控制台窗口"""
|
||||
if not self.console_redirector:
|
||||
self.console_redirector = ConsoleRedirector(self.log, self.original_stdout)
|
||||
sys.stdout = self.console_redirector
|
||||
|
||||
def _restore_stdout(self):
|
||||
"""恢复原始 stdout"""
|
||||
if self.console_redirector:
|
||||
sys.stdout = self.original_stdout
|
||||
self.console_redirector = None
|
||||
|
||||
def hide_window(self):
|
||||
"""隐藏窗口到托盘"""
|
||||
self.main_window.withdraw()
|
||||
|
||||
def show_window(self):
|
||||
"""显示主窗口"""
|
||||
self.main_window.deiconify()
|
||||
self.main_window.lift()
|
||||
self.main_window.focus_force()
|
||||
|
||||
def quit_app(self):
|
||||
"""退出应用程序"""
|
||||
self.is_quitting = True
|
||||
|
||||
# 恢复原始 stdout
|
||||
self._restore_stdout()
|
||||
|
||||
# 关闭自定义日志窗口
|
||||
if self.console_window and self.console_window.winfo_exists():
|
||||
self.console_window.destroy()
|
||||
|
||||
# 停止托盘图标
|
||||
if self.tray_icon:
|
||||
self.tray_icon.stop()
|
||||
|
||||
# 保存窗口大小
|
||||
try:
|
||||
width = self.main_window.winfo_width()
|
||||
height = self.main_window.winfo_height()
|
||||
self.config_manager.save_window_size(width, height)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 关闭应用
|
||||
self.main_window.after(0, self.main_window.quit)
|
||||
|
||||
def _keep_console_icon_alive(self):
|
||||
"""持续保持控制台图标不被覆盖"""
|
||||
try:
|
||||
if self.console_window and self.console_window.winfo_exists() and os.path.exists(self.icon_path):
|
||||
self.console_window.iconbitmap(self.icon_path)
|
||||
# 每 50ms 检查一次,持续 500ms
|
||||
if not hasattr(self, '_console_icon_check_count'):
|
||||
self._console_icon_check_count = 0
|
||||
|
||||
if self._console_icon_check_count < 10:
|
||||
self._console_icon_check_count += 1
|
||||
self.main_window.after(50, self._keep_console_icon_alive)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _on_log_window_close(self):
|
||||
"""日志窗口关闭事件"""
|
||||
self.hide_console()
|
||||
|
||||
def _toggle_console(self, icon=None, item=None):
|
||||
"""切换控制台窗口显示状态"""
|
||||
if self.console_visible:
|
||||
self.main_window.after(0, self.hide_console)
|
||||
else:
|
||||
self.main_window.after(0, self.show_console)
|
||||
|
||||
def _update_tray_menu(self):
|
||||
"""更新托盘菜单"""
|
||||
try:
|
||||
console_text = '隐藏日志' if self.console_visible else '显示日志'
|
||||
menu = pystray.Menu(
|
||||
item('显示主窗口', self._show_window, default=True),
|
||||
item('设置', self._show_settings),
|
||||
item(console_text, self._toggle_console),
|
||||
pystray.Menu.SEPARATOR,
|
||||
item('退出', self._quit_app)
|
||||
)
|
||||
self.tray_icon.menu = menu
|
||||
except Exception as e:
|
||||
print(f"Failed to update tray menu: {e}")
|
||||
|
||||
def _show_window(self, icon=None, item=None):
|
||||
"""显示主窗口"""
|
||||
self.main_window.after(0, self.show_window)
|
||||
|
||||
def _show_settings(self, icon=None, item=None):
|
||||
"""显示设置窗口"""
|
||||
self.main_window.after(0, self._do_show_settings)
|
||||
|
||||
def _do_show_settings(self):
|
||||
"""在主线程中显示设置窗口"""
|
||||
# 如果窗口隐藏,先显示主窗口
|
||||
if not self.main_window.winfo_viewable():
|
||||
self.main_window.deiconify()
|
||||
|
||||
# 调用主窗口的设置方法
|
||||
if hasattr(self.main_window, '_open_settings'):
|
||||
self.main_window._open_settings()
|
||||
|
||||
def _quit_app(self, icon=None, item=None):
|
||||
"""退出应用程序"""
|
||||
self.quit_app()
|
||||
67
ui/utilities/window_utils.py
Normal file
67
ui/utilities/window_utils.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
窗口工具模块
|
||||
负责窗口样式设置和对话框配置
|
||||
"""
|
||||
import ctypes
|
||||
from .icon_utils import setup_dialog_icon
|
||||
|
||||
|
||||
def set_dark_title_bar(window):
|
||||
"""设置窗口为深色标题栏(Windows 10/11)
|
||||
|
||||
Args:
|
||||
window: 窗口对象
|
||||
"""
|
||||
try:
|
||||
hwnd = ctypes.windll.user32.GetParent(window.winfo_id())
|
||||
DWMWA_USE_IMMERSIVE_DARK_MODE = 20
|
||||
value = ctypes.c_int(2)
|
||||
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
ctypes.byref(value),
|
||||
ctypes.sizeof(value)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to set dark title bar: {e}")
|
||||
|
||||
|
||||
def setup_dialog_window(dialog, title: str, width: int, height: int, parent=None, center: bool = True):
|
||||
"""统一配置对话框窗口
|
||||
|
||||
Args:
|
||||
dialog: 对话框对象
|
||||
title: 窗口标题
|
||||
width: 窗口宽度
|
||||
height: 窗口高度
|
||||
parent: 父窗口(可选)
|
||||
center: 是否居中显示,默认 True
|
||||
|
||||
Returns:
|
||||
配置好的对话框对象
|
||||
"""
|
||||
dialog.title(title)
|
||||
dialog.geometry(f"{width}x{height}")
|
||||
dialog.configure(fg_color="#2b2b2b")
|
||||
|
||||
# 设置图标
|
||||
setup_dialog_icon(dialog)
|
||||
|
||||
# 设置为模态
|
||||
if parent:
|
||||
dialog.transient(parent)
|
||||
dialog.grab_set()
|
||||
|
||||
# 设置深色标题栏
|
||||
dialog.after(10, lambda: set_dark_title_bar(dialog))
|
||||
|
||||
# 居中显示
|
||||
if center:
|
||||
dialog.update_idletasks()
|
||||
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
|
||||
y = (dialog.winfo_screenheight() // 2) - (height // 2)
|
||||
dialog.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
return dialog
|
||||
Reference in New Issue
Block a user