news 2026/9/7 19:55:08

Electron 应用菜单(Application Menu)完整指南:顶层菜单构建、角色复用与窗口级定制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Electron 应用菜单(Application Menu)完整指南:顶层菜单构建、角色复用与窗口级定制

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的结构:

  1. 命令式 append:先new Menu(),再反复调用menu.append(menuItem)
  2. 模板式 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 的处理流程为:

  • 校验模板必须是数组,且每个元素合法——必须声明labelroletype三者的至少一个;
  • 对模板按定位关键字排序(见后文"定位规则");
  • 清理掉多余的分隔符(折叠相邻 separator、剔除首尾分隔符);
  • 逐个把普通对象构造成MenuItemappend到新菜单。

模板数组的校验与清理逻辑位于 lib/browser/api/menu.ts:areValidTemplateItems会拒绝既无label也无role且非separator的条目;removeExtraSeparators会把连续的分隔符合并为一条,并删除出现在菜单最前或最后的separator——这正是"菜单不能以分隔符开头/结尾"这种平台规范的来源。另外,模板条目上附加的任意自定义字段会原样成为对应MenuItem的属性,可用来携带业务数据。

构造菜单时请记住两条规则:除separator外的每个菜单项必须有标签(手动label或由role自动继承);normal是默认菜单项类型,带submenu的条目会被自动判为submenu类型,其余显式类型还包括checkboxradiopalette(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))→ 不创建默认菜单;
  • 应用显式传nullMenu.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 一节。文档还提示:角色字符串不区分大小写toggleDevToolstoggledevtoolsTOGGLEDEVTOOLS等价。

源码级拆解: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)的优先级是:appMethodwindowMethod(需聚焦窗口存在)→webContentsMethod(需聚焦 WebContents 存在)。而在 macOS 上,若角色是原生 action(nonNativeMacOSRole为假),则execute直接返回false交给系统处理。

下表中高频角色的默认快捷键来自roleList(注意源码以全小写形式定义、对外大小写不敏感):

role默认标签示例默认快捷键执行目标
undo/redoUndo / RedoCommandOrControl+Z;Windows 上 Redo 为Control+Y,其余Shift+CommandOrControl+ZWebContents
cut/copy/pasteCut / Copy / PasteCommandOrControl+X/C/V(不注册为全局加速键)WebContents
pasteAndMatchStylePaste and Match StylemacOSCmd+Option+Shift+V,其余Shift+CommandOrControl+VWebContents
selectAll/deleteSelect All / DeleteCommandOrControl+A;Delete 无默认快捷键WebContents
reload/forceReloadReload / Force ReloadCmdOrCtrl+R/Shift+CmdOrCtrl+RWebContents
toggleDevToolsToggle Developer ToolsmacOSAlt+Command+I,其余Ctrl+Shift+IWebContents
resetZoom/zoomIn/zoomOutActual Size / Zoom In / Zoom OutCommandOrControl+0/CommandOrControl+Plus/CommandOrControl+-WebContents
togglefullscreenToggle Full ScreenmacOSControl+Command+F,其余F11聚焦窗口
minimizeMinimizeCommandOrControl+M聚焦窗口
closeClose Window(macOS) / CloseCommandOrControl+W聚焦窗口
quitQuit(macOS/Linux) / Exit(Windows)Windows 无默认快捷键,其余CommandOrControl+Qapp 全局
aboutAbout / About {app}Windows/Linux 上弹出自定义 About 面板
hide/hideOthers(macOS)Hide {app} / Hide OthersCommand+H/Command+Alt+H原生 action

两个可直接引用的工程结论:

  1. 能匹配到标准角色的菜单项,优先用role而非手写clicklabelaccelerator在声明role后可省略,Electron 会按平台填入恰当默认值;源码 menu.ts 也印证了这点——命令的加速键优先取自定义accelerator,缺省时才通过getDefaultRoleAccelerator()回退到角色默认值。原生角色实现的观感与行为通常好于手写,例如copy/cut/pasteregisterAccelerator: false是为了避免在菜单不可见时抢走系统快捷键。
  2. 角色命中后的启用态是动态计算的。源码 menu.ts 的_isCommandIdEnabled会针对minimizetogglefullscreenclose检查聚焦窗口的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()itemsmenu-will-show/menu-will-close事件等丰富能力,均可在同一 Menu 模型上复用。

