Update
This commit is contained in:
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()
|
||||
Reference in New Issue
Block a user