返回博客

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:单击托盘图标 → 菜单弹出 → click handler 完全没有执行

根因

macOS 的 NSStatusItem(系统状态栏 item)模型规定:一旦 status item 绑定了 menu,左键点击就由系统接管并自动弹出菜单——这是 macOS 的标准交互语义,不是 Electron 的 bug。

Electron 的 tray.setContextMenu(menu) 底层调用的正是 NSStatusItem.menu = ...,设置之后系统完全接管了点击事件,Electron 层的 click event 不再被派发。

Windows 的通知区域(notification area)模型完全不同:左键、右键、双击都是独立事件,setContextMenu 只负责右键自动弹,完全不影响左键的 click 事件派发。

坑:macOS 上 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) 只影响双击合并行为,解决不了这个问题。
正解:macOS 路径完全不调用 setContextMenu

darwin 分支只绑 click(左键 show)和 right-click(手动 popUpContextMenu),win/linux 分支保持原有 setContextMenu + click 组合。两端用户感知完全一致。

一句话外卖

macOS NSStatusItem 绑了 menu 就接管左键,Electron 跨平台 Tray 必须按 process.platform 分支,darwin 路径禁止调用 setContextMenu