小结

围绕 Electron 的顶层应用菜单,本文覆盖了一条完整的知识链:从"每应用一份、平台展示各异"的定位,到Menu.buildFromTemplate的构建与模板校验;从默认菜单的手动重建(含 macOS 专属的appMenu首项规范),到用fileMenu/editMenu/viewMenu/windowMenu批量复用标准子菜单;再到 Windows/Linux 上用win.setMenu/win.removeMenu做窗口级覆盖,以及role标签、快捷键与启用态在源码中的落点。

实践中的三条建议:

  1. 优先角色:凡匹配标准 role 的菜单项一律用role,让 Electron 处理平台差异与快捷键;
  2. 尽早设置:若不需要默认菜单,在应用启动早期调用Menu.setApplicationMenu(null)显式禁用,避免用户看到与业务不符的内置 File/Edit/View/Window;
  3. 分平台校验:编写菜单代码后分别在 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 19:55:02

现在热门的AI论文写作工具有哪些品牌?选对工具少走弯路

每到期末、毕业答辩、课题申报阶段,很多学子都会深陷论文难题:选题毫无头绪、搭建大纲逻辑混乱、正文撰写耗时长、参考文献格式出错、查重重复率偏高、AIGC检测告警、本校论文排版标准复杂。依靠纯人工从零开始撰写、一遍遍修改格式和降重,常…

作者头像 李华
网站建设 2026/9/7 19:55:00

Linux (ARM64 / Jetson) 挂载 exFAT 移动硬盘排障与离线安装指南

Linux (ARM64 / Jetson) 挂载 exFAT 移动硬盘排障与离线安装指南 1. 故障现象与原因分析 故障现象: 图形界面挂载外接移动硬盘时弹出报错 Error mounting /dev/sda1: unknown filesystem type exfat;终端执行 mount 提示 mount: unknown filesystem ty…

作者头像 李华
网站建设 2026/9/7 19:54:56

论文降AI率实战:8款工具测评与从68%到17%的操作全记录

又到了一年一度的毕业季,朋友圈里哀嚎最多的不是查重率,而是“AI率”。学校用的AIGC检测系统越来越精明,我见过不少初稿写得挺顺的同学,结果一查,AI率直接飙到70%以上,辛辛苦苦写的综述被标注成“疑似AI生成…

作者头像 李华
网站建设 2026/9/7 19:54:52

HarmonyOS 7.0 API26 AppStartup 入口恢复:通知冷启动后页面状态如何补齐

HarmonyOS 7.0 API26 AppStartup 入口恢复:通知冷启动后页面状态如何补齐 这篇只拆一个具体点:AppStartup 入口恢复。版本边界先放前面:下面的写法面向 HarmonyOS 7.0 / API 26。工程里如果还在混用旧 SDK、旧模拟器镜像或旧设备系统&#xf…

作者头像 李华
网站建设 2026/9/7 19:54:50

开源ChatGPT VSCode插件:源码解析与实战排查指南

1. 这个开源插件到底解决什么问题1.1 不再来回切换浏览器的开发流说实话,最打断写代码心流的动作不是报错本身,而是为处理一个小问题不得不切走编辑器再去翻文档、查对话记录。你需要解释一段不熟悉的代码、写一组边界测试、整理一条 commit message&…

作者头像 李华
网站建设 2026/9/7 19:53:28

QtScrcpy:手机投屏电脑免费搞定,键鼠直接操控还不用 root

QtScrcpy:手机投屏电脑免费搞定,键鼠直接操控还不用 root 【免费下载链接】QtScrcpy Android real-time display control software 项目地址: https://gitcode.com/GitHub_Trending/qt/QtScrcpy 手机屏幕太小、调试演示要来回低头抬头&#xff1f…

作者头像 李华