系统托盘
系统托盘(System Tray)允许应用在后台运行,通过托盘图标提供快速访问和状态显示。常用于音乐播放器、下载工具、即时通讯、系统监控等需要常驻后台的应用。
系统架构
code
┌─────────────────────────────────────────────────────────┐
│ Tray 模块 │
├─────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 托盘图标 │ │ 托盘菜单 │ │ 托盘事件 │ │
│ │ (Image) │ │ (Menu) │ │ (Events) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 核心功能模块 │
│ • 应用常驻后台 • 状态显示 • 快速访问 │
│ • 消息通知 • 右键菜单 • 窗口管理 │
├─────────────────────────────────────────────────────────┤
│ 平台适配层 │
│ macOS (右上角) Windows (右下角) Linux (系统托盘)│
└─────────────────────────────────────────────────────────┘核心功能模块
1. 托盘图标管理
托盘图标是应用在系统托盘中的视觉标识,支持多种格式和动态变化。
javascript
const { Tray, nativeImage } = require('electron')
const path = require('path')
// 创建托盘图标
const createTrayIcon = () => {
// 基础图标创建
const iconPath = path.join(__dirname, 'assets/tray.png')
let icon = nativeImage.createFromPath(iconPath)
// 根据平台调整尺寸
if (process.platform === 'darwin') {
icon = icon.resize({ width: 16, height: 16 })
} else if (process.platform === 'win32') {
icon = icon.resize({ width: 16, height: 16 })
}
return icon
}
// macOS Template Image (自动适配深色/浅色模式)
const createTemplateIcon = () => {
if (process.platform === 'darwin') {
// 文件名以 Template 结尾
return nativeImage.createFromPath(
path.join(__dirname, 'assets/trayTemplate.png')
)
}
return nativeImage.createFromPath(
path.join(__dirname, 'assets/tray.png')
)
}2. 托盘菜单管理
托盘菜单提供应用的主要操作入口。
javascript
const { Menu } = require('electron')
// 创建基础托盘菜单
const createTrayMenu = (mainWindow, app) => {
return Menu.buildFromTemplate([
{
label: '显示主窗口',
click: () => {
mainWindow.show()
mainWindow.focus()
}
},
{
label: '设置',
click: () => mainWindow.webContents.send('open-settings')
},
{ type: 'separator' },
{
label: '退出',
click: () => {
app.isQuiting = true
app.quit()
}
}
])
}3. 窗口状态管理
托盘应用需要妥善管理窗口的显示和隐藏。
javascript
const manageWindowState = (mainWindow, app) => {
// 最小化到托盘而不是关闭
mainWindow.on('close', (event) => {
if (!app.isQuiting) {
event.preventDefault()
mainWindow.hide()
}
return false
})
// macOS 点击 Dock 图标显示窗口
app.on('activate', () => {
if (mainWindow.isMinimized()) {
mainWindow.restore()
}
mainWindow.show()
})
}Tray API 完整说明
构造函数
javascript
new Tray(image, guid)| 参数 | 类型 | 说明 |
|---|---|---|
image | NativeImage | String | 托盘图标 |
guid | String (Windows) | Windows 下用于记住托盘图标位置 |
核心方法
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
destroy() | - | void | 销毁托盘图标 |
setImage(image) | NativeImage | void | 设置托盘图标 |
setPressedImage(image) | NativeImage | void | 设置按下状态的图标 (macOS) |
setToolTip(toolTip) | String | void | 设置鼠标悬停提示 |
setTitle(title) | String | void | 设置托盘标题 (macOS) |
getTitle() | - | String | 获取托盘标题 |
setContextMenu(menu) | Menu | null | void | 设置右键菜单 |
getBounds() | - | Rectangle | 获取托盘图标位置和尺寸 |
isDestroyed() | - | Boolean | 托盘是否已销毁 |
事件列表
| 事件名 | 参数 | 平台 | 说明 |
|---|---|---|---|
click | event, bounds | 全平台 | 单击托盘图标 |
right-click | event, bounds | Win/Linux | 右键单击 |
double-click | event, bounds | macOS/Win | 双击托盘图标 |
mouse-enter | event, bounds | 全平台 | 鼠标进入托盘区域 |
mouse-leave | event, bounds | 全平台 | 鼠标离开托盘区域 |
mouse-move | event, bounds | Win/macOS | 鼠标在托盘上移动 |
balloon-click | - | Windows | 点击气泡通知 |
balloon-closed | - | Windows | 气泡通知关闭 |
drop-text | event, text | macOS | 文本拖拽到托盘 |
drop-files | event, files | macOS | 文件拖拽到托盘 |
drag-enter | event | macOS | 拖拽进入托盘区域 |
drag-leave | event | macOS | 拖拽离开托盘区域 |
drag-end | event | macOS | 拖拽结束 |
基本用法
创建托盘图标
javascript
const { app, Tray, Menu, nativeImage } = require('electron')
const path = require('path')
let tray = null
function createTray() {
// 创建图标
const icon = nativeImage.createFromPath(path.join(__dirname, 'icon.png'))
// 创建托盘
tray = new Tray(icon)
// 设置托盘提示文字
tray.setToolTip('My Electron App')
// 设置托盘标题(macOS)
tray.setTitle('App Title')
// 点击托盘图标
tray.on('click', () => {
mainWindow.show()
})
}托盘菜单
右键菜单
javascript
const { Menu } = require('electron')
function createTray() {
const icon = nativeImage.createFromPath(path.join(__dirname, 'icon.png'))
tray = new Tray(icon)
const contextMenu = Menu.buildFromTemplate([
{ label: '显示窗口', click: () => mainWindow.show() },
{ label: '设置', click: () => openSettings() },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
])
tray.setContextMenu(contextMenu)
}动态菜单
javascript
let isPlaying = false
function updateTrayMenu() {
const contextMenu = Menu.buildFromTemplate([
{
label: isPlaying ? '暂停' : '播放',
click: () => {
isPlaying = !isPlaying
updateTrayMenu()
}
},
{ label: '上一曲', click: () => prevTrack() },
{ label: '下一曲', click: () => nextTrack() },
{ type: 'separator' },
{
label: '退出',
click: () => app.quit()
}
])
tray.setContextMenu(contextMenu)
}带子菜单的托盘菜单
javascript
function createAdvancedTrayMenu() {
const contextMenu = Menu.buildFromTemplate([
{
label: '播放控制',
submenu: [
{ label: '播放/暂停', click: () => togglePlay() },
{ label: '上一曲', click: () => prevTrack() },
{ label: '下一曲', click: () => nextTrack() }
]
},
{
label: '播放模式',
submenu: [
{ label: '顺序播放', type: 'radio', checked: true },
{ label: '随机播放', type: 'radio' },
{ label: '单曲循环', type: 'radio' }
]
},
{ type: 'separator' },
{
label: '音量',
submenu: [
{ label: '静音', type: 'checkbox', click: () => toggleMute() },
{ label: '音量 +', click: () => volumeUp() },
{ label: '音量 -', click: () => volumeDown() }
]
},
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
])
tray.setContextMenu(contextMenu)
}托盘图标
图标格式与尺寸
javascript
// PNG 图标(推荐)
const icon = nativeImage.createFromPath(path.join(__dirname, 'icon.png'))
// ICO 图标(Windows)
const icon = nativeImage.createFromPath(path.join(__dirname, 'icon.ico'))
// 根据平台创建不同尺寸的图标
const createPlatformIcon = () => {
const icon = nativeImage.createFromPath('icon.png')
switch (process.platform) {
case 'darwin':
// macOS: 16x16 或 22x22
return icon.resize({ width: 16, height: 16 })
case 'win32':
// Windows: 16x16
return icon.resize({ width: 16, height: 16 })
default:
// Linux: 取决于桌面环境
return icon.resize({ width: 22, height: 22 })
}
}动态图标
javascript
// 更换图标(状态指示)
function setTrayIcon(status) {
const icons = {
normal: 'icons/tray-normal.png',
active: 'icons/tray-active.png',
error: 'icons/tray-error.png'
}
const icon = nativeImage.createFromPath(
path.join(__dirname, icons[status])
)
tray.setImage(icon)
}
// 显示数字徽章(macOS)
function showBadge(count) {
if (process.platform === 'darwin') {
tray.setTitle(count > 0 ? `${count}` : '')
}
}
// 闪烁提示(用于吸引注意)
let flashInterval = null
function startFlash() {
const normalIcon = nativeImage.createFromPath('icon.png')
const emptyIcon = nativeImage.createEmpty()
flashInterval = setInterval(() => {
const currentIcon = tray.getImage()
tray.setImage(currentIcon.isEmpty() ? normalIcon : emptyIcon)
}, 500)
}
function stopFlash() {
if (flashInterval) {
clearInterval(flashInterval)
flashInterval = null
tray.setImage(nativeImage.createFromPath('icon.png'))
}
}Template Image (macOS)
macOS 支持 Template Image,自动适配深色和浅色模式:
javascript
// 创建 Template Image
const createTemplateImage = () => {
if (process.platform === 'darwin') {
// 方式1: 文件名以 Template 结尾
return nativeImage.createFromPath('trayTemplate.png')
// 方式2: 使用模板选项
const image = nativeImage.createFromPath('tray.png')
return image.resize({ width: 16, height: 16 })
}
// 其他平台使用普通图标
return nativeImage.createFromPath('tray.png')
}
// Template Image 设计规范
// - 使用黑色线条和透明背景
// - 不要使用抗锯齿
// - 保持简洁的设计风格托盘事件
事件处理
javascript
// 点击事件
tray.on('click', (event, bounds) => {
console.log('托盘被点击', bounds)
console.log('托盘位置:', bounds.x, bounds.y)
mainWindow.show()
})
// 右键点击(Windows/Linux)
tray.on('right-click', () => {
console.log('右键点击')
// 在 Windows 上,setContextMenu 已自动处理右键菜单
})
// 双击事件
tray.on('double-click', () => {
mainWindow.show()
mainWindow.focus()
})
// 鼠标悬停
tray.on('mouse-enter', () => {
console.log('鼠标进入托盘区域')
})
// 鼠标离开
tray.on('mouse-leave', () => {
console.log('鼠标离开托盘区域')
})
// 托盘图标被拖拽
tray.on('drag-start', (event) => {
console.log('开始拖拽')
})文件拖拽(macOS)
javascript
// 支持文件拖拽到托盘图标
tray.on('drop-files', (event, files) => {
console.log('拖放的文件:', files)
// 处理拖放的文件
files.forEach(file => {
openFile(file)
})
})
// 拖拽事件流程
tray.on('drag-enter', () => {
console.log('拖拽进入托盘区域')
// 可以改变图标样式提示用户
})
tray.on('drag-leave', () => {
console.log('拖拽离开托盘区域')
})
tray.on('drag-end', () => {
console.log('拖拽结束')
})完整示例:音乐播放器托盘
javascript
const { app, BrowserWindow, Tray, Menu, nativeImage, ipcMain } = require('electron')
const path = require('path')
let mainWindow
let tray
let isQuiting = false
let playerState = {
isPlaying: false,
currentTrack: '',
volume: 50,
isMuted: false,
playMode: 'sequence' // sequence, random, repeat
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1000,
height: 700,
show: false, // 初始隐藏
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
// 最小化到托盘而不是关闭
mainWindow.on('close', (event) => {
if (!isQuiting) {
event.preventDefault()
mainWindow.hide()
}
})
}
function createTray() {
const icon = nativeImage.createFromPath(
path.join(__dirname, 'assets/tray.png')
).resize({ width: 16, height: 16 })
tray = new Tray(icon)
tray.setToolTip('音乐播放器')
updateTrayMenu()
// 点击显示窗口
tray.on('click', () => {
mainWindow.show()
mainWindow.focus()
})
// 双击显示窗口
tray.on('double-click', () => {
mainWindow.show()
mainWindow.focus()
})
}
function updateTrayMenu() {
const contextMenu = Menu.buildFromTemplate([
{
label: playerState.isPlaying ? '暂停' : '播放',
click: () => {
playerState.isPlaying = !playerState.isPlaying
mainWindow.webContents.send('player-toggle')
updateTrayMenu()
}
},
{ label: '上一曲', click: () => mainWindow.webContents.send('player-prev') },
{ label: '下一曲', click: () => mainWindow.webContents.send('player-next') },
{ type: 'separator' },
{
label: '播放模式',
submenu: [
{
label: '顺序播放',
type: 'radio',
checked: playerState.playMode === 'sequence',
click: () => {
playerState.playMode = 'sequence'
updateTrayMenu()
}
},
{
label: '随机播放',
type: 'radio',
checked: playerState.playMode === 'random',
click: () => {
playerState.playMode = 'random'
updateTrayMenu()
}
},
{
label: '单曲循环',
type: 'radio',
checked: playerState.playMode === 'repeat',
click: () => {
playerState.playMode = 'repeat'
updateTrayMenu()
}
}
]
},
{ type: 'separator' },
{ label: '显示主窗口', click: () => mainWindow.show() },
{ label: '退出', click: () => {
isQuiting = true
app.quit()
}}
])
tray.setContextMenu(contextMenu)
// 更新托盘标题 (macOS)
if (process.platform === 'darwin' && playerState.isPlaying) {
tray.setTitle('▶ ' + playerState.currentTrack)
}
}
// IPC 通信:更新播放器状态
ipcMain.on('update-player-state', (event, state) => {
playerState = { ...playerState, ...state }
updateTrayMenu()
// 更新托盘图标状态
const iconPath = playerState.isPlaying
? 'assets/tray-playing.png'
: 'assets/tray.png'
const icon = nativeImage.createFromPath(
path.join(__dirname, iconPath)
).resize({ width: 16, height: 16 })
tray.setImage(icon)
})
app.whenReady().then(() => {
createWindow()
createTray()
})
// macOS 点击 Dock 图标显示窗口
app.on('activate', () => {
mainWindow.show()
})
// 真正退出前销毁托盘
app.on('before-quit', () => {
isQuiting = true
if (tray) tray.destroy()
})平台差异
平台特性对照表
| 功能特性 | macOS | Windows | Linux |
|---|---|---|---|
| 托盘位置 | 右上角菜单栏 | 右下角任务栏 | 系统托盘区域 |
| 图标尺寸 | 16x16 / 22x22 | 16x16 | 取决于桌面环境 |
| Template Image | ✅ 支持 | ❌ 不支持 | ❌ 不支持 |
| setTitle | ✅ 显示文本 | ❌ 不显示 | ❌ 不显示 |
| displayBalloon | ❌ 不支持 | ✅ 气泡通知 | ❌ 不支持 |
| 文件拖拽 | ✅ 支持 | ❌ 不支持 | ⚠️ 部分支持 |
| 右键菜单 | ⚠️ 需手动处理 | ✅ setContextMenu | ✅ setContextMenu |
| 高 DPI 支持 | ✅ 自动处理 | ✅ 自动处理 | ⚠️ 视环境而定 |
平台适配代码
javascript
function createPlatformTray() {
const isMac = process.platform === 'darwin'
const isWin = process.platform === 'win32'
// 图标适配
let icon
if (isMac) {
// macOS 使用 Template Image
icon = nativeImage.createFromPath(path.join(__dirname, 'trayTemplate.png'))
if (icon.isEmpty()) {
icon = nativeImage.createFromPath(path.join(__dirname, 'tray.png'))
.resize({ width: 16, height: 16 })
}
} else if (isWin) {
icon = nativeImage.createFromPath(path.join(__dirname, 'tray.ico'))
if (icon.isEmpty()) {
icon = nativeImage.createFromPath(path.join(__dirname, 'tray.png'))
.resize({ width: 16, height: 16 })
}
} else {
icon = nativeImage.createFromPath(path.join(__dirname, 'tray.png'))
.resize({ width: 22, height: 22 })
}
tray = new Tray(icon)
// macOS 特殊处理
if (isMac) {
tray.on('right-click', (event, bounds) => {
// macOS 右键需要手动弹出菜单
tray.popUpContextMenu()
})
}
// Windows 气泡通知
if (isWin) {
tray.displayBalloon({
icon: nativeImage.createFromPath(path.join(__dirname, 'tray.png')),
title: '应用已启动',
content: '应用已最小化到系统托盘',
respectQuietTime: true
})
}
return tray
}Windows 气泡通知
Windows 平台独有的气泡通知功能,用于向用户显示提醒信息。
javascript
// 显示气泡通知
function showBalloon(title, content) {
if (process.platform !== 'win32') {
console.log('气泡通知仅支持 Windows 平台')
return
}
tray.displayBalloon({
icon: nativeImage.createFromPath(path.join(__dirname, 'icon.png')),
title: title,
content: content,
respectQuietTime: true // 尊重免打扰模式
})
}
// 气泡通知事件
tray.on('balloon-click', () => {
mainWindow.show()
mainWindow.focus()
})
tray.on('balloon-closed', () => {
console.log('气泡通知已关闭')
})
// 示例:下载完成通知
function onDownloadComplete(fileName) {
showBalloon('下载完成', `${fileName} 已下载完成`)
}错误处理
图标加载错误
javascript
function createTraySafely() {
try {
let icon = nativeImage.createFromPath(path.join(__dirname, 'tray.png'))
// 检查图标是否加载成功
if (icon.isEmpty()) {
console.warn('托盘图标加载失败,使用默认图标')
// 创建默认图标
icon = nativeImage.createEmpty()
}
tray = new Tray(icon)
return tray
} catch (error) {
console.error('创建托盘失败:', error)
return null
}
}菜单更新错误
javascript
function updateTrayMenuSafely() {
if (!tray || tray.isDestroyed()) {
console.warn('托盘不存在或已销毁')
return
}
try {
const contextMenu = Menu.buildFromTemplate([
{ label: '菜单项', click: () => {} }
])
tray.setContextMenu(contextMenu)
} catch (error) {
console.error('更新托盘菜单失败:', error)
}
}托盘销毁处理
javascript
// 应用退出前销毁托盘
app.on('before-quit', () => {
if (tray && !tray.isDestroyed()) {
tray.destroy()
tray = null
}
})
// 安全更新托盘
function safeTrayUpdate(updateFn) {
if (!tray || tray.isDestroyed()) {
return
}
try {
updateFn()
} catch (error) {
console.error('托盘更新失败:', error)
}
}性能优化
1. 图标优化
javascript
// 缓存图标实例
const iconCache = new Map()
function getCachedIcon(iconPath) {
if (!iconCache.has(iconPath)) {
const icon = nativeImage.createFromPath(iconPath)
.resize({ width: 16, height: 16 })
iconCache.set(iconPath, icon)
}
return iconCache.get(iconPath)
}
// 预加载图标
function preloadIcons() {
const icons = ['tray.png', 'tray-playing.png', 'tray-paused.png']
icons.forEach(icon => {
getCachedIcon(path.join(__dirname, 'assets', icon))
})
}2. 菜单更新优化
javascript
// 避免频繁重建菜单
let lastMenuState = ''
function updateMenuIfNeeded(newState) {
const stateKey = JSON.stringify(newState)
if (lastMenuState !== stateKey) {
updateTrayMenu(newState)
lastMenuState = stateKey
}
}
// 使用防抖更新
const debounce = require('lodash.debounce')
const debouncedMenuUpdate = debounce((state) => {
updateTrayMenu(state)
}, 100)3. 事件处理优化
javascript
// 避免频繁的事件触发
let lastClickTime = 0
tray.on('click', (event, bounds) => {
const now = Date.now()
if (now - lastClickTime < 300) {
// 忽略快速连续点击
return
}
lastClickTime = now
mainWindow.show()
mainWindow.focus()
})安全注意事项
1. 托盘图标安全
javascript
// 验证图标路径
function validateIconPath(iconPath) {
const allowedDir = path.join(__dirname, 'assets')
const resolvedPath = path.resolve(iconPath)
if (!resolvedPath.startsWith(allowedDir)) {
throw new Error('Invalid icon path')
}
return resolvedPath
}
// 安全加载图标
function safeLoadIcon(iconName) {
try {
const iconPath = validateIconPath(
path.join(__dirname, 'assets', iconName)
)
return nativeImage.createFromPath(iconPath)
} catch (error) {
console.error('图标加载失败:', error)
return nativeImage.createEmpty()
}
}2. IPC 通信安全
javascript
// 主进程:验证消息来源
ipcMain.on('update-tray', (event, data) => {
// 验证发送者
const webContents = event.sender
const mainWindow = BrowserWindow.getAllWindows()[0]
if (webContents !== mainWindow.webContents) {
console.warn('拒绝来自未知源的托盘更新请求')
return
}
// 验证数据格式
if (typeof data !== 'object' || data === null) {
console.warn('无效的托盘更新数据')
return
}
updateTrayMenu(data)
})
// 渲染进程:使用 contextBridge 安全暴露
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('tray', {
updateMenu: (data) => ipcRenderer.send('update-tray', data)
})3. 防止托盘劫持
javascript
// 限制托盘实例数量
let trayInstance = null
function createTray() {
if (trayInstance && !trayInstance.isDestroyed()) {
console.warn('托盘实例已存在')
return trayInstance
}
trayInstance = new Tray(createIcon())
return trayInstance
}
// 应用退出时清理
app.on('will-quit', () => {
if (trayInstance) {
trayInstance.destroy()
trayInstance = null
}
})实际应用场景
场景 1:下载管理器
javascript
// 托盘菜单实时显示下载进度
let downloadProgress = 0
let isDownloading = false
function updateDownloadProgress(progress) {
downloadProgress = progress
// 更新托盘标题(macOS)
if (process.platform === 'darwin') {
tray.setTitle(`${Math.round(progress)}%`)
}
// 更新菜单
updateDownloadMenu()
}
function updateDownloadMenu() {
const contextMenu = Menu.buildFromTemplate([
{
label: isDownloading
? `下载中... ${Math.round(downloadProgress)}%`
: '无下载任务',
enabled: false
},
{ type: 'separator' },
{
label: isDownloading ? '暂停下载' : '继续下载',
enabled: true,
click: () => toggleDownload()
},
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
])
tray.setContextMenu(contextMenu)
}场景 2:系统监控工具
javascript
// 托盘实时显示系统状态
function updateSystemStatus(cpu, memory) {
// 更新托盘图标颜色指示状态
let iconColor = 'green'
if (cpu > 80 || memory > 80) {
iconColor = 'red'
} else if (cpu > 50 || memory > 50) {
iconColor = 'yellow'
}
const icon = nativeImage.createFromPath(
path.join(__dirname, `assets/tray-${iconColor}.png`)
)
tray.setImage(icon)
// 更新提示文本
tray.setToolTip(`CPU: ${cpu}% | 内存: ${memory}%`)
// 更新托盘标题(macOS)
if (process.platform === 'darwin') {
tray.setTitle(`${Math.round(cpu)}%`)
}
}
// 定时更新
setInterval(() => {
const cpuUsage = getCpuUsage()
const memoryUsage = getMemoryUsage()
updateSystemStatus(cpuUsage, memoryUsage)
}, 1000)场景 3:即时通讯工具
javascript
// 托盘显示未读消息数量
let unreadCount = 0
function updateUnreadCount(count) {
unreadCount = count
// 更新托盘标题(macOS)
if (process.platform === 'darwin') {
tray.setTitle(count > 0 ? `${count}` : '')
}
// 更新托盘图标(Windows/Linux)
if (count > 0) {
const icon = nativeImage.createFromPath(
path.join(__dirname, 'assets/tray-unread.png')
)
tray.setImage(icon)
// Windows 气泡通知
if (process.platform === 'win32') {
tray.displayBalloon({
title: '新消息',
content: `您有 ${count} 条未读消息`,
respectQuietTime: true
})
}
} else {
const icon = nativeImage.createFromPath(
path.join(__dirname, 'assets/tray.png')
)
tray.setImage(icon)
}
}常见问题解答
1. 如何让窗口关闭时最小化到托盘?
javascript
let mainWindow
let isQuiting = false
mainWindow.on('close', (event) => {
if (!isQuiting) {
event.preventDefault()
mainWindow.hide()
}
})
// 退出菜单项
{
label: '退出',
click: () => {
isQuiting = true
app.quit()
}
}2. 如何在 macOS 上正确显示托盘图标?
javascript
// macOS 使用 Template Image
if (process.platform === 'darwin') {
// 方式1: 文件名以 Template 结尾
const icon = nativeImage.createFromPath('trayTemplate.png')
// 方式2: 创建合适尺寸
const icon = nativeImage.createFromPath('tray.png')
.resize({ width: 16, height: 16 })
tray = new Tray(icon)
}3. 托盘图标不显示怎么办?
检查以下几点:
- 图标路径是否正确
- 图标是否加载成功(检查
icon.isEmpty()) - 是否在
app.whenReady()后创建托盘 - 图标尺寸是否符合平台要求
javascript
function createTray() {
const iconPath = path.join(__dirname, 'assets/tray.png')
const icon = nativeImage.createFromPath(iconPath)
if (icon.isEmpty()) {
console.error('图标加载失败:', iconPath)
return
}
tray = new Tray(icon)
}
app.whenReady().then(createTray)4. 如何实现托盘图标的右键菜单在 macOS 上正常工作?
javascript
function createTray() {
const tray = new Tray(icon)
const contextMenu = Menu.buildFromTemplate([
{ label: '菜单项', click: () => {} }
])
// macOS 需要监听右键事件
if (process.platform === 'darwin') {
tray.on('right-click', () => {
tray.popUpContextMenu(contextMenu)
})
} else {
// Windows/Linux 可以直接设置
tray.setContextMenu(contextMenu)
}
}5. 如何获取托盘图标的位置?
javascript
const bounds = tray.getBounds()
console.log('托盘位置:', bounds.x, bounds.y)
console.log('托盘尺寸:', bounds.width, bounds.height)
// 在托盘附近显示窗口
function showWindowNearTray() {
const { x, y, width, height } = tray.getBounds()
const windowWidth = 300
const windowHeight = 400
mainWindow.setPosition(
x - windowWidth / 2 + width / 2,
y - windowHeight
)
mainWindow.show()
}6. 如何防止托盘应用多开?
javascript
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
} else {
app.on('second-instance', () => {
// 当运行第二个实例时,聚焦到已有窗口
if (mainWindow) {
mainWindow.show()
mainWindow.focus()
}
})
app.whenReady().then(() => {
createWindow()
createTray()
})
}最佳实践
1. 图标准备
- macOS: 准备 16x16 和 22x22 的 Template Image
- Windows: 准备 16x16 的 ICO 或 PNG 图标
- Linux: 准备 22x22 的 PNG 图标
- 提供 dark 和 light 两套图标(非 macOS)
2. 用户体验
- 提供清晰的托盘提示文本(
setToolTip) - 确保点击托盘能恢复窗口
- 提供明确的退出菜单项
- 最小化到托盘而不是关闭窗口
3. 状态同步
- 菜单状态应与窗口状态同步
- 使用动态菜单反映当前状态
- 避免过度的菜单更新频率
4. 跨平台兼容
javascript
// 使用条件判断处理平台差异
const isMac = process.platform === 'darwin'
const isWin = process.platform === 'win32'
// macOS 特定功能
if (isMac) {
tray.setTitle('状态文本')
}
// Windows 特定功能
if (isWin) {
tray.displayBalloon({ title: '通知', content: '内容' })
}5. 资源管理
javascript
// 应用退出时清理资源
app.on('before-quit', () => {
if (tray) {
tray.destroy()
}
})
// 清理定时器和监听器
let updateInterval
function cleanup() {
if (updateInterval) {
clearInterval(updateInterval)
}
if (tray) {
tray.removeAllListeners()
tray.destroy()
}
}