菜单与托盘
菜单和系统托盘是桌面应用的重要组成部分。本节介绍如何创建应用菜单、上下文菜单和系统托盘图标。
应用菜单
应用菜单是桌面应用的标准组成部分,通常包含文件、编辑、视图等菜单项。
创建菜单
使用 Menu 模块创建应用菜单:
const { app, BrowserWindow, Menu } = require('electron')
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => createNewDocument()
},
{
label: '打开',
accelerator: 'CmdOrCtrl+O',
click: () => openFile()
},
{ type: 'separator' },
{
label: '退出',
accelerator: 'CmdOrCtrl+Q',
role: '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: [
{ role: 'minimize', label: '最小化' },
{ role: 'zoom', label: '缩放' },
{ type: 'separator' },
{ role: 'front', label: '前置全部窗口' }
]
},
{
label: '帮助',
submenu: [
{
label: '关于',
click: () => showAbout()
},
{
label: '文档',
click: async () => {
const { shell } = require('electron')
await shell.openExternal('https://example.com/docs')
}
}
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
内置角色
Electron 提供了内置角色,可以快速添加常用菜单项:
| 角色 | 说明 |
|---|---|
undo | 撤销 |
redo | 重做 |
cut | 剪切 |
copy | 复制 |
paste | 粘贴 |
selectAll | 全选 |
delete | 删除 |
reload | 重新加载 |
forceReload | 强制重新加载 |
toggleDevTools | 切换开发者工具 |
resetZoom | 重置缩放 |
zoomIn | 放大 |
zoomOut | 缩小 |
togglefullscreen | 切换全屏 |
minimize | 最小化 |
close | 关闭 |
quit | 退出 |
front | 前置窗口 |
快捷键
使用 accelerator 属性设置快捷键:
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => createNewDocument()
}
平台相关快捷键使用 CmdOrCtrl:
accelerator: 'CmdOrCtrl+S'
这会在 macOS 上映射为 Cmd+S,在 Windows/Linux 上映射为 Ctrl+S。
动态菜单
根据应用状态动态更新菜单:
const { Menu } = require('electron')
let isEditing = false
function updateMenu() {
const template = [
{
label: '编辑',
submenu: [
{ role: 'undo', enabled: isEditing },
{ role: 'redo', enabled: isEditing },
{ type: 'separator' },
{ role: 'cut', enabled: isEditing },
{ role: 'copy', enabled: isEditing },
{ role: 'paste', enabled: isEditing }
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
function setEditingState(state) {
isEditing = state
updateMenu()
}
macOS 特有菜单
macOS 有一些特殊的菜单约定:
应用菜单
第一个菜单应该是应用名称菜单:
const template = [
{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
}
]
上下文菜单
上下文菜单(右键菜单)在用户右键点击时显示。
创建上下文菜单
const { Menu } = require('electron')
const contextMenu = Menu.buildFromTemplate([
{ label: '复制', role: 'copy' },
{ label: '粘贴', role: 'paste' },
{ type: 'separator' },
{
label: '自定义操作',
click: () => console.log('自定义操作被点击')
}
])
// 在窗口中显示
contextMenu.popup()
在渲染进程中触发
主进程
const { ipcMain, Menu } = require('electron')
ipcMain.on('show-context-menu', (event) => {
const contextMenu = Menu.buildFromTemplate([
{ label: '复制', role: 'copy' },
{ label: '粘贴', role: 'paste' }
])
const win = BrowserWindow.fromWebContents(event.sender)
contextMenu.popup(win)
})
预加载脚本
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
showContextMenu: () => ipcRenderer.send('show-context-menu')
})
渲染进程
document.addEventListener('contextmenu', (e) => {
e.preventDefault()
window.electronAPI.showContextMenu()
})
使用 webContents 事件
更好的方式是监听 context-menu 事件:
const { BrowserWindow, Menu } = require('electron')
mainWindow.webContents.on('context-menu', (event, params) => {
const menu = Menu.buildFromTemplate([
{ label: '复制', role: 'copy', enabled: params.editFlags.canCopy },
{ label: '粘贴', role: 'paste', enabled: params.editFlags.canPaste },
{ type: 'separator' },
{ label: '检查元素', click: () => mainWindow.webContents.inspectElement(params.x, params.y) }
])
menu.popup()
})
params 对象包含丰富的上下文信息:
{
x: 100,
y: 200,
linkURL: 'https://example.com',
linkText: '链接文本',
pageURL: 'file:///path/to/page.html',
selectionText: '选中的文本',
editFlags: {
canUndo: true,
canRedo: false,
canCut: true,
canCopy: true,
canPaste: true,
canDelete: true,
canSelectAll: true
}
}
系统托盘
系统托盘图标可以让应用在后台运行,并提供快速访问入口。
创建托盘图标
const { app, BrowserWindow, Tray, Menu } = require('electron')
const path = require('node:path')
let tray
function createTray() {
const iconPath = path.join(__dirname, 'icon.png')
tray = new Tray(iconPath)
const contextMenu = Menu.buildFromTemplate([
{ label: '显示窗口', click: () => mainWindow.show() },
{ label: '隐藏窗口', click: () => mainWindow.hide() },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
])
tray.setToolTip('我的应用')
tray.setContextMenu(contextMenu)
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
})
}
app.whenReady().then(() => {
createWindow()
createTray()
})
托盘事件
tray.on('click', () => {
console.log('托盘被点击')
})
tray.on('double-click', () => {
console.log('托盘被双击')
})
tray.on('right-click', () => {
console.log('托盘被右键点击')
})
tray.on('mouse-enter', () => {
console.log('鼠标进入托盘')
})
tray.on('mouse-leave', () => {
console.log('鼠标离开托盘')
})
更新托盘图标
tray.setImage(path.join(__dirname, 'new-icon.png'))
tray.setToolTip('新提示文本')
托盘气泡通知
tray.displayBalloon({
title: '通知标题',
content: '通知内容',
iconType: 'info'
})
macOS 托盘特性
macOS 上托盘图标有一些特殊行为:
tray.setTitle('标题文字')
tray.setIgnoreDoubleClickEvents(true)
菜单项类型
普通菜单项
{
label: '普通菜单项',
click: () => console.log('被点击')
}
分隔符
{ type: 'separator' }
复选框
{
label: '自动保存',
type: 'checkbox',
checked: true,
click: (menuItem) => {
console.log('自动保存:', menuItem.checked)
}
}
单选按钮
{
label: '主题',
submenu: [
{ label: '浅色', type: 'radio', checked: true },
{ label: '深色', type: 'radio' },
{ label: '自动', type: 'radio' }
]
}
子菜单
{
label: '最近文件',
submenu: [
{ label: '文件1.txt' },
{ label: '文件2.txt' },
{ label: '文件3.txt' }
]
}
图标菜单项
{
label: '带图标的菜单项',
icon: nativeImage.createFromPath('icon.png')
}
完整示例
下面是一个完整的菜单和托盘示例:
const { app, BrowserWindow, Menu, Tray, shell, dialog } = require('electron')
const path = require('node:path')
let mainWindow
let tray
let recentFiles = []
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
mainWindow.on('close', (event) => {
if (!app.isQuitting) {
event.preventDefault()
mainWindow.hide()
}
})
}
function buildMenu() {
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => mainWindow.webContents.send('new-file')
},
{
label: '打开',
accelerator: 'CmdOrCtrl+O',
click: async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
filters: [{ name: '文本文件', extensions: ['txt', 'md'] }]
})
if (!canceled && filePaths.length > 0) {
const filePath = filePaths[0]
addRecentFile(filePath)
mainWindow.webContents.send('open-file', filePath)
}
}
},
{
label: '最近文件',
submenu: recentFiles.map(file => ({
label: file,
click: () => mainWindow.webContents.send('open-file', file)
}))
},
{ type: 'separator' },
{
label: '退出',
accelerator: 'CmdOrCtrl+Q',
click: () => {
app.isQuitting = true
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: 'toggleDevTools', label: '开发者工具' },
{ type: 'separator' },
{ role: 'togglefullscreen', label: '全屏' }
]
},
{
label: '帮助',
submenu: [
{
label: '文档',
click: async () => {
await shell.openExternal('https://example.com/docs')
}
},
{
label: '关于',
click: () => {
dialog.showMessageBox(mainWindow, {
type: 'info',
title: '关于',
message: '我的应用',
detail: '版本 1.0.0'
})
}
}
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
function createTray() {
const iconPath = path.join(__dirname, 'icon.png')
tray = new Tray(iconPath)
const contextMenu = Menu.buildFromTemplate([
{
label: '显示窗口',
click: () => {
mainWindow.show()
mainWindow.focus()
}
},
{ type: 'separator' },
{
label: '退出',
click: () => {
app.isQuitting = true
app.quit()
}
}
])
tray.setToolTip('我的应用')
tray.setContextMenu(contextMenu)
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
})
}
function addRecentFile(filePath) {
recentFiles = recentFiles.filter(f => f !== filePath)
recentFiles.unshift(filePath)
if (recentFiles.length > 10) {
recentFiles = recentFiles.slice(0, 10)
}
buildMenu()
}
app.whenReady().then(() => {
createWindow()
buildMenu()
createTray()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
最佳实践
菜单设计原则
- 遵循平台惯例:macOS 和 Windows 的菜单习惯不同
- 合理分组:使用分隔符分隔不同功能的菜单项
- 提供快捷键:常用操作应该有快捷键
- 禁用不可用项:根据应用状态禁用不合适的菜单项
托盘设计原则
- 提供基本操作:显示/隐藏窗口、退出应用
- 双击行为:双击托盘图标通常显示主窗口
- 右键菜单:提供更多操作选项
- 状态指示:通过图标变化显示应用状态
总结
菜单和托盘是桌面应用的重要交互方式:
- 使用
Menu.buildFromTemplate创建菜单 - 内置角色简化常用菜单项的创建
- 上下文菜单提供右键操作
- 系统托盘让应用在后台运行