Electron原生菜单完整指南


Electron 桌面应用提供了四类原生菜单,每种菜单都有其特定的用途和实现方式。本文档将详细介绍这四类菜单的实现方法、最佳实践和常见问题解决方案。

目录

  1. 窗口菜单 (Application Menu)
  2. 上下文菜单 (Context Menu)
  3. 托盘菜单 (Tray Menu)
  4. Dock 菜单 (Dock Menu)
  5. 最佳实践
  6. 常见问题
  7. 完整示例

窗口菜单 (Application Menu)

概述

窗口菜单是应用的主菜单,位于窗口顶部,包含标准的菜单项如文件、编辑、视图等。

特点

  • ✅ 所有平台支持
  • 📍 位于窗口顶部
  • 🎯 应用的主要导航入口
  • ⌨️ 支持键盘快捷键

实现方式

基础实现

const { Menu } = require("electron")

// 创建菜单模板
const template = [
  {
    label: "文件",
    submenu: [
      {
        label: "新建",
        accelerator: "CmdOrCtrl+N",
        click: () => {
          console.log("新建文件")
        },
      },
      {
        label: "打开",
        accelerator: "CmdOrCtrl+O",
        click: async () => {
          const result = await dialog.showOpenDialog(mainWindow, {
            properties: ["openFile"],
          })
          if (!result.canceled) {
            console.log("打开文件:", result.filePaths[0])
          }
        },
      },
      { type: "separator" },
      {
        label: "退出",
        accelerator: process.platform === "darwin" ? "Cmd+Q" : "Ctrl+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: "forceReload", label: "强制重新加载" },
      { role: "toggleDevTools", label: "切换开发者工具" },
      { type: "separator" },
      { role: "resetZoom", label: "实际大小" },
      { role: "zoomIn", label: "放大" },
      { role: "zoomOut", label: "缩小" },
      { type: "separator" },
      { role: "togglefullscreen", label: "切换全屏" },
    ],
  },
  {
    label: "帮助",
    submenu: [
      {
        label: "关于",
        click: () => {
          dialog.showMessageBox(mainWindow, {
            type: "info",
            title: "关于",
            message: "Electron 应用",
            detail: "版本 1.0.0",
          })
        },
      },
    ],
  },
]

// 设置应用菜单
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)

Mac 平台特有菜单

// Mac 平台的应用菜单
if (process.platform === "darwin") {
  template.unshift({
    label: app.getName(),
    submenu: [
      { role: "about", label: "关于" },
      { type: "separator" },
      { role: "services", label: "服务" },
      { type: "separator" },
      { role: "hide", label: "隐藏" },
      { role: "hideothers", label: "隐藏其他" },
      { role: "unhide", label: "显示全部" },
      { type: "separator" },
      { role: "quit", label: "退出" },
    ],
  })
}

最佳实践

  1. 快捷键适配
// 使用 CmdOrCtrl 适配不同平台
accelerator: "CmdOrCtrl+N" // Mac: Cmd+N, Windows/Linux: Ctrl+N
accelerator: "CmdOrCtrl+Shift+Z" // 跨平台快捷键
  1. 菜单项状态管理
{
  label: '保存',
  accelerator: 'CmdOrCtrl+S',
  enabled: hasUnsavedChanges,  // 根据状态启用/禁用
  click: () => saveFile()
}
  1. 动态菜单
function updateMenu() {
  const template = createMenuTemplate()
  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
}

上下文菜单 (Context Menu)

概述

上下文菜单在右键点击时显示,提供与当前内容相关的操作。

特点

  • ✅ 所有平台支持
  • ��️ 右键触发
  • �� 上下文相关操作
  • 🔄 可动态生成

实现方式

主进程处理

const { ipcMain, Menu } = require("electron")

// 监听显示上下文菜单请求
ipcMain.on("show-context-menu", (event) => {
  const template = [
    {
      label: "复制",
      role: "copy",
    },
    {
      label: "粘贴",
      role: "paste",
    },
    { type: "separator" },
    {
      label: "自定义操作",
      submenu: [
        {
          label: "操作 1",
          click: () => {
            event.sender.send("context-menu-action", "action1")
          },
        },
        {
          label: "操作 2",
          click: () => {
            event.sender.send("context-menu-action", "action2")
          },
        },
      ],
    },
    { type: "separator" },
    {
      label: "检查元素",
      role: "inspect",
    },
  ]

  const menu = Menu.buildFromTemplate(template)
  menu.popup({ window: mainWindow })
})

