如何以编程方式将自定义 NSToolbarItem 添加到现有工具栏

How to add a custom NSToolbarItem to an existing toolbar programmatically

我很难将自定义 NSToolbarItem 添加到我现有的工具栏。

NSToolbar 是在 NSWindowController 中创建的,然后我有一个以编程方式填充工具栏项的函数,代码为:

public func populateFileToolbarItem(_ toolbar: NSToolbar) -> Void{
    let itemId = NSToolbarItem.Identifier("FILE_OPEN")
    let index = toolbar.items.count
    var toolbarItem: NSToolbarItem
    toolbarItem = NSToolbarItem(itemIdentifier: itemId)
    toolbarItem.label = String("File")
    toolbarItem.paletteLabel = String("Open File")
    toolbarItem.toolTip = String("Open file to be handled")
    toolbarItem.tag = index
    toolbarItem.target = self
    toolbarItem.isEnabled = true
    toolbarItem.action = #selector(browseFile)
    toolbarItem.image = NSImage.init(named:NSImage.folderName)
    toolbar.insertItem(withItemIdentifier: itemId, at: index)
}

然后我调用这个函数将工具栏项添加到windowController中现有的工具栏

.......
  populateFileToolbarItem((self.window?.toolbar)!)
  self.window?.toolbar?.insertItem(withItemIdentifier: NSToolbarItem.Identifier.flexibleSpace, at: (self.window?.toolbar?.items.count)!)
  self.window?.toolbar?.insertItem(withItemIdentifier: NSToolbarItem.Identifier.print, at: (self.window?.toolbar?.items.count)!)
  print("after toolbaritems were inserted into toolbar. \(String(describing: self.window?.toolbar?.items.count))") 
......

控制台打印显示,只有两个工具栏项被添加到工具栏。

.......
after toolbaritems were inserted into toolbar. Optional(2)

并且工具栏中没有显示自定义项。

哪位有经验的,请指教!

对于工具栏中的 add/remove 个项目,您需要工具栏委托:NSToolbarDelegate。

这是我正在使用的实现模板(可能比您想要的多)。

用于创建各种类型的工具栏项的样板代码:


struct ToolbarIdentifiers {
    static let mainToolbar = NSToolbar.Identifier(stringLiteral: "MainToolbar")
    static let navGroupItem = NSToolbarItem.Identifier(rawValue: "NavGroupToolbarItem")
    static let shareItem = NSToolbarItem.Identifier(rawValue: "ShareToolBarItem")
    static let addItem = NSToolbarItem.Identifier(rawValue: "AddToolbarItem")
    static let statusItem = NSToolbarItem.Identifier(rawValue: "StatusToolbarItem")
    static let filterItem = NSToolbarItem.Identifier(rawValue: "FilterToolbarItem")
    static let sortItem = NSToolbarItem.Identifier(rawValue: "SortToolbarItem")
    static let cloudUploadItem = NSToolbarItem.Identifier(rawValue: "UploadToolbarItem")
    static let cloudDownloadItem = NSToolbarItem.Identifier(rawValue: "DownloadToolbarItem")
    static let leftButtonItem = NSToolbarItem.Identifier(rawValue: "leftButtonToolbarItem")
    static let rightButtonItem = NSToolbarItem.Identifier(rawValue: "rightButtonToolbarItem")
    static let hideShowItem = NSToolbarItem.Identifier(rawValue: "hideShowToolbarItem")
}

// Base toolbar item type, extended for segmented controls, buttons, etc.
struct ToolbarItem {
    let identifier: NSToolbarItem.Identifier
    let label: String
    let paletteLabel: String
    let tag: ToolbarTag
    let image: NSImage?
    let width: CGFloat
    let height: CGFloat
    let action: Selector?
    weak var target: AnyObject?
    var menuItem: NSMenuItem? = nil // Needs to be plugged in after App has launched.
    let group: [ToolbarItem]

