简体   繁体   English

将NSTreeController与NSOutlineView一起使用

[英]Using NSTreeController with NSOutlineView

I'm trying (unsuccessfully) to build a TreeController-controlled NSOutlineView. 我正在尝试(不成功)构建一个TreeController控制的NSOutlineView。 I've gone through a bunch of tutorials, but they all pre-load the data before starting anything, and this won't work for me. 我已经阅读了很多教程,但他们都在开始之前预先加载了数据,这对我来说不起作用。

I have a simple class for a device: 我有一个简单的设备类:

import Cocoa
class Device: NSObject {
    let name : String
    var children = [Service]()
    var serviceNo = 1
    var count = 0

    init(name: String){
        self.name = name
    }

    func addService(serviceName: String){
        let serv = "\(serviceName) # \(serviceNo)"
        children.append(Service(name: serv))
        serviceNo += 1
        count = children.count
    }

    func isLeaf() -> Bool {
        return children.count < 1
    }
}

I also have an even more simple class for the 'Service': 我还有一个更简单的“服务”课程:

import Cocoa
class Service: NSObject {

    let name: String

    init(name: String){
        self.name = name
    }
}

Finally, I have the ViewController: 最后,我有ViewController:

class ViewController: NSViewController {

    var stepper = 0

    dynamic var devices = [Device]()
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }

    @IBAction func addDeviceAction(_ sender: Any) {
        let str = "New Device #\(stepper)"
        devices.append(Device(name: str))
        stepper += 1
        print("Added Device: \(devices[devices.count-1].name)")    
    }

    @IBAction func addService(_ sender: Any) {
        for i in 0..<devices.count {
            devices[i].addService(serviceName: "New Service")
       }
    }
}

Obviously I have 2 buttons, one that adds a 'device' and one that adds a 'service' to each device. 显然我有2个按钮,一个添加“设备”,另一个为每个设备添加“服务”。

What I can't make happen is any of this data show up in the NSOutlineView. 无法实现的是NSOutlineView中显示的任何数据。 I've set the TreeController's Object Controller Property to Mode: Class and Class: Device, and without setting the Children, Count, or Leaf properties I get (predictably): 我已经将TreeController的对象控制器属性设置为Mode:Class和Class:Device,并且没有设置我得到的Children,Count或Leaf属性(可预测):

2017-01-04 17:20:19.337129 OutlineTest[12550:1536405] Warning: [object class: Device] childrenKeyPath cannot be nil. 2017-01-04 17:20:19.337129 OutlineTest [12550:1536405]警告:[object class:Device] childrenKeyPath不能为nil。 To eliminate this log message, set the childrenKeyPath attribute in Interface Builder 要消除此日志消息,请在Interface Builder中设置childrenKeyPath属性

If I then set the Children property to 'children' things go very bad: 如果我然后将Children属性设置为'children'则会非常糟糕:

2017-01-04 17:23:11.150627 OutlineTest[12695:1548039] [General] [ addObserver:forKeyPath:options:context:] is not supported. 2017-01-04 17:23:11.150627 OutlineTest [12695:1548039] [General] [addObserver:forKeyPath:options:context:]不受支持。 Key path: children 关键路径:儿童

All I'm trying to do is set up the NSOutlineView to take input from the NSTreeController so that when a new 'Device' is added to the devices[] array, it shows up in the Outline View. 我要做的就是设置NSOutlineView来从NSTreeController获取输入,这样当一个新的'Device'被添加到devices []数组时,它会显示在Outline视图中。

If anyone could point me in the right direction here I'd be most grateful. 如果有人能指出我正确的方向,我将非常感激。

Much gratitude to Warren for the hugely helpful work. 非常感谢沃伦的非常有益的工作。 I've got it (mostly) working. 我(大部分)都在工作。 A couple of things that I also needed to do, in addition to Warren's suggestions: 除了沃伦的建议之外,我还需要做一些事情:

设置TreeController的数据存储区 Set the datastore for the Tree Controller 设置树控制器的数据存储

绑定到TreeView Bind the OutlineView to the TreeController 将OutlineView绑定到TreeController

将列绑定到TreeController Bind the Column to the TreeController 将列绑定到TreeController

将表视图单元格绑定到表格单元视图 Bind the TableView Cell to the Table Cell View (yes, really) 将TableView单元格绑定到表格单元格视图(是的,真的)

Once all that was done, I had to play around with the actual datastore a bit: 完成所有这些后,我不得不稍微使用实际的数据存储区:

   var name = "Bluetooth Devices Root"
var deviceStore = [Device]()
@IBOutlet var treeController: NSTreeController!
@IBOutlet weak var outlineView: NSOutlineView!


override func viewDidLoad() {
    super.viewDidLoad()
    deviceStore.append(Device(name: "Bluetooth Devices"))
    self.treeController.content = self
}

override var representedObject: Any? {
    didSet {
    // Update the view, if already loaded.
    }
}