渲染进程触发

// 预加载脚本
const { contextBridge, ipcRenderer } = require("electron")

contextBridge.exposeInMainWorld("electronAPI", {
  menu: {
    showContextMenu: () => ipcRenderer.send("show-context-menu"),
    onContextMenuAction: (callback) => {
      ipcRenderer.on("context-menu-action", (event, action) => {
        callback(action)
      })
    },
  },
})

// 渲染进程
document.addEventListener("contextmenu", (e) => {
  e.preventDefault()
  window.electronAPI.menu.showContextMenu()
})

// 监听上下文菜单操作
window.electronAPI.menu.onContextMenuAction((action) => {
  switch (action) {
    case "action1":
      console.log("执行操作 1")
      break
    case "action2":
      console.log("执行操作 2")
      break
  }
})

最佳实践

  1. 根据上下文动态生成菜单
ipcMain.on("show-context-menu", (event, context) => {
  const template = []

  if (context.isTextSelected) {
    template.push({ label: "复制", role: "copy" })
  }

  if (context.canPaste) {
    template.push({ label: "粘贴", role: "paste" })
  }

  // 添加自定义操作
  template.push({
    label: "自定义操作",
    click: () => {
      event.sender.send("context-menu-action", "custom")
    },
  })

  const menu = Menu.buildFromTemplate(template)
  menu.popup({ window: mainWindow })
})
  1. 菜单位置控制
menu.popup({
  window: mainWindow,
  x: event.clientX,
  y: event.clientY,
})

托盘菜单 (Tray Menu)

概述

托盘菜单位于系统托盘区域,即使窗口最小化也能访问。

特点

  • ✅ 所有平台支持
  • �� 系统托盘区域
  • �� 窗口最小化时仍可访问
  • �� 快速操作入口

实现方式

基础实现

const { Tray, nativeImage, Menu } = require("electron")
const path = require("path")

let tray = null

function createTray() {
  // 创建托盘图标
  const iconPath = path.join(__dirname, "assets/icon.png")
  tray = new Tray(nativeImage.createFromPath(iconPath))
  tray.setToolTip("应用名称")

  // 创建托盘菜单
  const trayTemplate = [
    {
      label: "显示主窗口",
      click: () => {
        mainWindow.show()
      },
    },
    {
      label: "隐藏主窗口",
      click: () => {
        mainWindow.hide()
      },
    },
    { type: "separator" },
    {
      label: "托盘操作",
      submenu: [
        {
          label: "操作 1",
          click: () => {
            dialog.showMessageBox({
              type: "info",
              message: "托盘操作 1",
            })
          },
        },
        {
          label: "操作 2",
          click: () => {
            mainWindow.webContents.send("tray-action", "action2")
          },
        },
      ],
    },
    { type: "separator" },
    {
      label: "退出",
      click: () => {
        app.quit()
      },
    },
  ]

  const trayMenu = Menu.buildFromTemplate(trayTemplate)
  tray.setContextMenu(trayMenu)

  // 托盘图标点击事件
  tray.on("click", () => {
    mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
  })

  // 托盘图标双击事件
  tray.on("double-click", () => {
    mainWindow.show()
  })
}

托盘图标管理

// 更新托盘图标
function updateTrayIcon(iconPath) {
  if (tray) {
    tray.setImage(nativeImage.createFromPath(iconPath))
  }
}

// 更新托盘提示
function updateTrayTooltip(tooltip) {
  if (tray) {
    tray.setToolTip(tooltip)
  }
}

// 清理托盘
function destroyTray() {
  if (tray) {
    tray.destroy()
    tray = null
  }
}

// 应用退出时清理
app.on("before-quit", () => {
  destroyTray()
})

最佳实践

  1. 图标尺寸适配
// 不同平台使用不同尺寸的图标
const iconPath =
  process.platform === "darwin"
    ? path.join(__dirname, "assets/icon-16.png")
    : path.join(__dirname, "assets/icon-32.png")
  1. 托盘状态管理
