Electron 托盘菜单(Tray Menu)完整开发指南:创建系统托盘图标与右键菜单
本篇指南面向使用 Electron 构建桌面应用的开发者,系统讲解如何在应用的系统通知区域(托盘)中创建图标并为其挂载原生上下文菜单,覆盖托盘图标的创建前提、跨平台显示位置差异、"关闭所有窗口后驻留托盘"的实现方式,以及完整的可运行代码示例。读完本文,你将能够基于当前仓库(Electron 源码仓库)中的 Tray、Menu、NativeImage 三个 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,而app、Tray、Menu来自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 限制警告:
enabled与visibility属性对 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 覆盖了几个值得细读的实用手法:
- 从托盘重新唤起窗口:"Open App" 菜单项先通过
BrowserWindow.getAllWindows()判断窗口是否仍存在,不存在就重新createWindow(),存在则focus()。这与window-all-closed监听器配合,构成了完整的"窗口关闭 → 托盘常驻 → 菜单唤起"生命周期闭环。 - 动态切换图标:用
checkbox类型的菜单项控制tray.setImage()在红色与绿色NativeImage之间切换。setImage的底层实现(见 electron_api_tray.cc 的SetImage)在 Windows 上会通过native_image->GetHICON(...)把图像转为系统小图标句柄,其余平台则直接使用位图。 - macOS 标题动态开关:"Set Title" 通过
tray.setTitle()在菜单栏图标旁追加/移除文字(macOS 专属能力,支持 ANSI 颜色)。 - 工具提示:
tray.setToolTip('Tray Icon Demo')设置鼠标悬停提示。 - 关闭窗口不退出:
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 | 显示托盘气泡通知(需传 title 与 content,否则抛错),支持 iconType(none/info/warning/error/custom)、largeIcon、noSound、respectQuietTime 等选项 |
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-click、double-click(后两者 macOS、Windows)。 - Windows 专属:
middle-click、气泡通知三件套balloon-show/balloon-click/balloon-closed、focus(返回焦点到任务栏通知区)。 - macOS 专属:拖放系列
drop、drop-files、drop-text、drag-enter、drag-leave、drag-end;鼠标系列mouse-up、mouse-down、mouse-enter、mouse-leave、mouse-move(其中mouse-enter/mouse-leave/mouse-move在 Windows 同样可用)。
这些事件在 C++ 层由 TrayIcon 的观察者回调(如 OnClicked、OnRightClicked、OnDropFiles 等,见 electron_api_tray.cc)统一转译为 JS 层的 emit 调用,与文档声明一一对应。
常见陷阱清单
把上面的知识点浓缩成一份排错清单,供开发时对照:
- 在
ready前new Tray(...)—— 源码会直接抛错Cannot create Tray before app is ready;务必放入app.whenReady()回调。 - 未保存全局引用 —— 托盘实例被 GC 回收,图标闪现后消失;用模块级变量持有。
- 窗口全部关闭后进程退出 —— 需监听
window-all-closed且不调用app.quit(),才能实现最小化到托盘常驻。 - Linux 修改菜单不刷新 —— 修改
MenuItem后重新调用tray.setContextMenu(menu)。 - macOS 依赖
click弹菜单的场景 —— 设置了setContextMenu后mouse-up不再触发,注意交互语义变化。 - macOS 图标发虚/颜色不反转 —— 使用以
Template结尾的模板图像,并提供 144dpi 的@2x资源。 - Windows 图标效果差 —— 换用
ICO格式图标。
延伸阅读
- Tray 完整 API 参考(构造函数、全部方法、事件与平台注意事项)
- NativeImage API(图标资源的创建与处理)
- Menu / 菜单构建指南(role、类型、加速键、子菜单等模板语法)
- 普通上下文菜单指南(对比理解为何托盘菜单无需手动
popup) - 可运行 Fiddle 示例源码(
main.js+index.html) - Tray 单元测试(涵盖构造校验、
setContextMenu、destroy、macOS 的setIgnoreDoubleClickEvents等行为断言)
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00