构建带有复选标记的 NSOutline 视图

Building an NSOutline view with Check marks

我希望使用正确的 Apple 推荐方法将复选框添加到 NSOutlineview - 但文档中并不清楚。

如何添加允许用户的行为,如果我点击 parent 复选框,那么它会 select children,如果我取消点击它 - 它会取消select 那个项目的 children?

编辑:我简化了我的问题并添加了图像以使其更清晰(希望如此)

我的方法: 我一直在使用 Code Different 的精彩答案在我的 mac 应用程序中构建大纲视图。 - 我选择使用手动过程而不是使用 CocoaBindings 来填充 NSoutLine 视图。

我在堆栈视图中添加了一个复选框,这似乎是正确的方法:

我的解决方案包括创建一个数组来保存 viewcontroller 中的 selected 项,然后创建用于添加和删除的函数

var selectedItems: [Int]?

@objc func cellWasClicked(sender: NSButton) {
    let newCheckBoxState = sender.state
    let tag = sender.tag
    switch newCheckBoxState {
    case NSControl.StateValue.on:
        print("adding- \(sender.tag)")
    case NSControl.StateValue.off:
        print("removing- \(sender.tag)")
    default:
        print("unhandled button state \(newCheckBoxState)")
    }

我通过分配给复选框的标签来识别复选框

为了未来 Google 员工的利益,我将重复我在 中写的内容。这里的区别在于它有一个额外的要求,即列是可编辑的,我已经改进了技术。


NSOutlineView 的关键是您必须为每一行指定一个标识符,可以是字符串、数字或唯一标识该行的对象。 NSOutlineView 称之为 item。基于此item,您将查询您的数据模型以填充大纲。

在这个答案中,我们将设置一个包含 2 列的大纲视图:一个可编辑的 Is Selected 列和一个不可编辑的 Title列。


界面生成器设置

  • Select 第一列并将其标识符设置为 isSelected
  • Select 第二列并将其标识符设置为 title

  • Select 第一列中的单元格并将其标识符更改为 isSelectedCell
  • Select 第二列中的单元格并将其标识符更改为 titleCell

一致性在这里很重要。单元格的标识符应等于其列的标识符 + Cell.


带有复选框的单元格

默认 NSTableCellView 包含不可编辑的文本字段。我们想要一个复选框,所以我们必须设计我们自己的单元格。

CheckboxCellView.swift

import Cocoa

/// A set of methods that `CheckboxCelView` use to communicate changes to another object
protocol CheckboxCellViewDelegate {
    func checkboxCellView(_ cell: CheckboxCellView, didChangeState state: NSControl.StateValue)
}

class CheckboxCellView: NSTableCellView {

    /// The checkbox button
    @IBOutlet weak var checkboxButton: NSButton!

    /// The item that represent the row in the outline view
    /// We may potentially use this cell for multiple outline views so let's make it generic
    var item: Any?

    /// The delegate of the cell
    var delegate: CheckboxCellViewDelegate?

    override func awakeFromNib() {
        checkboxButton.target = self
        checkboxButton.action = #selector(self.didChangeState(_:))
    }

    /// Notify the delegate that the checkbox's state has changed
    @objc private func didChangeState(_ sender: NSObject) {
        delegate?.checkboxCellView(self, didChangeState: checkboxButton.state)
    }
}

连接插座

  • 删除 isSelected 列中的默认文本字段
  • 从对象库中拖入一个复选框
  • Select NSTableCellView 并将其 class 更改为 CheckboxCellView
  • 打开 Assistant Editor 并连接插座


视图控制器

最后是视图控制器的代码:

import Cocoa


/// A class that represents a row in the outline view. Add as many properties as needed
/// for the columns in your outline view.
class OutlineViewRow {
    var title: String
    var isSelected: Bool
    var children: [OutlineViewRow]

    init(title: String, isSelected: Bool, children: [OutlineViewRow] = []) {
        self.title = title
        self.isSelected = isSelected
        self.children = children
    }

    func setIsSelected(_ isSelected: Bool, recursive: Bool) {
        self.isSelected = isSelected
        if recursive {
            self.children.forEach { [=11=].setIsSelected(isSelected, recursive: true) }
        }
    }
}

/// A enum that represents the list of columns in the outline view. Enum is preferred over
/// string literals as they are checked at compile-time. Repeating the same strings over
/// and over again are error-prone. However, you need to make the Column Identifier in
/// Interface Builder with the raw value used here.
enum OutlineViewColumn: String {
    case isSelected = "isSelected"
    case title = "title"

    init?(_ tableColumn: NSTableColumn) {
        self.init(rawValue: tableColumn.identifier.rawValue)
    }

    var cellIdentifier: NSUserInterfaceItemIdentifier {
        return NSUserInterfaceItemIdentifier(self.rawValue + "Cell")
    }
}


class ViewController: NSViewController {
    @IBOutlet weak var outlineView: NSOutlineView!

    /// The rows of the outline view
    let rows: [OutlineViewRow] = {
        var child1 = OutlineViewRow(title: "p1-child1", isSelected: true)
        var child2 = OutlineViewRow(title: "p1-child2", isSelected: true)
        var child3 = OutlineViewRow(title: "p1-child3", isSelected: true)
        let parent1 = OutlineViewRow(title: "parent1", isSelected: true, children: [child1, child2, child3])

        child1 = OutlineViewRow(title: "p2-child1", isSelected: true)
        child2 = OutlineViewRow(title: "p2-child2", isSelected: true)
        child3 = OutlineViewRow(title: "p2-child3", isSelected: true)
        let parent2 = OutlineViewRow(title: "parent2", isSelected: true, children: [child1, child2, child3])

        child1 = OutlineViewRow(title: "p3-child1", isSelected: true)
        child2 = OutlineViewRow(title: "p3-child2", isSelected: true)
        child3 = OutlineViewRow(title: "p3-child3", isSelected: true)
        let parent3 = OutlineViewRow(title: "parent3", isSelected: true, children: [child1, child2, child3])

        child3 = OutlineViewRow(title: "p4-child3", isSelected: true)
        child2 = OutlineViewRow(title: "p4-child2", isSelected: true, children: [child3])
        child1 = OutlineViewRow(title: "p4-child1", isSelected: true, children: [child2])
        let parent4 = OutlineViewRow(title: "parent4", isSelected: true, children: [child1])

        return [parent1, parent2, parent3, parent4]
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        outlineView.dataSource = self
        outlineView.delegate = self
    }
}

extension ViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
    /// Returns how many children a row has. `item == nil` means the root row (not visible)
    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        switch item {
        case nil: return rows.count
        case let row as OutlineViewRow: return row.children.count
        default: return 0
        }
    }

    /// Returns the object that represents the row. `NSOutlineView` calls this the `item`
    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        switch item {
        case nil: return rows[index]
        case let row as OutlineViewRow: return row.children[index]
        default: return NSNull()
        }
    }

    /// Returns whether the row can be expanded
    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        switch item {
        case nil: return !rows.isEmpty
        case let row as OutlineViewRow: return !row.children.isEmpty
        default: return false
        }
    }

    /// Returns the view that display the content for each cell of the outline view
    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        guard let item = item as? OutlineViewRow, let column = OutlineViewColumn(tableColumn!) else { return nil }

        switch column {
        case .isSelected:
            let cell = outlineView.makeView(withIdentifier: column.cellIdentifier, owner: self) as! CheckboxCellView
            cell.checkboxButton.state = item.isSelected ? .on : .off
            cell.delegate = self
            cell.item = item
            return cell

        case .title:
            let cell = outlineView.makeView(withIdentifier: column.cellIdentifier, owner: self) as! NSTableCellView
            cell.textField?.stringValue = item.title
            return cell
        }
    }
}

extension ViewController: CheckboxCellViewDelegate {
    /// A delegate function where we can act on update from the checkbox in the "Is Selected" column
    func checkboxCellView(_ cell: CheckboxCellView, didChangeState state: NSControl.StateValue) {
        guard let item = cell.item as? OutlineViewRow else { return }

        // The row and its children are selected if state == .on
        item.setIsSelected(state == .on, recursive: true)

        // This is more efficient than calling reload on every child since collapsed children are
        // not reloaded. They will be reloaded when they become visible
        outlineView.reloadItem(item, reloadChildren: true)
    }
}

结果