@IBAction func addDeviceAction(_ sender: Any) {
    if(deviceStore[0].name == "Bluetooth Devices"){
        deviceStore.remove(at: 0)
    }

Turns out the Root cannot be child-less at the beginning, at least as far as I can tell. 事实证明,Root在开始时不能没有孩子,至少就我所知。 Once I add a child, I can delete the place-holder value and the tree seems to work (mostly) as I want. 一旦我添加了一个孩子,我就可以删除占位符值,并且树似乎可以正常工作(大部分)。 One other thing is that I have to reload the data and redisplay the outline whenever the data changes: 另一件事是我必须重新加载数据并在数据发生变化时重新显示大纲:

 outlineView.reloadData()
 outlineView.setNeedsDisplay()

Without that, nothing. 没有它,没有。 I still don't have the data updating correctly (see comments below Warren's answer) but I'm almost there. 我仍然没有正确更新数据(请参阅Warren的回答下面的评论),但我差不多了。

To state the obvious, a NSTreeController manages a tree of objects all of which need to answer the following three questions/requests. 为了说明这一点, NSTreeController管理一个对象树,所有这些对象都需要回答以下三个问题/请求。

  • Are you a leaf ie do you have no children? 你是一片叶子,即你没有孩子吗? = leafKeyPath = leafKeyPath
  • If you are not a leaf, how many children do you have ? 如果你不是叶子,你有多少个孩子? = countKeyPath = countKeyPath
  • Give me your children! 把你的孩子给我! = childrenKeyPath = childrenKeyPath

Its simple to set these up in IB or programatically. 它很容易在IB或编程中设置它们。 A fairly standard set of properties is respectively. 分别是一组相当标准的属性。

  • isLeaf
  • childCount
  • children

But its totally arbitrary and can be any set of properties that answer those questions. 但它完全是任意的,可以是回答这些问题的任何属性集。

I normally set up a protocol named something like TreeNode and make all my objects conform to it. 我通常会设置一个名为TreeNode的协议,并使我的所有对象都符合它。

@objc protocol TreeNode:class { 
    var isLeaf:Bool { get }
    var childCount:Int { get }
    var children:[TreeNode] { get } 
}

For your Device object you answer 2 out 3 question with isLeaf and children but don't answer the childCount question. 对于您的Device对象,您可以使用isLeafchildren回答2个问题,但不回答childCount问题。

Your Device's children are Service objects and they answer none of that which is some of the reason why you are getting the exceptions. 您的设备的子项是Service对象,它们不会回答这一点,这也是您获得例外的原因之一。

So to fix up your code a possible solution is ... 因此,要修复代码,可能的解决方案是......

The Service object 服务对象

class Service: NSObject, TreeNode {

    let name: String
    init(name: String){
        self.name = name
    }

    var isLeaf:Bool {
        return true
    }
    var childCount:Int {
        return 0
    }
    var children:[TreeNode] {
        return []
    }
}

The Device object Device对象

class Device: NSObject, TreeNode {
    let name : String
    var serviceStore = [Service]()

    init(name: String){
        self.name = name
    }

    var isLeaf:Bool {
        return serviceStore.isEmpty
    }
    var childCount:Int {
        return serviceStore.count
    }
    var children:[TreeNode] {
        return serviceStore
    }

}

And a horrible thing to do from a MVC perspective but convenient for this answer. 从MVC的角度来看,这是一个可怕的事情,但这个答案很方便。 The root object. 根对象。

class ViewController: NSViewController, TreeNode {

    var deviceStore = [Device]()
    var name = "Henry" //whatever you want to name your root

    var isLeaf:Bool {
        return deviceStore.isEmpty
    }
    var childCount:Int {
        return deviceStore.count
    }
    var children:[TreeNode] {
        return deviceStore
    }

}

So all you need to do is set the content of your treeController. 所以你需要做的就是设置treeController的内容。 Lets assume you have an IBOutlet to it in your ViewController . 让我们假设您在ViewController有一个IBOutlet

class ViewController: NSViewController, TreeNode {

    @IBOutlet var treeController:NSTreeController!
    @IBOutlet var outlineView:NSOutlineView!


    override func viewDidLoad() {
        super.viewDidLoad()
        treeController.content = self
    } 

Now each time you append a Device or add a Service just call reloadItem on the outlineView (that you also need an outlet to) 现在,每次附加设备或添加服务时,只需在reloadItem上调用outlineView (您还需要一个插座)

@IBAction func addDeviceAction(_ sender: Any) {
        let str = "New Device #\(stepper)"
        devices.append(Device(name: str))
        stepper += 1
        print("Added Device: \(devices[devices.count-1].name)")
        outlineView.reloadItem(self, reloadChildren: true)    
    }

Thats the basics and should get you started but the docs for NSOutlineView & NSTreeController have a lot more info. 这是基础知识,应该让你开始,但NSOutlineViewNSTreeController的文档有更多的信息。

EDIT 编辑

In addition to the stuff above you need to bind your outline view to your tree controller. 除了上面的内容之外,您还需要将大纲视图绑定到树控制器。

First ensure your Outline View is in view mode. 首先确保您的大纲视图处于查看模式。

在视图模式下放置大纲视图

Next bind the table column to arrangedObjects on the tree controller. 接下来的表格列绑定到树控制器上arrangedObjects。

将列绑定到树控制器

Last bind the text cell to the relevant key path. 最后将文本单元格绑定到相关的键路径。 In your case it's name . 在你的情况下它的名字 objectValue is the reference to your object in the cell. objectValue是对单元格中对象的引用。

将数据绑定到单元格

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM