首页
/ Electron 托盘菜单(Tray Menu)完整开发指南:创建系统托盘图标与右键菜单

Electron 托盘菜单(Tray Menu)完整开发指南:创建系统托盘图标与右键菜单

2026-09-07 18:47:41作者:胡易黎Nicole

本篇指南面向使用 Electron 构建桌面应用的开发者,系统讲解如何在应用的系统通知区域(托盘)中创建图标并为其挂载原生上下文菜单,覆盖托盘图标的创建前提、跨平台显示位置差异、"关闭所有窗口后驻留托盘"的实现方式,以及完整的可运行代码示例。读完本文,你将能够基于当前仓库(Electron 源码仓库)中的 TrayMenuNativeImage 三个 API,独立实现一个带右键菜单、支持动态切换图标与标题的托盘应用。

托盘图标显示在哪里:三种操作系统的位置约定

通过 Tray API 创建的图标会进入操作系统各自的"系统托盘"区域,但不同系统对该区域的称呼和位置截然不同,这决定了你的应用在三个平台上的外观预期:

  • macOS:图标位于屏幕右上角的菜单栏附加区(menu bar extras,即状态栏右侧的图标区域)。
  • Windows:图标位于任务栏末尾的通知区域(notification area),也就是时钟旁边"显示隐藏的图标"所收纳的系统托盘。
  • Linux:托盘图标的位置由桌面环境决定,不同发行版与桌面(如 GNOME、KDE 等)实现并不统一,位置与行为可能存在差异。

关于平台差异的更多细节,可以查阅 Tray API 文档 中 "Platform Considerations" 一节;而各类原生菜单(应用菜单、普通上下文菜单、托盘菜单、macOS Dock 菜单)的整体关系,可参见 Menus 教程

创建 Tray 图标

托盘图标不像网页元素那样可以通过 HTML 声明,它必须以编程方式通过 Tray 类的实例来创建。Tray 的构造函数定义在 Tray API 文档 中:

new Tray(image, [guid])
  • image:一个 NativeImage 实例,或一个指向兼容图标文件的路径字符串。
  • guid(可选,Windows / macOS):用于标识托盘图标的唯一字符串,必须符合 UUID 格式(详见下文"高级能力"小节)。

底层实现位于仓库的 C++ 层 electron_api_tray.cc,构造函数会依次完成图标设置与事件观察者注册。这里有两个必须遵守的硬性约束:

约束一:必须在 ready 事件之后创建。 从源码可以看到,Tray::New 会先检查 Browser::Get()->is_ready(),如果应用尚未就绪就直接抛出错误 "Cannot create Tray before app is ready"。因此所有托盘初始化代码都应放进 app.whenReady() 的回调里:

const { app, Tray } = require('electron/main')

let tray = null

app.whenReady().then(() => {
  tray = new Tray('/path/to/my/icon')
})

约束二:必须保存对 Tray 实例的引用。 如果只是局部变量,一旦回调函数执行结束,实例就可能被垃圾回收,托盘图标随之消失。规范做法是在模块顶层用一个 let/const 变量持有引用(示例代码中的 let tray = null 就是这个作用)。

图标文件格式因系统而异

不要指望一张 PNG 图片能通吃三个平台,官方在 API 文档中对各平台的图标建议是:

  • macOS:应使用 模板图像(Template Image) 作为传入构造函数的图标,系统会自动根据深色/浅色菜单栏反转其颜色;为保证 Retina 屏幕清晰度,@2x 高清图的 DPI 应为 144;文件名必须以 Template 结尾(且打包时不能被 hash 混淆文件名),@2x 图与普通图需同名。16×16(72dpi)与 32×32@2x(144dpi)对大多数图标都适用。
  • Windows:推荐使用 ICO 图标以获得最佳视觉效果。
  • Linux:默认走 freedesktop 的 StatusNotifierItem 规范,桌面环境不支持时自动回退到 GtkStatusIcon

最小化到托盘:让应用在窗口关闭后继续驻留

很多聊天、下载、网盘类工具关闭主窗口后并不退出,而是收缩为托盘图标继续运行。要做到这一点,关键在于 app 模块的 window-all-closed 事件监听器——只要存在该事件的监听器,应用在全部窗口关闭时就不会自动退出。

