macOS顶部和上下文菜单NSMenu
macOS顶部和上下文菜单NSMenu

macOS顶部和上下文菜单NSMenu

NSMenu 是 macOS App 中用于构建顶栏菜单(菜单栏)或上下文菜单的类,是 AppKit 的重要组成部分。

在SwiftUI中,可以通过commands实现菜单的配置。

例如,Mac应用顶部的菜单,都是 NSMenu 和 NSMenuItem 构成。

基本结构为:

NSApplication.shared.mainMenu
├── NSMenuItem("轻压图片") → submenu: NSMenu(...)
│   ├── NSMenuItem("关于轻压图片") ...
│   ├── NSMenuItem("服务")
│   └── ...
├── NSMenuItem("文件") → submenu: NSMenu(...)
│   ├── NSMenuItem("新建窗口")
│   ├── NSMenuItem("关闭")
│   └── ...
└── ...

每个 NSMenuItem 都可以有一个 submenu(子菜单)。

基本用法

let mainMenu = NSMenu()

let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)

// 创建“App”菜单
let appMenu = NSMenu()
let quitItem = NSMenuItem(title: "退出 App", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
appMenu.addItem(quitItem)
appMenuItem.submenu = appMenu

// 设置为主菜单
NSApplication.shared.mainMenu = mainMenu

常用方法

1、NSMenu:菜单容器,用于顶部菜单 / 子菜单。

2、NSMenuItem:单个菜单项,类似“打开文件”。

3、submenu:子菜单,常见于多级结构,菜单项下的菜单。

4、action:方法选择器,#selector(someFunc),点击后的方法。

5、keyEquivalent:快捷键,”q” 表示 Cmd+Q,可为空。

在SwiftUI项目中添加菜单(推荐AppDelegate)

import SwiftUI

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        setupMenu()
        // 监听 App 激活状态
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(reapplyMenu),
            name: NSApplication.didBecomeActiveNotification,
            object: nil
        )
    }
    
    @objc func reapplyMenu() {
        setupMenu()
    }
    
    @objc func showAbout() {
        print("点击关于")
    }
    
    func setupMenu() {
        let mainMenu = NSMenu()
        let appMenuItem = NSMenuItem()
        mainMenu.addItem(appMenuItem)
        
        let appMenu = NSMenu(title: "App")
        appMenuItem.submenu = appMenu
        
        appMenu.addItem(withTitle: "关于", action: #selector(showAbout), keyEquivalent: "")
        appMenu.addItem(NSMenuItem.separator())
        appMenu.addItem(withTitle: "退出", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
        
        NSApplication.shared.mainMenu = mainMenu
    }
}

@main
struct ImageSlimApp: App {
    
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

在实际测试中发现,只有首次打开应用才会显示自定义的菜单。关闭应用,重新打开后,会还原回默认的菜单,这个问题在后续实际应用到NSMenu时,再进行处理。

同理,如果是在视图中通过按钮调用自定义菜单的功能:

struct ContentView: View {
    var body: some View {
        VStack {
            Color.blue
        }
        .padding()
        .contextMenu {
            Button("设置菜单") {
                let mainMenu = NSMenu()

                let appMenuItem = NSMenuItem()
                mainMenu.addItem(appMenuItem)

                // 创建“App”菜单
                let appMenu = NSMenu()
                let quitItem = NSMenuItem(title: "退出 App", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
                appMenu.addItem(quitItem)
                appMenuItem.submenu = appMenu

                // 设置为主菜单
                NSApplication.shared.mainMenu = mainMenu

            }
        }
    }
}

也会在关闭并重新打开应用后,恢复默认的菜单栏。

也可能是 NSApplication.shared.mainMenu 仅限于临时修改菜单栏。

上下文菜单

NSMenu.popUp(positioning:at:in:) 是 macOS 中的一个非常实用的方法,用于手动弹出一个上下文菜单(弹出菜单),可以在任意位置展示它,而不依赖系统默认事件(比如 right-click)。

func popUp(positioning item: NSMenuItem?, at location: NSPoint, in view: NSView?)

参数解析:

item:可选:要高亮的菜单项,一般传 nil。

location:要弹出的位置(以 view 的坐标为基准)。

view:位置基准视图;如果为 nil,以屏幕为基准。

import SwiftUI
import AppKit

class MenuHandler: NSObject {
    @objc func menuAction(_ sender: NSMenuItem) {
        print("点击了菜单项:\(sender.title)")
    }
}

struct ContentView: View {
    let handler = MenuHandler()
    var body: some View {
        VStack {
            Button("显示菜单") {
                let menu = NSMenu()
                
                let itemA = NSMenuItem(title: "选项 A", action: #selector(MenuHandler.menuAction(_:)), keyEquivalent: "")
                itemA.target = handler // 明确指定响应对象
                menu.addItem(itemA)
                
                let itemB = NSMenuItem(title: "选项 B", action: #selector(MenuHandler.menuAction(_:)), keyEquivalent: "")
                menu.addItem(itemB)
                menu.addItem(NSMenuItem.separator())
                menu.addItem(withTitle: "退出", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
                
                // 获取当前点击的窗口位置
                if let window = NSApp.keyWindow,
                   let contentView = window.contentView {
                    let mouseLocation = NSEvent.mouseLocation
                    let locationInWindow = window.convertPoint(fromScreen: mouseLocation)
                    let locationInView = contentView.convert(locationInWindow, from: nil)
                    
                    // 弹出菜单
                    menu.popUp(positioning: nil, at: locationInView, in: contentView)
                }
            }
        }
        .padding()
    }
}

需要注意的是,菜单项如果不设置响应的action对象,也就是NSMenuItem().target的话,菜单项就会变成不可点击的灰色。

在SwiftUI中,还可以通过contextMenu更简单的实现右键菜单的效果:

import SwiftUI
import AppKit

struct ContentView: View {
    var body: some View {
        VStack {
            Color.blue
        }
        .padding()
        .contextMenu {
            Button("选项 A") {
                print("点击了 A")
            }
            Button("选项 B") {
                print("点击了 B")
            }
            Divider()
            Button("退出") {
                NSApp.terminate(nil)
            }
        }
    }
}

总结

通过NSApplication.shared.mainMenu设置顶部菜单,NSMenuItem(title:action:keyEquivalent:)设置单个菜单。

在SwiftUI中添加菜单时,推荐使用 .commands 替代 NSMenu。同样,如果需要设置上下文菜单,推荐使用 contextMenu 替代NSMenu。

扩展知识

NSMenu和commands是什么关系?

commands 是 SwiftUI 的语法糖,最终还是生成 AppKit 的 NSMenu 菜单结构。

SwiftUI的.commands {}对应NSMenu + NSMenuItem,.CommandMenu()对应NSMenu 的一个子菜单。

SwiftUI 只是用声明式语法,自动创建了 NSMenu 结构,背后依然是 AppKit 驱动菜单栏。

例如:

// SwiftUI 中添加菜单栏
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands {
            CommandMenu("工具") {
                Button("刷新数据") {
                    print("刷新数据")
                }
            }
        }
    }
}

背后做了什么?

1、创建一个 NSMenu,标题是 “工具”。

2、创建一个 NSMenuItem,标题是 “刷新数据”,action 是调用绑定的 Swift 方法。

3、注册为主菜单(mainMenu)。

可以在运行 App 时:

⌘点击菜单栏“工具”。

看到菜单项“刷新数据”。

触发 SwiftUI 中绑定的操作。

使用场景

简单固定菜单,使用SwiftUI 的 .commands 最方便。

动态生成菜单,需要使用NSMenu 手动控制。

上下文菜单,使用.contextMenu {}(SwiftUI)或 NSMenu.popUp(AppKit)。

多级嵌套、图标菜单、禁用控制,使用 NSMenu。

相关文章

1、SwiftUI macOS的commands菜单栏修饰符:https://fangjunyu.com/2025/06/19/swiftui-macos%e7%9a%84commands%e8%8f%9c%e5%8d%95%e6%a0%8f%e4%bf%ae%e9%a5%b0%e7%ac%a6/

2、SwiftUI长按手势弹出上下文菜单contextMenu:https://fangjunyu.com/2024/12/10/swift%e4%b8%8a%e4%b8%8b%e6%96%87%e8%8f%9c%e5%8d%95contextmenu/

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注