Electron 应用菜单(Application Menu)完整指南:顶层菜单构建、角色复用与窗口级定制
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
导读:Electron 的每个应用有且仅有一份顶层应用菜单,它是桌面应用体验的"指挥中心",在不同平台上有完全不同的呈现形态。本文以 Electron 仓库中 docs/tutorial/application-menu.md 为主干,结合 Menu API 参考 与 lib/browser/api/menu.ts 等真实源码实现,完整讲解应用菜单的构建、默认菜单的手动重建、标准 OS 角色(role)的批量复用,以及 Windows/Linux 上按窗口覆盖菜单的进阶技巧,帮助你写出真正跨平台、原生观感的菜单代码。
应用菜单的定位:全应用唯一的一份顶层菜单
Electron 中,"应用菜单(Application Menu)"指的是应用顶层的菜单栏,每个应用同一时刻只有一份:
- 在 macOS 上,这份菜单显示在系统全局菜单栏(System Menu Bar)中,即屏幕顶部始终存在的那条菜单,即使应用窗口不在前台也可见。
- 在 Windows 和 Linux 上,这份菜单显示在每个 BaseWindow 窗口内部的最上方。
它的设置入口是 Menu 类 的静态方法Menu.setApplicationMenu(menu):把一份构建好的Menu实例传进去即可。该方法在源码 lib/browser/api/menu.ts 中的行为是分平台的:
- 在 macOS(
darwin)上,把菜单交给原生绑定bindings.setApplicationMenu(menu),走系统菜单栏;若传入null则直接返回、不设置。 - 在 Windows / Linux 上,
Menu.setApplicationMenu(menu)实际是把菜单逐一设置到每一个已存在的窗口上:windows.map((w) => w.setMenu(menu))。
需要特别强调一条硬性约束:应用菜单模板数组中,每个顶层数组元素必须是带submenu的子菜单(顶层菜单栏只支持"顶级菜单 → 下拉子菜单"这种两级结构,且子菜单不能为空数组)。另外,Electron 内置类不允许被用户代码继承子类化,直接使用 API 即可。
[!NOTE] 菜单的视觉实现也因平台而异:Windows / Linux 上与 Chromium 观感接近,macOS 上则是真正的原生菜单。这也意味着同一个
Menu实例跨平台表现略有差异,符合"原生优先"的设计哲学。
构建应用菜单的两条路径
Electron 提供两种向菜单中添加菜单项的方式,二者都基于MenuItem与嵌套submenu的结构:
- 命令式 append:先
new Menu(),再反复调用menu.append(menuItem)。 - 模板式 buildFromTemplate:调用静态方法
Menu.buildFromTemplate(template),用一个数组一次性描述整份菜单。
模板方式消除了逐条 append 的样板代码,是文档与实践中最常见的用法。以模板方式为例:
const { Menu } = require('electron/main') const menu = Menu.buildFromTemplate([ { label: 'Menu', submenu: [ { label: 'Hello' }, { type: 'separator' }, { label: 'Electron', type: 'checkbox', checked: true } ] } ]) Menu.setApplicationMenu(menu)模板数组既可以是MenuItemConstructorOptions(普通对象),也可以直接放入已实例化的MenuItem。源码中 Menu.buildFromTemplate 的处理流程为:
- 校验模板必须是数组,且每个元素合法——必须声明
label、role、type三者的至少一个; - 对模板按定位关键字排序(见后文"定位规则");
- 清理掉多余的分隔符(折叠相邻 separator、剔除首尾分隔符);
- 逐个把普通对象构造成
MenuItem并append到新菜单。
模板数组的校验与清理逻辑位于 lib/browser/api/menu.ts:areValidTemplateItems会拒绝既无label也无role且非separator的条目;removeExtraSeparators会把连续的分隔符合并为一条,并删除出现在菜单最前或最后的separator——这正是"菜单不能以分隔符开头/结尾"这种平台规范的来源。另外,模板条目上附加的任意自定义字段会原样成为对应MenuItem的属性,可用来携带业务数据。
构造菜单时请记住两条规则:除separator外的每个菜单项必须有标签(手动label或由role自动继承);normal是默认菜单项类型,带submenu的条目会被自动判为submenu类型,其余显式类型还包括checkbox、radio、palette(macOS 14+ 横向排列的调色板式子菜单)与header(macOS 14+ 分区标题)。
手把手:用 role 手动重建 Electron 默认菜单
如果从不调用Menu.setApplicationMenu,Electron 会自动为应用创建一份默认菜单。其实际实现位于 lib/browser/default-menu.ts,只是一份由四个"标准角色子菜单"拼成的精简模板(macOS 额外含appMenu):
const template = [ ...(isMac ? [{ role: 'appMenu' }] : []), { role: 'fileMenu' }, { role: 'editMenu' }, { role: 'viewMenu' }, { role: 'windowMenu' } ];下面这份完整示例等价于在代码里手动重建该默认菜单,可当作"从零开始的完整应用菜单"学习蓝本。它充分利用了平台条件展开(isMac判断)来同时适配 macOS 与 Windows/Linux:
const { shell } = require('electron/common') const { app, Menu } = require('electron/main') const isMac = process.platform === 'darwin' const template = [ // { role: 'appMenu' } —— 仅在 macOS 上存在:以应用名称为标签的首个子菜单 ...(isMac ? [{ label: app.name, submenu: [ { role: 'about' }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit' } ] }] : []), // { role: 'fileMenu' } { label: 'File', submenu: [ isMac ? { role: 'close' } : { role: 'quit' } ] }, // { role: 'editMenu' } { label: 'Edit', submenu: [ { role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, ...(isMac ? [ { role: 'pasteAndMatchStyle' }, { role: 'delete' }, { role: 'selectAll' }, { type: 'separator' }, { label: 'Speech', submenu: [ { role: 'startSpeaking' }, { role: 'stopSpeaking' } ] } ] : [ { role: 'delete' }, { type: 'separator' }, { role: 'selectAll' } ]) ] }, // { role: 'viewMenu' } { label: 'View', submenu: [ { role: 'reload' }, { role: 'forceReload' }, { role: 'toggleDevTools' }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' } ] }, // { role: 'windowMenu' } { label: 'Window', submenu: [ { role: 'minimize' }, { role: 'zoom' }, ...(isMac ? [ { type: 'separator' }, { role: 'front' }, { type: 'separator' }, { role: 'window' } ] : [ { role: 'close' } ]) ] }, { role: 'help', submenu: [ { label: 'Learn More', click: async () => { const { shell } = require('electron') await shell.openExternal('https://electronjs.org') } } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu)这段代码的关键点值得逐条消化:
- macOS 第一个子菜单固定以应用名为标签。系统要求应用菜单的第一项永远展示你的应用名称,一般用
appMenu角色(或手动构造label: app.name的子菜单)来填充它。若平台不是 macOS,这段会被展开为空数组从而整体跳过。 isMac条件展开是平台差异化惯用法。例如 Edit 菜单在 macOS 上多了pasteAndMatchStyle、Speech 子菜单等;File 菜单在 macOS 上是close(关闭窗口),Windows/Linux 上是quit(退出应用);Window 菜单在 macOS 上额外包含front(Bring All to Front)。这与 menu-item-roles.ts 中 editmenu/windowmenu/filemenu 的默认定义 完全一致,可见平台差异是 Electron 原生规范的一部分,需要开发者显式表达。learn more里的shell.openExternal展示了一个自定义click回调的标准写法:点击该菜单项时通过系统浏览器打开外部网址。
[!IMPORTANT] 在 macOS 上,应用菜单的第一个子菜单永远以你的应用名称为标签。一般建议通过条件性地加入
appMenu角色菜单项来填充它,而不是依赖手写标签——因为系统会强制改写标签文本。
默认菜单的自动创建由"是否显式设置过"控制
自动创建默认菜单并不是无条件的。源码 lib/browser/default-menu.ts 中用模块级标志位applicationMenuWasSet做守卫:一旦任何代码路径调用了Menu.setApplicationMenu(无论传入菜单还是null),该标志即被置位,此后默认菜单不再被创建。对应测试位于 spec/api-app-spec.ts,覆盖三个场景:
- 应用从未设置菜单 → 自动创建默认菜单;
- 应用设置了自定义菜单(测试桩见 spec/fixtures/api/default-menu/main.js,
new Menu()后Menu.setApplicationMenu(expectedMenu))→ 不创建默认菜单; - 应用显式传
null(Menu.setApplicationMenu(null))→ 同样不创建默认菜单。
换句话说,若你想彻底禁用默认菜单,最直接的方式是启动早期调用一次Menu.setApplicationMenu(null)。这与 API 文档行为一致:在 Windows/Linux 上,传入null还会额外移除窗口的菜单栏。
菜单查询与"运行时不可增删"限制
设置后可用Menu.getApplicationMenu()取回当前应用菜单(未设置则返回null)。需要留意的是,取回的实例不支持动态增删菜单项,但实例属性仍可修改。macOS 上还有静态方法Menu.sendActionToFirstResponder(action)(_macOS_),用于模拟默认菜单行为,日常场景更推荐使用role而非手动触发 action。
角色复用:用子菜单角色拼装标准菜单栏
逐条手写每个子菜单非常冗长。若只是想复用 Electron 内置的标准子菜单,可以使用一组"子菜单级角色(default menu roles)"。把上一节的完整模板压缩后如下:
const { shell } = require('electron/common') const { app, Menu } = require('electron/main') const template = [ ...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : []), { role: 'fileMenu' }, { role: 'editMenu' }, { role: 'viewMenu' }, { role: 'windowMenu' }, { role: 'help', submenu: [ { label: 'Learn More', click: async () => { const { shell } = require('electron') await shell.openExternal('https://electronjs.org') } } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu)四种核心子菜单角色的行为(参见 角色表 与源码 menu-item-roles.ts):
appMenu(_macOS_)—— 整份默认应用菜单(About、Services、Hide、Quit 等),标签自动取app.name;fileMenu—— File 菜单(Close / Quit,随平台切换);editMenu—— Edit 菜单(Undo、Copy、Paste 等,macOS 额外包含 Substitutions、Speech 子菜单);viewMenu—— View 菜单(Reload、Toggle Developer Tools、缩放与全屏);windowMenu—— Window 菜单(Minimize、Zoom 等,macOS 额外含 Bring All to Front)。
注意help并不是fileMenu那样的"带默认内容的角色",它只提供顶层 "Help" 菜单框架:在 macOS 上它会获得内置的菜单搜索栏,但要正常工作你必须自己向它的submenu中添加条目。
[!TIP] 若要查看每一类 role 可用的具体取值,参见 MenuItem roles 一节。文档还提示:角色字符串不区分大小写,
toggleDevTools、toggledevtools、TOGGLEDEVTOOLS等价。
源码级拆解:role 究竟做了什么
菜单项指定role之后,标签、快捷键、点击行为多数由 Electron 替你补齐,其定义集中在 lib/browser/api/menu-item-roles.ts 的roleList中。每个 role 大致携带以下信息:
label:默认标签,部分为惰性 getter(如about返回About ${app.name}、quit在 Windows 上返回Exit);accelerator:默认快捷键字符串;appMethod/windowMethod/webContentsMethod:三种可能的执行目标,分别作用于 app 全局、聚焦窗口(BaseWindow)、聚焦的 WebContents;nonNativeMacOSRole:标记该角色并非 macOS 原生 action,需要 Electron 手动执行;其余角色在 macOS 上直接由系统 AppKit 处理。
执行函数execute(role, focusedWindow, focusedWebContents)的优先级是:appMethod→windowMethod(需聚焦窗口存在)→webContentsMethod(需聚焦 WebContents 存在)。而在 macOS 上,若角色是原生 action(nonNativeMacOSRole为假),则execute直接返回false交给系统处理。
下表中高频角色的默认快捷键来自roleList(注意源码以全小写形式定义、对外大小写不敏感):
| role | 默认标签示例 | 默认快捷键 | 执行目标 |
|---|---|---|---|
undo/redo | Undo / Redo | CommandOrControl+Z;Windows 上 Redo 为Control+Y,其余Shift+CommandOrControl+Z | WebContents |
cut/copy/paste | Cut / Copy / Paste | CommandOrControl+X/C/V(不注册为全局加速键) | WebContents |
pasteAndMatchStyle | Paste and Match Style | macOSCmd+Option+Shift+V,其余Shift+CommandOrControl+V | WebContents |
selectAll/delete | Select All / Delete | CommandOrControl+A;Delete 无默认快捷键 | WebContents |
reload/forceReload | Reload / Force Reload | CmdOrCtrl+R/Shift+CmdOrCtrl+R | WebContents |
toggleDevTools | Toggle Developer Tools | macOSAlt+Command+I,其余Ctrl+Shift+I | WebContents |
resetZoom/zoomIn/zoomOut | Actual Size / Zoom In / Zoom Out | CommandOrControl+0/CommandOrControl+Plus/CommandOrControl+- | WebContents |
togglefullscreen | Toggle Full Screen | macOSControl+Command+F,其余F11 | 聚焦窗口 |
minimize | Minimize | CommandOrControl+M | 聚焦窗口 |
close | Close Window(macOS) / Close | CommandOrControl+W | 聚焦窗口 |
quit | Quit(macOS/Linux) / Exit(Windows) | Windows 无默认快捷键,其余CommandOrControl+Q | app 全局 |
about | About / About {app} | 无 | Windows/Linux 上弹出自定义 About 面板 |
hide/hideOthers(macOS) | Hide {app} / Hide Others | Command+H/Command+Alt+H | 原生 action |
两个可直接引用的工程结论:
- 能匹配到标准角色的菜单项,优先用
role而非手写click。label与accelerator在声明role后可省略,Electron 会按平台填入恰当默认值;源码 menu.ts 也印证了这点——命令的加速键优先取自定义accelerator,缺省时才通过getDefaultRoleAccelerator()回退到角色默认值。原生角色实现的观感与行为通常好于手写,例如copy/cut/paste的registerAccelerator: false是为了避免在菜单不可见时抢走系统快捷键。 - 角色命中后的启用态是动态计算的。源码 menu.ts 的
_isCommandIdEnabled会针对minimize、togglefullscreen、close检查聚焦窗口的isMinimizable()/isFullScreenable()/isClosable(),窗口不支持时对应菜单项自动置灰——这就是原生行为的体现。
若需要自定义click回调,其签名为click(event, focusedWindow, focusedWebContents),与源码 menu.ts 的_executeCommand保持一致:它先从BaseWindow.getFocusedWindow()取聚焦窗口,再调用command.click(event, focusedWindow, webContents.getFocusedWebContents())。
Windows / Linux 专属:按窗口覆盖应用菜单
此节内容仅适用于 Windows 与 Linux。由于在 Windows/Linux 上"应用菜单"物理上存在于每个BaseWindow窗口内,因此你完全可以用窗口方法做更细粒度的覆盖——让不同窗口显示不同菜单栏。
设置窗口专属菜单的完整示例:
const { BrowserWindow, Menu } = require('electron/main') const win = new BrowserWindow() const menu = Menu.buildFromTemplate([ { label: 'my custom menu', submenu: [ { role: 'copy' }, { role: 'paste' } ] } ]) win.setMenu(menu)注意代码中的顺序:先创建BrowserWindow,再构建Menu,最后调用窗口方法win.setMenu(menu)(_Linux_ _Windows_)。实际 API 定义在 BaseWindow 上(BrowserWindow继承自BaseWindow),它把传入的Menu直接设为该窗口的菜单栏。
配套的清除能力有以下几种,可按需选用:
win.removeMenu()(_Linux_ _Windows_)—— 移除该窗口的菜单栏;win.setMenu(null)—— 同样可以去掉窗口菜单;Menu.setApplicationMenu(null)—— 移除所有窗口的菜单栏(仅限 Windows/Linux),因为此前已说明该方法在非 macOS 上会广播给每个窗口;- 若只是不想让菜单栏抢焦点,可配合
autoHideMenuBar窗口选项与win.setMenuBarVisibility(visible)在隐藏/显示之间切换。
[!TIP] 面向 macOS 开发时注意,macOS 的菜单位于全局系统菜单栏,不支持
win.setMenu这种窗口级覆盖;这正是 Menu.setApplicationMenu 的实现按process.platform === 'darwin'分叉的根本原因。
进阶:模板中控制菜单项位置
菜单模板并非只能按数组顺序排列。Electron 支持通过id与定位属性让模板定义顺序与最终展示顺序解耦,适用于动态拼接菜单、多个模块贡献菜单项的场景(规则细节见 Menus 指南的 Programmatic item positioning,排序实现见 lib/browser/api/menu-utils.ts):
before: ['id']/after: ['id']—— 插入到指定id项之前/之后,并把本项归入目标项的"分组";若引用项不存在则追加到末尾。beforeGroupContaining: ['id']/afterGroupContaining: ['id']—— 把本项所在的整个分组(以分隔符为界)移动到指定项所在分组的前/后。id—— 配合上述属性使用的定位锚点;菜单构建后可用menu.getMenuItemById(id)按 id 找回菜单项(会递归搜索子菜单,见 menu.ts)。
例如[{ id: '1', label: 'one', after: ['3'] }, { id: '2', label: 'two', before: ['1'] }, { id: '3', label: 'three' }]最终会呈现为three → two → one的顺序。构建流程先经 menu-utils 的排序逻辑 重排模板,再进入前面提到的合法性校验与分隔符清理。默认(不写定位属性)时仍按模板原顺序插入。
顺带一提,模板条目若含有icon(配合nativeImage使用)、sublabel(macOS 14.4+ 的副标题)、toolTip(macOS 悬停提示)等附加属性,会分别映射到对应原生能力;Menu层面还提供popup()(弹出上下文菜单)、closePopup()、items、menu-will-show/menu-will-close事件等丰富能力,均可在同一 Menu 模型上复用。
小结
围绕 Electron 的顶层应用菜单,本文覆盖了一条完整的知识链:从"每应用一份、平台展示各异"的定位,到Menu.buildFromTemplate的构建与模板校验;从默认菜单的手动重建(含 macOS 专属的appMenu首项规范),到用fileMenu/editMenu/viewMenu/windowMenu批量复用标准子菜单;再到 Windows/Linux 上用win.setMenu/win.removeMenu做窗口级覆盖,以及role标签、快捷键与启用态在源码中的落点。
实践中的三条建议:
- 优先角色:凡匹配标准 role 的菜单项一律用
role,让 Electron 处理平台差异与快捷键; - 尽早设置:若不需要默认菜单,在应用启动早期调用
Menu.setApplicationMenu(null)显式禁用,避免用户看到与业务不符的内置 File/Edit/View/Window; - 分平台校验:编写菜单代码后分别在 macOS 与 Windows/Linux 上跑一遍,重点检查首个子菜单是否应用名、Edit/Window 子菜单的项目差异是否如期出现。
想继续深挖可参考仓库中的 Menus 总指南、Menu API 参考、MenuItem 参考 以及相关测试 spec/api-menu-spec.ts 与 spec/api-app-spec.ts。
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考