6月4日 13:58

Electron菜单和托盘:跨平台差异、右键菜单和托盘图标坑

菜单和托盘是桌面应用的"门面"——用户通过菜单找到功能,通过托盘保持应用在后台运行。Electron 提供了 Menu 和 Tray API,但跨平台差异和坑不少:macOS 的菜单栏和 Windows 完全不同,托盘图标格式要求也不一样。这篇文章把菜单和托盘的常见实现都过一遍。

应用菜单

创建基础菜单

macOS 应用的菜单栏是系统级的,不创建菜单连快捷键都不好使。Windows/Linux 的菜单可以藏在窗口里。

javascript
const { app, Menu, BrowserWindow } = require('electron') app.whenReady().then(() => { const mainWindow = new BrowserWindow({ /* ... */ }) const template = [ { label: '文件', submenu: [ { label: '新建', accelerator: 'CmdOrCtrl+N', click: () => createNewFile() }, { label: '打开', accelerator: 'CmdOrCtrl+O', click: () => openFile() }, { type: 'separator' }, { label: '保存', accelerator: 'CmdOrCtrl+S', click: () => saveFile() }, { type: 'separator' }, { label: '退出', accelerator: 'CmdOrCtrl+Q', click: () => app.quit() } ] }, { label: '编辑', submenu: [ { role: 'undo', label: '撤销' }, { role: 'redo', label: '重做' }, { type: 'separator' }, { role: 'cut', label: '剪切' }, { role: 'copy', label: '复制' }, { role: 'paste', label: '粘贴' }, { role: 'selectAll', label: '全选' } ] }, { label: '视图', submenu: [ { role: 'reload', label: '刷新' }, { role: 'toggleDevTools', label: '开发者工具' }, { type: 'separator' }, { role: 'resetZoom', label: '重置缩放' }, { role: 'zoomIn', label: '放大' }, { role: 'zoomOut', label: '缩小' }, { type: 'separator' }, { role: 'togglefullscreen', label: '全屏' } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) })

macOS 的特殊处理

macOS 第一项菜单名必须是应用名,系统会自动加"关于""隐藏""退出"等菜单项:

javascript
const isMac = process.platform === 'darwin' const template = [ ...(isMac ? [{ label: app.name, submenu: [ { role: 'about', label: `关于 ${app.name}` }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide', label: '隐藏' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit', label: '退出' } ] }] : []), // ... 其他菜单 ]

如果不加这个,macOS 上应用菜单的行为会很奇怪——没有"关于"和"偏好设置"入口,也不支持 Cmd+H 隐藏窗口。

动态菜单

菜单项可以运行时修改——打勾、禁用、改文字:

javascript
const menu = Menu.buildFromTemplate([ { label: '视图', submenu: [ { label: '深色模式', type: 'checkbox', checked: store.get('darkMode', false), click: (menuItem) => { store.set('darkMode', menuItem.checked) applyTheme(menuItem.checked) } }, { label: '导出', enabled: false, // 初始禁用,打开文件后启用 id: 'exportMenu' } ] } ]) // 打开文件后启用导出 const exportItem = menu.getMenuItemById('exportMenu') exportItem.enabled = true

隐藏默认菜单

如果你不需要菜单栏(如工具类应用),可以设为 null:

javascript
Menu.setApplicationMenu(null)

注意:设为 null 后,复制粘贴等默认快捷键也会失效。如果你只是想隐藏菜单栏但保留快捷键,用 autoHideMenuBar: true 创建窗口(Windows/Linux 上按 Alt 显示菜单)。

右键菜单是最常用的交互——在列表上右键编辑删除,在输入框里右键复制粘贴:

javascript
const { Menu, ipcMain } = require('electron') // 渲染进程通过 IPC 请求弹出右键菜单 ipcMain.on('show-context-menu', (event, type) => { const template = getContextMenuTemplate(type) const menu = Menu.buildFromTemplate(template) // 在当前窗口弹出 const win = BrowserWindow.fromWebContents(event.sender) menu.popup({ window: win }) }) function getContextMenuTemplate(type) { if (type === 'file') { return [ { label: '打开', click: () => openFile() }, { label: '重命名', click: () => renameFile() }, { type: 'separator' }, { label: '删除', click: () => deleteFile() } ] } if (type === 'text') { return [ { role: 'copy' }, { role: 'cut' }, { role: 'paste' } ] } return [{ role: 'copy' }, { role: 'paste' }] }

渲染进程触发:

javascript
// preload.js contextBridge.exposeInMainWorld('electronAPI', { showContextMenu: (type) => ipcRenderer.send('show-context-menu', type) }) // renderer.js window.addEventListener('contextmenu', (e) => { e.preventDefault() const type = e.target.closest('.file-item') ? 'file' : 'text' window.electronAPI.showContextMenu(type) })

也可以用 electron-context-menu 这个库,自动给输入框加复制粘贴菜单、给图片加保存菜单。

系统托盘(Tray)

托盘让应用最小化到系统托盘区,不占任务栏位置——后台工具、音乐播放器、下载器几乎都要托盘。

基础实现

javascript
const { Tray, Menu, nativeImage } = require('electron') let tray = null app.whenReady().then(() => { const iconPath = path.join(__dirname, 'assets', 'tray-icon.png') const icon = nativeImage.createFromPath(iconPath) tray = new Tray(icon.resize({ width: 16, height: 16 })) tray.setToolTip('我的应用') const contextMenu = Menu.buildFromTemplate([ { label: '显示窗口', click: () => mainWindow.show() }, { label: '暂停', type: 'checkbox', checked: false, click: (item) => togglePause(item.checked) }, { type: 'separator' }, { label: '退出', click: () => app.quit() } ]) tray.setContextMenu(contextMenu) })

点击托盘图标显示窗口

Windows/Linux 上点击托盘图标通常应该显示/聚焦窗口,macOS 上则弹出菜单(系统规范):

javascript
tray.on('click', () => { if (process.platform === 'darwin') return // macOS 用菜单 if (mainWindow.isVisible()) { mainWindow.hide() } else { mainWindow.show() mainWindow.focus() } })

托盘图标格式

不同平台对图标的要求不同:

平台推荐格式尺寸注意
Windows.ico 或 .png16x16支持 ICO 多尺寸
macOS.png 或 Template16x16深色模式用 Template 图标
Linux.png16x16部分桌面环境要求 22x22

macOS 深色模式适配:图标文件名以 Template 结尾(如 tray-iconTemplate.png),系统会自动根据明暗主题反色。使用方式:

javascript
const iconPath = path.join(__dirname, 'assets', process.platform === 'darwin' ? 'tray-iconTemplate.png' : 'tray-icon.png')

最小化到托盘而非关闭

点击关闭按钮时隐藏到托盘,而不是退出应用:

javascript
mainWindow.on('close', (event) => { if (!app.isQuitting) { event.preventDefault() mainWindow.hide() } }) app.on('before-quit', () => { app.isQuitting = true })

app.isQuitting 是自定义标志——只有通过托盘的"退出"或 Cmd+Q 触发的退出才会真正关闭窗口。直接点关闭按钮只是隐藏。

动态托盘图标

下载进度、新消息通知等场景需要动态更新托盘图标:

javascript
// 用 Canvas 生成带数字的图标 const { nativeImage } = require('electron') function createBadgeIcon(count) { const { createCanvas } = require('canvas') // 需要 npm install canvas const size = 16 const canvas = createCanvas(size * 2, size * 2) // 2x for retina const ctx = canvas.getContext('2d') ctx.drawImage(baseIcon, 0, 0, size * 2, size * 2) if (count > 0) { ctx.fillStyle = '#FF3B30' ctx.beginPath() ctx.arc(size * 1.5, size * 0.5, 8, 0, Math.PI * 2) ctx.fill() ctx.fillStyle = 'white' ctx.font = 'bold 10px sans-serif' ctx.textAlign = 'center' ctx.fillText(count > 9 ? '9+' : String(count), size * 1.5, size * 0.5 + 4) } return nativeImage.createFromBuffer(canvas.toBuffer()) } tray.setImage(createBadgeIcon(unreadCount))

macOS 上更简单——系统原生支持 Dock 徽标:

javascript
app.dock.setBadge(unreadCount > 0 ? String(unreadCount) : '')
标签:Electron