    init(_ identifier: NSToolbarItem.Identifier, label: String = "", tag: ToolbarTag = .separator, image: NSImage? = nil,
         width: CGFloat = 38.0, height: CGFloat = 28.0,
         action: Selector? = nil, target: AnyObject? = nil, group: [ToolbarItem] = [], paletteLabel: String = "") {
        self.identifier = identifier
        self.label = label
        self.paletteLabel = paletteLabel
        self.tag = tag
        self.width = width
        self.height = height
        self.image = image
        self.action = action
        self.target = target
        self.group = group
    }
}
// Image button -- creates NSToolbarItem
extension ToolbarItem {
    func imageButton() -> NSToolbarItem {
        let item = NSToolbarItem(itemIdentifier: identifier)
        item.label = label
        item.paletteLabel = label
        item.menuFormRepresentation = menuItem // Need this for text-only to work
        item.tag = tag.rawValue
        let button = NSButton(image: image!, target: target, action: action)
        button.widthAnchor.constraint(equalToConstant: width).isActive = true
        button.heightAnchor.constraint(equalToConstant: height).isActive = true
        button.title = ""
        button.imageScaling = .scaleProportionallyDown
        button.bezelStyle = .texturedRounded
        button.tag = tag.rawValue
        button.focusRingType = .none
        item.view = button
        return item
    }
}
// Segmented control -- creates NSToolbarItemGroup containing multiple instances of NSToolbarItem
extension ToolbarItem {
    func segmentedControl() -> NSToolbarItemGroup {
        let itemGroup = NSToolbarItemGroup(itemIdentifier: identifier)
        let control = NSSegmentedControl(frame: NSRect(x: 0, y: 0, width: width, height: height))
        control.segmentStyle = .texturedSquare
        control.trackingMode = .momentary
        control.segmentCount = group.count
        control.focusRingType = .none
        control.tag = tag.rawValue

        var items = [NSToolbarItem]()
        var iSeg = 0
        for segment in group {
            let item = NSToolbarItem(itemIdentifier: segment.identifier)
            items.append(item)
            item.label = segment.label
            item.tag = segment.tag.rawValue
            item.action = action
            item.target = target
            control.action = segment.action // button & container send to separate handlers
            control.target = segment.target
            control.setImage(segment.image, forSegment: iSeg)
            control.setImageScaling(.scaleProportionallyDown, forSegment: iSeg)
            control.setWidth(segment.width, forSegment: iSeg)
            control.setTag(segment.tag.rawValue, forSegment: iSeg)
            iSeg += 1
        }
        itemGroup.paletteLabel = paletteLabel
        itemGroup.subitems = items
        itemGroup.view = control
        return itemGroup
    }
}
// Text field -- creates NSToolbarItem containing NSTextField
extension ToolbarItem {
    func textfieldItem() -> NSToolbarItem {
        let item = NSToolbarItem(itemIdentifier: identifier)
        item.label = ""
        item.paletteLabel = label
        item.tag = tag.rawValue
        let field = NSTextField(string: label)
        field.widthAnchor.constraint(equalToConstant: width).isActive = true
        field.heightAnchor.constraint(equalToConstant: height).isActive = true
        field.tag = tag.rawValue
        field.isSelectable = false
        item.view = field
        return item
    }
}
// Menu item -- creates an empty NSMenuItem so that user can click on the label
// definitely a work-around till we implement the menus
extension ToolbarItem {
    mutating func createMenuItem(_ action: Selector) {
        let item = NSMenuItem()
        item.action = action
        item.target = target
        item.title = label
        item.tag = tag.rawValue
        self.menuItem = item
    }
}
/*
 * Create specialized toolbar items with graphics, labels, actions, etc
 * Encapsulates implementation-specific details in code, because the table-driven version was hard to read.
 */
