Electron 原生右键上下文菜单开发指南:context-menu 事件监听、IPC 弹出与 macOS 写作工具集成
在 Electron 中,"右键菜单"并非开箱即用的能力——桌面端没有浏览器默认的右键菜单可依赖,需要开发者自行监听右键事件、构建 Menu 实例并调用 menu.popup 完成弹窗。本文以 官方教程文档 为主体,结合仓库内配套的可运行 fiddle 示例、Menu.popup 选项定义 与 webContents 的 context-menu 事件说明,系统讲解主进程、渲染进程两种监听右键事件的方案,以及 menu.popup 各参数、针对 macOS 原生 Writing Tools/AutoFill/Services 菜单项的启用方式,帮助你为应用写出真正贴近操作系统的右键交互。
先理解 Electron 中"没有默认菜单"的设计
原生 Web 页面里右键往往自带浏览器的默认菜单(刷新、查看源代码等),而 Electron 出于定制自由的考虑,默认不渲染任何上下文菜单。开发者需要:
- 用 Menu.buildFromTemplate 或 new Menu 构建一个菜单实例;
- 监听"右键触发"这一行为;
- 在合适的时机调用实例方法
menu.popup([options])弹出菜单。
Electron 提供了两条监听右键事件的技术路线,对应两种弹出方式:
| 方案 | 监听位置 | 弹出方式 | 适用场景 |
|---|---|---|---|
webContents 的 context-menu 事件 |
主进程 | 直接调用 menu.popup() |
需要全页面统一菜单、或根据命中元素类型精确分流 |
DOM 的 contextmenu 事件(如 Shift + F10、鼠标右键) |
渲染进程 | 通过 IPC 通知主进程再 menu.popup() |
仅对特定 DOM 元素生效、或与页面内交互逻辑耦合 |
值得说明的是,Windows 上按下 Shift + F10 之类的快捷键同样会派发右键/上下文菜单事件,因此两种方案的触发源并不局限于鼠标右键。
方案一:在主进程监听 context-menu 事件(全局统一菜单)
事件与 params 参数结构
只要在某个 WebContents 的可视区域检测到右键,主进程就会收到一次 context-menu 事件。事件监听器会拿到一个 params 对象,它携带了非常丰富的命中信息,用于判断"此刻右键落在了什么元素上"。仓库中 web-contents.md 记录的关键字段包括:
| 字段 | 类型/取值 | 含义 |
|---|---|---|
linkURL / linkText |
string | 右键命中链接时,该链接的 URL 与文字(图片链接文字可能为空字符串) |
isEditable |
boolean | 命中元素是否可编辑(如 <textarea/>、contenteditable) |
pageURL / frameURL |
string | 右键所在顶层页面 / 子 frame 的 URL |
srcURL |
string | 命中带来源元素(img/audio/video)时的源 URL |
mediaType |
none image audio video canvas file plugin 等 |
命中节点的媒体类型 |
selectionText |
string | 当前选中的文本 |
misspelledWord / dictionarySuggestions |
string / string[] | 拼写检查命中的错误词及替换建议(需启用拼写检查) |
formControlType |
string | 命中的表单控件类型(如 input-text、text-area、select-one 等) |
menuSourceType |
string | 触发来源:mouse keyboard touch touchMenu longPress stylus 等 |
x / y |
number | 触发坐标 |
frame |
WebFrameMain 或 null |
发起菜单的 frame;若 frame 已导航或被销毁则为 null |
说明:
params中还包含hasImageContents、titleText、altText、suggestedFilename、mediaFlags、spellcheckEnabled等字段。完整清单请直接阅读 web-contents.md 中 Event: 'context-menu' 一节。
例如只对链接弹出"复制链接地址"菜单,可判断 params.linkURL;只对可编辑区域(如 <textarea/>)弹出"粘贴"菜单,则判断 params.isEditable。
用 role 快速构建标准编辑菜单
主进程方案完全不需要 preload 脚本与 IPC,因为它直接作用于 webContents。仓库配套示例 docs/fiddles/menus/context-menu/web-contents/main.js 给出了一个完整可运行的写法:
const { app, BrowserWindow, Menu } = require('electron/main')
function createWindow () {
const win = new BrowserWindow()
// 使用内置 role 构建标准编辑菜单,菜单只需创建一次,可复用
const menu = Menu.buildFromTemplate([
{ role: 'copy' },
{ role: 'cut' },
{ role: 'paste' },
{ role: 'selectall' }
])
win.webContents.on('context-menu', (_event, params) => {
// 仅当命中的元素可编辑时才显示上下文菜单
if (params.isEditable) {
menu.popup()
}
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
配套页面 index.html 只放了一个 <textarea></textarea> 作为可编辑测试对象。点击页面其他空白区域不会出现菜单,右键文本域时才弹出原生"复制 / 剪切 / 粘贴 / 全选"菜单——这正是靠 params.isEditable 做条件分流的效果。
role 机制是 MenuItem 的约定角色,它让 Electron 根据操作系统自动提供正确的标签、快捷键与行为(例如 macOS 上会自动展示"拷贝/粘贴"对应的系统文案)。除了 copy、cut、paste、selectall,还有 undo、redo、editMenu 等角色可选用。
按命中元素精细化分流
由于 context-menu 事件覆盖整个 WebContents,多个业务场景可以在同一个监听器内完成分流,例如:命中图片走"保存图片/复制图片地址",命中链接走"在新窗口打开/复制链接",命中可编辑区走编辑菜单,命中普通文本则返回不弹出。用 params.mediaType、params.linkURL 与 params.isEditable 组合判断即可,这也是方案一相对渲染进程方案的最大优势——一处监听、全局生效,且菜单逻辑集中在主进程,便于统一维护。
方案二:在渲染进程监听 DOM contextmenu 事件,经 IPC 弹出菜单
整体工作流
有些场景下,你只希望页面里某个特定区域出现右键菜单,此时可以回到 Web 平台的写法:在 DOM 元素上监听 [contextmenu 事件],preventDefault() 阻止任何潜在默认行为,再通过 IPC 把"弹菜单"的请求发到主进程,由主进程调用 menu.popup。之所以绕道主进程,是因为原生菜单的构建与弹出必须发生在主进程(Menu 是主进程模块)。仓库配套示例位于 docs/fiddles/menus/context-menu/dom/,三处代码分别如下。
页面:为 <textarea> 挂上监听
index.html 中给文本域加了 id,便于渲染脚本选中它:
<textarea id="editable"></textarea>
preload:通过 contextBridge 暴露受控的 IPC 通道
出于安全考虑(详见 context isolation 教程 与 安全指南),渲染进程默认无法直接访问 Node.js 与 Electron 模块。示例的 preload.js 在渲染脚本的 DOMContentLoaded 阶段给元素挂 contextmenu 监听,然后用 ipcRenderer.send 上报:
const { ipcRenderer } = require('electron/renderer')
document.addEventListener('DOMContentLoaded', () => {
const textarea = document.getElementById('editable')
textarea.addEventListener('contextmenu', (event) => {
event.preventDefault()
ipcRenderer.send('context-menu')
})
})
这段示例直接使用了 ipcRenderer。在真实项目里更推荐的做法是:用 contextBridge.exposeInMainWorld 在 preload 中只暴露一个窄接口(例如 window.api.showContextMenu(),内部封装 ipcRenderer.send('context-menu')),而不是把整个 ipcRenderer.send 暴露给渲染进程,以最小化安全攻击面。IPC 的基础模式可参考仓库中的 IPC 指南(尤其是"渲染进程到主进程(单向)"的 pattern 1)。
主进程:收到消息后用 popup 弹出菜单
main.js 中,主进程预先构建好菜单,并在 ipcMain.on('context-menu') 回调里调用 menu.popup。由于此刻主进程并不自动知道消息来自哪个窗口,需要借助 event.sender 反查出对应的窗口,并作为 window 选项传入,确保菜单弹在正确的 BrowserWindow 上:
const { app, BrowserWindow, ipcMain, Menu } = require('electron/main')
const path = require('node:path')
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
const menu = Menu.buildFromTemplate([
{ role: 'copy' },
{ role: 'cut' },
{ role: 'paste' },
{ role: 'selectall' }
])
ipcMain.on('context-menu', (event) => {
menu.popup({
window: BrowserWindow.fromWebContents(event.sender)
})
})
}
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
这一方案的取舍很明显:菜单的"出现条件"由渲染进程 DOM 逻辑决定,适合只在页面局部(如富文本编辑器、输入框、右键面板)弹出菜单的场景;代价是需要多维护一条 IPC 通路。而方案一中主进程直接监听即可覆盖全局。
menu.popup 选项详解:弹出位置与窗口归属
无论采用哪种触发方式,最终都落到 menu.popup([options])。根据 menu.md 的 popup 小节,可传参数如下:
| 参数 | 类型 | 默认值 | 作用 |
|---|---|---|---|
window |
BaseWindow | 当前聚焦窗口 | 指定菜单弹出的目标窗口 |
frame |
WebFrameMain | — | 提供触发菜单相关的 frame,用于启用某些依赖 frame 上下文的 OS 级能力(如 macOS Writing Tools);一般取 context-menu 事件的 params.frame 或 webContents.focusedFrame |
x / y |
number | 当前鼠标光标位置 | 指定菜单弹出坐标;两者必须同时声明 |
positioningItem |
number(仅 macOS) | -1 | 让菜单中索引为该项的菜单项正好落在鼠标/坐标位置之下 |
sourceType |
string(仅 Windows/Linux) | — | 与 context-menu 事件中 menuSourceType 对应,用于告知系统菜单的输入来源(mouse/keyboard/touch 等)。官方不推荐手工设置,仅建议透传事件返回值或保持 undefined |
callback |
Function | — | 菜单关闭时的回调 |
弹起菜单后,实例还会在显示/关闭前后发出 menu-will-show、menu-will-close 等事件(见 menu.md 的实例事件),可用于动态增删菜单项或记录菜单行为。若需要主动收起某窗口中的上下文菜单,可调用 menu.closePopup([window])。
macOS 专属:为上下文菜单启用 Writing Tools、AutoFill 与 Services
macOS 的文本框右键菜单通常会包含系统级的 Writing Tools(写作工具)、AutoFill(自动填充)、Services(服务) 等条目。而 Electron 出于框架层面的保守策略,这些系统菜单项默认是关闭的。要启用它们,需要在调用 menu.popup 时把与目标 webContents 相关联的 WebFrameMain 通过 frame 参数传进去。教程文档给出的最小示例(const { BrowserWindow, Menu } = require('electron/main')):
const { BrowserWindow, Menu } = require('electron/main')
const menu = Menu.buildFromTemplate([{ role: 'editMenu' }])
const win = new BrowserWindow()
win.webContents.on('context-menu', (_event, params) => {
// 仅在可编辑上下文中弹出菜单
if (params.isEditable) {
menu.popup({
frame: params.frame
})
}
})
此处 params.frame 正是 context-menu 事件 携带的 frame 字段。事件触发时该 frame 仍然有效,因此直接透传即可;若因异步操作导致 frame 已被导航或销毁,该字段会变为 null(详见事件参数说明)。模板中选用 { role: 'editMenu' } 让 Electron 生成一组贴合 macOS 习惯的编辑菜单项,其中即包含这些系统能力。
需要注意的是,frame 参数的意义并不止于 macOS 写作工具——凡是需要依赖确切 frame 上下文的 OS 集成(例如某些输入法/文本服务),在传入 frame 后都会更可靠地工作,因此即便不做平台特判,把 params.frame 透传给 popup 也是一种更稳妥的默认写法。
常见问题与实战小结
- 没有任何菜单出现:Electron 不内置默认上下文菜单,请确认监听器确实挂载(主进程
webContents.on('context-menu', ...)或渲染进程 DOMcontextmenu)且调用了menu.popup()。 - 渲染进程方案菜单弹错窗口:多窗口场景务必用
BrowserWindow.fromWebContents(event.sender)推导窗口并传入window选项,不要依赖"聚焦窗口"这一默认值。 - 主进程全局分流:通过
params.isEditable、params.linkURL、params.mediaType等字段在单个context-menu监听器内实现"编辑区菜单 / 链接菜单 / 图片菜单"的差异化处理。 - 可复用菜单实例:菜单模板只构建一次、由多个触发点复用即可,无需每次右键都重新
buildFromTemplate。 - macOS 行为差异:如需 Writing Tools / AutoFill / Services 等系统条目,记得传
frame: params.frame,并优先使用editMenu等角色以贴合系统习惯。
若想继续深入了解,可结合 Menu 完整 API、MenuItem 角色列表、webContents 事件 与 IPC 通信指南 阅读;动手实践可直接运行仓库中的两个示例:主进程方案 web-contents 与渲染进程方案 dom。在上下文隔离开启的现代 Electron 应用中,把"构建与弹出菜单"留在主进程、把"何时弹出"的决定权以受控 IPC 的形式暴露给渲染层,是兼具原生质感与安全性的推荐架构。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00