ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Electron Menu 类深度解析:应用菜单与上下文菜单的完整实现指南

Electron Menu 类深度解析:应用菜单与上下文菜单的完整实现指南 Electron Menu 类深度解析应用菜单与上下文菜单的完整实现指南【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron本文围绕 Electron 的 Menu API 展开系统讲解Menu类的创建、应用菜单设置、上下文菜单弹出、菜单事件与实例属性等全部核心能力并结合 Electron 仓库中的 C 原生实现electron_api_menu.cc、TypeScript 绑定层menu.ts与测试用例api-menu-spec.ts剖析从 JS 模板到原生菜单项的完整调用链帮助开发者在跨平台桌面应用中正确构建并动态管理菜单。一、Menu 类概览与跨平台表现差异Menu类用于创建应用菜单application menu和上下文菜单context menu仅在主进程可用Process: Main。不同操作系统下菜单的呈现方式存在本质差异Windows / Linux菜单在视觉上与 Chromium 一致由 Views 框架渲染Menu.setApplicationMenu()设置的菜单会作为每个窗口的顶部菜单栏menu barmacOS菜单是原生 NSMenu应用菜单显示在系统级菜单栏中。注意Electron 的内置类不允许在用户代码中继承子类化。更多背景可参考 FAQ。new Menu()创建一个空菜单实例。更完整的菜单编写指南如各 role 的用法、平台差异建议见 menus 教程本文聚焦于Menu类本身的 API 与实现原理。二、静态方法setApplicationMenu / getApplicationMenuMenu.setApplicationMenu(menu)menuMenu | null在 macOS 上该调用把menu设为系统应用菜单在 Windows 和 Linux 上menu会被设置为每个窗口的顶部菜单栏。助记符Windows / Linux 特有在顶级菜单项名称中使用指定哪个字母应生成快捷键。例如文件菜单命名为File会生成Alt-F快捷键用于打开对应菜单该字母在按钮标签上带下划线本身不显示。若要转义字符需写两个例如File会在按钮标签上显示File。传入null的效果抑制默认菜单。在 Windows 和 Linux 上还有额外效果——移除窗口上的菜单栏。若应用从未设置过菜单Electron 会自动创建包含File、Edit、View、Window等标准项的默认菜单。源码实现细节JS 层的 Menu.setApplicationMenu 做了类型校验后区分平台处理macOS 分支先调用menu._callMenuWillShow()预激活菜单触发所有子菜单项的初始化钩子再通过原生绑定bindings.setApplicationMenu(menu)安装到 NSMenuWindows / Linux 分支遍历BaseWindow.getAllWindows()并逐个调用w.setMenu(menu)即应用菜单实际被展开为每个窗口的菜单栏——这也解释了为什么在这两个平台上菜单是每窗口一份的。同时该函数会调用 default-menu.ts 中的setApplicationMenuWasSet()打标一旦打过标应用启动时setDefaultApplicationMenu()就不再自动构建默认菜单——这就是手动设置后默认菜单消失的机制。默认菜单的模板本身非常简单仅为若干 role 的组合setDefaultApplicationMenuconst template: Electron.MenuItemConstructorOptions[] [ ...(isMac ? [{ role: appMenu }] : []), { role: fileMenu }, { role: editMenu }, { role: viewMenu }, { role: windowMenu } ];Menu.getApplicationMenu()返回Menu | null已设置的应用菜单或null。注意返回的Menu实例不支持动态增删菜单项append/insert仅对新构建的菜单有意义但实例属性仍可动态修改。从 JS 实现看getApplicationMenu只是返回模块级变量applicationMenumenu.ts#L204而原生模型层对已安装为应用菜单的 model 不再接受结构性变更。Menu.sendActionToFirstResponder(action)macOSactionstring向应用的第一响应者first responder发送action用于模拟 macOS 默认菜单行为。通常更推荐做法是给MenuItem设置role属性由 role 自动映射到正确的原生动作。源码细节该静态方法仅在 macOS 编译分支中注册到模块导出electron_api_menu.cc#L367-L371 中#if BUILDFLAG(IS_MAC)守卫setApplicationMenu与sendActionToFirstResponder两个原生绑定JS 层 menu.ts#L206 直接将其挂到Menu构造函数上。三、Menu.buildFromTemplate(template)从模板构建菜单Menu.buildFromTemplate(template) - template ([MenuItemConstructorOptions](https://link.gitcode.com/i/eb4c4d2d97c67e49423a80d3331d8e23#new-menuitemoptions) | [MenuItem](https://link.gitcode.com/i/eb4c4d2d97c67e49423a80d3331d8e23))[] Returns: Menutemplate通常是构造 MenuItem 的options数组元素既可以是选项对象也可以是已构建好的MenuItem实例。你还可以在模板元素上附加任意额外字段这些字段会成为所构建菜单项的属性——这在模板中携带自定义数据如状态标识、回调标记时非常实用。模板验证与预处理流程JS 实现Menu.buildFromTemplate并非直接逐项 append而是经过三步处理验证areValidTemplateItems模板必须是数组且每个元素必须至少拥有label、role之一或type separator否则抛出TypeError排序sortTemplate调用 menu-utils.ts 中的sortMenuItems按菜单项的id/before/after/beforeGroupContaining/afterGroupContaining属性做拓扑排序让多个上下文菜单来源可以声明我的项应该插在某 id 项之前/之后或我的整组应该放在某组之前排序对子菜单递归生效清理分隔线removeExtraSeparators折叠相邻的 separator并移除首尾的 separatorvisible false的项跳过检查。测试用例spec/api-menu-spec.ts覆盖了这些行为例如空模板元素、null项、非数组模板均会抛错before/after排序有专门的 describe 块验证。一个典型的模板示例结合 MenuItemConstructorOptions 的常用字段const { Menu, app } require(electron); const template [ ...(process.platform darwin ? [{ role: appMenu }] : []), { role: fileMenu }, { label: 编辑, submenu: [ { role: undo }, { role: redo }, { type: separator }, { role: cut }, { role: copy }, { role: paste }, { type: separator }, { role: selectAll } ] }, { label: 视图, submenu: [ { role: reload }, { role: toggleDevTools }, { type: separator }, { role: resetZoom }, { role: zoomIn }, { role: zoomOut }, { type: separator }, { role: togglefullscreen } ] }, { role: windowMenu }, { label: 帮助, submenu: [ { label: 关于本应用, click: async () { const { dialog } require(electron); await dialog.showMessageBox({ type: info }); } } ] } ]; const menu Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu);菜单项类型的分发逻辑menu.append(item)内部通过 insertItemByType 按item.type分发到不同原生插入方法对应 C 层 FillObjectTemplate 注册的方法type原生方法说明normal/headerinsertItem(pos, commandId, label)普通项checkboxinsertCheckItem(pos, commandId, label)勾选框点击自动翻转checkedradioinsertRadioItem(pos, commandId, label, groupId)单选项同组互斥separatorinsertSeparator(pos)分隔线submenu/paletteinsertSubMenu(pos, commandId, label, submenu)子菜单Menu可嵌套在MenuItem.submenu上其中radio类型有一段值得注意的实现generateGroupIdmenu.ts#L274-L288会在分隔线范围内查找相邻的 radio 项复用其groupId从而把同一菜单中分隔线隔开的多段 radio 项自动归入同一互斥组同时通过Object.defineProperty重定义checkedsetter保证设置某项为选中时自动取消同组其他项的选中状态。四、实例方法popup 与 closePopupmenu.popup([options])optionsObject (optional)windowBaseWindow (optional) - 默认为当前聚焦窗口。frameWebFrameMain (optional) - 如果希望 Writing ToolsmacOS等 OS 级功能正确工作应提供相关 frame。通常应取WebContents的context-menu事件中的params.frame或focusedFrame属性。xnumber (optional) - 默认为当前鼠标光标位置。声明了y时声明x为必填。ynumber (optional) - 默认为当前鼠标光标位置。声明了x时声明y为必填。positioningItemnumber (optional)macOS- 指定坐标处应位于鼠标光标下方的菜单项索引默认 -1。sourceTypestring (optional)WindowsLinux- 应映射为context-menu事件提供的menuSourceType。不建议手动设置该值只提供从其他 API 收到的值或保持undefined。可取none、mouse、keyboard、touch、touchMenu、longPress、longTap、touchHandle、stylus、adjustSelection、adjustSelectionReset。callbackFunction (optional) - 菜单关闭时调用。在BaseWindow中将该菜单弹为上下文菜单。更多细节见 Context Menu 指南。JS 层默认值与窗口选择逻辑Menu.prototype.popupx/y缺省为 -1表示跟随鼠标positioningItem缺省 -1sourceType缺省mouse若window参数不在BaseWindow.getAllWindows()中则回退到聚焦窗口、再回退到第一个窗口一个窗口都不存在时抛出Error: Cannot open Menu without a BaseWindow present。原生层定位逻辑MenuViews::PopupAtWindows / Linuxx -1 || y -1时取display::Screen::Get()-GetCursorScreenPoint()否则以窗口内容区原点为基准换算为屏幕坐标随后用views::MenuRunner带CONTEXT_MENU | HAS_MNEMONICS标志运行菜单并把sourceType透传给RunMenuAt——这正是sourceType选项影响系统级行为如键盘可访问性标注的落点。menu.closePopup([window])windowBaseWindow (optional) - 默认为聚焦窗口。关闭window中的上下文菜单。实现上若传入BaseWindow实例则调用closePopupAt(window.id)否则传 -1使原生层 ClosePopupAt 关闭该菜单打开的所有menu runner——因为一个Menu可能同时在多个窗口弹出。五、项管理append / insert / getMenuItemById 与 items 属性menu.append(menuItem)-menuItemMenuItem把menuItem追加到菜单末尾。实现即insert(this.getItemCount(), item)menu.ts#L158-L160。menu.insert(pos, menuItem)-posInteger、menuItemMenuItem插入到pos位置。JS 层会校验项类型必须是MenuItem否则TypeError: Invalid item且pos不能小于 0 或大于当前项总数否则RangeError插入后同步设置toolTip、icon、role、自定义typepalette/header、macOSbadge并把menu反向挂到该项上。menu.getMenuItemById(id)-idstring返回MenuItem | null即指定id的项。实现menu.ts#L145-L156是递归搜索先查当前层items找不到则逐个进入子菜单继续查找因此可以定位任意深度子菜单中的项。menu.itemsmenu对象还具有实例属性menu.items一个MenuItem[]数组包含该菜单的所有项。每个Menu由多个MenuItem组成每个MenuItem又可以通过其submenu属性嵌套一个Menu——这构成递归的树形结构。JS 侧的this.items在_init中初始化menu.ts#L15-L19与 C 模型层的commandId一一对应保证两端结构同步。六、事件menu-will-show / menu-will-close由new Menu创建或Menu.buildFromTemplate返回的对象会发出以下事件部分事件仅限特定操作系统文档中会标注Event: menu-will-show返回eventEvent。当menu.popup()被调用时发出更准确地说在菜单即将展示的原生钩子处。Event: menu-will-close返回eventEvent。当弹出菜单被手动关闭或被menu.closePopup()关闭时发出。实现链路C 的 ElectronMenuModel 是ui::SimpleMenuModel的派生类原生菜单展示/关闭时回调到 Menu::OnMenuWillShow / OnMenuWillClose其中OnMenuWillShow先把自身压入keep_alive_SelfKeepAlive防止弹出中的菜单被 GC再Emit(menu-will-show)。OnMenuWillShow还经由 ui::SimpleMenuModel::Delegate 回调 触发 JS 侧_menuWillShow负责确保每个 radio 组至少有一项被选中menu.ts#L96-L102——若组内无选中项则默认选中第一项。测试中通过 spec/api-menu-spec.ts#L855-L863 的once(menu, menu-will-show)/once(menu, menu-will-close)验证了两个事件的触发。七、架构剖析JS Menu 与原生 ElectronMenuModel 的双层模型从源码结构看Electron 菜单采用JS 侧持有状态 C 侧持有原生模型的双层架构理解它有助于解释文档中的种种限制JS 层 (lib/browser/api/menu.ts) C 层 (shell/browser) ────────────────────────────── ───────────────────────────── Menu 实例 electron::api::Menu ├─ items: MenuItem[] └─ model_: ElectronMenuModel ├─ commandsMap: { commandId - item } (继承 ui::SimpleMenuModel) └─ groupsMap: { groupId - radio[] } └─ 平台实现: │ commandId 为 JS 为每项 ├─ macOS: NSMenu (electron_menu_controller.mm) │ 分配的数字 ID └─ Win/Linux: views::MenuRunner ▼ (electron_api_menu_views.cc) C 通过 Delegate 接口反查 JS IsCommandIdChecked / GetLabelForCommandId / GetAcceleratorForCommandIdWithParams / ExecuteCommand ... │ 通过 gin_helper::CallMethod 调用 │ JS 的 _isCommandIdChecked 等下划线方法 ▼ 菜单项的 label、icon、accelerator、enabled 等实际都保存在 JS MenuItem 上 原生模型每次展示时按需查询。关键机制electron_api_menu.cc#L122-L199属性按需拉取ui::SimpleMenuModel::Delegate的每个查询方法IsCommandIdChecked、IsCommandIdEnabled、GetLabelForCommandId、GetIconForCommandId、GetAcceleratorForCommandIdWithParams等都通过gin_helper::CallMethod回调 JS 中对应_xxx下划线方法再由 JS 从commandsMap[id]上读取MenuItem的真实属性。因此MenuItem的label/enabled/accelerator等属性可以在运行时动态修改并立即生效这正是文档提示实例属性可动态修改的实现基础。窗口感知的 enabledJS 侧 _isCommandIdEnabled 对特殊 role 做了焦点窗口联动——minimize取决于聚焦窗口是否isMinimizable()togglefullscreen取决于isFullScreenable()close取决于isClosable()。命令执行用户点击菜单项时C ExecuteCommand 回调 JS_executeCommand最终执行command.click(event, focusedWindow, focusedWebContents)menu.ts#L89-L94——这解释了click回调中window可能是undefined的原因以聚焦窗口为准无窗口时为 undefined。内存管理弹出中的菜单由 keep_alive_SelfKeepAlive 保持存活popup的callback则通过 BindSelfToClosure 持有 JS 引用直至回调执行防止回调触发前菜单被 GC。ElectronMenuModel额外维护了原生侧才有的元数据映射tooltip、role、自定义类型palette/header、macOS badge 与 SharingItemelectron_menu_model.cc、electron_api_menu.cc#L26-L64这些在insert时由 JS 层按平台条件调用setToolTip/setRole/setCustomType/setBadge写入menu.ts#L177-L185。八、实战结合 context-menu 事件构建动态上下文菜单综合上述 API一个标准的上下文菜单实现如下webContents的context-menu事件签名见 web-contents.mdconst { Menu } require(electron); function onContextMenu(e, params) { const menu Menu.buildFromTemplate([ { label: 复制, role: copy }, { label: 粘贴, role: paste, enabled: !!params.misspelledWord.length false }, ...(params.misspelledWord ? [{ type: separator }, ...params.misspelledWord.slice(0, 3).map(word ({ label: word, click: () params.replaceMisspelling(word) }))] : []), { type: separator }, { label: 刷新页面, accelerator: CmdOrCtrlR, click: () e.reload() } ]); // frame 与 sourceType 直接透传 context-menu 事件参数 // 保证 macOS Writing Tools 等 OS 级功能正常工作 menu.popup({ window: e.getOwnerWindow(), frame: params.frame, sourceType: params.menuSourceType }); }要点复述template元素上可附加自定义字段构建后成为 MenuItem 属性便于携带逻辑标记相邻与首尾的separator会被自动折叠/移除模板里无需精确控制分隔线数量通过idbefore/after可让多个菜单来源如插件声明相对位置构建时自动拓扑排序getMenuItemById支持跨子菜单递归查找适合在菜单弹出后按 id 动态更新项状态menu-will-show事件是更新时机。九、参考路径汇总内容路径Menu API 文档本文主体docs/api/menu.mdMenuItem API 文档docs/api/menu-item.mdmenus 教程roles 详解docs/tutorial/menus.md上下文菜单指南docs/tutorial/context-menu.md默认菜单构建lib/browser/default-menu.tsJS 绑定层lib/browser/api/menu.ts模板排序工具lib/browser/api/menu-utils.tsC 核心实现shell/browser/api/electron_api_menu.cc菜单模型shell/browser/ui/electron_menu_model.ccViews 平台弹出实现shell/browser/api/electron_api_menu_views.cc单元测试spec/api-menu-spec.ts【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表