328 lines
11 KiB
Python
328 lines
11 KiB
Python
#!/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()
|