electron系列10: 应用菜单、右键菜单、全局快捷键
electron系列10: 应用菜单、右键菜单、全局快捷键
·
一、核心概念
在 Electron 中,菜单系统分为三个层次:
| 类型 | 作用域 | 触发方式 | 典型场景 |
|---|---|---|---|
| 应用菜单 | 整个应用 | macOS 顶部栏 / Windows 窗口标题栏下 | 文件、编辑、视图、帮助等标准菜单 |
| 右键菜单 | 特定元素/区域 | 鼠标右键点击 | 复制粘贴、刷新、自定义操作 |
| 全局快捷键 | 系统级 | 键盘组合键(即使应用失焦) | 截图、录音、快速唤出窗口 |
二、应用菜单 (Application Menu)
2.1 基础结构
// main.js - 主进程
const { Menu, app, BrowserWindow } = require('electron')
const template = [
{
label: '文件',
submenu: [
{
label: '新建窗口',
click: () => {
new BrowserWindow({ width: 800, height: 600 })
},
accelerator: 'CmdOrCtrl+N' // 快捷键
},
{
label: '打开文件',
click: async () => {
const { dialog } = require('electron')
const result = await dialog.showOpenDialog({ properties: ['openFile'] })
console.log(result.filePaths)
},
accelerator: 'CmdOrCtrl+O'
},
{ type: 'separator' }, // 分隔线
{
label: '退出',
click: () => app.quit(),
accelerator: 'CmdOrCtrl+Q'
}
]
},
{
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' },
{ label: '开发者工具', role: 'toggleDevTools' },
{ type: 'separator' },
{
label: '实际大小',
role: 'resetZoom'
},
{
label: '放大',
role: 'zoomIn'
},
{
label: '缩小',
role: 'zoomOut'
}
]
},
{
label: '帮助',
submenu: [
{
label: '关于',
click: () => {
const { dialog } = require('electron')
dialog.showMessageBox({
title: '关于',
message: '我的 Electron 应用 v1.0.0'
})
}
}
]
}
]
// macOS 特殊处理:第一个菜单会自动变成应用名菜单
if (process.platform === 'darwin') {
template.unshift({
label: app.getName(),
submenu: [
{ label: '关于', role: 'about' },
{ type: 'separator' },
{ label: '偏好设置', accelerator: 'CmdOrCtrl+,', click: () => {} },
{ type: 'separator' },
{ label: '服务', role: 'services' },
{ type: 'separator' },
{ label: '隐藏', role: 'hide' },
{ label: '隐藏其他', role: 'hideOthers' },
{ label: '显示全部', role: 'unhide' },
{ type: 'separator' },
{ label: '退出', role: 'quit' }
]
})
}
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
2.2 常用 Role(内置行为)
Electron 提供了大量内置 role,无需手动实现:
// 标准 role 列表
const roles = {
// 窗口控制
'minimize': '最小化',
'maximize': '最大化',
'close': '关闭',
// 编辑操作
'undo': '撤销',
'redo': '重做',
'cut': '剪切',
'copy': '复制',
'paste': '粘贴',
'selectAll': '全选',
// 视图操作
'reload': '重载页面',
'forceReload': '强制重载',
'toggleDevTools': '打开/关闭开发者工具',
'resetZoom': '重置缩放',
'zoomIn': '放大',
'zoomOut': '缩小',
// macOS 专属
'about': '关于',
'hide': '隐藏应用',
'hideOthers': '隐藏其他应用',
'unhide': '显示所有应用',
'quit': '退出'
}
2.3 动态菜单更新
// 根据窗口状态动态修改菜单
function updateMenu(windowCount) {
const menu = Menu.getApplicationMenu()
const fileMenu = menu.items.find(item => item.label === '文件')
if (fileMenu && fileMenu.submenu) {
const closeItem = fileMenu.submenu.items.find(item => item.label === '关闭窗口')
if (closeItem) {
closeItem.enabled = windowCount > 0
closeItem.visible = windowCount > 0
}
}
}
// 监听窗口事件
app.on('browser-window-created', () => updateMenu(BrowserWindow.getAllWindows().length))
app.on('browser-window-destroyed', () => updateMenu(BrowserWindow.getAllWindows().length))
三、右键菜单 (Context Menu)
3.1 基础右键菜单
// preload.js - 暴露 API 给渲染进程
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
showContextMenu: (options) => ipcRenderer.invoke('show-context-menu', options)
})
// main.js - 主进程处理
ipcMain.handle('show-context-menu', async (event, options) => {
const template = [
{
label: '复制',
role: 'copy',
accelerator: 'CmdOrCtrl+C'
},
{
label: '粘贴',
role: 'paste',
accelerator: 'CmdOrCtrl+V'
},
{ type: 'separator' },
{
label: '自定义选项',
click: () => {
event.sender.send('context-menu-click', '自定义操作')
}
}
]
// 添加动态选项
if (options.hasSelection) {
template.unshift({
label: `搜索 "${options.selection}"`,
click: () => {
// 执行搜索
}
})
}
const menu = Menu.buildFromTemplate(template)
menu.popup()
})
<!-- renderer.vue - 渲染进程 -->
<template>
<div @contextmenu.prevent="handleContextMenu">
<p>右键点击这个区域</p>
<input v-model="text" @contextmenu.prevent="handleContextMenu" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const text = ref('')
const handleContextMenu = (event) => {
const selection = window.getSelection().toString()
window.electronAPI.showContextMenu({
hasSelection: selection.length > 0,
selection: selection,
x: event.clientX,
y: event.clientY
})
}
</script>
3.2 编辑器右键菜单(增强版)
// main.js - 完整编辑器右键菜单
function createEditorContextMenu(selectionText, isEditable) {
const template = []
// 根据是否有选中文本显示不同选项
if (selectionText) {
template.push(
{ label: `复制 "${selectionText.substring(0, 20)}${selectionText.length > 20 ? '...' : ''}"`, role: 'copy' },
{ label: '剪切', role: 'cut', enabled: isEditable },
{ type: 'separator' }
)
}
template.push(
{ label: '粘贴', role: 'paste', enabled: isEditable },
{ label: '删除', role: 'delete', enabled: isEditable },
{ type: 'separator' },
{ label: '全选', role: 'selectAll' }
)
// 额外功能
template.push(
{ type: 'separator' },
{
label: '格式化',
submenu: [
{ label: '转为大写', click: () => {} },
{ label: '转为小写', click: () => {} }
]
}
)
return Menu.buildFromTemplate(template)
}
四、全局快捷键 (Global Shortcut)
4.1 基础使用
// main.js
const { globalShortcut, app } = require('electron')
app.whenReady().then(() => {
// 注册全局快捷键
const ret = globalShortcut.register('CommandOrControl+Shift+S', () => {
console.log('截图快捷键被按下')
// 触发截图功能
captureScreen()
})
if (!ret) {
console.log('快捷键注册失败,可能被其他应用占用')
}
// 注册多个快捷键
globalShortcut.register('CommandOrControl+Alt+R', () => {
// 录音功能
startRecording()
})
globalShortcut.register('MediaPlayPause', () => {
// 媒体控制键
togglePlayback()
})
})
// 应用退出前注销所有快捷键
app.on('will-quit', () => {
globalShortcut.unregisterAll()
})
4.2 动态管理快捷键
class ShortcutManager {
constructor() {
this.shortcuts = new Map()
}
// 注册快捷键
register(name, accelerator, callback) {
if (this.shortcuts.has(name)) {
this.unregister(name)
}
const success = globalShortcut.register(accelerator, callback)
if (success) {
this.shortcuts.set(name, { accelerator, callback })
console.log(`快捷键 ${name} (${accelerator}) 注册成功`)
} else {
console.error(`快捷键 ${name} (${accelerator}) 注册失败`)
}
return success
}
// 注销单个快捷键
unregister(name) {
const shortcut = this.shortcuts.get(name)
if (shortcut) {
globalShortcut.unregister(shortcut.accelerator)
this.shortcuts.delete(name)
}
}
// 切换快捷键启用状态
toggle(name, enabled) {
const shortcut = this.shortcuts.get(name)
if (shortcut) {
if (enabled) {
globalShortcut.register(shortcut.accelerator, shortcut.callback)
} else {
globalShortcut.unregister(shortcut.accelerator)
}
}
}
// 检查快捷键是否被注册
isRegistered(accelerator) {
return globalShortcut.isRegistered(accelerator)
}
}
// 使用示例
const shortcutManager = new ShortcutManager()
// 注册截图快捷键
shortcutManager.register('screenshot', 'CommandOrControl+Shift+S', () => {
captureScreen()
})
// 根据用户设置动态启用/禁用
shortcutManager.toggle('screenshot', userSettings.enableShortcut)
4.3 快捷键冲突处理
// 检测快捷键冲突
async function registerWithConflictCheck(accelerator, callback) {
// 检查是否已被其他应用占用
if (globalShortcut.isRegistered(accelerator)) {
const { dialog } = require('electron')
const result = await dialog.showMessageBox({
type: 'warning',
title: '快捷键冲突',
message: `快捷键 ${accelerator} 已被其他应用占用`,
detail: '是否仍然要注册该快捷键?',
buttons: ['仍然注册', '取消']
})
if (result.response === 1) return false
}
return globalShortcut.register(accelerator, callback)
}
五、完整实战示例
5.1 带配置的完整菜单系统
// main.js - 完整应用
const { app, BrowserWindow, Menu, globalShortcut, ipcMain } = require('electron')
const Store = require('electron-store')
const store = new Store({ name: 'shortcuts-config' })
class MenuManager {
constructor() {
this.window = null
this.shortcuts = {}
}
init(mainWindow) {
this.window = mainWindow
this.loadUserShortcuts()
this.setupApplicationMenu()
this.setupGlobalShortcuts()
this.setupIPCHandlers()
}
loadUserShortcuts() {
// 从存储加载用户自定义快捷键
const saved = store.get('shortcuts', {})
this.shortcuts = {
'screenshot': saved.screenshot || 'CommandOrControl+Shift+S',
'recording': saved.recording || 'CommandOrControl+Shift+R',
'showWindow': saved.showWindow || 'CommandOrControl+Shift+W',
...saved
}
}
setupApplicationMenu() {
const template = [
{
label: '应用',
submenu: [
{
label: '偏好设置',
accelerator: 'CommandOrControl+,',
click: () => {
this.window.webContents.send('open-settings')
}
},
{ type: 'separator' },
{ label: '退出', role: 'quit' }
]
},
{
label: '工具',
submenu: [
{
label: '截图',
accelerator: this.shortcuts.screenshot,
click: () => this.captureScreen()
},
{
label: '录音',
accelerator: this.shortcuts.recording,
click: () => this.startRecording()
},
{
label: '显示主窗口',
accelerator: this.shortcuts.showWindow,
click: () => this.showMainWindow()
}
]
},
{
label: '帮助',
submenu: [
{
label: '快捷键设置',
click: () => {
this.window.webContents.send('open-shortcut-settings')
}
}
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
setupGlobalShortcuts() {
// 截图快捷键
globalShortcut.register(this.shortcuts.screenshot, () => {
this.captureScreen()
})
// 录音快捷键
globalShortcut.register(this.shortcuts.recording, () => {
this.startRecording()
})
// 显示窗口快捷键
globalShortcut.register(this.shortcuts.showWindow, () => {
this.showMainWindow()
})
}
updateShortcut(name, newAccelerator) {
// 更新存储
this.shortcuts[name] = newAccelerator
store.set(`shortcuts.${name}`, newAccelerator)
// 重新注册快捷键
globalShortcut.unregisterAll()
this.setupGlobalShortcuts()
// 更新菜单显示
this.setupApplicationMenu()
}
captureScreen() {
console.log('执行截图...')
this.window.webContents.send('trigger-screenshot')
}
startRecording() {
console.log('开始录音...')
this.window.webContents.send('start-recording')
}
showMainWindow() {
if (this.window.isMinimized()) this.window.restore()
this.window.show()
this.window.focus()
}
setupIPCHandlers() {
ipcMain.handle('update-shortcut', (event, { name, accelerator }) => {
this.updateShortcut(name, accelerator)
return { success: true }
})
ipcMain.handle('get-shortcuts', () => {
return this.shortcuts
})
}
}
5.2 渲染进程配置界面
<!-- ShortcutSettings.vue -->
<template>
<div class="shortcut-settings">
<h2>快捷键设置</h2>
<div v-for="shortcut in shortcuts" :key="shortcut.name" class="shortcut-item">
<label>{{ shortcut.label }}</label>
<div class="shortcut-input">
<input
v-model="shortcut.accelerator"
@keydown="handleKeydown($event, shortcut)"
:disabled="shortcut.isRecording"
placeholder="点击录制"
/>
<button @click="startRecording(shortcut)">录制</button>
<button @click="resetShortcut(shortcut)">重置</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const shortcuts = ref([
{ name: 'screenshot', label: '截图', accelerator: 'CommandOrControl+Shift+S', isRecording: false },
{ name: 'recording', label: '开始录音', accelerator: 'CommandOrControl+Shift+R', isRecording: false },
{ name: 'showWindow', label: '显示主窗口', accelerator: 'CommandOrControl+Shift+W', isRecording: false }
])
const handleKeydown = (event, shortcut) => {
if (!shortcut.isRecording) return
event.preventDefault()
const keys = []
if (event.ctrlKey) keys.push('Ctrl')
if (event.shiftKey) keys.push('Shift')
if (event.altKey) keys.push('Alt')
if (event.metaKey) keys.push('Command')
const key = event.key.length === 1 ? event.key.toUpperCase() : event.key
if (key !== 'Control' && key !== 'Shift' && key !== 'Alt' && key !== 'Meta') {
keys.push(key)
const accelerator = keys.join('+')
// 更新本地显示
shortcut.accelerator = accelerator
shortcut.isRecording = false
// 保存到主进程
window.electronAPI.updateShortcut({
name: shortcut.name,
accelerator: accelerator
})
}
}
const startRecording = (shortcut) => {
// 停止其他正在录制的
shortcuts.value.forEach(s => s.isRecording = false)
shortcut.isRecording = true
shortcut.accelerator = '按下快捷键...'
}
const resetShortcut = (shortcut) => {
const defaults = {
screenshot: 'CommandOrControl+Shift+S',
recording: 'CommandOrControl+Shift+R',
showWindow: 'CommandOrControl+Shift+W'
}
shortcut.accelerator = defaults[shortcut.name]
window.electronAPI.updateShortcut({
name: shortcut.name,
accelerator: defaults[shortcut.name]
})
}
onMounted(async () => {
const saved = await window.electronAPI.getShortcuts()
shortcuts.value.forEach(shortcut => {
if (saved[shortcut.name]) {
shortcut.accelerator = saved[shortcut.name]
}
})
})
</script>
六、常见坑点与最佳实践
坑点总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
| macOS 应用菜单不显示 | 未处理 macOS 特殊逻辑 | 在 template 开头添加应用名菜单 |
| 快捷键注册失败 | 被其他应用占用 | 检查 globalShortcut.isRegistered() 并提示用户 |
| 右键菜单不显示 | 未调用 preventDefault() |
在 @contextmenu 事件中添加 .prevent |
| 无边框窗口无法拖拽 | 缺少 CSS 属性 | 设置 -webkit-app-region: drag |
| 快捷键在开发工具中失效 | DevTools 抢占了焦点 | 区分开发/生产环境处理 |
七、调试技巧
// 开发环境显示菜单调试信息
if (process.env.NODE_ENV === 'development') {
const menu = Menu.getApplicationMenu()
console.log('当前菜单结构:', JSON.stringify(menu, null, 2))
// 添加开发菜单
const devMenu = {
label: '开发',
submenu: [
{ label: '重新加载菜单', click: () => Menu.setApplicationMenu(Menu.buildFromTemplate(template)) },
{ label: '列出所有快捷键', click: () => console.log(globalShortcut.getAll()) }
]
}
const existingMenu = Menu.getApplicationMenu()
const newTemplate = [...existingMenu.items, devMenu]
Menu.setApplicationMenu(Menu.buildFromTemplate(newTemplate))
}
更多推荐
所有评论(0)