{T}

# 菜单系统

菜单是桌面应用程序的核心交互组件之一,用于组织和管理应用功能。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 菜单仅 macOSDock 图标右键应用快捷操作和状态切换

应用菜单

应用菜单是桌面应用的标准菜单栏,在不同操作系统上显示位置略有不同:

  • 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)

核心属性

属性类型说明示例
labelString菜单项显示文本{ label: '新建' }
iconNativeImage菜单项图标{ icon: nativeImage.createFromPath('icon.png') }
typeString菜单项类型'normal', 'separator', 'submenu', 'checkbox', 'radio'
submenuMenuItem[]子菜单数组{ submenu: [{ label: '子菜单' }] }
clickFunction点击回调函数{ click: () => console.log('clicked') }
roleString预定义角色'undo', 'redo', 'cut', 'copy', 'paste', 'quit'
acceleratorString快捷键'CmdOrCtrl+N', 'Shift+Delete'
enabledBoolean是否启用{ enabled: false }
visibleBoolean是否可见{ visible: false }
checkedBoolean选中状态(checkbox/radio){ checked: true }
idString唯一标识符{ id: 'menu-item-new' }
beforeString在指定 ID 菜单项之前插入{ before: 'menu-item-save' }
afterString在指定 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()
      }
    ]
  }
]

常用修饰键:

修饰键macOSWindows/Linux
CmdOrCtrlCommandControl
CommandCommand-
CtrlControlControl
ShiftShiftShift
AltOptionAlt
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()
      }
    ]
  }
]

图标规格建议:

平台推荐尺寸格式
macOS16x16, 32x32 (@2x)PNG
Windows16x16ICO / PNG
Linux16x16PNG

动态菜单

运行时更新菜单

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()
})

跨平台兼容性

平台差异对照表

功能特性macOSWindowsLinux
应用菜单位置屏幕顶部窗口顶部窗口顶部
第一个菜单项固定显示应用名自定义标签自定义标签
Dock 菜单✅ 支持❌ 不支持❌ 不支持
菜单图标✅ 支持✅ 支持⚠️ 部分支持
CmdOrCtrlCommandControlControl

跨平台最佳实践

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 上菜单不显示怎么办?

确保以下几点:

  1. 调用了 Menu.setApplicationMenu(menu)
  2. 使用 app.whenReady() 后再创建菜单
  3. 检查菜单模板是否有语法错误
javascript
app.whenReady().then(() => {
  createWindow()
  createMenu()  // 确保在 app ready 后创建
})

最佳实践

1. 遵循平台惯例

  • macOS:使用应用菜单,添加应用名菜单
  • Windows/Linux:窗口菜单,提供退出菜单项
  • 使用 CmdOrCtrl 实现跨平台快捷键

2. 菜单组织结构

code
文件 (File)     - 文件操作:新建、打开、保存、退出
编辑 (Edit)     - 编辑操作:撤销、重做、剪切、复制、粘贴
视图 (View)     - 视图控制:缩放、全屏、开发者工具
窗口 (Window)   - 窗口管理:最小化、关闭、前置(macOS)
帮助 (Help)     - 帮助信息:文档、关于

3. 快捷键一致性

功能macOSWindows/Linux
新建Cmd+NCtrl+N
打开Cmd+OCtrl+O
保存Cmd+SCtrl+S
撤销Cmd+ZCtrl+Z
重做Cmd+Shift+ZCtrl+Shift+Z
退出Cmd+QAlt+F4

4. 性能优化

  • 避免频繁重建整个菜单,使用 getMenuItemById() 更新单个项
  • 图标使用合适尺寸,避免过大图片
  • 复杂菜单使用懒加载

5. 无障碍支持

  • 为菜单项添加清晰的标签
  • 提供快捷键支持
  • 确保菜单可通过键盘导航

参考链接