Electron Tray API 知识点总结
目录
1. 概述
什么是系统托盘?
系统托盘是位于屏幕底部(Windows/Linux)或顶部菜单栏右侧(macOS)的常驻图标。
┌─────────────────────────────────────────────────────────────┐
│ Windows/Linux │
│ ┌──────┐ ┌──────────────────────────────────────────┐ │
│ │ 📁 │ │ 应用程序区域 │ │
│ └──────┘ └──────────────────────────────────────────┘ │
│ ↑ │
│ 系统托盘 │
├─────────────────────────────────────────────────────────────┤
│ macOS │
│ ┌──────────────────────────────┐ ┌────┐ ┌────┐ │
│ │ 应用程序菜单 │ │ 📁 │ │ 🔊 │ ← 托盘 │
│ └──────────────────────────────┘ └────┘ └────┘ │
└─────────────────────────────────────────────────────────────┘
Tray 模块作用
- 在系统托盘区域显示图标
- 提供右键上下文菜单
- 处理点击事件(单击、双击、右键)
- 显示气泡通知(Windows)
- 支持拖拽操作(macOS)
2. 创建托盘
2.1 基本创建
const { app, Tray, Menu } = require('electron')
let tray = null
app.whenReady().then(() => {
tray = new Tray('/path/to/icon.png')
tray.setToolTip('我的应用')
const contextMenu = Menu.buildFromTemplate([
{ label: '显示窗口', click: () => mainWindow.show() },
{ label: '隐藏窗口', click: () => mainWindow.hide() },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
])
tray.setContextMenu(contextMenu)
})
2.2 构造函数参数
new Tray(image, [guid])
| 参数 | 类型 | 说明 | 平台 |
|---|
image | NativeImage/string | 托盘图标 | 全部 |
guid | string | 唯一标识符 | Windows/macOS |
2.3 图标要求
| 平台 | 推荐格式 | 尺寸要求 |
|---|
| Windows | ICO | 16x16, 32x32 等多尺寸 |
| macOS | Template Image | 16x16 @1x, 32x32 @2x (144dpi) |
| Linux | PNG | 16x16 或 22x22 |
2.4 macOS 模板图片
const { nativeImage } = require('electron')
const icon = nativeImage.createFromPath('icon.png')
icon.setTemplateImage(true)
const tray = new Tray(icon)
3. 实例事件
3.1 鼠标点击事件
| 事件 | 触发条件 | 平台 |
|---|
click | 单击图标 | 全部 |
right-click | 右键点击 | macOS/Windows |
double-click | 双击图标 | macOS/Windows |
middle-click | 中键点击 | Windows |
tray.on('click', (event, bounds) => {
mainWindow.show()
})
tray.on('right-click', (event, bounds) => {
tray.popUpContextMenu(customMenu)
})
tray.on('double-click', () => {
mainWindow.show()
mainWindow.focus()
})
3.2 鼠标移动事件
| 事件 | 触发条件 | 平台 |
|---|
mouse-enter | 鼠标进入图标区域 | macOS/Windows |
mouse-leave | 鼠标离开图标区域 | macOS/Windows |
mouse-move | 鼠标在图标上移动 | macOS/Windows |
mouse-down | 鼠标按下 | macOS |
mouse-up | 鼠标释放 | macOS |
tray.on('mouse-enter', () => {
})
tray.on('mouse-leave', () => {
})
3.3 拖拽事件 (macOS)
| 事件 | 触发条件 |
|---|
drop | 有拖拽项进入图标区域 |
drop-files | 文件拖入图标 |
drop-text | 文本拖入图标 |
drag-enter | 拖拽进入 |
drag-leave | 拖拽离开 |
drag-end | 拖拽结束 |
tray.on('drop-files', (event, files) => {
console.log('拖放的文件:', files)
files.forEach(file => processFile(file))
})
tray.on('drop-text', (event, text) => {
console.log('拖放的文本:', text)
})
3.4 气泡通知事件 (Windows)
| 事件 | 触发条件 |
|---|
balloon-show | 气泡显示 |
balloon-click | 气泡被点击 |
balloon-closed | 气泡关闭 |
4. 实例方法
4.1 图标操作
tray.setImage(icon)
tray.setPressedImage(icon)
tray.isDestroyed()
tray.destroy()
4.2 提示文本
tray.setToolTip('这是我的应用程序')
const tooltip = tray.getToolTip()
4.3 macOS 特有方法
tray.setTitle('App Name')
const title = tray.getTitle()
tray.setIgnoreDoubleClickEvents(true)
const isIgnored = tray.getIgnoreDoubleClickEvents()
const bounds = tray.getBounds()
4.4 菜单操作
tray.setContextMenu(menu)
tray.popUpContextMenu([menu, position])
tray.closeContextMenu()
4.5 Windows 气泡通知
tray.displayBalloon({
iconType: 'info',
title: '新消息',
content: '您有一条新通知',
largeIcon: true,
noSound: false,
respectQuietTime: false
})
tray.removeBalloon()
tray.focus()
5. 托盘菜单
5.1 基本菜单
const contextMenu = Menu.buildFromTemplate([
{ label: '显示主窗口', click: () => showWindow() },
{ label: '设置', click: () => openSettings() },
{ type: 'separator' },
{ label: '关于', click: () => showAbout() },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
])
tray.setContextMenu(contextMenu)
5.2 动态菜单
function buildContextMenu() {
return Menu.buildFromTemplate([
{
label: '通知',
submenu: [
{ label: '启用通知', type: 'checkbox', checked: true },
{ label: '静默模式', type: 'checkbox', checked: false }
]
},
{ type: 'separator' },
{
label: `状态: ${isRunning ? '运行中' : '已停止'}`,
enabled: false
},
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
])
}
tray.setContextMenu(buildContextMenu())
5.3 根据状态动态切换图标
function updateTrayIcon(isActive) {
const icon = isActive ? 'active.png' : 'inactive.png'
tray.setImage(icon)
tray.setToolTip(isActive ? '应用运行中' : '应用已停止')
}
6. 托盘气泡通知
6.1 Windows 气泡通知
tray.displayBalloon({
iconType: 'info',
title: '下载完成',
content: '文件已成功下载到 Downloads 文件夹'
})
tray.displayBalloon({
icon: path.join(__dirname, 'custom-icon.png'),
iconType: 'custom',
title: '自定义通知',
content: '这是一条自定义通知'
})
6.2 监听气泡事件
tray.on('balloon-click', () => {
mainWindow.show()
mainWindow.focus()
})
tray.on('balloon-closed', () => {
console.log('气泡通知已关闭')
})
7. 实际应用示例
7.1 完整的托盘应用
const { app, Tray, Menu, nativeImage, BrowserWindow } = require('electron')
let tray = null
let mainWindow = null
function createTray() {
const icon = nativeImage.createFromPath('icon.png')
tray = new Tray(icon.resize({ width: 16 }))
tray.setToolTip('我的 Electron 应用')
const contextMenu = Menu.buildFromTemplate([
{
label: '显示窗口',
click: () => {
if (mainWindow) {
mainWindow.show()
mainWindow.focus()
}
}
},
{
label: '隐藏窗口',
click: () => {
if (mainWindow) {
mainWindow.hide()
}
}
},
{ type: 'separator' },
{
label: '运行状态',
submenu: [
{ label: '● 运行中', type: 'radio', checked: true },
{ label: '○ 已暂停', type: 'radio', checked: false }
]
},
{ type: 'separator' },
{
label: '退出',
click: () => {
app.quit()
}
}
])
tray.setContextMenu(contextMenu)
tray.on('click', () => {
if (mainWindow) {
if (mainWindow.isVisible()) {
mainWindow.hide()
} else {
mainWindow.show()
mainWindow.focus()
}
}
})
tray.on('right-click', () => {
tray.popUpContextMenu(contextMenu)
})
}
function showNotification(message) {
if (process.platform === 'win32') {
tray.displayBalloon({
iconType: 'info',
title: '通知',
content: message
})
}
}
app.whenReady().then(() => {
mainWindow = new BrowserWindow()
mainWindow.loadURL('https://example.com')
createTray()
})
mainWindow.on('close', (event) => {
if (!app.isQuitting) {
event.preventDefault()
mainWindow.hide()
}
})
app.on('before-quit', () => {
app.isQuitting = true
})
7.2 音乐播放器托盘
class MusicPlayerTray {
constructor(player) {
this.player = player
this.tray = null
this.init()
}
init() {
this.tray = new Tray(this.getIcon())
this.tray.setToolTip('音乐播放器')
this.updateMenu()
this.tray.on('click', () => this.togglePlay())
}
getIcon() {
return this.player.isPlaying ? 'playing.png' : 'paused.png'
}
updateMenu() {
const menu = Menu.buildFromTemplate([
{
label: this.player.isPlaying ? '⏸ 暂停' : '▶ 播放',
click: () => this.togglePlay()
},
{ type: 'separator' },
{
label: '上一首',
click: () => this.player.previous()
},
{
label: '下一首',
click: () => this.player.next()
},
{ type: 'separator' },
{
label: `正在播放: ${this.player.currentSong}`,
enabled: false
},
{ type: 'separator' },
{
label: '退出',
click: () => app.quit()
}
])
this.tray.setContextMenu(menu)
this.tray.setImage(this.getIcon())
}
togglePlay() {
this.player.isPlaying ? this.player.pause() : this.player.play()
this.updateMenu()
}
}
7.3 下载管理器托盘
class DownloadManagerTray {
constructor(downloadManager) {
this.downloadManager = downloadManager
this.tray = null
this.init()
}
init() {
this.tray = new Tray('download-icon.png')
this.updateTray()
this.tray.on('drop-files', (event, files) => {
files.forEach(file => this.downloadManager.add(file))
})
}
updateTray() {
const active = this.downloadManager.getActiveDownloads()
const tooltip = active.length > 0
? `正在下载 ${active.length} 个文件`
: '没有正在下载'
this.tray.setToolTip(tooltip)
const menu = Menu.buildFromTemplate([
{
label: `下载中 (${active.length})`,
submenu: active.map(d => ({
label: `${d.name} - ${d.progress}%`,
enabled: false
}))
},
{ type: 'separator' },
{
label: '全部暂停',
click: () => this.downloadManager.pauseAll(),
enabled: active.length > 0
},
{
label: '全部取消',
click: () => this.downloadManager.cancelAll(),
enabled: active.length > 0
},
{ type: 'separator' },
{
label: '打开下载文件夹',
click: () => shell.openPath(app.getPath('downloads'))
},
{ type: 'separator' },
{
label: '退出',
click: () => app.quit()
}
])
this.tray.setContextMenu(menu)
}
}
8. 平台差异
8.1 功能支持
| 功能 | macOS | Windows | Linux |
|---|
| 单击事件 | ✅ | ✅ | ⚠️ |
| 右键菜单 | ✅ | ✅ | ✅ |
| 双击事件 | ✅ | ✅ | ❌ |
| 气泡通知 | ❌ | ✅ | ❌ |
| 拖拽支持 | ✅ | ❌ | ❌ |
| 标题显示 | ✅ | ❌ | ❌ |
| 模板图片 | ✅ | ❌ | ❌ |
| GUID 持久化 | ✅ | ✅ | ❌ |
8.2 图标要求
| 平台 | 推荐格式 | 尺寸 | 备注 |
|---|
| Windows | ICO | 16x16, 32x32 | 多尺寸 ICO 最佳 |
| macOS | PNG (模板) | 16x16 @1x, 32x32 @2x | 白色透明图 |
| Linux | PNG | 22x22 | 取决于桌面环境 |
8.3 macOS 模板图片注意事项
✅ 正确:
icon.png (16x16)
icon@2x.png (32x32, 144dpi)
文件名包含 "Template" 或设置 setTemplateImage(true)
❌ 错误:
icon-hashed.png (文件名被 webpack 哈希)
icon@3x.png (不支持的分辨率)
8.4 Linux 特殊处理
const contextMenu = Menu.buildFromTemplate([...])
tray.setContextMenu(contextMenu)
contextMenu.items[0].checked = false
tray.setContextMenu(contextMenu)
8.5 Windows GUID
const tray = new Tray(icon, {
guid: 'my-app-tray-uuid'
})
const tray = new Tray(icon, {
guid: 'my-app-tray-uuid'
})
最佳实践
✅ 推荐做法
app.on('before-quit', () => {
app.isQuitting = true
if (tray) {
tray.destroy()
}
})
mainWindow.on('close', (event) => {
if (!app.isQuitting) {
event.preventDefault()
mainWindow.hide()
}
})
const icon = nativeImage.createFromPath('icon.png')
icon.setTemplateImage(true)
if (process.platform === 'win32') {
tray.displayBalloon({ ... })
}
❌ 避免做法
mainWindow.on('close', (event) => {
event.preventDefault()
mainWindow.hide()
})
app.on('quit', () => {
if (tray) {
tray.destroy()
}
})
事件速查表
点击事件
click 单击(全部平台)
right-click 右键(macOS/Windows)
double-click 双击(macOS/Windows)
middle-click 中键(Windows)
鼠标事件
mouse-enter 进入图标区域(macOS/Windows)
mouse-leave 离开图标区域(macOS/Windows)
mouse-move 鼠标移动(macOS/Windows)
mouse-down 鼠标按下(macOS)
mouse-up 鼠标释放(macOS)
拖拽事件 (macOS)
drop 拖拽进入
drop-files 拖拽文件
drop-text 拖拽文本
drag-enter 拖入
drag-leave 拖出
drag-end 拖拽结束
气泡事件 (Windows)
balloon-show 气泡显示
balloon-click 气泡点击
balloon-closed 气泡关闭
方法速查表
基础
new Tray(image, [guid]) 创建托盘
tray.destroy() 销毁托盘
tray.isDestroyed() 检查是否已销毁
外观
tray.setImage(image) 设置图标
tray.setToolTip(text) 设置提示
tray.setTitle(title) 设置标题(macOS)
tray.getTitle() 获取标题(macOS)
菜单
tray.setContextMenu(menu) 设置右键菜单
tray.popUpContextMenu([menu]) 弹出菜单
tray.closeContextMenu() 关闭菜单
气泡 (Windows)
tray.displayBalloon(options) 显示气泡
tray.removeBalloon() 移除气泡
tray.focus() 聚焦托盘
文档基于 Electron v28+ Tray API 编写