function updateTrayMenu(isOnline) {
  const template = [
    {
      label: isOnline ? "在线" : "离线",
      enabled: false,
    },
    { type: "separator" },
    // ... 其他菜单项
  ]

  const menu = Menu.buildFromTemplate(template)
  tray.setContextMenu(menu)
}

Dock 菜单 (Dock Menu)

概述

Dock 菜单仅在 Mac 平台可用,右键点击 Dock 中的应用图标时显示。

特点

  • �� 仅 Mac 平台支持
  • �� 快速操作入口
  • 🆕 支持新建窗口
  • ⚡ 高效操作

实现方式

const { app, Menu } = require("electron")

function createDockMenu() {
  if (process.platform === "darwin") {
    const dockTemplate = [
      {
        label: "新建窗口",
        click: () => {
          createWindow()
        },
      },
      {
        label: "快速操作",
        submenu: [
          {
            label: "快速操作 1",
            click: () => {
              console.log("Dock 快速操作 1")
              mainWindow.webContents.send("dock-action", "action1")
            },
          },
          {
            label: "快速操作 2",
            click: () => {
              console.log("Dock 快速操作 2")
              mainWindow.webContents.send("dock-action", "action2")
            },
          },
        ],
      },
      {
        label: "最近文件",
        submenu: [
          {
            label: "文件 1",
            click: () => {
              openRecentFile("file1.txt")
            },
          },
          {
            label: "文件 2",
            click: () => {
              openRecentFile("file2.txt")
            },
          },
        ],
      },
    ]

    const dockMenu = Menu.buildFromTemplate(dockTemplate)
    app.dock.setMenu(dockMenu)
  }
}

最佳实践

  1. 平台检查
if (process.platform === "darwin") {
  // 仅在 Mac 平台设置 Dock 菜单
  app.dock.setMenu(dockMenu)
}
  1. 动态 Dock 菜单
function updateDockMenu(recentFiles) {
  if (process.platform === "darwin") {
    const template = [
      {
        label: "新建窗口",
        click: () => createWindow(),
      },
    ]

    if (recentFiles.length > 0) {
      template.push({
        label: "最近文件",
        submenu: recentFiles.map((file) => ({
          label: file.name,
          click: () => openFile(file.path),
        })),
      })
    }

    const menu = Menu.buildFromTemplate(template)
    app.dock.setMenu(menu)
  }
}

最佳实践

1. 安全性

使用预加载脚本

// 预加载脚本
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  menu: {
    showContextMenu: () => ipcRenderer.send('show-context-menu'),
    onMenuAction: (callback) => {
      ipcRenderer.on('menu-action', (event, action, data) => {
        callback(action, data)
      })
    }
  }
})

// 主进程配置
webPreferences: {
  contextIsolation: true,
  nodeIntegration: false,
  preload: path.join(__dirname, 'preload.js')
}

错误处理

ipcMain.handle("menu-action", async (event, data) => {
  try {
    const result = await processMenuAction(data)
    return { success: true, data: result }
  } catch (error) {
    console.error("菜单操作失败:", error)
    return { success: false, error: error.message }
  }
})

2. 平台兼容性

快捷键适配

const accelerators = {
  new: process.platform === "darwin" ? "Cmd+N" : "Ctrl+N",
  open: process.platform === "darwin" ? "Cmd+O" : "Ctrl+O",
  save: process.platform === "darwin" ? "Cmd+S" : "Ctrl+S",
  quit: process.platform === "darwin" ? "Cmd+Q" : "Ctrl+Q",
}

平台特有功能

// Mac 特有功能
if (process.platform === "darwin") {
  // Dock 菜单
  app.dock.setMenu(dockMenu)

  // 应用菜单
  template.unshift({
    label: app.getName(),
    submenu: [
      { role: "about" },
      { type: "separator" },
      { role: "services" },
      { type: "separator" },
      { role: "hide" },
      { role: "hideothers" },
      { role: "unhide" },
      { type: "separator" },
      { role: "quit" },
    ],
  })
}

3. 用户体验

菜单项状态管理

