【问题标题】:Building an NSOutline view with Check marks使用复选标记构建 NSOutline 视图
【发布时间】:2018-12-31 05:45:13
【问题描述】:

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

如何添加允许用户的行为,如果我单击父复选框,那么它将选择子项,如果我取消单击它 - 它会取消选择该项目的子项?

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

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

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

我的解决方案包括创建一个数组来保存视图控制器中的选定项目,然后创建用于添加和删除的函数

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)")
    }

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

【问题讨论】:

  • 您打算手动填充大纲视图还是使用 Cocoa 绑定?
  • 有什么问题,如何添加复选框或如何选中孩子的复选框?
  • CodeDifferent - 我更新它说我使用了你推荐的手动过程。 @Willeke - 我对其进行了编辑以使其更清晰 - 感谢您的反馈 - 我正在尝试使用正确的方法来选择/取消选择子节点

标签: swift macos cocoa nsoutlineview


【解决方案1】:

为了未来 Google 员工的利益,我将重复我在 other answer 中所写的内容。这里的不同之处在于这有一个额外的要求,即列是可编辑的,并且我已经改进了这项技术。


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

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


界面生成器设置

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

  • 选择第一列中的单元格并将其标识符更改为isSelectedCell
  • 选择第二列中的单元格并将其标识符更改为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列中的默认文本字段
  • 从对象库中拖入一个复选框
  • 选择NSTableCellView并将其类更改为CheckboxCellView
  • 打开助手编辑器并连接插座


视图控制器

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

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 { $0.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)
    }
}

结果

【讨论】:

  • 这太棒了!感谢您的努力
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-03-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-19
  • 1970-01-01
相关资源
最近更新 更多