默认的 Electron 应用模板通常也会监听这个事件,但会在 Windows 和 Linux 上显式退出应用以模拟操作系统的常规行为。如果你希望"最小化到托盘",就不要调用 app.quit()

app.on('window-all-closed', () => {
  // 只要存在该监听器,应用在窗口全部关闭后就不会退出
})

关于该事件的确切触发时机,可查阅 app 事件 window-all-closed

为 Tray 挂载上下文菜单

上下文菜单通过 Menu 实例挂载。与普通网页上下文菜单(需要手动调用 menu.popup 并监听触发时机)不同,Tray 的上下文菜单不需要任何手动 popup 调用——Tray 对象会自行处理鼠标点击事件并在合适时机弹出菜单。当然,Tray API 也保留了一系列点击类事件供高级场景使用(详见后文)。

把菜单交给图标只需一行:调用 tray.setContextMenu(menu),参数既可以是 Menu 实例,也可以是 null(表示移除菜单)。

下面是从 tray 教程原示例 完整摘录的最小可运行示例,它创建了一个 16×16 的红色圆点图标,并挂载一个仅含"退出"项的菜单:

const { nativeImage } = require('electron/common')
const { app, Tray, Menu } = require('electron/main')

// 在全局保存 Tray 引用,避免被垃圾回收
let tray

// 16x16 红色圆点 data URL
const icon = nativeImage.createFromDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACTSURBVHgBpZKBCYAgEEV/TeAIjuIIbdQIuUGt0CS1gW1iZ2jIVaTnhw+Cvs8/OYDJA4Y8kR3ZR2/kmazxJbpUEfQ/Dm/UG7wVwHkjlQdMFfDdJMFaACebnjJGyDWgcnZu1/lrCrl6NCoEHJBrDwEr5NrT6ko/UV8xdLAC2N49mlc5CylpYh8wCwqrvbBGLoKGvz8Bfq0QPWEUo/EAAAAASUVORK5CYII=')

// Tray 只能在 'ready' 事件触发之后实例化
app.whenReady().then(() => {
  tray = new Tray(icon)
  const contextMenu = Menu.buildFromTemplate([
    { role: 'quit' }
  ])
  tray.setContextMenu(contextMenu)
})

示例代码的关键细节

  • 模块拆分加载:示例中 nativeImage 来自 electron/common,而 appTrayMenu 来自 electron/main。这是当前 Electron 推荐的按需模块加载写法——Tray 仅能运行在主进程,因为其进程类型标注为 Main,若在渲染进程使用将无法工作。
  • 用 data URL 免去图片文件依赖nativeImage.createFromDataURL 把内嵌的 Base64 PNG 直接解码为 NativeImage,这样示例无需任何外部资源文件即可运行,非常适合演示与原型验证。生产代码中你也可以直接传本地路径字符串,例如 new Tray('/path/to/my/icon.png')
  • { role: 'quit' }Menu.buildFromTemplate 的模板项既可以写 label,也可以直接引用 role 获得跨平台的标准行为(此处的 quit 即退出应用)。Electron 会为带 role 的菜单项自动补充合适的默认 label 与快捷键。完整 role 清单与菜单构建技巧见 Menus 构建指南
  • macOS 限制警告enabledvisibility 属性对 macOS 托盘菜单的顶层菜单项不生效,这是平台 API 的固有限制。

平台注意要点:Linux 与 macOS 的特殊行为

即使代码逻辑完全一致,托盘菜单在部分平台上仍有需要规避的坑,以下是 Tray API 文档 中明确列出的平台注意点。

Linux:修改菜单项后必须重新 setContextMenu

在 Linux(走 StatusNotifierItem / GtkStatusIcon 时)上,直接修改某个 MenuItem 的属性不会自动反映到托盘菜单中,必须重新调用一次 tray.setContextMenu() 让改动生效:

const { app, Menu, Tray } = require('electron')

let appIcon = null
app.whenReady().then(() => {
  appIcon = new Tray('/path/to/my/icon')
  const contextMenu = Menu.buildFromTemplate([
    { label: 'Item1', type: 'radio' },
    { label: 'Item2', type: 'radio' }
  ])

  // 对上下文菜单做出修改
  contextMenu.items[1].checked = false

  // Linux 上因为修改了菜单,必须重新调用一次
  appIcon.setContextMenu(contextMenu)
})

