菜单与托盘 #
一、应用菜单 #
1.1 菜单概述 #
text
┌─────────────────────────────────────────────────────────────┐
│ 文件 编辑 视图 窗口 帮助 │ ← 菜单栏
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ │
│ │ 新建窗口 │ │
│ │ 新建标签页 │ │
│ ├─────────────┤ │
│ │ 打开... │ Ctrl+O │
│ │ 最近打开 │ ► │
│ ├─────────────┤ │
│ │ 保存 │ Ctrl+S │
│ │ 另存为... │ Ctrl+Shift+S │
│ ├─────────────┤ │
│ │ 退出 │ Ctrl+Q │
│ └─────────────┘ │
│ │
│ 应用内容区域 │
│ │
└─────────────────────────────────────────────────────────────┘
1.2 创建菜单 #
javascript
const { Menu, BrowserWindow, app, shell } = require('electron');
const template = [
{
label: '文件',
submenu: [
{
label: '新建窗口',
accelerator: 'CmdOrCtrl+N',
click: () => createNewWindow()
},
{
label: '新建标签页',
accelerator: 'CmdOrCtrl+T',
click: () => createNewTab()
},
{ type: 'separator' },
{
label: '打开...',
accelerator: 'CmdOrCtrl+O',
click: async () => {
const result = await dialog.showOpenDialog({});
if (!result.canceled) {
openFile(result.filePaths[0]);
}
}
},
{ type: 'separator' },
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
click: () => saveFile()
},
{
label: '另存为...',
accelerator: 'CmdOrCtrl+Shift+S',
click: () => saveFileAs()
},
{ type: 'separator' },
{
label: '退出',
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Alt+F4',
click: () => app.quit()
}
]
},
{
label: '编辑',
submenu: [
{ role: 'undo', label: '撤销' },
{ role: 'redo', label: '重做' },
{ type: 'separator' },
{ role: 'cut', label: '剪切' },
{ role: 'copy', label: '复制' },
{ role: 'paste', label: '粘贴' },
{ role: 'delete', label: '删除' },
{ type: 'separator' },
{ role: 'selectAll', label: '全选' }
]
},
{
label: '视图',
submenu: [
{ role: 'reload', label: '重新加载' },
{ role: 'forceReload', label: '强制重新加载' },
{ role: 'toggleDevTools', label: '开发者工具' },
{ type: 'separator' },
{ role: 'resetZoom', label: '重置缩放' },
{ role: 'zoomIn', label: '放大' },
{ role: 'zoomOut', label: '缩小' },
{ type: 'separator' },
{ role: 'togglefullscreen', label: '全屏' }
]
},
{
label: '窗口',
submenu: [
{ role: 'minimize', label: '最小化' },
{ role: 'zoom', label: '缩放' },
{ type: 'separator' },
{ role: 'front', label: '前置所有窗口' },
{ type: 'separator' },
{ role: 'close', label: '关闭' }
]
},
{
label: '帮助',
submenu: [
{
label: '文档',
click: async () => {
await shell.openExternal('https://example.com/docs');
}
},
{
label: '报告问题',
click: async () => {
await shell.openExternal('https://github.com/user/repo/issues');
}
},
{ type: 'separator' },
{
label: '关于',
click: () => showAboutDialog()
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
1.3 内置角色 #
| 角色 | 说明 |
|---|---|
| undo | 撤销 |
| redo | 重做 |
| cut | 剪切 |
| copy | 复制 |
| paste | 粘贴 |
| pasteAndMatchStyle | 粘贴并匹配样式 |
| delete | 删除 |
| selectAll | 全选 |
| reload | 重新加载 |
| forceReload | 强制重新加载 |
| toggleDevTools | 切换开发者工具 |
| resetZoom | 重置缩放 |
| zoomIn | 放大 |
| zoomOut | 缩小 |
| togglefullscreen | 切换全屏 |
| minimize | 最小化 |
| close | 关闭 |
| zoom | 缩放 |
| front | 前置窗口 |
| about | 关于(macOS) |
| services | 服务(macOS) |
| hide | 隐藏(macOS) |
| hideOthers | 隐藏其他(macOS) |
| unhide | 显示全部(macOS) |
| quit | 退出(macOS) |
| startSpeaking | 开始朗读(macOS) |
| stopSpeaking | 停止朗读(macOS) |
1.4 动态菜单 #
javascript
// 动态更新菜单项
function updateMenu(recentFiles) {
const recentFilesMenu = {
label: '最近打开',
submenu: recentFiles.length > 0
? recentFiles.map(file => ({
label: file,
click: () => openFile(file)
}))
: [{ label: '无最近文件', enabled: false }]
};
// 找到文件菜单并更新
const fileMenuIndex = template.findIndex(m => m.label === '文件');
const recentIndex = template[fileMenuIndex].submenu.findIndex(
item => item.label === '最近打开'
);
if (recentIndex !== -1) {
template[fileMenuIndex].submenu[recentIndex] = recentFilesMenu;
}
// 重建菜单
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
1.5 上下文菜单 #
javascript
// 右键菜单
const contextMenu = Menu.buildFromTemplate([
{ label: '复制', role: 'copy' },
{ label: '粘贴', role: 'paste' },
{ type: 'separator' },
{
label: '自定义操作',
click: () => console.log('自定义操作')
}
]);
// 监听右键事件
mainWindow.webContents.on('context-menu', (event, params) => {
// 根据上下文调整菜单
if (params.selectionText) {
contextMenu.popup(mainWindow, params.x, params.y);
}
});
1.6 macOS 特殊菜单 #
javascript
// macOS 应用菜单
if (process.platform === 'darwin') {
template.unshift({
label: app.getName(),
submenu: [
{ role: 'about' },
{ type: 'separator' },
{
label: '偏好设置...',
accelerator: 'Cmd+,',
click: () => openSettings()
},
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
});
}
二、系统托盘 #
2.1 创建托盘 #
javascript
const { Tray, nativeImage, Menu } = require('electron');
const path = require('path');
let tray = null;
function createTray() {
// 创建图标
const iconPath = path.join(__dirname, 'assets', 'tray.png');
const icon = nativeImage.createFromPath(iconPath);
// macOS 使用模板图标
const trayIcon = process.platform === 'darwin'
? icon.resize({ width: 16, height: 16 })
: icon;
tray = new Tray(trayIcon);
// 设置托盘菜单
const contextMenu = Menu.buildFromTemplate([
{ label: '显示窗口', click: () => mainWindow.show() },
{ label: '新建窗口', click: () => createNewWindow() },
{ type: 'separator' },
{ label: '设置', click: () => openSettings() },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
]);
tray.setToolTip('我的应用');
tray.setContextMenu(contextMenu);
// 点击托盘图标
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});
// 双击托盘图标
tray.on('double-click', () => {
mainWindow.show();
mainWindow.focus();
});
}
2.2 托盘图标 #
javascript
// 不同状态的图标
const normalIcon = nativeImage.createFromPath('tray.png');
const activeIcon = nativeImage.createFromPath('tray-active.png');
const errorIcon = nativeImage.createFromPath('tray-error.png');
// 设置图标
tray.setImage(normalIcon);
// macOS 托盘图标模板
// 使用文件名后缀 Template,如 trayTemplate.png
// 系统会自动处理深色/浅色模式
const templateIcon = nativeImage.createFromPath('trayTemplate.png');
tray.setImage(templateIcon);
2.3 动态托盘菜单 #
javascript
function updateTrayMenu(status) {
const statusLabel = {
idle: '空闲',
working: '工作中...',
error: '错误'
};
const contextMenu = Menu.buildFromTemplate([
{
label: `状态: ${statusLabel[status]}`,
enabled: false
},
{ type: 'separator' },
{ label: '显示窗口', click: () => mainWindow.show() },
{
label: '开始任务',
enabled: status === 'idle',
click: () => startTask()
},
{
label: '停止任务',
enabled: status === 'working',
click: () => stopTask()
},
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
]);
tray.setContextMenu(contextMenu);
// 更新图标
const icons = {
idle: normalIcon,
working: activeIcon,
error: errorIcon
};
tray.setImage(icons[status]);
}
2.4 托盘通知 #
javascript
const { Notification } = require('electron');
function showTrayNotification(title, body) {
if (Notification.isSupported()) {
const notification = new Notification({
title: title,
body: body,
icon: path.join(__dirname, 'assets', 'icon.png')
});
notification.on('click', () => {
mainWindow.show();
mainWindow.focus();
});
notification.show();
}
}
// 使用
showTrayNotification('任务完成', '文件已成功下载');
2.5 托盘气球提示(Windows) #
javascript
// Windows 气球提示
if (process.platform === 'win32') {
tray.displayBalloon({
icon: nativeImage.createFromPath('icon.png'),
title: '提示标题',
content: '这是气球提示的内容'
});
}
三、Dock 图标(macOS) #
3.1 设置 Dock 图标 #
javascript
const { app, nativeImage } = require('electron');
// 设置 Dock 图标
const dockIcon = nativeImage.createFromPath('icon.png');
app.dock.setIcon(dockIcon);
// 设置 Dock 菜单
const dockMenu = Menu.buildFromTemplate([
{ label: '新建窗口', click: () => createNewWindow() },
{ label: '新建标签页', click: () => createNewTab() }
]);
app.dock.setMenu(dockMenu);
// 设置 Dock 徽章
app.dock.setBadge('3'); // 显示数字徽章
// 隐藏 Dock 图标
app.dock.hide();
// 显示 Dock 图标
app.dock.show();
3.2 Dock 弹跳动画 #
javascript
// 请求用户注意
app.dock.bounce(); // 弹跳一次
app.dock.bounce('critical'); // 持续弹跳直到应用获得焦点
// 取消弹跳
const bounceId = app.dock.bounce();
app.dock.cancelBounce(bounceId);
四、任务栏(Windows) #
4.1 任务栏按钮 #
javascript
const { BrowserWindow } = require('electron');
// 设置任务栏按钮
mainWindow.setThumbarButtons([
{
tooltip: '播放',
icon: nativeImage.createFromPath('play.png'),
click: () => play()
},
{
tooltip: '暂停',
icon: nativeImage.createFromPath('pause.png'),
flags: ['disabled'],
click: () => pause()
},
{
tooltip: '停止',
icon: nativeImage.createFromPath('stop.png'),
click: () => stop()
}
]);
4.2 任务栏进度条 #
javascript
// 设置进度条
mainWindow.setProgressBar(0.5); // 50%
// 不确定进度
mainWindow.setProgressBar(2); // 不确定进度模式
// 清除进度条
mainWindow.setProgressBar(-1);
4.3 任务栏覆盖图标 #
javascript
// 设置覆盖图标
mainWindow.setOverlayIcon(
nativeImage.createFromPath('overlay.png'),
'状态描述'
);
// 清除覆盖图标
mainWindow.setOverlayIcon(null, '');
五、菜单与托盘最佳实践 #
5.1 跨平台处理 #
javascript
function setupAppMenu() {
const isMac = process.platform === 'darwin';
const template = [
// 文件菜单
{
label: '文件',
submenu: [
{ label: '新建', accelerator: 'CmdOrCtrl+N', click: createNew },
{ label: '打开', accelerator: 'CmdOrCtrl+O', click: openFile },
{ type: 'separator' },
{ label: '保存', accelerator: 'CmdOrCtrl+S', click: saveFile },
{ type: 'separator' },
isMac ? { role: 'close' } : { label: '退出', click: () => app.quit() }
]
},
// 编辑菜单
{
label: '编辑',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' }
]
}
];
// macOS 添加应用菜单
if (isMac) {
template.unshift({
label: app.getName(),
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'preferences' },
{ type: 'separator' },
{ role: 'quit' }
]
});
}
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
5.2 无菜单窗口 #
javascript
// 创建无菜单的窗口
const win = new BrowserWindow({
autoHideMenuBar: true, // 自动隐藏菜单栏
// 或完全隐藏
// frame: false
});
// 隐藏菜单栏
win.setMenuBarVisibility(false);
5.3 托盘最小化 #
javascript
// 最小化到托盘而不是任务栏
mainWindow.on('minimize', (event) => {
event.preventDefault();
mainWindow.hide();
tray.displayBalloon({
title: '应用已最小化',
content: '点击托盘图标恢复窗口'
});
});
// 点击托盘恢复
tray.on('click', () => {
mainWindow.show();
mainWindow.restore();
mainWindow.focus();
});
六、总结 #
6.1 核心要点 #
| 要点 | 说明 |
|---|---|
| 应用菜单 | Menu.buildFromTemplate 创建菜单 |
| 内置角色 | 使用 role 简化常见操作 |
| 系统托盘 | Tray 创建托盘图标和菜单 |
| 跨平台 | 根据平台调整菜单结构 |
| Dock/任务栏 | 平台特定的功能集成 |
6.2 下一步 #
现在你已经掌握了菜单与托盘的使用,接下来让我们学习 对话框与通知,深入了解原生对话框和系统通知的实现!
最后更新:2026-03-28