NW.js 菜单栏定制全指南:跨平台 Menubar 的创建、平台差异与最佳实践
【免费下载链接】nw.jsCall all Node.js modules directly from DOM/WebWorker and enable a new way of writing applications with all Web technologies.项目地址: https://gitcode.com/gh_mirrors/nw/nw.js
NW.js 允许应用直接通过nw.Menu与nw.WindowAPI 为窗口创建原生菜单栏(Menubar),从而在不引入任何第三方 UI 框架的情况下获得真正的系统级菜单体验。本文以 Customize Menubar 为主线,结合仓库源码与参考文档,系统讲解菜单栏的创建与挂载方式、Windows/Linux 与 macOS 的语义差异、createMacBuiltin内置菜单的定制技巧,以及多平台兼容的最佳实践。读完本文,你将能够在自己的 NW.js 应用中搭建一套"一处建模、处处可用"的菜单栏方案。
创建并设置 Menubar
使用type: 'menubar'创建菜单栏
与右键上下文菜单不同,菜单栏是常驻窗口顶部的一级菜单结构。创建一个菜单栏,只需在构造nw.Menu时传入type: 'menubar':
var your_menu = new nw.Menu({ type: 'menubar' });从源码实现看,type的取值会在创建时被严格校验:在 src/api/menu/menu.js 中,只有contextmenu与menubar两种取值被接受,其他取值会直接抛出TypeError('Invalid menu type: ...');若完全不传option,则默认按contextmenu处理。因此,菜单栏必须显式指定type: 'menubar'。
一级菜单项必须携带子菜单
菜单栏上的每个一级菜单项,都应当挂载一个子菜单(submenu)。纯文本、没有子菜单的菜单项在大多数平台上都无法提供任何交互,没有存在意义。下面的代码演示了如何组装一个完整的两级菜单栏:
// 创建二级子菜单 var submenu = new nw.Menu(); submenu.append(new nw.MenuItem({ label: 'Item A' })); submenu.append(new nw.MenuItem({ label: 'Item B' })); // 一级菜单项必须携带 submenu your_menu.append(new nw.MenuItem({ label: 'First Menu', submenu: submenu }));底层实现印证了这一要求:在 Windows/Linux 的视图层实现 src/api/menu/menu_views.cc 中,Menu::Append()会优先检查menu_item->submenu_,存在子菜单时调用menu_model_->AddSubMenu(...)生成可展开的菜单项,否则才按normal/checkbox/separator等类型生成普通项。菜单栏只有挂上带submenu的项,才会真正呈现下拉交互。
将菜单栏挂载到窗口
菜单栏创建完成后,通过设置窗口的menu属性即可显示:
nw.Window.get().menu = your_menu;win.menu是 Window 参考文档 中定义的读写属性:设置一个type为menubar的Menu对象即可挂载菜单栏;设置为null时,Windows 与 Linux 上会彻底移除菜单栏,macOS 上则会清空应用菜单。
挂载动作的底层由 src/api/nw_window_api.cc 中的NwCurrentWindowInternalSetMenuFunction完成:在 Linux/Windows 分支中,会新建MenuBarView并将其作为子视图加入BrowserView,随后调用menubar->UpdateMenu(menu->model())渲染菜单并绑定键盘快捷键(menu->UpdateKeys(...));而在 Mac 分支中则调用NWChangeAppMenu(menu)替换全局应用菜单。对应的移除逻辑(NwCurrentWindowInternalClearMenuFunction,见 src/api/nw_window_api.cc)在 Windows/Linux 上从布局中移除MenuBarView并解除快捷键,在 Mac 上通过NWChangeAppMenu(NULL)清除菜单。
有关Menu与WindowAPI 的完整细节,请分别参阅 Menu 参考文档 与 Window 参考文档。
平台差异:同一个菜单,不同的语义
不同平台对"窗口菜单"的定义截然不同,理解这些差异是写出跨平台应用的前提。
Windows 与 Linux:每个窗口各有一个菜单栏
在 Windows 与 Linux 上,菜单栏的行为完全一致:每个窗口都可以拥有自己的菜单栏,所有菜单栏都位于标题栏之下。
提示:全屏 / Kiosk 模式下的菜单栏在 Windows 与 Linux 上,进入全屏(fullscreen)或 kiosk 模式后,菜单栏依然显示在窗口顶部。若希望在全屏模式下彻底隐藏菜单,可将
win.menu设置为null,即可完全移除菜单栏。相关说明同样记录在 Window.md#winmenu。
平台实现上两者各有侧重:Linux 使用 GTK 原生控件,在 src/api/menu/menu_gtk.cc 中,type为menubar时创建gtk_menu_bar_new(),否则创建gtk_menu_new(),随后的Append/Insert/Remove均直接操作GtkMenuShell;Windows 则通过 Chromium 的 views 框架实现(src/api/menu/menu_views.cc),菜单最终交由MenuBarView渲染进窗口布局。
macOS:整个应用只有一个菜单
macOS 的菜单体系是全局的——无论应用打开多少个窗口,整个应用只能有一个菜单,即"应用菜单"(application menu),它挂在屏幕顶部的系统菜单栏上。许多常用快捷键(如Quit、Close、Copy)都依赖应用菜单的存在,因此移除应用菜单会同时破坏这些系统快捷键。
警告:0.13.0 起行为发生变化自 0.13.0 起,macOS 上菜单栏的行为与 0.12 时代已有显著差异,详见 从 0.12 迁移到 0.13 的说明。
NW.js 应用在 macOS 上默认会以一组"出厂菜单"启动,包含三个一级菜单:应用名(your-app-name)、Edit与Window。你可以通过menu.createMacBuiltin(appname)获取这套默认菜单,再按需增删:
var mb = new nw.Menu({type: "menubar"}); mb.createMacBuiltin("your-app-name"); // 对 mb 执行 append / insert / delete 操作,定制你自己的菜单 // 然后 ... nw.Window.get().menu = mb;这套内置菜单的完整构造逻辑在 src/api/menu/menu.js 中可以看到:createMacBuiltin会依次组装三个子菜单——应用菜单(包含 About、Hide、Hide Others、Show All、Quit,其中 Quit 绑定Cmd+Q)、Edit 菜单(Undo/Redo/Cut/Copy/Paste/Delete/Select All 及对应快捷键)和 Window 菜单(MinimizeCmd+M、CloseCmd+W、Bring All to Front)。菜单项通过 Cocoa 的selector:机制(如orderFrontStandardAboutPanel:、hide:、closeAllWindowsQuit:等)与系统行为挂钩。
createMacBuiltin还接受可选的第二个参数options,用于裁剪内置菜单:
mb.createMacBuiltin("your-app-name", { hideEdit: true, // 不填充 Edit 菜单 hideWindow: true // 不填充 Window 菜单 });对应地,0.12 时代的no-edit-menumanifest 字段已在 0.13 中废弃(见 迁移文档),请改用上述options控制。createMacBuiltin的完整签名与参数说明可参阅 Menu 参考文档。
注意:修正应用菜单标题应用菜单的第一项默认显示的是nwjs而不是你传入的
appname。要修正它,需要把nwjs.app/Contents/Resources/*.lproj/InfoPlist.strings中所有文件里的CFBundleName值从nwjs改为your-app-name。0.13 起修改的目标文件从Contents/Info.plist变更为上述各语言的InfoPlist.strings,详见 迁移说明。
最佳实践:让菜单栏在所有平台都"体面"地工作
综合上述平台差异,可以归纳出三条可落地的实践准则:
1. 只为主窗口设置菜单
Windows/Linux 上每个窗口都可以有一个菜单栏,而 macOS 整个应用只有一个应用菜单。因此最稳妥的做法是:只为你的主窗口设置菜单,并尽量避免在存在多个主窗口的场景下各自挂载窗口菜单,以免不同平台出现"菜单漂移"的割裂体验。
2. 按平台定制菜单结构
你完全可能希望为不同平台设计不同的菜单。NW.js 应用内可以直接使用 Node.js 的process.platform判断当前运行平台:
var isMac = (process.platform === 'darwin'); var mb = new nw.Menu({ type: 'menubar' }); if (isMac) { // macOS:以系统内置菜单为基底,再追加业务菜单 mb.createMacBuiltin('your-app-name', { hideEdit: false, hideWindow: false }); mb.append(new nw.MenuItem({ label: '业务菜单', submenu: buildBusinessSubmenu() })); } else { // Windows / Linux:直接构建完整菜单栏 mb.append(new nw.MenuItem({ label: 'File', submenu: buildFileSubmenu() })); mb.append(new nw.MenuItem({ label: 'Help', submenu: buildHelpSubmenu() })); } nw.Window.get().menu = mb;process.platform的取值说明可参考 Node.js 官方文档对process.platform的定义(Linux 返回'linux',Windows 返回'win32',macOS 返回'darwin')。按平台分支构建菜单后,再统一赋值给win.menu,即可用一份代码兼顾三种桌面环境。
3. 注意快捷键与菜单项细节
菜单项的key与modifiers属性同样存在平台差异:cmd在 Windows/Linux 上对应Windows键,在 macOS 上对应⌘键,super与command是cmd的别名(详见 MenuItem 参考文档)。设计快捷键时务必考虑跨平台映射关系。
另外,若菜单创建在会发生导航(navigation)的页面中,页面 reload 或跳转后菜单将失效——因为菜单对象与页面会被 JS 引擎一并垃圾回收以防止内存泄漏(见 Menu 参考文档 中的警告)。推荐将菜单创建逻辑放在背景页(background page)中执行,背景页与应用生命周期等长,可确保菜单长期有效。
深入:菜单栏 API 的源码级解读
菜单操作方法的完整集合
nw.Menu除了append之外,还提供insert(item, i)、remove(item)、removeAt(i)等运行时编辑方法,以及用于上下文菜单的popup(x, y)。这些 JS 方法最终通过nw.callObjectMethod走 IPC 到达原生层,在 src/api/menu/menu.cc 的Menu::Call()中按方法名分发:Append/Insert/Remove直接操作对应的MenuItem,Popup则额外做了页面缩放因子的换算(将文档坐标乘以zoom_factor映射到屏幕坐标),EnableShowEvent用于 macOS 上控制菜单show事件的开关。
值得注意的是,menu.items属性是只读的(在 menu.js 中,对items的 setter 会抛出Menu.items is immutable),菜单内容的变更一律通过append/insert/remove/removeAt完成。
macOS 的菜单即应用菜单
在 src/api/menu/menu_mac.mm 中,每个Menu在 macOS 上都对应一个NSMenu实例;当它作为菜单栏被挂载(win.menu = mb)时,NWChangeAppMenu()会将其设置为主应用菜单,替换默认的 NSApplication 主菜单。这也是 macOS 上"一个应用只有一个菜单"的根源——应用菜单是进程级的全局对象,与窗口数量无关。
参考文档速查
- Menu 参考文档:
Menu全部构造参数、items属性、append/insert/remove/removeAt/popup方法、createMacBuiltin详细签名 - MenuItem 参考文档:菜单项的全部属性(
label、icon、type、click、enabled、checked、submenu、key、modifiers) - Window 参考文档:
win.menu属性的读写语义 - 从 0.12 迁移到 0.13:macOS 默认菜单与
InfoPlist.strings修改方式的迁移说明
小结
在 NW.js 中定制菜单栏的关键在于"认知平台差异、复用统一 API":用new nw.Menu({ type: 'menubar' })创建结构、用win.menu完成挂载,Windows/Linux 上按窗口管理菜单栏、macOS 上依托createMacBuiltin定制全局应用菜单,再以process.platform分支处理差异化需求。理解nw_window_api.cc中SetMenuFunction与ClearMenuFunction的底层分工,也能帮助你在排查"菜单没显示""快捷键失效"等问题时快速定位症结。
【免费下载链接】nw.jsCall all Node.js modules directly from DOM/WebWorker and enable a new way of writing applications with all Web technologies.项目地址: https://gitcode.com/gh_mirrors/nw/nw.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考