此外 Linux 的 click 事件语义与其他平台不同:StatusNotifierItem 规范并未规定何种操作触发激活,有的桌面环境是单击左键,有的则是双击左键。因此不要把核心交互逻辑绑定在 Linux 的 click 事件上

macOS:mouse-up 事件与 setContextMenu 互斥

macOS 上设置了 setContextMenu 之后,mouse-up 事件将不再触发——这是 macOS 系统层面的约束。同样地,若想借助 click 事件响应单击,需注意 macOS 默认单击弹菜单/双击发送事件的行为与 setIgnoreDoubleClickEvents 的配合。

一个可运行的完整 Demo:动态控制图标与菜单状态

仓库在 docs/fiddles/menus/tray-menu 目录下提供了一个可直接运行(Electron Fiddle)的完整示例,其入口文件 main.js 完整演示了"托盘驻留 + 状态控制菜单"的典型结构。它是理解 Tray 用法的最佳范本,结构如下:

const { app, BrowserWindow, Menu, Tray } = require('electron/main')
const { nativeImage } = require('electron/common')

// 保存全局引用,防止托盘被垃圾回收
let tray = null

function createWindow () {
  const mainWindow = new BrowserWindow()
  mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
  createWindow()

  const red = nativeImage.createFromDataURL('data:image/png;base64,…')   // 红色圆点图标
  const green = nativeImage.createFromDataURL('data:image/png;base64,…') // 绿色圆点图标

  tray = new Tray(red)
  tray.setToolTip('Tray Icon Demo')

  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Open App',
      click: () => {
        const wins = BrowserWindow.getAllWindows()
        if (wins.length === 0) {
          createWindow()
        } else {
          wins[0].focus()
        }
      }
    },
    {
      label: 'Set Green Icon',
      type: 'checkbox',
      click: ({ checked }) => {
        checked ? tray.setImage(green) : tray.setImage(red)
      }
    },
    {
      label: 'Set Title',
      type: 'checkbox',
      click: ({ checked }) => {
        checked ? tray.setTitle('Title') : tray.setTitle('')
      }
    },
    { role: 'quit' }
  ])

  tray.setContextMenu(contextMenu)
})

app.on('window-all-closed', function () {
  // 阻止窗口关闭时应用退出
})

app.on('activate', function () {
  if (BrowserWindow.getAllWindows().length === 0) createWindow()
})

这个 Demo 覆盖了几个值得细读的实用手法:

  1. 从托盘重新唤起窗口:"Open App" 菜单项先通过 BrowserWindow.getAllWindows() 判断窗口是否仍存在,不存在就重新 createWindow(),存在则 focus()。这与 window-all-closed 监听器配合,构成了完整的"窗口关闭 → 托盘常驻 → 菜单唤起"生命周期闭环。
  2. 动态切换图标:用 checkbox 类型的菜单项控制 tray.setImage() 在红色与绿色 NativeImage 之间切换。setImage 的底层实现(见 electron_api_tray.ccSetImage)在 Windows 上会通过 native_image->GetHICON(...) 把图像转为系统小图标句柄,其余平台则直接使用位图。
  3. macOS 标题动态开关:"Set Title" 通过 tray.setTitle() 在菜单栏图标旁追加/移除文字(macOS 专属能力,支持 ANSI 颜色)。
  4. 工具提示tray.setToolTip('Tray Icon Demo') 设置鼠标悬停提示。
  5. 关闭窗口不退出window-all-closed 监听器为空实现;同时响应 macOS 的 activate 事件,保证从 Dock 重新激活时能重建窗口。

其配套页面 index.html 只是演示说明页,并通过 CSP 限制脚本来源,页面中提示用户:应用会在所有窗口关闭后继续运行,并逐一列出了四个菜单项(Open App / Set Green Icon / Set Title / Quit)的功能说明。运行该示例时,右键(或按平台习惯单击)托盘图标即可看到完整菜单。

在托盘 API 之上做更多:事件、方法与高级配置

当教程类的基础用法不能满足需求时,Tray API 文档 提供了完整的能力矩阵,这里按类别整理,方便按需查阅。

构造参数 guid(Windows / macOS)

