首页 / Electron 入门教程 / 右键上下文菜单

Electron 入门教程

右键上下文菜单

本教程共 45 篇 · 第 19 篇 · 更新于 2026-08-03

Electron上下文菜单右键菜单MenupopupIPC

19. 右键上下文菜单

本节目标

  • 在主进程监听 webContents 的 context-menu 事件
  • 在渲染进程经 IPC 触发菜单弹出
  • 理解 popup 的位置与窗口绑定
  • 根据场景动态构建上下文菜单

右键点下去弹出的那个小菜单,叫上下文菜单(context menu)。Electron 默认不会给任何区域加右键菜单,需要你用 Menupopup 方法自己触发。

触发时机有两种:一种是在主进程监听 webContentscontext-menu 事件;另一种是在渲染进程监听 DOM 的 contextmenu 事件,再经 IPC 让主进程弹菜单。本章两种都讲,并演示如何根据场景动态生成菜单。

19-1 在主进程监听 context-menu 事件

webContents 会在用户右键时抛出 context-menu 事件,回调里带一个 params 对象,描述右击发生在什么元素上(链接、可编辑区、图片等)。你可以据此决定是否弹菜单。

// main.js(主进程)
const { app, BrowserWindow, Menu } = require('electron/main')

function createWindow() {
  const win = new BrowserWindow()

  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)

params.isEditabletrue 表示右键落在输入框或文本域里。类似地,params.linkURL 表示点在了链接上。依这些标志做判断,菜单才「有上下文」。

19-2 在渲染进程触发并走 IPC

更多时候,你希望由页面逻辑决定菜单内容,比如在某个自定义组件上右键。这时在渲染进程监听 DOM 的 contextmenu 事件,通过 ipcRenderer.send 通知主进程,再由主进程 popup

// main.js(主进程)
const { app, BrowserWindow, ipcMain, Menu } = require('electron/main')
const path = require('node:path')

function createWindow() {
  const win = new BrowserWindow({
    webPreferences: { preload: path.join(__dirname, 'preload.js') }
  })

  const menu = Menu.buildFromTemplate([
    { role: 'copy' },
    { role: 'cut' },
    { role: 'paste' },
    { role: 'selectall' }
  ])

  ipcMain.on('show-context-menu', (event) => {
    menu.popup({ window: BrowserWindow.fromWebContents(event.sender) })
  })

  win.loadFile('index.html')
}

app.whenReady().then(createWindow)
// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('menu', {
  showContextMenu: () => ipcRenderer.send('show-context-menu')
})
// 渲染进程脚本
document.getElementById('editor').addEventListener('contextmenu', (e) => {
  e.preventDefault()
  window.menu.showContextMenu()
})

这种写法符合安全范式:菜单在主进程构建,渲染进程只发一个「请弹菜单」的信号,不直接拿到 Menu 对象。

19-3 popup 的位置与窗口绑定

menu.popup 默认在当前鼠标位置弹出,并关联当前聚焦窗口。你也可以显式指定窗口,或传入 xy 坐标。

// 指定窗口与坐标
menu.popup({
  window: win,
  x: 100,
  y: 200
})

在 macOS 上,若想让上下文菜单支持「写作工具」「自动填充」等系统项,需要把对应的 frame 传进去。这个 frame 来自 context-menu 事件的 params.frame

win.webContents.on('context-menu', (_event, params) => {
  if (params.isEditable) {
    menu.popup({ frame: params.frame })
  }
})

19-4 构建动态菜单

静态模板适合固定菜单,但上下文菜单常常要「看情况变」。比如右键一张图片,应该出现「保存图片」;右键普通文字,则出现「复制」。做法是在事件发生时临时 buildFromTemplate

// main.js(主进程)
const { BrowserWindow, Menu } = require('electron/main')

win.webContents.on('context-menu', (_event, params) => {
  const items = []

  if (params.linkURL) {
    items.push({ label: '复制链接地址', click: () => console.log(params.linkURL) })
  }
  if (params.hasImageContents) {
    items.push({ label: '保存图片', click: () => console.log('保存', params.srcURL) })
  }
  if (params.isEditable) {
    items.push({ role: 'copy' }, { role: 'paste' })
  }
  if (items.length === 0) return

  Menu.buildFromTemplate(items).popup()
})

动态菜单的好处是精准:用户看到的就是当前内容真正能做的事,而不是一长串永远用不上的选项。

19-5 在渲染进程内直接弹菜单

除了经由主进程,渲染进程自己也能构建并弹出菜单——但有个前提:菜单对象要在渲染进程里拿到。在 v43.2.0 中,旧版那种让渲染进程直接访问主进程模块的写法已被移除,更安全的做法是把菜单逻辑放在主进程,渲染进程只发信号。

