首页 / Electron 入门教程 / 系统托盘 Tray

Electron 入门教程

系统托盘 Tray

本教程共 45 篇 · 第 20 篇 · 更新于 2026-08-03

Electron系统托盘Tray托盘菜单图标跨平台

20. 系统托盘 Tray

本节目标

  • 创建托盘图标并设置提示文字
  • 为托盘绑定右键菜单与点击行为
  • 掌握 Windows 与 macOS 在图标格式上的差异
  • 配合单实例锁实现常驻应用

很多桌面应用关掉窗口后仍在后台运行,任务栏右下角留一个小图标,那就是系统托盘(system tray)。Electron 用 Tray 模块实现它,常见于音乐播放器、即时通讯、云盘同步这类需要常驻的工具。

本章讲如何创建托盘、设置图标和提示文字、绑定右键菜单,以及点击图标时的行为。最后对照 Windows 与 macOS 在图标格式、事件上的差异。

20-1 创建一个托盘图标

Tray 的构造参数是一个图片(路径字符串或 NativeImage)。应用就绪后创建即可,注意要保住引用,别让它在函数结束时被回收。

// main.js(主进程)
const { app, Menu, Tray } = require('electron/main')

let tray = null

app.whenReady().then(() => {
  tray = new Tray('/path/to/icon.png')
  tray.setToolTip('我的应用')
  tray.setContextMenu(Menu.buildFromTemplate([
    { label: '打开', click: () => console.log('打开') },
    { label: '退出', click: () => app.quit() }
  ]))
})

setToolTip 设置鼠标悬停时显示的文字;setContextMenu 设置右键弹出的菜单。这样用户右键图标就能看到「打开 / 退出」等项。

20-2 给托盘加菜单

托盘菜单本质上是 Menu 实例,写法和应用菜单一致。常用的是「显示主窗口」「退出」这类动作。

const contextMenu = Menu.buildFromTemplate([
  { label: '显示窗口', click: () => mainWindow.show() },
  { label: '设置', click: () => console.log('设置') },
  { type: 'separator' },
  { label: '退出', click: () => app.quit() }
])

tray.setContextMenu(contextMenu)

在 Linux 上有一个坑:如果你修改了菜单项(比如改 checked 状态),必须再调一次 setContextMenu 才能生效。Windows 和 macOS 没有这个限制,但为兼容性统一重设一次最稳妥。

20-3 点击托盘图标的行为

Tray 会抛出 click 事件,适合用来切换主窗口的显示与隐藏(单击显示、再单击隐藏)。

tray.on('click', () => {
  if (mainWindow.isVisible()) {
    mainWindow.hide()
  } else {
    mainWindow.show()
  }
})

不同系统支持的事件略有差异。click 三平台通用;right-clickdouble-click 在 macOS 和 Windows 上可用;middle-click 仅 Windows。macOS 上若已设置 setContextMenumouse-up 等事件不会触发(被菜单接管),这是系统层面的限制。

20-4 Windows 与 macOS 的差异

图标格式差异最明显。Windows 官方建议用 ICO 图标,视觉效果最稳。macOS 则要求用「模板图片」(Template Image):图标文件名要以 Template 结尾,系统会按前景色自动反色,适配深色模式。

// macOS 推荐:文件名以 Template 结尾,如 iconTemplate.png
const tray = new Tray('/path/to/iconTemplate.png')

retina 屏上,建议准备 @2x 高分辨率图(如 iconTemplate@2x.png),命名不要被打包工具哈希打乱,否则 macOS 无法识别模板约定。16x16 与 32x32@2x 对多数图标已够用。

macOS 还支持 tray.setTitle 在图标旁显示文字,以及 setPressedImage 设置按下态图标。这些 Windows 没有。Windows 独有 displayBalloon 显示气泡提示,以及 guid 参数用于固定托盘图标位置。

20-5 销毁与生命周期

托盘图标应在应用退出前妥善销毁,避免资源泄漏。一般把 tray 设为模块级变量,在 will-quit 时置空即可;Tray 也提供 tray.destroy() 立即销毁。

app.on('will-quit', () => {
  if (tray) {
    tray.destroy()
    tray = null
  }
})

注意:若主窗口关闭就直接 app.quit,托盘也会随之消失。常驻类应用通常会拦截 window-all-closed,仅隐藏窗口而不退出,让托盘继续工作。

20-6 托盘图标的显示与隐藏