除图标外,构造函数还可传入 UUID 格式的 guid 字符串,用于唯一标识托盘图标:

  • Windows:若可执行文件已代码签名且签名含组织信息,GUID 会与该签名永久绑定,系统托盘中的图标位置等 OS 级设置即使可执行文件路径改变也能保留;若未签名则 GUID 与可执行文件路径绑定,路径改变会导致托盘创建失败并需更换 GUID。官方强烈建议仅配合签名程序使用 guid;一个应用若创建多个托盘图标,每个图标必须使用各自的 GUID。
  • macOS:相同 GUID 可在应用重启后把新托盘项恢复到原位置。

源码中 Tray::New 会校验 GUID 格式并透传(见 electron_api_tray.cc),错误格式会抛出 "Invalid GUID format - GUID must be a string"。配套的单元测试 spec/api-tray-spec.ts 中也覆盖了"非法 GUID 抛错""接受合法 GUID"等场景。

常用实例方法一览

方法 平台 说明
tray.destroy() 全部 立即销毁托盘图标;源码中会同时清空菜单与内部 tray_icon_,销毁后 isDestroyed() 返回 true
tray.setImage(image) 全部 更换托盘图标
tray.setPressedImage(image) macOS 设置图标被按下时显示的图像
tray.setToolTip(toolTip) 全部 设置悬停提示文字,传空字符串可移除
tray.setTitle(title[, options]) macOS 在菜单栏图标旁显示标题,支持 ANSI 颜色;fontType 可为 monospaced / monospacedDigit,源码会对非法取值抛出 TypeError
tray.setIgnoreDoubleClickEvents(ignore) macOS 忽略双击事件以便检测每一次单击,默认 false
tray.displayBalloon(options) Windows 显示托盘气泡通知(需传 titlecontent,否则抛错),支持 iconTypenone/info/warning/error/custom)、largeIconnoSoundrespectQuietTime 等选项
tray.popUpContextMenu([menu, position]) macOS、Windows 主动弹出托盘上下文菜单;传入 menu 时显示指定菜单而非已设置的菜单;position 仅 Windows 可用,默认 (0, 0)
tray.setContextMenu(menu) 全部 设置或(传 null)移除上下文菜单
tray.getBounds() macOS、Windows 获取托盘图标的矩形边界
tray.isDestroyed() 全部 判断托盘是否已被销毁

细节提示:setImage / setPressedImage 底层在 Windows 分支使用 SM_CXSMICON 尺寸生成 HICON(见 electron_api_tray.cc),这正是 Windows 上推荐直接使用 ICO 资源的原因——矢量 ICO 在不同 DPI 下都能渲染清晰。

事件列表

Tray 是 EventEmitter 的子类(在 api/tray.md 中注明),主要事件按平台划分如下:

  • 通用点击click(返回键盘事件 event、图标边界 bounds、坐标 position;Linux 上激活来源不一定是左键单击)、right-clickdouble-click(后两者 macOS、Windows)。
  • Windows 专属middle-click、气泡通知三件套 balloon-show / balloon-click / balloon-closedfocus(返回焦点到任务栏通知区)。
  • macOS 专属:拖放系列 dropdrop-filesdrop-textdrag-enterdrag-leavedrag-end;鼠标系列 mouse-upmouse-downmouse-entermouse-leavemouse-move(其中 mouse-enter/mouse-leave/mouse-move 在 Windows 同样可用)。

这些事件在 C++ 层由 TrayIcon 的观察者回调(如 OnClickedOnRightClickedOnDropFiles 等,见 electron_api_tray.cc)统一转译为 JS 层的 emit 调用,与文档声明一一对应。

常见陷阱清单

把上面的知识点浓缩成一份排错清单,供开发时对照:

  1. readynew Tray(...) —— 源码会直接抛错 Cannot create Tray before app is ready;务必放入 app.whenReady() 回调。
  2. 未保存全局引用 —— 托盘实例被 GC 回收,图标闪现后消失;用模块级变量持有。
  3. 窗口全部关闭后进程退出 —— 需监听 window-all-closed 且不调用 app.quit(),才能实现最小化到托盘常驻。
  4. Linux 修改菜单不刷新 —— 修改 MenuItem 后重新调用 tray.setContextMenu(menu)
  5. macOS 依赖 click 弹菜单的场景 —— 设置了 setContextMenumouse-up 不再触发,注意交互语义变化。
  6. macOS 图标发虚/颜色不反转 —— 使用以 Template 结尾的模板图像,并提供 144dpi 的 @2x 资源。
  7. Windows 图标效果差 —— 换用 ICO 格式图标。

延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388