适配macos中对pytray支持不好的情况。使用系统菜单。
This commit is contained in:
+105
-38
@@ -1,82 +1,149 @@
|
||||
# coding:utf-8
|
||||
# MainEntry 负责应用主窗口与菜单的统一构建:
|
||||
# - 通过 build_menu_model 定义跨平台统一的菜单数据结构
|
||||
# - 在 macOS 上使用 Tk 菜单栏;在非 macOS 上使用 pystray 系统托盘
|
||||
# - 所有菜单项均绑定到同名处理函数,切换平台无需改动业务逻辑
|
||||
import tkinter as tk
|
||||
from core.logger import LogLevel, PrintLog
|
||||
from PIL import Image
|
||||
import pystray
|
||||
import threading
|
||||
import sys
|
||||
|
||||
class MainEntry:
|
||||
def __init__(self, master):
|
||||
# 初始化 Tk 窗口属性与基础状态
|
||||
self.master = master
|
||||
self.master.title("Main Board")
|
||||
self.master.geometry("800x600")
|
||||
self.master.configure(bg="#f0f0f0")
|
||||
self.master.resizable(False, False)
|
||||
self.master.protocol("WM_DELETE_WINDOW", self.hide_window)
|
||||
# QMT 开关状态用于动态更新菜单文案
|
||||
self.qmt_enabled = False
|
||||
self.icon = None
|
||||
self.create_systray_icon()
|
||||
# 非 macOS 使用系统托盘(pystray);macOS 使用原生菜单栏
|
||||
self.systray_supported = sys.platform != "darwin"
|
||||
# 首次进入根据平台构建菜单
|
||||
self.create_menu()
|
||||
|
||||
def create_systray_icon(self):
|
||||
# 加载图标文件(需确保路径正确)
|
||||
image = Image.open("logo.png")
|
||||
# 定义托盘菜单
|
||||
menu = (
|
||||
pystray.MenuItem('交易大师', None, enabled=False),
|
||||
pystray.MenuItem(" - 交易复盘", self.handler),
|
||||
pystray.MenuItem(" - 市场数据", self.handler),
|
||||
pystray.MenuItem(" - 快速下单", self.handler),
|
||||
pystray.MenuItem('策略交易', None, enabled=False),
|
||||
pystray.MenuItem(" - 交易看板", self.handler),
|
||||
pystray.MenuItem(" - 策略中心", None),
|
||||
pystray.MenuItem(" - 策略定制", None),
|
||||
pystray.MenuItem('实时数据', None, enabled=False),
|
||||
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)
|
||||
)
|
||||
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},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
if self.icon:
|
||||
self.icon.menu = menu
|
||||
self.icon.update_menu()
|
||||
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")
|
||||
items = []
|
||||
for group in model:
|
||||
# 分组标题作为禁用的头部项
|
||||
items.append(pystray.MenuItem(group["label"], None, enabled=False))
|
||||
for it in group["items"]:
|
||||
if it.get("separator"):
|
||||
items.append(pystray.Menu.SEPARATOR)
|
||||
else:
|
||||
fn = getattr(self, it["action"]) if it.get("action") else None
|
||||
items.append(pystray.MenuItem(it["label"], fn, default=it.get("default", False), enabled=it.get("enabled", True)))
|
||||
menu = tuple(items)
|
||||
if self.icon:
|
||||
# 已存在托盘图标:更新菜单
|
||||
self.icon.menu = menu
|
||||
self.icon.update_menu()
|
||||
else:
|
||||
# 首次创建托盘图标并在后台线程运行
|
||||
self.icon = pystray.Icon("name", image, "标题", menu)
|
||||
self.trayThread = threading.Thread(target=self.icon.run, daemon=True)
|
||||
self.trayThread.start()
|
||||
else:
|
||||
# 创建托盘图标
|
||||
self.icon = pystray.Icon("name", image, "标题", menu)
|
||||
self.trayThread = threading.Thread(target=self.icon.run, daemon=True)
|
||||
self.trayThread.start()
|
||||
# 在后台线程运行托盘
|
||||
# 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):
|
||||
# 切换 QMT 开关,并触发菜单重建以更新文案
|
||||
if self.qmt_enabled:
|
||||
self.qmt_enabled = False
|
||||
PrintLog(LogLevel.INFO, "QMT 市场数据已关闭")
|
||||
else:
|
||||
self.qmt_enabled = True
|
||||
PrintLog(LogLevel.INFO, "QMT 市场数据已开启")
|
||||
self.create_systray_icon()
|
||||
self.create_menu()
|
||||
|
||||
def handler(self):
|
||||
# 通用占位处理:当前仅记录点击行为,后续可替换为具体业务逻辑
|
||||
PrintLog(LogLevel.INFO, f"点击了")
|
||||
|
||||
def hide_window(self):
|
||||
# 关闭窗口事件:隐藏但不退出应用
|
||||
PrintLog(LogLevel.INFO, "隐藏主窗口")
|
||||
self.master.withdraw() # 隐藏主窗口
|
||||
|
||||
def show_window(self):
|
||||
self.icon.visible = True
|
||||
# 显示主窗口;在非 macOS 平台同步让托盘图标可见
|
||||
if self.icon:
|
||||
self.icon.visible = True
|
||||
PrintLog(LogLevel.INFO, "显示主窗口")
|
||||
self.master.deiconify() # 显示主窗口
|
||||
|
||||
def quit_window(self, icon: pystray.Icon):
|
||||
icon.stop()
|
||||
def quit_window(self, icon=None):
|
||||
# 退出应用;在非 macOS 平台时关闭托盘图标
|
||||
if icon:
|
||||
icon.stop()
|
||||
PrintLog(LogLevel.INFO, "退出应用")
|
||||
self.master.quit()
|
||||
self.master.destroy()
|
||||
|
||||
def run(self):
|
||||
# 主事件循环入口
|
||||
self.master.mainloop()
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user