# 菜单系统
菜单是桌面应用程序的核心交互组件之一,用于组织和管理应用功能。Electron 提供了强大的 Menu 模块,支持创建应用菜单、上下文菜单和 Dock 菜单等多种类型的菜单。
系统架构
code
┌─────────────────────────────────────────────────────────┐
│ Menu 模块 │
├─────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 应用菜单 │ │ 上下文菜单 │ │ Dock 菜单 │ │
│ │ (Application)│ │ (Context) │ │ (macOS) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Menu Item 配置 │
│ • label (标签) • icon (图标) • role (角色) │
│ • submenu (子菜单) • type (类型) • click (点击) │
│ • accelerator (快捷键) • enabled/disabled (状态) │
└─────────────────────────────────────────────────────────┘菜单类型概览
| 菜单类型 | 平台支持 | 位置说明 | 主要用途 |
|---|---|---|---|
| 应用菜单 | 全平台 | macOS: 屏幕顶部<br>Windows/Linux: 窗口顶部 | 应用主菜单,包含文件、编辑、视图等 |
| 上下文菜单 | 全平台 | 鼠标右键位置 | 提供上下文相关的快捷操作 |
| Dock 菜单 | 仅 macOS | Dock 图标右键 | 应用快捷操作和状态切换 |
应用菜单
应用菜单是桌面应用的标准菜单栏,在不同操作系统上显示位置略有不同:
- macOS:显示在屏幕顶部菜单栏
- Windows/Linux:显示在窗口顶部
基本创建
javascript
const { app, Menu } = require('electron')
function createMenu() {
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => createNewFile()
},
{
label: '打开',
accelerator: 'CmdOrCtrl+O',
click: () => openFile()
},
{ type: 'separator' },
{
label: '退出',
accelerator: 'CmdOrCtrl+Q',
role: 'quit'
}
]
},
{
label: '编辑',
submenu: [
{ label: '撤销', role: 'undo' },
{ label: '重做', role: 'redo' },
{ type: 'separator' },
{ label: '剪切', role: 'cut' },
{ label: '复制', role: 'copy' },
{ label: '粘贴', role: 'paste' }
]
},
{
label: '视图',
submenu: [
{ label: '重新加载', role: 'reload' },
{ label: '强制重新加载', role: 'forceReload' },
{ type: 'separator' },
{ label: '实际大小', role: 'resetZoom' },
{ label: '放大', role: 'zoomIn' },
{ label: '缩小', role: 'zoomOut' },
{ type: 'separator' },
{ label: '全屏', role: 'togglefullscreen' }
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
app.whenReady().then(createMenu)macOS 特殊处理
在 macOS 中,应用菜单的第一个项目始终显示为应用名称,而不是自定义标签。要正确显示菜单,需要特殊处理:
javascript
const { app, Menu } = require('electron')
function createMenu() {
const template = [
{
label: '文件',
submenu: [
{ label: '新建', click: () => {} },
{ label: '打开', click: () => {} }
]
},
{
label: '编辑',
submenu: [
{ label: '撤销', role: 'undo' },
{ label: '重做', role: 'redo' }
]
}
]
// macOS 特殊处理:添加应用菜单
if (process.platform === 'darwin') {
template.unshift({
label: app.getName(),
submenu: [
{ label: '关于', role: 'about' },
{ type: 'separator' },
{ label: '服务', role: 'services', submenu: [] },
{ type: 'separator' },
{ label: '隐藏', role: 'hide' },
{ label: '隐藏其他', role: 'hideOthers' },
{ label: '显示全部', role: 'unhide' },
{ type: 'separator' },
{ label: '退出', role: 'quit' }
]
})
}
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}效果对比:
| 处理方式 | macOS 显示结果 |
|---|---|
| 不特殊处理 | 第一个菜单显示为 "Electron" |
| 特殊处理后 | 第一个菜单显示为应用名称 |
上下文菜单
上下文菜单(右键菜单)提供与当前上下文相关的快捷操作。
实现方式
上下文菜单需要通过 IPC(进程间通信)在主进程和渲染进程之间协调:
javascript
// 主进程 main.js
const { ipcMain, Menu, BrowserWindow } = require('electron')
ipcMain.handle('show-context-menu', (event, menuItems) => {
const template = menuItems.map(item => ({
label: item.label,
click: () => {
event.sender.send('context-menu-command', item.action)
}
}))
const menu = Menu.buildFromTemplate(template)
const window = BrowserWindow.fromWebContents(event.sender)
menu.popup({ window })
})javascript
// 渲染进程 renderer.js
const { ipcRenderer } = require('electron')
// 阻止默认右键菜单
window.addEventListener('contextmenu', (e) => {
e.preventDefault()
const menuItems = [
{ label: '复制', action: 'copy' },
{ label: '粘贴', action: 'paste' },
{ label: '删除', action: 'delete' }
]
ipcRenderer.invoke('show-context-menu', menuItems)
})
// 监听菜单命令
ipcRenderer.on('context-menu-command', (event, action) => {
switch (action) {
case 'copy':
document.execCommand('copy')
break
case 'paste':
document.execCommand('paste')
break
case 'delete':
// 执行删除操作
break
}
})动态上下文菜单
根据不同场景显示不同的菜单项:
javascript
// 主进程
ipcMain.handle('show-context-menu', (event, context) => {
let template = []
if (context.type === 'text') {
template = [
{ label: '复制', click: () => event.sender.send('action', 'copy') },
{ label: '剪切', click: () => event.sender.send('action', 'cut') },
{ type: 'separator' },
{ label: '搜索', click: () => event.sender.send('action', 'search') }
]
} else if (context.type === 'image') {
template = [
{ label: '保存图片', click: () => event.sender.send('action', 'save-image') },
{ label: '复制图片', click: () => event.sender.send('action', 'copy-image') }
]
} else if (context.type === 'file') {
template = [
{ label: '打开', click: () => event.sender.send('action', 'open') },
{ label: '重命名', click: () => event.sender.send('action', 'rename') },
{ type: 'separator' },
{ label: '删除', click: () => event.sender.send('action', 'delete') }
]
}
const menu = Menu.buildFromTemplate(template)
const window = BrowserWindow.fromWebContents(event.sender)
menu.popup({ window })
})Dock 菜单(仅 macOS)
Dock 菜单是 macOS 特有的功能,用户右键点击 Dock 图标时显示。
javascript
const { app, Menu } = require('electron')
function createDockMenu() {
const template = [
{
label: '新建窗口',
click: () => createNewWindow()
},
{
label: '新建文件',
click: () => createNewFile()
},
{ type: 'separator' },
{
label: '最近文件',
submenu: [
{ label: '文件1.txt', click: () => openFile('file1.txt') },
{ label: '文件2.txt', click: () => openFile('file2.txt') }
]
},
{ type: 'separator' },
{
label: '退出',
click: () => app.quit()
}
]
if (app.dock) {
const dockMenu = Menu.buildFromTemplate(template)
app.dock.setMenu(dockMenu)
}
}
app.whenReady().then(createDockMenu)Menu Item API 详解
核心属性
| 属性 | 类型 | 说明 | 示例 |
|---|---|---|---|
label | String | 菜单项显示文本 | { label: '新建' } |
icon | NativeImage | 菜单项图标 | { icon: nativeImage.createFromPath('icon.png') } |
type | String | 菜单项类型 | 'normal', 'separator', 'submenu', 'checkbox', 'radio' |
submenu | MenuItem[] | 子菜单数组 | { submenu: [{ label: '子菜单' }] } |
click | Function | 点击回调函数 | { click: () => console.log('clicked') } |
role | String | 预定义角色 | 'undo', 'redo', 'cut', 'copy', 'paste', 'quit' 等 |
accelerator | String | 快捷键 | 'CmdOrCtrl+N', 'Shift+Delete' |
enabled | Boolean | 是否启用 | { enabled: false } |
visible | Boolean | 是否可见 | { visible: false } |
checked | Boolean | 选中状态(checkbox/radio) | { checked: true } |
id | String | 唯一标识符 | { id: 'menu-item-new' } |
before | String | 在指定 ID 菜单项之前插入 | { before: 'menu-item-save' } |
after | String | 在指定 ID 菜单项之后插入 | { after: 'menu-item-open' } |
预定义角色(role)
使用预定义角色可以自动实现常见功能,无需编写 click 处理函数:
| 角色 | 功能描述 | 快捷键 |
|---|---|---|
undo | 撤销 | Cmd/Ctrl+Z |
redo | 重做 | Cmd/Ctrl+Shift+Z |
cut | 剪切 | Cmd/Ctrl+X |
copy | 复制 | Cmd/Ctrl+C |
paste | 粘贴 | Cmd/Ctrl+V |
selectAll | 全选 | Cmd/Ctrl+A |
delete | 删除 | Delete |
minimize | 最小化窗口 | Cmd/Ctrl+M |
close | 关闭窗口 | Cmd/Ctrl+W |
quit | 退出应用 | Cmd/Ctrl+Q |
reload | 重新加载页面 | Cmd/Ctrl+R |
forceReload | 强制重新加载 | Cmd/Ctrl+Shift+R |
toggleDevTools | 开发者工具 | Cmd/Ctrl+Shift+I |
togglefullscreen | 全屏切换 | F11 |
zoomIn | 放大 | Cmd/Ctrl+Plus |
zoomOut | 缩小 | Cmd/Ctrl+- |
resetZoom | 重置缩放 | Cmd/Ctrl+0 |
快捷键配置
Electron 使用 Accelerator 字符串定义快捷键:
javascript
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N', // macOS: Cmd+N, Windows/Linux: Ctrl+N
click: () => createNew()
},
{
label: '保存',
accelerator: 'CmdOrCtrl+Shift+S', // Cmd/Ctrl + Shift + S
click: () => save()
},
{
label: '另存为',
accelerator: 'CmdOrCtrl+Alt+S', // Cmd/Ctrl + Alt + S
click: () => saveAs()
}
]
}
]常用修饰键:
| 修饰键 | macOS | Windows/Linux |
|---|---|---|
CmdOrCtrl | Command | Control |
Command | Command | - |
Ctrl | Control | Control |
Shift | Shift | Shift |
Alt | Option | Alt |
Super | - | Win 键 |
特殊键:
F1-F24:功能键Plus:加号键(+)Space:空格键Tab:Tab 键Backspace:退格键Delete:删除键Insert:插入键Return/Enter:回车键Up/Down/Left/Right:方向键Home/End:Home / End 键PageUp/PageDown:翻页键Escape/Esc:Esc 键
菜单图标
为菜单项添加图标可以提升用户体验:
javascript
const { nativeImage } = require('electron')
const path = require('path')
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
icon: nativeImage.createFromPath(path.join(__dirname, 'icons/new.png')),
accelerator: 'CmdOrCtrl+N',
click: () => createNew()
},
{
label: '打开',
icon: nativeImage.createFromPath(path.join(__dirname, 'icons/open.png')),
accelerator: 'CmdOrCtrl+O',
click: () => openFile()
},
{
label: '保存',
icon: nativeImage.createFromPath(path.join(__dirname, 'icons/save.png')),
accelerator: 'CmdOrCtrl+S',
click: () => saveFile()
}
]
}
]图标规格建议:
| 平台 | 推荐尺寸 | 格式 |
|---|---|---|
| macOS | 16x16, 32x32 (@2x) | PNG |
| Windows | 16x16 | ICO / PNG |
| Linux | 16x16 | PNG |
动态菜单
运行时更新菜单
javascript
let menu
let autoSaveEnabled = false
function updateMenu() {
const template = [
{
label: '文件',
submenu: [
{
label: '自动保存',
type: 'checkbox',
checked: autoSaveEnabled,
click: () => {
autoSaveEnabled = !autoSaveEnabled
updateMenu() // 重新构建菜单
}
}
]
}
]
menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}根据窗口状态更新
javascript
function createMenu() {
const template = [
{
label: '编辑',
submenu: [
{
label: '撤销',
enabled: false, // 初始禁用
id: 'undo-item',
click: () => undo()
},
{
label: '重做',
enabled: false,
id: 'redo-item',
click: () => redo()
}
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
// 根据编辑器状态更新菜单项
function updateEditMenu(canUndo, canRedo) {
const menu = Menu.getApplicationMenu()
const undoItem = menu.getMenuItemById('undo-item')
const redoItem = menu.getMenuItemById('redo-item')
if (undoItem) undoItem.enabled = canUndo
if (redoItem) redoItem.enabled = canRedo
}完整示例:文本编辑器菜单
javascript
const { app, Menu, BrowserWindow, dialog } = require('electron')
const path = require('path')
let mainWindow
let currentFile = null
let hasUnsavedChanges = false
function createWindow() {
mainWindow = new BrowserWindow({
width: 1000,
height: 700,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
mainWindow.loadFile('index.html')
}
function createMenu() {
const template = [
// 文件菜单
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => newFile()
},
{
label: '打开',
accelerator: 'CmdOrCtrl+O',
click: () => openFile()
},
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
enabled: hasUnsavedChanges,
click: () => saveFile()
},
{
label: '另存为',
accelerator: 'CmdOrCtrl+Shift+S',
click: () => saveFileAs()
},
{ type: 'separator' },
{
label: '退出',
accelerator: 'CmdOrCtrl+Q',
click: () => app.quit()
}
]
},
// 编辑菜单
{
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' },
{ type: 'separator' },
{
label: '查找',
accelerator: 'CmdOrCtrl+F',
click: () => mainWindow.webContents.send('find')
},
{
label: '替换',
accelerator: 'CmdOrCtrl+H',
click: () => mainWindow.webContents.send('replace')
}
]
},
// 视图菜单
{
label: '视图',
submenu: [
{ label: '重新加载', role: 'reload' },
{ label: '强制重新加载', role: 'forceReload' },
{ type: 'separator' },
{ label: '实际大小', role: 'resetZoom' },
{ label: '放大', role: 'zoomIn' },
{ label: '缩小', role: 'zoomOut' },
{ type: 'separator' },
{ label: '全屏', role: 'togglefullscreen' },
{ type: 'separator' },
{ label: '开发者工具', role: 'toggleDevTools' }
]
},
// 窗口菜单
{
label: '窗口',
submenu: [
{ label: '最小化', role: 'minimize' },
{ label: '关闭', role: 'close' }
]
},
// 帮助菜单
{
label: '帮助',
submenu: [
{
label: '关于',
click: () => {
dialog.showMessageBox(mainWindow, {
type: 'info',
title: '关于',
message: '文本编辑器 v1.0.0',
detail: '一个简单的文本编辑器示例'
})
}
}
]
}
]
// macOS 特殊处理
if (process.platform === 'darwin') {
template.unshift({
label: app.getName(),
submenu: [
{ label: '关于', role: 'about' },
{ type: 'separator' },
{ label: '服务', role: 'services', submenu: [] },
{ type: 'separator' },
{ label: '隐藏', role: 'hide' },
{ label: '隐藏其他', role: 'hideOthers' },
{ label: '显示全部', role: 'unhide' },
{ type: 'separator' },
{ label: '退出', role: 'quit' }
]
})
// 窗口菜单
template.splice(4, 1, {
label: '窗口',
submenu: [
{ label: '最小化', role: 'minimize' },
{ label: '缩放', role: 'zoom' },
{ type: 'separator' },
{ label: '前置全部窗口', role: 'front' }
]
})
}
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
async function newFile() {
if (hasUnsavedChanges) {
const result = await dialog.showMessageBox(mainWindow, {
type: 'warning',
buttons: ['保存', '不保存', '取消'],
message: '当前文件未保存,是否保存?'
})
if (result.response === 0) {
await saveFile()
} else if (result.response === 2) {
return
}
}
currentFile = null
hasUnsavedChanges = false
mainWindow.webContents.send('new-file')
}
async function openFile() {
const result = await dialog.showOpenDialog(mainWindow, {
filters: [{ name: '文本文件', extensions: ['txt', 'md'] }],
properties: ['openFile']
})
if (!result.canceled && result.filePaths.length > 0) {
currentFile = result.filePaths[0]
mainWindow.webContents.send('open-file', currentFile)
}
}
async function saveFile() {
if (currentFile) {
mainWindow.webContents.send('save-file', currentFile)
hasUnsavedChanges = false
} else {
await saveFileAs()
}
}
async function saveFileAs() {
const result = await dialog.showSaveDialog(mainWindow, {
filters: [{ name: '文本文件', extensions: ['txt', 'md'] }]
})
if (!result.canceled && result.filePath) {
currentFile = result.filePath
mainWindow.webContents.send('save-file', currentFile)
hasUnsavedChanges = false
}
}
app.whenReady().then(() => {
createWindow()
createMenu()
})跨平台兼容性
平台差异对照表
| 功能特性 | macOS | Windows | Linux |
|---|---|---|---|
| 应用菜单位置 | 屏幕顶部 | 窗口顶部 | 窗口顶部 |
| 第一个菜单项 | 固定显示应用名 | 自定义标签 | 自定义标签 |
| Dock 菜单 | ✅ 支持 | ❌ 不支持 | ❌ 不支持 |
| 菜单图标 | ✅ 支持 | ✅ 支持 | ⚠️ 部分支持 |
| CmdOrCtrl | Command | Control | Control |
跨平台最佳实践
javascript
const { app, Menu } = require('electron')
function createMenu() {
const isMac = process.platform === 'darwin'
const template = [
// 文件菜单
{
label: '文件',
submenu: [
isMac ? { label: '新建窗口', click: () => {} } : { label: '新建', click: () => {} },
{ label: '打开', click: () => {} },
{ type: 'separator' },
isMac ? { label: '关闭窗口', role: 'close' } : { label: '退出', role: 'quit' }
]
},
// 编辑菜单
{
label: '编辑',
submenu: [
{ label: '撤销', role: 'undo' },
{ label: '重做', role: 'redo' },
{ type: 'separator' },
{ label: '剪切', role: 'cut' },
{ label: '复制', role: 'copy' },
{ label: '粘贴', role: 'paste' },
isMac ? { label: '删除', role: 'delete' } : { label: '删除', role: 'delete' },
{ type: 'separator' },
{ label: '全选', role: 'selectAll' }
]
}
]
// macOS 特殊处理
if (isMac) {
template.unshift({
label: app.getName(),
submenu: [
{ label: '关于', role: 'about' },
{ type: 'separator' },
{ label: '服务', role: 'services', submenu: [] },
{ type: 'separator' },
{ label: '隐藏', role: 'hide' },
{ label: '隐藏其他', role: 'hideOthers' },
{ label: '显示全部', role: 'unhide' },
{ type: 'separator' },
{ label: '退出', role: 'quit' }
]
})
}
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}常见问题解答
1. 如何隐藏默认菜单?
javascript
// 设置为 null 即可隐藏菜单
Menu.setApplicationMenu(null)2. 如何在渲染进程中触发菜单项?
javascript
// 主进程
const { ipcMain } = require('electron')
ipcMain.on('trigger-menu-item', (event, itemId) => {
const menu = Menu.getApplicationMenu()
const menuItem = menu.getMenuItemById(itemId)
if (menuItem && menuItem.click) {
menuItem.click()
}
})
// 渲染进程
const { ipcRenderer } = require('electron')
ipcRenderer.send('trigger-menu-item', 'menu-item-new')3. 菜单项点击后如何更新菜单状态?
javascript
let autoSave = false
function createMenu() {
const template = [
{
label: '选项',
submenu: [
{
label: '自动保存',
type: 'checkbox',
checked: autoSave,
click: (menuItem) => {
autoSave = !autoSave
// 方式1:重新构建菜单
createMenu()
// 方式2:直接修改菜单项状态
// menuItem.checked = autoSave
}
}
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}4. 如何在菜单中添加分隔线?
javascript
const template = [
{
label: '文件',
submenu: [
{ label: '新建', click: () => {} },
{ label: '打开', click: () => {} },
{ type: 'separator' }, // 分隔线
{ label: '保存', click: () => {} },
{ label: '另存为', click: () => {} },
{ type: 'separator' },
{ label: '退出', role: 'quit' }
]
}
]5. 如何禁用某个菜单项?
javascript
// 方式1:创建时禁用
const template = [
{
label: '编辑',
submenu: [
{ label: '撤销', enabled: false },
{ label: '重做', enabled: false }
]
}
]
// 方式2:运行时禁用
const menu = Menu.getApplicationMenu()
const menuItem = menu.getMenuItemById('undo-item')
if (menuItem) {
menuItem.enabled = false
}6. macOS 上菜单不显示怎么办?
确保以下几点:
- 调用了
Menu.setApplicationMenu(menu) - 使用
app.whenReady()后再创建菜单 - 检查菜单模板是否有语法错误
javascript
app.whenReady().then(() => {
createWindow()
createMenu() // 确保在 app ready 后创建
})最佳实践
1. 遵循平台惯例
- macOS:使用应用菜单,添加应用名菜单
- Windows/Linux:窗口菜单,提供退出菜单项
- 使用
CmdOrCtrl实现跨平台快捷键
2. 菜单组织结构
code
文件 (File) - 文件操作:新建、打开、保存、退出
编辑 (Edit) - 编辑操作:撤销、重做、剪切、复制、粘贴
视图 (View) - 视图控制:缩放、全屏、开发者工具
窗口 (Window) - 窗口管理:最小化、关闭、前置(macOS)
帮助 (Help) - 帮助信息:文档、关于3. 快捷键一致性
| 功能 | macOS | Windows/Linux |
|---|---|---|
| 新建 | Cmd+N | Ctrl+N |
| 打开 | Cmd+O | Ctrl+O |
| 保存 | Cmd+S | Ctrl+S |
| 撤销 | Cmd+Z | Ctrl+Z |
| 重做 | Cmd+Shift+Z | Ctrl+Shift+Z |
| 退出 | Cmd+Q | Alt+F4 |
4. 性能优化
- 避免频繁重建整个菜单,使用
getMenuItemById()更新单个项 - 图标使用合适尺寸,避免过大图片
- 复杂菜单使用懒加载
5. 无障碍支持
- 为菜单项添加清晰的标签
- 提供快捷键支持
- 确保菜单可通过键盘导航