如何在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
相关产品推荐
相关产品推荐