struct InitializeToolbar {
}
extension InitializeToolbar {
    static func navGroupItem(_ action: Selector, segmentAction: Selector, target: AnyObject) -> ToolbarItem {
        var group = [ToolbarItem]()
        group.append(ToolbarItem(NSToolbarItem.Identifier(rawValue: "BackToolbarItem"), label: "Prev", tag: .navPrev,
                                 image: NSImage(named: NSImage.goBackTemplateName), action: segmentAction, target: target))
        group.append(ToolbarItem(NSToolbarItem.Identifier(rawValue: "FwdToolbarItem"), label: "Next", tag: .navNext,
                                 image: NSImage(named: NSImage.goForwardTemplateName), action: segmentAction, target: target))
        let item = ToolbarItem(ToolbarIdentifiers.navGroupItem, tag: .navGroup, width: 85, height: 28,
                               action: action, target: target, group: group, paletteLabel: "Navigation")
        return item
    }
}
extension InitializeToolbar {
    static func hideShowItem(_ action: Selector, segmentAction: Selector, target: AnyObject) -> ToolbarItem {
        var group = [ToolbarItem]()
        group.append(ToolbarItem(NSToolbarItem.Identifier(rawValue: "HideLeftItem"), label: "", tag: .leftButton,
                                 image: NSImage(named: "leftButton"), action: segmentAction, target: target))
        group.append(ToolbarItem(NSToolbarItem.Identifier(rawValue: "HideRightItem"), label: "", tag: .rightButton,
                                 image: NSImage(named: "rightButton"), action: segmentAction, target: target))
        let item = ToolbarItem(ToolbarIdentifiers.hideShowItem, tag: .hideShow, width: 85, height: 28,
                               action: action, target: target, group: group, paletteLabel: "Hide/Show")
        return item
    }
}
extension InitializeToolbar {
    static func addItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
        let item = ToolbarItem(ToolbarIdentifiers.addItem, label: "Add", tag: .add, image: NSImage(named: NSImage.addTemplateName), action: action, target: target)
        return item
    }
}
extension InitializeToolbar {
    static func shareItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
        let item = ToolbarItem(ToolbarIdentifiers.shareItem, label: "Share", tag: .share, image: NSImage(named: NSImage.shareTemplateName), action: action, target: target)
        return item
    }
}
extension InitializeToolbar {
    static func filterItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
        let item = ToolbarItem(ToolbarIdentifiers.filterItem, label: "Filter", tag: .filter, image: NSImage(named: "filter"), action: action, target: target)
        return item
    }
}
extension InitializeToolbar {
    static func sortItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
        let item = ToolbarItem(ToolbarIdentifiers.sortItem, label: "Sort", tag: .sort, image: NSImage(named: "sort"), action: action, target: target)
        return item
    }
}
extension InitializeToolbar {
    static func cloudDownloadItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
        let item = ToolbarItem(ToolbarIdentifiers.cloudDownloadItem, label: "Down", tag: .cloudDownload, image: NSImage(named: "cloudDownload"), action: action, target: target)
        return item
    }
}
extension InitializeToolbar {
    static func cloudUploadItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
        let item = ToolbarItem(ToolbarIdentifiers.cloudUploadItem, label: "Up", tag: .cloudUpload, image: NSImage(named: "cloudUpload"), action: action, target: target)
        return item
    }
}
extension InitializeToolbar {
    static func leftButtonItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
        let item = ToolbarItem(ToolbarIdentifiers.leftButtonItem, label: "", tag: .leftButton, image: NSImage(named: "leftButton"), action: action, target: target)
        return item
    }
}
extension InitializeToolbar {
    static func rightButtonItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
        let item = ToolbarItem(ToolbarIdentifiers.rightButtonItem, label: "", tag: .rightButton, image: NSImage(named: "rightButton"), action: action, target: target)
        return item
    }
}

extension InitializeToolbar {
    static func textItem() -> ToolbarItem {
        return ToolbarItem(ToolbarIdentifiers.statusItem, label: "Watch This Space", tag: .status, width: 300, height: 24)
    }
}

这是工具栏 class,它实现了初始化器和委托:

/*
 * Initializer builds a specialized toolbar.
 */
enum ToolbarTag: Int {
    case separator = 1
    case navGroup
    case navPrev
    case navNext
    case add
    case share
    case filter
    case sort
    case cloudDownload
    case cloudUpload
    case leftButton
    case rightButton
    case hideShow
    case status
}
class Toolbar: NSObject, NSToolbarDelegate, Actor {
    var actorDelegate: ActorDelegate?
    var identifier: NSUserInterfaceItemIdentifier?
    var toolbarItemList = [ToolbarItem]()
    var toolbarItemIdentifiers: [NSToolbarItem.Identifier] { return toolbarItemList.map({ [=11=].identifier }) }
    var toolbarDefaultItemList = [ToolbarItem]()
    var toolbarDefaultItemIdentifiers: [NSToolbarItem.Identifier] { return toolbarDefaultItemList.map({ [=11=].identifier }) }

    // Delegate toolbar actions
    @objc func controlSentAction(_ sender: Any) {
        guard let control = sender as? NSControl else { return }
        guard let tag = ToolbarTag(rawValue: control.tag) else { return }
        actorDelegate?.actor(self, initiator: control, tag: tag, obj: nil)
    }
    @objc func segmentedControlSentAction(_ sender: Any) {
        guard let segmented = sender as? NSSegmentedControl else { return }
        guard let tag = ToolbarTag(rawValue: segmented.tag(forSegment: segmented.selectedSegment)) else { return }
        actorDelegate?.actor(self, initiator: segmented, tag: tag, obj: nil)
    }
    // These don't get called at the moment
    @objc func toolbarItemSentAction(_ sender: Any) { ddt("toolbarItemSentAction") }
    @objc func menuSentAction(_ sender: Any) { ddt("menuSentAction") }

