系统菜单与托盘
原创2026/9/9...大约 6 分钟
系统菜单与托盘 :knife_fork_plate:
系统菜单和系统托盘是桌面应用区别于 Web 应用的重要特性。本章我们学习如何为 Electron 应用添加原生菜单与托盘图标。
应用菜单 💎
应用菜单通常显示在窗口顶部(Windows / Linux)或屏幕顶部(macOS),是桌面应用的重要入口。
创建自定义菜单 💎
// src/main/menu.ts
import { Menu, MenuItemConstructorOptions, app, shell } from 'electron'
export function createApplicationMenu(): void {
const isMac = process.platform === 'darwin'
const template: MenuItemConstructorOptions[] = [
// macOS 上应用菜单的第一个菜单是应用名
...(isMac
? [
{
label: app.name,
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: '退出' },
],
},
]
: []),
// 文件菜单
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: (menuItem, browserWindow) => {
// 通过 webContents 通知渲染进程
browserWindow?.webContents.send('menu:new-file')
},
},
{
label: '打开',
accelerator: 'CmdOrCtrl+O',
click: (_, browserWindow) => {
browserWindow?.webContents.send('menu:open-file')
},
},
{ type: 'separator' },
isMac ? { role: 'close', label: '关闭窗口' } : { role: 'quit', label: '退出' },
],
},
// 编辑菜单
{
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: '缩放' },
...(isMac
? [
{ type: 'separator' },
{ role: 'front', label: '前置所有窗口' },
]
: [{ role: 'close', label: '关闭' }]),
],
},
// 帮助菜单
{
label: '帮助',
submenu: [
{
label: '访问官网',
click: async () => {
await shell.openExternal('https://www.electronjs.org/')
},
},
{
label: '关于',
click: () => {
// 弹出关于对话框
},
},
],
},
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}在主进程中加载 💎
// src/main/index.ts
import { app, BrowserWindow } from 'electron'
import { createApplicationMenu } from './menu'
app.whenReady().then(() => {
createApplicationMenu()
createWindow()
})渲染进程响应菜单事件 💎
预加载脚本中暴露监听 API:
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('menuApi', {
onNewFile: (callback: () => void) => {
ipcRenderer.on('menu:new-file', callback)
},
onOpenFile: (callback: () => void) => {
ipcRenderer.on('menu:open-file', callback)
},
})渲染进程(Vue 示例):
import { onMounted, onUnmounted } from 'vue'
let offNew: (() => void) | null = null
let offOpen: (() => void) | null = null
onMounted(() => {
offNew = (window as any).menuApi.onNewFile(() => {
console.log('点击了"新建"菜单')
// 处理新建逻辑
})
offOpen = (window as any).menuApi.onOpenFile(() => {
console.log('点击了"打开"菜单')
// 处理打开逻辑
})
})
onUnmounted(() => {
offNew?.()
offOpen?.()
})常用内置角色 💎
Electron 内置了多个菜单角色(role),可以快速创建标准菜单项:
| 角色 | 作用 |
|---|---|
undo | 撤销 |
redo | 重做 |
cut | 剪切 |
copy | 复制 |
paste | 粘贴 |
selectAll | 全选 |
reload | 重新加载 |
forceReload | 强制重新加载 |
toggleDevTools | 切换开发者工具 |
resetZoom | 重置缩放 |
zoomIn | 放大 |
zoomOut | 缩小 |
togglefullscreen | 切换全屏 |
minimize | 最小化 |
maximize | 最大化 |
close | 关闭 |
quit | 退出 |
about | 关于 |
hide | 隐藏(macOS) |
hideOthers | 隐藏其他(macOS) |
showAll | 显示全部(macOS) |
动态菜单 💎
菜单可以根据应用状态动态变化:
function createContextMenu(isLoggedIn: boolean): void {
const template: MenuItemConstructorOptions[] = [
{
label: '用户',
submenu: [
{
label: isLoggedIn ? '退出登录' : '登录',
click: (_, win) => {
win?.webContents.send('toggle-login')
},
},
],
},
]
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
}
// 登录状态变化时重新构建菜单
function updateMenu(isLoggedIn: boolean) {
createContextMenu(isLoggedIn)
}右键上下文菜单 💎
在窗口内右键点击时弹出的菜单,常用于"复制/粘贴/全选"或自定义操作:
// src/main/index.ts
import { ipcMain, Menu, BrowserWindow } from 'electron'
ipcMain.on('show-context-menu', (event) => {
const win = BrowserWindow.fromWebContents(event.sender)!
const template = [
{ label: '复制', role: 'copy' },
{ label: '粘贴', role: 'paste' },
{ type: 'separator' },
{
label: '自定义操作',
click: () => {
win.webContents.send('custom-action')
},
},
]
const menu = Menu.buildFromTemplate(template)
menu.popup({ window: win })
})渲染进程触发:
// 在元素上监听 contextmenu 事件
document.getElementById('my-text')?.addEventListener('contextmenu', (e) => {
e.preventDefault()
;(window as any).electronAPI.showContextMenu()
})系统托盘 💎
系统托盘(也叫状态栏图标)允许应用在后台保持运行,用户可以从托盘图标快速访问功能。
创建托盘 💎
// src/main/tray.ts
import { Tray, Menu, nativeImage, app, BrowserWindow } from 'electron'
import { join } from 'path'
let tray: Tray | null = null
export function createTray(): void {
// 加载托盘图标(建议使用 16x16 或 32x32 的 PNG)
const iconPath = join(__dirname, '../../resources/icon.png')
const icon = nativeImage.createFromPath(iconPath)
// macOS 上使用 Template 图标(黑白色,自动适应主题)
if (process.platform === 'darwin') {
icon.setTemplateImage(true)
}
tray = new Tray(icon)
tray.setToolTip('我的 Electron 应用')
// 创建托盘菜单
const contextMenu = Menu.buildFromTemplate([
{
label: '打开主窗口',
click: () => {
const wins = BrowserWindow.getAllWindows()
if (wins.length) {
wins[0].show()
wins[0].focus()
} else {
// 如果没有窗口,可以重新创建
}
},
},
{ type: 'separator' },
{
label: '设置',
click: () => {
// 打开设置窗口
},
},
{ type: 'separator' },
{
label: '退出',
click: () => {
app.quit()
},
},
])
tray.setContextMenu(contextMenu)
// 单击托盘图标
tray.on('click', () => {
const win = BrowserWindow.getAllWindows()[0]
if (win.isVisible()) {
win.hide()
} else {
win.show()
win.focus()
}
})
// 双击托盘图标
tray.on('double-click', () => {
const win = BrowserWindow.getAllWindows()[0]
win.show()
win.focus()
})
}
export function destroyTray(): void {
tray?.destroy()
tray = null
}在主进程中加载 💎
// src/main/index.ts
import { app, BrowserWindow } from 'electron'
import { createTray, destroyTray } from './tray'
app.whenReady().then(() => {
createWindow()
createTray()
})
app.on('before-quit', () => {
destroyTray()
})托盘闪烁 💎
通过修改托盘图标的 title 可以实现消息提示效果:
let unreadCount = 0
let flashInterval: NodeJS.Timeout | null = null
function flashTray() {
if (flashInterval) return
const emptyIcon = nativeImage.createEmpty()
let showIcon = true
flashInterval = setInterval(() => {
if (showIcon) {
tray?.setImage(emptyIcon)
} else {
tray?.setImage(nativeImage.createFromPath(iconPath))
}
showIcon = !showIcon
}, 500)
}
function stopFlash() {
if (flashInterval) {
clearInterval(flashInterval)
flashInterval = null
tray?.setImage(nativeImage.createFromPath(iconPath))
}
}
// 设置未读数
function setUnreadCount(count: number) {
unreadCount = count
if (count > 0) {
tray?.setTitle(` ${count}`) // macOS 上显示数字
flashTray()
} else {
tray?.setTitle('')
stopFlash()
}
}全局快捷键 💎
通过 globalShortcut 模块可以注册系统级快捷键,即使应用不在前台也能响应:
// src/main/shortcut.ts
import { globalShortcut, BrowserWindow } from 'electron'
export function registerShortcuts(): void {
// 注册全局快捷键:Ctrl+Shift+I 打开开发者工具
globalShortcut.register('CommandOrControl+Shift+I', () => {
const win = BrowserWindow.getFocusedWindow()
win?.webContents.toggleDevTools()
})
// 全局快捷键:Ctrl+Shift+Space 呼出主窗口
globalShortcut.register('CommandOrControl+Shift+Space', () => {
const win = BrowserWindow.getAllWindows()[0]
if (win.isVisible()) {
win.hide()
} else {
win.show()
win.focus()
}
})
}
export function unregisterShortcuts(): void {
globalShortcut.unregisterAll()
}// src/main/index.ts
import { registerShortcuts, unregisterShortcuts } from './shortcut'
app.whenReady().then(() => {
registerShortcuts()
})
app.on('will-quit', () => {
unregisterShortcuts()
})全局快捷键注意事项
- 占用系统级资源,不要注册太多,避免与其它应用冲突
- 应用退出时记得调用
globalShortcut.unregisterAll()释放 - macOS 上如果快捷键包含特殊功能键,可能需要在
Info.plist中额外配置
实战案例:仿微信客户端菜单 💎
// 仿微信的菜单结构
function createWechatLikeMenu(): void {
const template: MenuItemConstructorOptions[] = [
{
label: '聊天',
submenu: [
{ label: '发起群聊', accelerator: 'CmdOrCtrl+Shift+G', click: onMenuAction },
{ label: '添加好友', accelerator: 'CmdOrCtrl+Shift+A', click: onMenuAction },
{ label: '扫一扫', accelerator: 'CmdOrCtrl+Shift+S', click: onMenuAction },
],
},
{
label: '通讯录',
submenu: [
{ label: '新的朋友', click: onMenuAction },
{ label: '仅聊天的朋友', click: onMenuAction },
{ label: '群聊', click: onMenuAction },
{ label: '标签', click: onMenuAction },
{ label: '公众号', click: onMenuAction },
],
},
{
label: '收藏',
submenu: [
{ label: '文件', click: onMenuAction },
{ label: '链接', click: onMenuAction },
{ label: '笔记', click: onMenuAction },
{ label: '语音', click: onMenuAction },
],
},
{
label: '设置',
submenu: [
{ label: '通用设置', click: onMenuAction },
{ label: '账号设置', click: onMenuAction },
{ label: '消息通知', click: onMenuAction },
{ label: '快捷键', click: onMenuAction },
{ type: 'separator' },
{ label: '切换账号', click: onMenuAction },
{ label: '退出登录', click: onMenuAction },
],
},
]
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
}
function onMenuAction(menuItem: MenuItem, browserWindow?: BrowserWindow) {
browserWindow?.webContents.send('menu-click', {
label: menuItem.label,
})
}小结
本章我们学习了 Electron 桌面特性的核心三件套:
- 应用菜单:通过
Menu模块构建应用顶部菜单 - 系统托盘:通过
Tray模块在状态栏展示应用图标与菜单 - 全局快捷键:通过
globalShortcut注册系统级快捷键
这些能力让 Electron 应用具备了"原生桌面应用"的体验,也是 Electron 区别于 Web 应用的核心价值。
下一章我们将学习 本地数据存储。
至此,本章节的学习就到此结束了,如有疑惑,可对接技术客服进行相关咨询。