Electron 桌面应用提供了四类原生菜单,每种菜单都有其特定的用途和实现方式。本文档将详细介绍这四类菜单的实现方法、最佳实践和常见问题解决方案。
目录
窗口菜单 (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: "退出" },
],
})
}
最佳实践
- 快捷键适配
// 使用 CmdOrCtrl 适配不同平台
accelerator: "CmdOrCtrl+N" // Mac: Cmd+N, Windows/Linux: Ctrl+N
accelerator: "CmdOrCtrl+Shift+Z" // 跨平台快捷键
- 菜单项状态管理
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
enabled: hasUnsavedChanges, // 根据状态启用/禁用
click: () => saveFile()
}
- 动态菜单
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
}
})
最佳实践
- 根据上下文动态生成菜单
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 })
})
- 菜单位置控制
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()
})
最佳实践
- 图标尺寸适配
// 不同平台使用不同尺寸的图标
const iconPath =
process.platform === "darwin"
? path.join(__dirname, "assets/icon-16.png")
: path.join(__dirname, "assets/icon-32.png")
- 托盘状态管理
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)
}
}
最佳实践
- 平台检查
if (process.platform === "darwin") {
// 仅在 Mac 平台设置 Dock 菜单
app.dock.setMenu(dockMenu)
}
- 动态 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 的四类原生菜单为桌面应用提供了丰富的交互方式:
- 窗口菜单 - 应用的主要导航和操作入口
- 上下文菜单 - 提供与当前内容相关的快捷操作
- 托盘菜单 - 即使窗口最小化也能访问应用功能
- Dock 菜单 - Mac 平台特有的快速操作入口
关键要点
- ✅ 使用
contextIsolation
确保安全性 - ✅ 通过预加载脚本暴露 API
- ✅ 适配不同平台的快捷键
- ✅ 正确处理事件监听器的清理
- ✅ 实现菜单状态管理
- ✅ 提供用户友好的错误处理
通过合理使用这四类菜单,可以大大提升桌面应用的用户体验和操作便利性。记住要遵循平台规范,确保安全性和兼容性。