如何为同一应用的不同活动窗口设置不同的NSMenu?
要在Mac应用中为不同活动窗口配置不同的全局菜单,核心思路是监听窗口的激活状态变化,动态替换NSApplication的mainMenu属性。下面分步骤详细说明实现方式:
一、准备不同窗口对应的菜单实例
首先,为每个需要自定义菜单的窗口创建专属的NSMenu实例。可以在AppDelegate中定义懒加载的菜单对象,或者封装到窗口子类中。
示例代码(Swift):
import Cocoa @main class AppDelegate: NSObject, NSApplicationDelegate { var window1: NSWindow! var window2: NSWindow! var defaultMainMenu: NSMenu? // 窗口1的自定义菜单 lazy var window1Menu: NSMenu = { let menu = NSMenu(title: "Window 1 Menu") // 添加菜单项 let fileMenu = NSMenu(title: "File") fileMenu.addItem(withTitle: "Window 1 Save", action: #selector(saveWindow1), keyEquivalent: "s") menu.addItem(withTitle: "File", submenu: fileMenu) let editMenu = NSMenu(title: "Edit") editMenu.addItem(withTitle: "Window 1 Special Edit", action: #selector(specialEditWindow1), keyEquivalent: "e") menu.addItem(withTitle: "Edit", submenu: editMenu) return menu }() // 窗口2的自定义菜单 lazy var window2Menu: NSMenu = { let menu = NSMenu(title: "Window 2 Menu") let fileMenu = NSMenu(title: "File") fileMenu.addItem(withTitle: "Window 2 Export", action: #selector(exportWindow2), keyEquivalent: "e") menu.addItem(withTitle: "File", submenu: fileMenu) let viewMenu = NSMenu(title: "View") viewMenu.addItem(withTitle: "Window 2 Toggle Preview", action: #selector(togglePreviewWindow2), keyEquivalent: "p") menu.addItem(withTitle: "View", submenu: viewMenu) return menu }() // 菜单对应的动作方法 @objc func saveWindow1() { print("Saving Window 1 content") } @objc func specialEditWindow1() { print("Special edit for Window 1") } @objc func exportWindow2() { print("Exporting Window 2 content") } @objc func togglePreviewWindow2() { print("Toggling preview for Window 2") } }
二、监听窗口激活通知,动态切换菜单
在应用启动时,保存默认的全局菜单,并注册监听窗口成为主窗口的通知,当窗口激活时替换mainMenu。
在AppDelegate的applicationDidFinishLaunching方法中添加:
func applicationDidFinishLaunching(_ aNotification: Notification) { // 保存默认菜单,用于恢复 defaultMainMenu = NSApp.mainMenu // 创建两个测试窗口 window1 = NSWindow( contentRect: NSRect(x: 100, y: 100, width: 400, height: 300), styleMask: [.titled, .closable, .resizable, .miniaturizable], backing: .buffered, defer: false ) window1.title = "Window 1" window1.makeKeyAndOrderFront(nil) window2 = NSWindow( contentRect: NSRect(x: 600, y: 100, width: 400, height: 300), styleMask: [.titled, .closable, .resizable, .miniaturizable], backing: .buffered, defer: false ) window2.title = "Window 2" window2.makeKeyAndOrderFront(nil) // 注册窗口激活通知 NotificationCenter.default.addObserver( self, selector: #selector(windowDidBecomeMain(_:)), name: NSWindow.didBecomeMainNotification, object: nil ) // 注册窗口关闭通知,处理菜单恢复逻辑 NotificationCenter.default.addObserver( self, selector: #selector(windowWillClose(_:)), name: NSWindow.willCloseNotification, object: nil ) }
然后实现通知处理方法:
@objc func windowDidBecomeMain(_ notification: Notification) { guard let activeWindow = notification.object as? NSWindow else { return } // 根据当前激活窗口切换菜单 switch activeWindow { case window1: NSApp.mainMenu = window1Menu case window2: NSApp.mainMenu = window2Menu default: // 其他窗口恢复默认菜单 NSApp.mainMenu = defaultMainMenu } } @objc func windowWillClose(_ notification: Notification) { guard let closedWindow = notification.object as? NSWindow else { return } // 如果关闭的是当前主窗口,切换到其他可见窗口的菜单或默认菜单 if closedWindow.isMainWindow { if let nextMainWindow = NSApp.windows.first(where: { $0 != closedWindow && $0.isVisible }) { // 手动触发菜单切换 windowDidBecomeMain(Notification(name: NSWindow.didBecomeMainNotification, object: nextMainWindow)) } else { NSApp.mainMenu = defaultMainMenu } } }
三、更封装的实现方式(自定义NSWindow子类)
如果你的应用有大量自定义窗口,推荐将菜单逻辑封装到NSWindow子类中,让窗口自身管理菜单切换:
class CustomMenuWindow: NSWindow { // 为窗口绑定专属菜单 var customMenu: NSMenu? override func becomeMain() { super.becomeMain() // 当窗口成为主窗口时,替换应用菜单 if let menu = customMenu { NSApp.mainMenu = menu } } }
然后创建窗口时直接绑定菜单:
// 在applicationDidFinishLaunching中替换窗口创建代码 window1 = CustomMenuWindow( contentRect: NSRect(x: 100, y: 100, width: 400, height: 300), styleMask: [.titled, .closable, .resizable, .miniaturizable], backing: .buffered, defer: false ) window1.title = "Window 1" window1.customMenu = window1Menu window1.makeKeyAndOrderFront(nil) window2 = CustomMenuWindow( contentRect: NSRect(x: 600, y: 100, width: 400, height: 300), styleMask: [.titled, .closable, .resizable, .miniaturizable], backing: .buffered, defer: false ) window2.title = "Window 2" window2.customMenu = window2Menu window2.makeKeyAndOrderFront(nil)
这种方式让窗口和菜单的关联更清晰,代码更易维护。
注意事项
- 确保在应用退出前移除通知观察者,避免内存泄漏(Swift中如果用
default.addObserver(self, ...),在deinit中移除即可); - 当所有窗口都关闭时,记得恢复默认菜单,避免菜单为空;
- 如果你的应用支持多文档(NSDocument),可以结合
NSDocument的windowControllerDidLoadNib方法,为每个文档窗口绑定专属菜单。
内容的提问来源于stack exchange,提问作者sourav
相关产品推荐
相关产品推荐