    // Toolbar initialize
    init(_ window: Window) {
        super.init()
        identifier = Identifier.View.toolbar

        let toolbar = NSToolbar(identifier: ToolbarIdentifiers.mainToolbar)
        toolbar.centeredItemIdentifier = ToolbarIdentifiers.statusItem

        // Build the initial toolbar
        // Text field
        toolbarItemList.append(ToolbarItem(.flexibleSpace))
        toolbarItemList.append(InitializeToolbar.textItem())
        toolbarItemList.append(ToolbarItem(.flexibleSpace))
        // Show/Hide
        toolbarItemList.append(InitializeToolbar.hideShowItem(#selector(toolbarItemSentAction), segmentAction: #selector(segmentedControlSentAction), target: self))
        // Save initial toolbar as default
        toolbarDefaultItemList = toolbarItemList
        // Also allow these, just to demo adding
        toolbarItemList.append(InitializeToolbar.cloudDownloadItem(#selector(controlSentAction), target: self))
        toolbarItemList.append(InitializeToolbar.sortItem(#selector(controlSentAction), target: self))

        toolbar.allowsUserCustomization = true
        toolbar.displayMode = .default
        toolbar.delegate = self
        window.toolbar = toolbar
    }

    deinit {
        ddt("deinit", caller: self)
    }
}
/*
 * Implement NSToolbarDelegate
 */
extension Toolbar {

    // Build toolbar
    func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
        guard let item = toolbarItemList.firstIndex(where: { [=11=].identifier == itemIdentifier }) else { return nil }
        switch toolbarItemList[item].identifier {
        case ToolbarIdentifiers.navGroupItem, ToolbarIdentifiers.hideShowItem:
            return toolbarItemList[item].segmentedControl()
        case ToolbarIdentifiers.addItem, ToolbarIdentifiers.shareItem, ToolbarIdentifiers.sortItem, ToolbarIdentifiers.filterItem, ToolbarIdentifiers.cloudUploadItem, ToolbarIdentifiers.cloudDownloadItem,
             ToolbarIdentifiers.leftButtonItem, ToolbarIdentifiers.rightButtonItem:
            return toolbarItemList[item].imageButton()
        case ToolbarIdentifiers.statusItem:
            return toolbarItemList[item].textfieldItem()
        default:
            return nil
        }
    } // end of toolbar

    func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return toolbarDefaultItemIdentifiers;
    }

    func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return toolbarItemIdentifiers
    }

    func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return []
    }

    func toolbarWillAddItem(_ notification: Notification) {
    }

    func toolbarDidRemoveItem(_ notification: Notification) {
    }
} // End of extension

初始工具栏:

自定义下拉菜单,Cocoa 为您做的:

添加云按钮后:

希望这对您有所帮助。

已添加以澄清 2019 年 4 月 28 日:

我的工具栏 class 不是 NSToolbar 子class。它的初始值设定项传递了对 window 的引用,因此最后它将 window 的工具栏设置为其创建的工具栏:

    init(_ window: Window) {
        super.init()
        identifier = Identifier.View.toolbar

**** stuff removed for clarity ****

        let toolbar = NSToolbar(identifier: ToolbarIdentifiers.mainToolbar)
        toolbar.allowsUserCustomization = true
        toolbar.displayMode = .default
        toolbar.delegate = self
        window.toolbar = toolbar
    }

也许这会混淆语义,但它会创建工具栏并充当工具栏委托,正如您在扩展中看到的那样。

"Actor" 协议是我的协调框架的一部分,对于构建工具栏本身并不重要。我将不得不包括整个演示应用程序来展示这一点,并且我假设您有自己的设计来将工具栏操作传递给您的 controllers/models.

此应用程序是 Xcode 10.2/Swift 5,尽管我认为它没有使用任何新的 Swift 5 功能。

How Toolbars Work

To create a toolbar, you must create a delegate that provides important information:

  • A list of default toolbar identifiers. This list is used when reverting to default, and constructing the initial toolbar. The default set of toolbar items can also be specified using toolbar items found in the Interface Builder library.
  • A list of allowed item identifiers. The allowed item list is used to construct the customization palette, if the toolbar is customizable.
  • The toolbar item for a given item identifier.

例如添加灵活空间、打印和自定义项目:

class MyWindowController: NSWindowController, NSToolbarDelegate {

    var toolbarIdentifier = NSToolbarItem.Identifier("FILE_OPEN")

    func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return [NSToolbarItem.Identifier.flexibleSpace, NSToolbarItem.Identifier.print, toolbarIdentifier]
    }

    func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return [NSToolbarItem.Identifier.flexibleSpace, NSToolbarItem.Identifier.print, toolbarIdentifier]
    }

    func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
        willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
        if itemIdentifier == toolbarIdentifier {
            let toolbarItem = NSToolbarItem(itemIdentifier: toolbarIdentifier)
            toolbarItem.label = String("File")
            toolbarItem.paletteLabel = String("Open File")
            toolbarItem.toolTip = String("Open file to be handled")
            toolbarItem.isEnabled = true
            toolbarItem.target = self
            toolbarItem.action = #selector(browseFile)
            toolbarItem.image = NSImage.init(named:NSImage.folderName)
            return toolbarItem
        }
        else {
            return NSToolbarItem(itemIdentifier: itemIdentifier)
        }
    }

}

也可以在 IB 中添加部分或全部标准 and/or 自定义项目。