在 Electron 应用中显示上下文菜单的方法

日本語|English|中国语
| 5 min read
Author: masahiro-kondo masahiro-kondoの画像
Information

为了覆盖更广泛的受众,这篇文章已从日语翻译而来。
您可以在这里找到原始版本。

引言

#

去年将 Electron 更新到 v33.2.1 后,应用的上下文菜单突然无法显示了。

起初我以为是使用的库(后文会提到)没有跟上版本更新,于是进行了调查,但相关库的仓库里并没有类似的问题被提出。于是,我将 Electron 的版本降级后暂时搁置了这个问题。后来,Electron 发布了 v33.3.0,从发布说明里了解到,似乎是 Electron 本体的上下文菜单事件存在一个 bug。

升级到 Electron v33.3.0 后,上下文菜单能正常显示了。

在处理上下文菜单时,我使用了经典的 electron-context-menu

刚开始使用 Electron 时,这个库非常方便,所以就引入了。但因为 Electron 的升级问题,这个库曾多次出现无法运行的状况,我调试后也发布过 PR 来修复这些问题。渐渐地,每次出现问题都要调试让我感到很麻烦。

当然,Electron 本身提供了菜单的 API,如果使用 popup 方法的话,可以显示上下文菜单。而且,在 context-menu 事件的参数中,包含了各种上下文信息,大多数的逻辑处理都可以通过这些数据实现[1]。因此,我决定自己构建上下文菜单,以减少对第三方库的依赖。

Information

关于 Electron 随着版本升级的架构演变,我之前已经详细探讨过。Electron 的破坏性更新较多,而且需要处理的 Window 类和 View 类也在不断增加,对于库的维护者来说,确实是个不小的负担。

虽然 Electron 本身的进化存在一定原因,但作为依赖了全世界都在使用的活跃项目 Chrome,这是不可避免的。

Electron 的 Menu API

#

关于 Electron 的菜单 API 和相关事件,官方文档里有详细的说明:

通过菜单 API 和事件实现上下文菜单的代码大致如下[2]

import { app, BrowserWindow, Menu } from 'electron';

let mainWindow;

app.whenReady().then(() => {
  createWindow();

  mainWindow.webContents.on('context-menu', (e, params) => {  // 1
    const menuTemplate = buildMenuTemplate(params);           // 2
    const contextMenu = Menu.buildFromTemplate(menuTemplate); // 3
    contextMenu.popup({ window: mainWindow.webContents });    // 4
  });
});

function buildMenuTemplate(params) {
  const menuTemplate = [
    {
      label: 'menu1',
      click: () => { console.log('menu1 clicked'); }
    },
    {
      label: 'menu2',
      click: () => { console.log('menu2 clicked'); }
    },
  ];
  return menuTemplate;
}

function createWindow() {
  mainWindow = new BrowserWindow({
    // 属性设置
  });
  // 加载网站等
}
  1. 使用 webContents 属性处理上下文菜单的 context-menu 事件(此例中为 BrowserWindow)。params 参数对象中包含了与正在显示的内容相关的各种信息。
  2. 自定义的 buildMenuTemplate 函数,用来构建 MenuItem 对象的数组。可以根据 params 的值控制各菜单项的显示与隐藏。在实际应用中,这个函数的实现量通常是最多的。
  3. 使用 Menu.buildFromTemplate 方法生成上下文菜单。
  4. 使用上下文菜单的 popup 方法显示菜单。
Information

webContents 对象存在于以下类中:

  • BrowserWindow
  • BrowserView
  • WebContentsView

由于 BrowserView 已被弃用,因此其 webContents 对象也被标记为不推荐使用。此外,作为 WebContentsView 容器的 BaseWindow 并不包含 webContents 对象。可以参考以下文章了解其结构:

可视化 Electron WebContentsView 时代的应用结构

链接识别、选中文本获取

#

在 Electron 应用中,根据点击位置的内容动态显示上下文菜单非常重要(这是上下文的核心概念)。在我编写的应用中,链接点击和选中文本处理是一种非常常见的场景。

以下是右键点击链接时有效的菜单示例。是否为链接可以通过 params 对象中的 linkURL 属性是否有值来判断。

// 菜单模板数组的定义片段
    {
      label: '打开',
      click: () => { openLink(params.linkURL); },  // 1
      visible: params.linkURL                      // 2
    },
  1. 通过调用自定义的 openLink 函数来打开 params.linkURLopenLink 函数里可以使用 shell.openExternal 方法等来实现打开新窗口或在浏览器中打开链接的功能。
  2. 使用 visible 属性,当 params.linkURL 有值时显示菜单。

实际效果如下:
open link

以下是当选中文本时有效的菜单示例。这实现了一个基于选中文本的 Google 搜索功能。是否选中了文本,可以通过 params.selectionText 中是否包含有效值来判断。

// 菜单模板数组的定义片段
    {
      label: `使用 Google 搜索 '${params.selectionText}'`, // 1
      click: () => {                                        // 2
        const url = new URL('https://www.google.com/search');
        url.searchParams.set('q', params.selectionText);
        shell.openExternal(url.toString());
      },
      visible: params.selectionText.trim().length > 0      // 3
    },
  1. 在菜单中显示选中的文字。
  2. 实现 Google 搜索的功能。
  3. 只有在有选中文本时才显示菜单。

实际效果如下:
Google search

图片复制及其他

#

除了链接和选中文本之外,可能还需要实现将显示的图片或图片链接复制到剪贴板的功能。

图片复制功能可以用非常方便的 webContents.copyImageAt 方法实现。图片的 URL 信息则可以从 params.srcURL 中获取。

关键点在于通过 params.mediaType 属性来判断 visible 的值。

// 菜单模板数组的定义片段
    {
      label: '复制图片',
      click: () => { content.copyImageAt(params.x, params.y); },
      visible: params.mediaType === 'image'
    },
    {
      label: '复制图片 URL',
      click: () => { clipboard.writeText(params.srcURL); },
      visible: params.mediaType === 'image'
    },

实际效果如下:
copy image

最后

#

用了多年的 electron-context-menu 终于可以告别了。实现起来其实很简单。对于菜单内容而言,库所减少的代码量其实并没有菜单定义本身的代码量多,也就没有显得特别冗长。如果担心具体细节,可以提前将它拆分至单独的模块中。

需要注意的是,electron-context-menu 拥有自定义的 webContents 编辑功能,同时对于事件处理器等资源的释放也做得非常到位。目前继续使用它并没有问题。然而,考虑到可能会失去维护的风险,建议尽可能仅通过 Electron 的 API 来实现这样的功能。


  1. 我记得在早期的 Electron 中,显示上下文菜单非常麻烦。electron-context-menu 的开发初衷可能就是为了提供一个简便的 API。 ↩︎

  2. 为了支持 ESM 的代码实现。关于 Electron 应用中 ESM 的支持,请参阅《Electron v28 开始支持 ESM 的实践》。 ↩︎

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。