You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在NSToolbar中实现类似Mail.app的带下拉菜单工具栏按钮

实现类似Mail.app的带下拉菜单的NSToolbar按钮

我太懂你要的效果了——就是那种点击按钮左侧区域触发主操作,点右侧下拉箭头弹出额外菜单的工具栏组件对吧?之前你试的两种方法确实都有局限性:用menuFormRepresentation只会在溢出时显示菜单,直接用NSPopUpButton又没法区分左右交互。下面是我实际项目里用过的解决方案,完美实现你要的效果:

核心思路

自定义一个NSToolbarItem,内部包含一个自定义NSView,这个View里拆成两个独立控件:

  • 左侧:普通NSButton,负责主操作(比如Mail里直接标记为默认旗标)
  • 右侧:仅显示下拉箭头的NSPopUpButton,负责弹出菜单(比如选择不同颜色的旗标)

这样就能完全分开左右区域的交互逻辑,同时保持和系统工具栏一致的外观。

分步实现

1. 自定义拆分按钮视图

先创建一个SplitToolbarButtonView,用来承载两个控件并处理布局:

import Cocoa

class SplitToolbarButtonView: NSView {
    // 主操作按钮
    let mainButton = NSButton()
    // 下拉菜单按钮(仅显示箭头)
    let menuButton = NSPopUpButton()
    
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        setupViews()
        setupLayout()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupViews()
        setupLayout()
    }
    
    private func setupViews() {
        // 配置主按钮:和系统工具栏按钮风格一致
        mainButton.bezelStyle = .texturedRounded
        mainButton.title = "Flag"
        mainButton.target = self
        mainButton.action = #selector(mainButtonTapped(_:))
        
        // 配置下拉按钮:隐藏边框,只显示下拉箭头
        menuButton.bezelStyle = .rounded
        menuButton.isBordered = false
        menuButton.behavior = .pullDown
        // 去掉默认的菜单标题,只留箭头
        menuButton.title = ""
        // 用系统默认的操作图标作为下拉箭头
        menuButton.image = NSImage(named: NSImageNameActionTemplate)
        
        // 添加子视图
        addSubview(mainButton)
        addSubview(menuButton)
    }
    
    private func setupLayout() {
        mainButton.translatesAutoresizingMaskIntoConstraints = false
        menuButton.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            // 主按钮占满左侧,和父视图顶部、底部、左侧对齐
            mainButton.topAnchor.constraint(equalTo: topAnchor),
            mainButton.bottomAnchor.constraint(equalTo: bottomAnchor),
            mainButton.leadingAnchor.constraint(equalTo: leadingAnchor),
            // 下拉按钮和主按钮右侧对齐,宽度固定为工具栏箭头标准宽度
            menuButton.leadingAnchor.constraint(equalTo: mainButton.trailingAnchor),
            menuButton.topAnchor.constraint(equalTo: topAnchor),
            menuButton.bottomAnchor.constraint(equalTo: bottomAnchor),
            menuButton.widthAnchor.constraint(equalToConstant: 24),
            // 下拉按钮右侧和父视图对齐
            menuButton.trailingAnchor.constraint(equalTo: trailingAnchor)
        ])
    }
    
    // 主按钮点击事件,替换成你自己的业务逻辑
    @objc func mainButtonTapped(_ sender: NSButton) {
        print("执行主操作:标记默认旗标")
    }
}

2. 自定义NSToolbarItem

接下来创建自定义的NSToolbarItem,把上面的视图设置为它的自定义视图:

class SplitToolbarItem: NSToolbarItem {
    override init(itemIdentifier: NSToolbarItem.Identifier) {
        super.init(itemIdentifier: itemIdentifier)
        setupCustomView()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupCustomView()
    }
    
    private func setupCustomView() {
        let splitView = SplitToolbarButtonView()
        // 给下拉按钮配置菜单选项
        let menu = NSMenu()
        menu.addItem(withTitle: "红色旗标", action: #selector(flagColorSelected(_:)), keyEquivalent: "")
        menu.addItem(withTitle: "橙色旗标", action: #selector(flagColorSelected(_:)), keyEquivalent: "")
        menu.addItem(withTitle: "黄色旗标", action: #selector(flagColorSelected(_:)), keyEquivalent: "")
        splitView.menuButton.menu = menu
        splitView.menuButton.target = self
        
        // 设置自定义视图为工具栏项的内容
        self.view = splitView
    }
    
    @objc func flagColorSelected(_ sender: NSMenuItem) {
        print("选择了旗标颜色:\(sender.title)")
    }
}

3. 添加到NSToolbar

最后在你的工具栏代理里注册并添加这个自定义项:

// 在你的WindowController或ViewController中实现工具栏代理
extension ViewController: NSToolbarDelegate {
    func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return [.init("SplitFlagButton"), .space, .flexibleSpace]
    }
    
    func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return [.init("SplitFlagButton"), .flexibleSpace]
    }
    
    func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
        if itemIdentifier == .init("SplitFlagButton") {
            return SplitToolbarItem(itemIdentifier: itemIdentifier)
        }
        return nil
    }
}

额外优化点

  • 外观适配:可以监听工具栏的显示模式(紧凑/扩展),动态调整按钮的大小和样式,和系统控件完全对齐
  • 高亮同步:监听主按钮的高亮状态,同步到下拉按钮,保证整体视觉一致性
  • 菜单美化:给菜单选项添加对应颜色的图标,和Mail.app的旗标菜单保持一致

这样实现出来的效果就和Mail.app的Flag按钮完全一致了:点左侧直接触发默认操作,点右侧箭头弹出所有可选菜单,既解决了溢出模式才显示菜单的问题,也区分了左右区域的不同交互。

内容的提问来源于stack exchange,提问作者marcprux

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:12:17