有时候你想临时隐藏托盘图标(比如进入「纯净模式」),可以调用 tray.destroy() 彻底销毁,也可以保留引用仅停止展示。需要再次显示时,重新 new Tray(...) 即可。

function hideTray() {
  if (tray) {
    tray.destroy()
    tray = null
  }
}

function showTray() {
  if (!tray) {
    tray = new Tray('/path/to/icon.png')
    tray.setContextMenu(contextMenu)
  }
}

注意 destroy 之后原 Tray 实例不可再用,必须新建。所以切换显隐时不要复用旧引用,重新创建最稳妥。

20-7 单实例与托盘的配合

托盘常驻应用一般配合单实例锁(app.requestSingleInstanceLock),避免用户多次双击启动多个副本。第二个实例启动时,把已有窗口从托盘唤出而不是再开一个。

const gotLock = app.requestSingleInstanceLock()
if (!gotLock) {
  app.quit()
} else {
  app.on('second-instance', () => {
    if (mainWindow) {
      mainWindow.show()
      mainWindow.focus()
    }
  })
}

这样无论用户点任务栏图标、托盘图标还是重复启动,都只对应一个应用实例,体验才顺。托盘在这里承担「入口」角色,点击即把隐藏的窗口带回到前台。

20-8 用原生图标与高分屏适配

托盘图标质量直接影响观感。Windows 上优先提供 .ico,它内部可含多尺寸,系统自动挑最合适的;若用 PNG,建议至少准备 16、32、64 像素几档。macOS 上模板图标用单色(通常指黑色)绘制,系统按前景色渲染,深色模式下自动反白。

高分屏务必配 @2x 图。否则图标会被拉伸发虚,尤其 macOS retina 上很明显。打包时确认文件名未被哈希改写,否则 Template 约定失效,图标不会反色。

// macOS:文件名以 Template 结尾,含 @2x 高分辨率版本
const tray = new Tray('/path/to/iconTemplate@2x.png')

Linux 上托盘用 StatusNotifierItem,个别桌面环境回退到 GtkStatusIcon。图标格式兼容 PNG 与 SVG,基本无需特殊处理。

20-9 完整示例:可切换显隐的托盘

把图标、菜单、点击切换窗口、单实例锁串起来,下面是一个常驻托盘的最小可运行骨架。它让应用关窗口不退出,点托盘图标切换显示,重复启动只唤起已有窗口。

// main.js(主进程)
const { app, BrowserWindow, Menu, Tray } = require('electron/main')
const path = require('node:path')

let win = null
let tray = null

function createWindow() {
  win = new BrowserWindow({ show: false })
  win.loadFile('index.html')
  // 关闭窗口改为隐藏,而非退出
  win.on('close', (e) => {
    e.preventDefault()
    win.hide()
  })
}

app.whenReady().then(() => {
  createWindow()
  tray = new Tray(path.join(__dirname, 'icon.png'))
  tray.setToolTip('常驻应用')
  tray.setContextMenu(Menu.buildFromTemplate([
    { label: '显示', click: () => win.show() },
    { label: '退出', click: () => app.quit() }
  ]))
  tray.on('click', () => (win.isVisible() ? win.hide() : win.show()))
})

这个骨架体现了托盘应用的典型生命周期:窗口可隐藏、应用常驻、入口在托盘。按此扩展,就能做出音乐播放器、下载工具这类后台程序。

托盘设计的小建议

托盘适合「后台常驻、偶尔交互」的应用,不适合作为主界面入口。如果你的应用主要就在前台用,硬编码一个托盘反而多余,还占用系统区域。想清楚是否真需要常驻,再决定做不做。

托盘菜单要简洁。常见组合是「显示主窗口 / 设置 / 退出」三五项,不要堆功能。图标要清晰、在小尺寸下可辨,最好准备多分辨率与模板图,避免高分屏发虚或深色模式看不清。

最后记得生命周期配合:窗口关闭时隐藏而非退出,应用退出时销毁托盘。漏掉任何一步,都可能出现「关了窗口托盘还在」或「退出后图标残留」的怪现象。

小结

Tray 让应用能常驻系统托盘。创建时传入图标路径,用 setContextMenu 绑定右键菜单、setToolTip 设悬停文字、click 事件切换窗口显隐。

跨平台要点:Windows 用 ICO,macOS 用 Template 结尾的模板图标并备好 @2x;事件支持程度各系统不同。托盘引用要保活,退出前记得销毁。