适配macos中对pytray支持不好的情况。使用系统菜单。

This commit is contained in:
2025-12-06 00:12:44 +08:00
parent b435f12c49
commit 988947aa1a
+97 -30
View File
@@ -1,82 +1,149 @@
# coding:utf-8 # coding:utf-8
# MainEntry 负责应用主窗口与菜单的统一构建:
# - 通过 build_menu_model 定义跨平台统一的菜单数据结构
# - 在 macOS 上使用 Tk 菜单栏;在非 macOS 上使用 pystray 系统托盘
# - 所有菜单项均绑定到同名处理函数,切换平台无需改动业务逻辑
import tkinter as tk import tkinter as tk
from core.logger import LogLevel, PrintLog from core.logger import LogLevel, PrintLog
from PIL import Image
import pystray
import threading import threading
import sys
class MainEntry: class MainEntry:
def __init__(self, master): def __init__(self, master):
# 初始化 Tk 窗口属性与基础状态
self.master = master self.master = master
self.master.title("Main Board") self.master.title("Main Board")
self.master.geometry("800x600") self.master.geometry("800x600")
self.master.configure(bg="#f0f0f0") self.master.configure(bg="#f0f0f0")
self.master.resizable(False, False) self.master.resizable(False, False)
self.master.protocol("WM_DELETE_WINDOW", self.hide_window) self.master.protocol("WM_DELETE_WINDOW", self.hide_window)
# QMT 开关状态用于动态更新菜单文案
self.qmt_enabled = False self.qmt_enabled = False
self.icon = None self.icon = None
self.create_systray_icon() # 非 macOS 使用系统托盘(pystray);macOS 使用原生菜单栏
self.systray_supported = sys.platform != "darwin"
# 首次进入根据平台构建菜单
self.create_menu()
def create_systray_icon(self): def build_menu_model(self):
# 加载图标文件(需确保路径正确) # 菜单模型统一描述所有菜单:
# - 每个分组包含 label 与 items
# - item 支持:label 文案、action 处理函数名、enabled 启用状态、default 默认项、separator 分隔符
# - 文案可根据状态动态生成(如 QMT 开关)
qmt_label = "QMT (已开启)" if self.qmt_enabled else "QMT (已关闭)"
return [
{
"label": "交易大师",
"items": [
{"label": "交易复盘", "action": "handler", "enabled": True},
{"label": "市场数据", "action": "handler", "enabled": True},
{"label": "快速下单", "action": "handler", "enabled": True},
],
},
{
"label": "策略交易",
"items": [
{"label": "交易看板", "action": "handler", "enabled": True},
{"label": "策略中心", "action": None, "enabled": False},
{"label": "策略定制", "action": None, "enabled": False},
],
},
{
"label": "实时数据",
"items": [
{"label": qmt_label, "action": "marketDataSwitch", "enabled": True},
],
},
{
"label": "系统",
"items": [
{"label": "控制台", "action": "show_window", "enabled": True, "default": True},
{"label": "设置", "action": "marketDataSwitch", "enabled": True},
{"separator": True},
{"label": "退出", "action": "quit_window", "enabled": True},
],
},
]
def create_menu(self):
# 根据统一菜单模型与平台类型,渲染到系统托盘或 Tk 菜单栏
model = self.build_menu_model()
if self.systray_supported:
# 非 macOS:延迟导入 pystray 与 PIL,避免在 macOS 上引入不兼容依赖
from PIL import Image
import pystray
image = Image.open("logo.png") image = Image.open("logo.png")
# 定义托盘菜单 items = []
menu = ( for group in model:
pystray.MenuItem('交易大师', None, enabled=False), # 分组标题作为禁用的头部项
pystray.MenuItem(" - 交易复盘", self.handler), items.append(pystray.MenuItem(group["label"], None, enabled=False))
pystray.MenuItem(" - 市场数据", self.handler), for it in group["items"]:
pystray.MenuItem(" - 快速下单", self.handler), if it.get("separator"):
pystray.MenuItem('策略交易', None, enabled=False), items.append(pystray.Menu.SEPARATOR)
pystray.MenuItem(" - 交易看板", self.handler), else:
pystray.MenuItem(" - 策略中心", None), fn = getattr(self, it["action"]) if it.get("action") else None
pystray.MenuItem(" - 策略定制", None), items.append(pystray.MenuItem(it["label"], fn, default=it.get("default", False), enabled=it.get("enabled", True)))
pystray.MenuItem('实时数据', None, enabled=False), menu = tuple(items)
pystray.MenuItem(" - QMT (已关闭)" if not self.qmt_enabled else " - QMT (已开启)", self.marketDataSwitch),
pystray.Menu.SEPARATOR,
pystray.MenuItem(" - 控制台", self.show_window, default=True),
pystray.MenuItem(" - 设置", self.marketDataSwitch),
pystray.MenuItem(" - 退出", self.quit_window)
)
if self.icon: if self.icon:
# 已存在托盘图标:更新菜单
self.icon.menu = menu self.icon.menu = menu
self.icon.update_menu() self.icon.update_menu()
else: else:
# 创建托盘图标 # 首次创建托盘图标并在后台线程运行
self.icon = pystray.Icon("name", image, "标题", menu) self.icon = pystray.Icon("name", image, "标题", menu)
self.trayThread = threading.Thread(target=self.icon.run, daemon=True) self.trayThread = threading.Thread(target=self.icon.run, daemon=True)
self.trayThread.start() self.trayThread.start()
# 在后台线程运行托盘 else:
# macOS:使用 Tk 菜单栏
menu_bar = tk.Menu(self.master)
for group in model:
m = tk.Menu(menu_bar, tearoff=0)
for it in group["items"]:
if it.get("separator"):
m.add_separator()
else:
fn = getattr(self, it["action"]) if it.get("action") else None
if it.get("enabled", True) and fn:
m.add_command(label=it["label"], command=fn)
else:
m.add_command(label=it["label"], state="disabled")
menu_bar.add_cascade(label=group["label"], menu=m)
self.master.config(menu=menu_bar)
def marketDataSwitch(self): def marketDataSwitch(self):
# 切换 QMT 开关,并触发菜单重建以更新文案
if self.qmt_enabled: if self.qmt_enabled:
self.qmt_enabled = False self.qmt_enabled = False
PrintLog(LogLevel.INFO, "QMT 市场数据已关闭") PrintLog(LogLevel.INFO, "QMT 市场数据已关闭")
else: else:
self.qmt_enabled = True self.qmt_enabled = True
PrintLog(LogLevel.INFO, "QMT 市场数据已开启") PrintLog(LogLevel.INFO, "QMT 市场数据已开启")
self.create_systray_icon() self.create_menu()
def handler(self): def handler(self):
# 通用占位处理:当前仅记录点击行为,后续可替换为具体业务逻辑
PrintLog(LogLevel.INFO, f"点击了") PrintLog(LogLevel.INFO, f"点击了")
def hide_window(self): def hide_window(self):
# 关闭窗口事件:隐藏但不退出应用
PrintLog(LogLevel.INFO, "隐藏主窗口") PrintLog(LogLevel.INFO, "隐藏主窗口")
self.master.withdraw() # 隐藏主窗口 self.master.withdraw() # 隐藏主窗口
def show_window(self): def show_window(self):
# 显示主窗口;在非 macOS 平台同步让托盘图标可见
if self.icon:
self.icon.visible = True self.icon.visible = True
PrintLog(LogLevel.INFO, "显示主窗口") PrintLog(LogLevel.INFO, "显示主窗口")
self.master.deiconify() # 显示主窗口 self.master.deiconify() # 显示主窗口
def quit_window(self, icon: pystray.Icon): def quit_window(self, icon=None):
# 退出应用;在非 macOS 平台时关闭托盘图标
if icon:
icon.stop() icon.stop()
PrintLog(LogLevel.INFO, "退出应用") PrintLog(LogLevel.INFO, "退出应用")
self.master.quit() self.master.quit()
self.master.destroy() self.master.destroy()
def run(self): def run(self):
# 主事件循环入口
self.master.mainloop() self.master.mainloop()