如果你确实需要在渲染侧决定菜单内容,推荐让主进程 ipcMain.handle 收一个描述数组,再返回结果,菜单仍由主进程 popup。这样既灵活又不破坏进程边界。

// main.js(主进程)
const { ipcMain, Menu } = require('electron/main')

ipcMain.handle('build-menu', (event, items) => {
  Menu.buildFromTemplate(items).popup({
    window: require('electron/main').BrowserWindow.fromWebContents(event.sender)
  })
})

19-6 常见问题与排查

右键没反应,先确认是否调用了 event.preventDefault()。浏览器默认右键会弹出系统菜单或什么都不做,你的自定义菜单必须阻止默认行为才能显示。

菜单项点了没反应,检查 role 是否覆盖了 click——一旦设了 role,点击行为由系统接管,click 不生效。另外,menu.popup() 不带窗口参数时,会绑定当前聚焦窗口;若窗口未聚焦,弹出的菜单可能位置或归属不对。

19-7 结合应用菜单复用

上下文菜单不必从零写。你可以复用应用菜单里已经定义好的标准项,比如直接引用 editMenuviewMenu 这类 role,保证右键菜单和顶部菜单行为一致。

const { Menu } = require('electron/main')

const contextMenu = Menu.buildFromTemplate([
  { role: 'cut' },
  { role: 'copy' },
  { role: 'paste' },
  { type: 'separator' },
  { role: 'selectAll' }
])

这样复制、粘贴在右键和菜单栏里表现完全相同,用户不会困惑。对于自定义业务项,再单独 label + click 补充即可。保持一致性,是菜单设计的基本原则。

19-8 完整示例:文本编辑器右键菜单

把渲染进程触发、IPC、动态判断串起来,下面是一个文本编辑场景的右键菜单。它在可编辑区显示复制/粘贴,在普通区显示刷新,逻辑都在主进程集中管理。

// main.js(主进程)
const { app, BrowserWindow, ipcMain, Menu } = require('electron/main')
const path = require('node:path')

function createWindow() {
  const win = new BrowserWindow({
    webPreferences: { preload: path.join(__dirname, 'preload.js') }
  })
  ipcMain.on('ctx', (event, kind) => {
    const items = kind === 'editable'
      ? [{ role: 'copy' }, { role: 'paste' }, { role: 'selectAll' }]
      : [{ label: '刷新', click: () => win.reload() }]
    Menu.buildFromTemplate(items).popup({
      window: BrowserWindow.fromWebContents(event.sender)
    })
  })
  win.loadFile('index.html')
}
app.whenReady().then(createWindow)
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('ctx', {
  open: (kind) => ipcRenderer.send('ctx', kind)
})
// 渲染进程脚本
document.addEventListener('contextmenu', (e) => {
  e.preventDefault()
  const kind = e.target.matches('input, textarea') ? 'editable' : 'normal'
  window.ctx.open(kind)
})

这种结构把「何时弹、弹什么」都收敛到主进程,渲染进程只负责告诉主进程当前上下文类型,安全且好维护。

右键菜单的设计建议

右键菜单讲究「恰到好处」。它的内容应随右键位置变化,只给当前上下文真正用得到的操作。比如在图片上右键,给保存和复制图片;在文字上右键,给复制和搜索。给得太多反而干扰。

判断上下文靠 params 里的标志位,这是主进程监听方式的最大优势。若用渲染进程监听,则要靠 e.target 的标签或类名判断,灵活但要把判断逻辑写清楚。

另一个建议是保持与顶部菜单一致。能用 role 的标准项就别重写,用户从右键和菜单栏得到的体验应当统一。最后别忘了调用 preventDefault,否则系统默认菜单会盖掉你的自定义菜单。

一个易错点回顾

最后提醒一个高频错误:很多新手在渲染进程里监听 contextmenu,却忘记 preventDefault,结果自己的菜单一闪而过,系统默认菜单取而代之。一定要先阻止默认行为,再发 IPC 或构建菜单。

另一个易错点是把 roleclick 混用。一旦写了 role,点击事件就交给系统,你写的 click 不会执行。若需要自定义行为,就不要设 role,直接写 labelclick

小结

上下文菜单的核心是「监听右键 + 调用 menu.popup」。监听可在主进程(webContentscontext-menu 事件),也可在渲染进程(DOM 的 contextmenu 事件)经 IPC 触发。

判断该弹什么菜单,靠 params 里的 isEditablelinkURLsrcURL 等标志。需要随场景变化时,就在事件里临时构建模板。记住菜单在主进程构建、渲染进程只发信号,安全又清晰。