Electron macOS Tray:setContextMenu 会吞掉 click 事件
给桌面应用加系统托盘是常规需求。想做一个对称的 UX:左键点击托盘图标显示主窗口,右键弹出菜单。逻辑写完,Windows 上一切正常;切到 Mac 本机一测——单击托盘,菜单弹出来了,窗口没有出现。在 click handler 里加了 console.log,Mac 上永远不打印。
现象
朴素写法,Windows 和 macOS 共用一套:
const tray = new Tray(iconPath)
tray.setContextMenu(menu)
tray.on('click', () => showWindow())- Windows:单击托盘图标 →
click事件触发 → 窗口显示。 - macOS:单击托盘图标 → 菜单弹出 →
clickhandler 完全没有执行。
根因
macOS 的 NSStatusItem(系统状态栏 item)模型规定:一旦 status item 绑定了 menu,左键点击就由系统接管并自动弹出菜单——这是 macOS 的标准交互语义,不是 Electron 的 bug。
Electron 的 tray.setContextMenu(menu) 底层调用的正是 NSStatusItem.menu = ...,设置之后系统完全接管了点击事件,Electron 层的 click event 不再被派发。
Windows 的通知区域(notification area)模型完全不同:左键、右键、双击都是独立事件,setContextMenu 只负责右键自动弹,完全不影响左键的 click 事件派发。
在 macOS 上,只要调用过 tray.setContextMenu(menu),tray.on('click', ...) 就永远不会触发——不管注册顺序如何,也不管注册几次。这是 macOS NSStatusItem 的系统级行为,无法在 Electron 层绕过。
正解
按平台分支:macOS 不调用 setContextMenu,改在 right-click 事件里手动调用 tray.popUpContextMenu(menu)。
import { app, Menu, Tray } from 'electron'
export function createTray(iconPath: string, showWindow: () => void): Tray {
const tray = new Tray(iconPath)
tray.setToolTip(app.getName())
const buildMenu = () => Menu.buildFromTemplate([
{ label: '显示主窗口', click: showWindow },
{ type: 'separator' },
{ label: `退出 ${app.getName()}`, click: () => app.quit() },
])
if (process.platform === 'darwin') {
// macOS:不调用 setContextMenu,左键 show,右键手动弹菜单
tray.on('click', showWindow)
tray.on('right-click', () => tray.popUpContextMenu(buildMenu()))
} else {
// Windows / Linux:setContextMenu 绑右键自动弹,左键 show
tray.setContextMenu(buildMenu())
tray.on('click', showWindow)
}
return tray
}几个细节:
buildMenu()每次right-click重新构建,对于少量菜单项来说开销 < 1ms,同时能天然拿到最新的运行时状态。- 不要尝试用双击绕过——macOS status item 没有
double-click事件。 tray.setIgnoreDoubleClickEvents(true)只影响双击合并行为,解决不了这个问题。
darwin 分支只绑 click(左键 show)和 right-click(手动 popUpContextMenu),win/linux 分支保持原有 setContextMenu + click 组合。两端用户感知完全一致。
一句话外卖
macOS NSStatusItem 绑了 menu 就接管左键,Electron 跨平台 Tray 必须按
process.platform分支,darwin 路径禁止调用setContextMenu。