function updateMenuState() {
  const template = [
    {
      label: "保存",
      accelerator: "CmdOrCtrl+S",
      enabled: hasUnsavedChanges,
      click: () => saveFile(),
    },
    {
      label: "撤销",
      accelerator: "CmdOrCtrl+Z",
      enabled: canUndo,
      click: () => undo(),
    },
  ]

  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
}

键盘快捷键

// 注册全局快捷键
const { globalShortcut } = require("electron")

app.whenReady().then(() => {
  // 注册全局快捷键
  globalShortcut.register("CmdOrCtrl+Shift+I", () => {
    mainWindow.webContents.toggleDevTools()
  })

  globalShortcut.register("CmdOrCtrl+Shift+N", () => {
    createWindow()
  })
})

// 应用退出时注销快捷键
app.on("will-quit", () => {
  globalShortcut.unregisterAll()
})

4. 性能优化

菜单缓存

let cachedMenu = null

function getMenu() {
  if (!cachedMenu) {
    cachedMenu = Menu.buildFromTemplate(createMenuTemplate())
  }
  return cachedMenu
}

function invalidateMenuCache() {
  cachedMenu = null
}

事件清理

class MenuManager {
  constructor() {
    this.listeners = []
  }

  addListener(event, callback) {
    ipcRenderer.on(event, callback)
    this.listeners.push({ event, callback })
  }

  cleanup() {
    this.listeners.forEach(({ event, callback }) => {
      ipcRenderer.removeListener(event, callback)
    })
    this.listeners = []
  }
}

常见问题

Q1: 托盘图标不显示

A: 检查图标文件路径和格式

// 确保图标文件存在且格式正确
const iconPath = path.join(__dirname, "assets/icon.png")
const icon = nativeImage.createFromPath(iconPath)

// 检查图标是否加载成功
if (icon.isEmpty()) {
  console.error("图标文件加载失败")
  return
}

tray = new Tray(icon)

Q2: 上下文菜单位置不正确

A: 使用正确的弹出位置

menu.popup({
  window: mainWindow,
  x: event.clientX,
  y: event.clientY,
})

Q3: 快捷键不工作

A: 检查快捷键格式和冲突

// 正确的快捷键格式
accelerator: "CmdOrCtrl+N" // 跨平台
accelerator: "Cmd+N" // 仅 Mac
accelerator: "Ctrl+N" // 仅 Windows/Linux

// 避免快捷键冲突
const isRegistered = globalShortcut.isRegistered("CmdOrCtrl+N")
if (!isRegistered) {
  globalShortcut.register("CmdOrCtrl+N", () => {
    // 处理快捷键
  })
}

Q4: 菜单项状态不同步

A: 实现菜单状态管理

function updateMenuState() {
  const template = createMenuTemplate()
  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
}

// 在状态变化时调用
ipcMain.on("update-menu-state", () => {
  updateMenuState()
})

Q5: 内存泄漏

A: 正确清理事件监听器

// 在组件卸载时清理
function cleanup() {
  ipcRenderer.removeAllListeners("menu-action")
  ipcRenderer.removeAllListeners("context-menu-action")
}

// 或在应用退出时清理
app.on("before-quit", () => {
  cleanup()
})

平台兼容性对比

菜单类型 Windows macOS Linux
窗口菜单
上下文菜单
托盘菜单
Dock 菜单

总结

Electron 的四类原生菜单为桌面应用提供了丰富的交互方式:

  1. 窗口菜单 - 应用的主要导航和操作入口
  2. 上下文菜单 - 提供与当前内容相关的快捷操作
  3. 托盘菜单 - 即使窗口最小化也能访问应用功能
  4. Dock 菜单 - Mac 平台特有的快速操作入口

关键要点

  • ✅ 使用 contextIsolation 确保安全性
  • ✅ 通过预加载脚本暴露 API
  • ✅ 适配不同平台的快捷键
  • ✅ 正确处理事件监听器的清理
  • ✅ 实现菜单状态管理
  • ✅ 提供用户友好的错误处理

通过合理使用这四类菜单,可以大大提升桌面应用的用户体验和操作便利性。记住要遵循平台规范,确保安全性和兼容性。


文章作者: 高红翔
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 高红翔 !
  目录