系统托盘(System Tray)基础
系统托盘是桌面上常驻的小图标,点击可以显示菜单或窗口。
创建托盘图标
const { Tray, Menu, nativeImage, app } = require('electron');
const path = require('path');
let tray = null;
function createTray() {
// 创建托盘图标
const iconPath = path.join(__dirname, 'icon.png');
const icon = nativeImage.createFromPath(iconPath);
// macOS 推荐使用模板图片(Template Image)
// 模板图片会自动适配浅色/深色模式
const templateIcon = nativeImage.createFromPath(iconPath);
templateIcon.setTemplateImage(true);
tray = new Tray(templateIcon);
// 设置提示文字(鼠标悬停时显示)
tray.setToolTip('My Electron App');
// 设置上下文菜单
const contextMenu = Menu.buildFromTemplate([
{ label: '显示主窗口', click: showMainWindow },
{ type: 'separator' },
{ label: '关于', click: showAbout },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
]);
tray.setContextMenu(contextMenu);
// 点击托盘图标的事件
tray.on('click', (event) => {
showMainWindow();
});
// macOS 专用:点击时显示菜单
tray.on('click', (event, bounds) => {
// 在 macOS 上,点击托盘图标通常显示菜单
// 但也可以自定义行为
});
}
托盘图标尺寸
不同操作系统的托盘图标尺寸不同:
| 系统 | 推荐尺寸 |
|---|---|
| Windows | 16x16, 32x32 |
| macOS | 16x16 (@1x), 32x32 (@2x) |
| Linux | 22x22, 32x32 |
动态更新托盘图标
// 更新托盘图标
function updateTrayIcon(status) {
const iconPath = status === 'busy'
? path.join(__dirname, 'icon-busy.png')
: path.join(__dirname, 'icon-idle.png');
const newIcon = nativeImage.createFromPath(iconPath);
tray.setImage(newIcon);
}
// 更新提示文字
tray.setToolTip(`My App - ${currentStatus}`);
托盘右键菜单
完整菜单示例
const contextMenuTemplate = [
{
label: '打开主窗口',
click: () => {
const mainWindow = BrowserWindow.getAllWindows()[0];
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
}
},
{
label: '快速操作',
submenu: [
{
label: '新建文档',
accelerator: 'CmdOrCtrl+N',
click: () => createNewDocument()
},
{
label: '打开文件',
accelerator: 'CmdOrCtrl+O',
click: () => openFile()
}
]
},
{ type: 'separator' },
{
label: '运行状态',
enabled: false // 禁用项,显示当前状态
},
{
label: '● 已连接',
enabled: false
},
{ type: 'separator' },
{
label: '设置',
click: () => openSettings()
},
{
label: '检查更新',
click: () => checkForUpdates()
},
{ type: 'separator' },
{
label: '关于',
click: () => showAboutDialog()
},
{
label: '退出',
accelerator: 'CmdOrCtrl+Q',
click: () => app.quit()
}
];
const contextMenu = Menu.buildFromTemplate(contextMenuTemplate);
tray.setContextMenu(contextMenu);
子菜单
子菜单可以嵌套多层:
{
label: '编辑',
submenu: [
{ label: '撤销', role: 'undo' },
{ label: '重做', role: 'redo' },
{ type: 'separator' },
{ label: '剪切', role: 'cut' },
{ label: '复制', role: 'copy' },
{ label: '粘贴', role: 'paste' },
{ type: 'separator' },
{
label: '查找',
submenu: [
{ label: '查找下一个', accelerator: 'F3', click: () => findNext() },
{ label: '查找上一个', accelerator: 'Shift+F3', click: () => findPrevious() }
]
}
]
}
应用菜单(Application Menu)
创建应用菜单
const { Menu, app, shell } = require('electron');
function createApplicationMenu() {
const template = [
{
label: app.name, // macOS 会显示为应用名
submenu: [
{ label: '关于', role: 'about' },
{ type: 'separator' },
{ label: '设置', accelerator: 'Cmd+,', click: () => openSettings() },
{ type: 'separator' },
{ label: '隐藏', role: 'hide' },
{ label: '隐藏其他', role: 'hideOthers' },
{ label: '显示全部', role: 'unhide' },
{ type: 'separator' },
{ label: '退出', role: 'quit' }
]
},
{
label: '文件',
submenu: [
{ label: '新建', accelerator: 'CmdOrCtrl+N', click: () => newFile() },
{ label: '打开', accelerator: 'CmdOrCtrl+O', click: () => openFile() },
{
label: '打开最近',
role: 'recentDocuments',
submenu: [
{ label: '清除最近文件', role: 'clearRecentDocuments' }
]
},
{ type: 'separator' },
{ label: '保存', accelerator: 'CmdOrCtrl+S', click: () => saveFile() },
{ label: '另存为', accelerator: 'CmdOrCtrl+Shift+S', click: () => saveFileAs() },
{ type: 'separator' },
{ label: '导出 PDF', click: () => exportPDF() }
]
},
{
label: '编辑',
submenu: [
{ label: '撤销', role: 'undo' },
{ label: '重做', role: 'redo' },
{ type: 'separator' },
{ label: '剪切', role: 'cut' },
{ label: '复制', role: 'copy' },
{ label: '粘贴', role: 'paste' },
{ label: '删除', role: 'delete' },
{ type: 'separator' },
{ label: '全选', role: 'selectAll' }
]
},
{
label: '视图',
submenu: [
{ label: '重新加载', role: 'reload' },
{ label: '强制重新加载', role: 'forceReload' },
{ label: '切换开发者工具', role: 'toggleDevTools' },
{ type: 'separator' },
{ label: '放大', role: 'zoomIn' },
{ label: '缩小', role: 'zoomOut' },
{ label: '重置缩放', role: 'resetZoom' },
{ type: 'separator' },
{ label: '全屏', role: 'togglefullscreen' }
]
},
{
label: '窗口',
submenu: [
{ label: '最小化', role: 'minimize' },
{ label: '缩放', role: 'zoom' },
{ type: 'separator' },
{ label: '前置全部窗口', role: 'front' }
]
},
{
label: '帮助',
submenu: [
{
label: '文档',
click: () => {
shell.openExternal('https://electronjs.org/docs');
}
},
{
label: '报告问题',
click: () => {
shell.openExternal('https://github.com/electron/electron/issues');
}
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
macOS 特有菜单行为
在 macOS 上,第一个菜单项的名称是应用名称,并且有些 role 行为不同:
{
label: app.name, // macOS 会使用应用的实际名称
submenu: [
{ label: '关于', role: 'about' },
{ type: 'separator' },
{ label: '服务', role: 'services', submenu: [] }, // macOS 专用
{ type: 'separator' },
{ label: '隐藏', role: 'hide' },
{ label: '隐藏其他', role: 'hideOthers' },
{ label: '显示全部', role: 'unhide' },
{ type: 'separator' },
{ label: '退出', role: 'quit' }
]
}
上下文菜单(Context Menu)
在渲染进程触发右键菜单
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
showContextMenu: (menuTemplate) => {
return ipcRenderer.invoke('show-context-menu', menuTemplate);
}
});
// main process
ipcMain.handle('show-context-menu', async (event, menuTemplate) => {
const menu = Menu.buildFromTemplate(menuTemplate);
// 在当前窗口显示上下文菜单
const win = BrowserWindow.fromWebContents(event.sender);
menu.popup({ window: win });
});
使用示例
// renderer.js
document.addEventListener('contextmenu', (e) => {
e.preventDefault();
const menuTemplate = [
{
label: '复制',
accelerator: 'CmdOrCtrl+C',
click: () => document.execCommand('copy')
},
{
label: '粘贴',
accelerator: 'CmdOrCtrl+V',
click: () => document.execCommand('paste')
},
{ type: 'separator' },
{
label: '全选',
click: () => document.execCommand('selectAll')
}
];
window.electronAPI.showContextMenu(menuTemplate);
});
动态上下文菜单
根据选中内容显示不同菜单:
document.addEventListener('contextmenu', (e) => {
e.preventDefault();
const selection = window.getSelection().toString();
const hasSelection = selection.length > 0;
const menuTemplate = [
{
label: '复制',
enabled: hasSelection,
click: () => document.execCommand('copy')
},
{
label: '剪切',
enabled: hasSelection,
click: () => document.execCommand('cut')
},
{ type: 'separator' },
{
label: '搜索:' + (hasSelection ? `"${selection}"` : ''),
enabled: hasSelection,
click: () => {
shell.openExternal(`https://www.google.com/search?q=${encodeURIComponent(selection)}`);
}
},
{
label: '在词典中查找',
enabled: hasSelection,
click: () => {
shell.openExternal(`https://dict.cn/${encodeURIComponent(selection)}`);
}
}
];
window.electronAPI.showContextMenu(menuTemplate);
});
菜单项属性详解
完整菜单项配置
{
label: '菜单项名称', // 显示文字
type: 'normal', // 类型:normal | separator | submenu | checkbox | radio
enabled: true, // 是否可用
visible: true, // 是否可见
accelerator: 'CmdOrCtrl+S', // 快捷键
toolTip: '保存文件', // 提示文字
icon: nativeImage, // 图标(可选)
sublabel: 'Ctrl+S', // 副标签(macOS)
// 点击回调
click: () => { /* action */ },
// 角色(使用系统预设行为)
role: 'undo' // undo, redo, cut, copy, paste, delete, selectAll, reload, forceReload,
// toggleDevTools, togglefullscreen, minimize, zoom, close, quit,
// about, hide, hideOthers, unhide, front, help, services
}
Checkbox 菜单项
{
label: '自动保存',
type: 'checkbox',
checked: autoSaveEnabled,
click: (menuItem) => {
autoSaveEnabled = menuItem.checked;
updateSettings('autoSave', autoSaveEnabled);
}
}
Radio 菜单项
{
label: '视图模式',
submenu: [
{
label: '紧凑',
type: 'radio',
checked: viewMode === 'compact',
click: () => setViewMode('compact')
},
{
label: '舒适',
type: 'radio',
checked: viewMode === 'comfortable',
click: () => setViewMode('comfortable')
},
{
label: '扩展',
type: 'radio',
checked: viewMode === 'expanded',
click: () => setViewMode('expanded')
}
]
}
快捷键(Accelerator)
格式说明
| 修饰键 | Windows/Linux | macOS |
|---|---|---|
| Ctrl | Ctrl | Ctrl |
| Alt | Alt | Alt |
| Shift | Shift | Shift |
| Cmd | 不支持 | Cmd |
| CmdOrCtrl | Ctrl | Cmd |
| Plus | + | + |
示例
// 单键
{ label: '保存', accelerator: 'CmdOrCtrl+S' }
// 组合键
{ label: '强制刷新', accelerator: 'CmdOrCtrl+Shift+R' }
// 功能键
{ label: '全屏', accelerator: 'F11' }
{ label: '退出', accelerator: 'Cmd+Q' } // macOS 常用
// 空格键
{ label: '播放/暂停', accelerator: 'Space' }
本地化快捷键显示
不同系统显示不同:
- Windows:
Ctrl+S - macOS:
⌘S
Electron 会自动根据系统渲染 accelerator。
Dock 菜单(macOS)
macOS 独有的 Dock 图标右键菜单:
const { Menu, app } = require('electron');
function createDockMenu() {
const dockMenu = Menu.buildFromTemplate([
{
label: '新建窗口',
click: () => {
createNewWindow();
}
},
{
label: '新建文档',
click: () => {
createNewDocument();
}
},
{ type: 'separator' },
{
label: '最近文档',
submenu: [
{ label: '文档 1.pdf' },
{ label: '文档 2.pdf' }
]
}
]);
app.dock.setMenu(dockMenu);
}
Dock Badge
显示通知数量:
// 设置 badge(macOS)
app.dock.setBadge('5'); // 显示 "5"
app.dock.setBadge(''); // 清除
// Windows/Linux 可以用 tray 设置
tray.setToolTip('5 条新消息');
菜单设计最佳实践
1. 遵循平台惯例
- macOS:使用标准 role,尽量让系统处理菜单行为
- Windows/Linux:支持 CmdOrCtrl 组合键
2. 常用快捷键对照
| 操作 | Windows | macOS |
|---|---|---|
| 保存 | Ctrl+S | ⌘S |
| 复制 | Ctrl+C | ⌘C |
| 粘贴 | Ctrl+V | ⌘V |
| 撤销 | Ctrl+Z | ⌘Z |
| 全选 | Ctrl+A | ⌘A |
| 查找 | Ctrl+F | ⌘F |
| 关闭 | Ctrl+W | ⌘W |
| 退出 | Alt+F4 | ⌘Q |
3. 菜单分层不要过深
建议最多两层子菜单,过深的菜单难以使用。
4. 动态更新菜单
function updateFileMenu(hasUnsavedChanges) {
const menu = Menu.getApplicationMenu();
const fileMenu = menu.items.find(item => item.label === '文件');
if (hasUnsavedChanges) {
// 添加或更新"保存"菜单项
}
}
这一章想说的
系统托盘和菜单是桌面应用的重要组成部分:
- 系统托盘:常驻图标 + 右键菜单,适合后台运行应用
- 应用菜单:顶栏菜单,包含文件、编辑、视图等标准项
- 上下文菜单:右键弹出,根据选中内容动态变化
- Dock 菜单:macOS 专用,Dock 图标右键菜单
良好的菜单设计应遵循平台惯例,提供清晰的快捷键支持。