将 NSTreeController 与 NSOutlineView 一起使用
Using NSTreeController with NSOutlineView
我正在尝试(未成功)构建 TreeController-controlled NSOutlineView。我已经阅读了很多教程,但它们都是 pre-load 开始任何事情之前的数据,这对我不起作用。
我有一个简单的 class 设备:
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
}
}
我还有一个更简单的 class 'Service':
import Cocoa
class Service: NSObject {
let name: String
init(name: String){
self.name = name
}
}
最后,我得到了 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")
}
}
}
显然我有 2 个按钮,一个添加一个 'device',一个添加一个 'service' 到每个设备。
我不能实现的是在 NSOutlineView 中显示任何这些数据。我已将 TreeController 的 Object 控制器 属性 设置为模式:Class 和 Class:设备,并且没有设置 Children、计数或叶属性我得到(可以预见):
2017-01-04 17:20:19.337129 OutlineTest[12550:1536405] Warning: [object class: Device] childrenKeyPath cannot be nil. To eliminate this log message, set the childrenKeyPath attribute in Interface Builder
如果我然后将 Children 属性 设置为 'children' 事情会变得非常糟糕:
2017-01-04 17:23:11.150627 OutlineTest[12695:1548039] [General] [ addObserver:forKeyPath:options:context:] is not supported. Key path: children
我想要做的就是设置 NSOutlineView 以从 NSTreeController 获取输入,这样当一个新的 'Device' 添加到 devices[] 数组时,它会显示在大纲视图中。
如果有人能在这里指出正确的方向,我将不胜感激。
非常感谢 Warren 所做的非常有帮助的工作。我已经(大部分)工作了。除了 Warren 的建议之外,我还需要做几件事:
为树控制器设置数据存储
将 OutlineView 绑定到 TreeController
将列绑定到 TreeController
将 TableView Cell 绑定到 Table Cell View(是的,真的)
完成所有 后,我不得不稍微尝试一下实际的数据存储:
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)
}
原来 Root 开头不能是 child-less,至少据我所知。添加 child 后,我可以删除 place-holder 值,树似乎(大部分)按我的意愿工作。另一件事是,每当数据发生变化时,我都必须重新加载数据并重新显示大纲:
outlineView.reloadData()
outlineView.setNeedsDisplay()
没有它,什么都没有。我仍然没有正确更新数据(请参阅 Warren 的回答下方的评论),但我快到了。
显而易见,一个NSTreeController
管理一棵object的树,所有这些都需要回答以下三个questions/requests。
- 你是叶子吗,也就是说你没有children? =
leafKeyPath
- 如果你不是一片叶子,你有几片children? =
countKeyPath
- 给我你的children! =
childrenKeyPath
在 IB 中或以编程方式设置它们很简单。一组相当标准的属性分别是。
isLeaf
childCount
children
但这完全是任意的,可以是回答这些问题的任何一组属性。
我通常会设置一个类似于 TreeNode
的协议,并使我所有的 object 都遵守它。
@objc protocol TreeNode:class {
var isLeaf:Bool { get }
var childCount:Int { get }
var children:[TreeNode] { get }
}
对于 Device
object,您用 isLeaf
和 children
回答了 3 个问题中的 2 个,但没有回答 childCount 问题。
您设备的 children 是 Service
object,它们会回答 none,这就是您出现异常的部分原因。
因此,要修复您的代码,一个可能的解决方案是...
服务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 []
}
}
设备object
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
}
}
从 MVC 的角度来看,这是一件可怕的事情,但对于这个答案来说很方便。根 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
}
}
所以您需要做的就是设置您的 treeController 的内容。假设您的 ViewController
中有一个 IBOutlet
。
class ViewController: NSViewController, TreeNode {
@IBOutlet var treeController:NSTreeController!
@IBOutlet var outlineView:NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
treeController.content = self
}
现在每次添加设备或添加服务时,只需在 outlineView
上调用 reloadItem
(您还需要一个插座)
@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)
}
这就是基础知识,应该可以帮助您入门,但是 NSOutlineView
和 NSTreeController
的文档有更多信息。
编辑
除了上述内容之外,您还需要将大纲视图绑定到树控制器。
首先确保您的大纲视图处于查看模式。
接下来将 table 列绑定到树控制器上的 arrangedObjects。
最后将文本单元格绑定到相关的键路径。在您的例子中,它是 name。 object值 是对单元格中 object 的引用。
我正在尝试(未成功)构建 TreeController-controlled NSOutlineView。我已经阅读了很多教程,但它们都是 pre-load 开始任何事情之前的数据,这对我不起作用。
我有一个简单的 class 设备:
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
}
}
我还有一个更简单的 class 'Service':
import Cocoa
class Service: NSObject {
let name: String
init(name: String){
self.name = name
}
}
最后,我得到了 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")
}
}
}
显然我有 2 个按钮,一个添加一个 'device',一个添加一个 'service' 到每个设备。
我不能实现的是在 NSOutlineView 中显示任何这些数据。我已将 TreeController 的 Object 控制器 属性 设置为模式:Class 和 Class:设备,并且没有设置 Children、计数或叶属性我得到(可以预见):
2017-01-04 17:20:19.337129 OutlineTest[12550:1536405] Warning: [object class: Device] childrenKeyPath cannot be nil. To eliminate this log message, set the childrenKeyPath attribute in Interface Builder
如果我然后将 Children 属性 设置为 'children' 事情会变得非常糟糕:
2017-01-04 17:23:11.150627 OutlineTest[12695:1548039] [General] [ addObserver:forKeyPath:options:context:] is not supported. Key path: children
我想要做的就是设置 NSOutlineView 以从 NSTreeController 获取输入,这样当一个新的 'Device' 添加到 devices[] 数组时,它会显示在大纲视图中。
如果有人能在这里指出正确的方向,我将不胜感激。
非常感谢 Warren 所做的非常有帮助的工作。我已经(大部分)工作了。除了 Warren 的建议之外,我还需要做几件事:
完成所有 后,我不得不稍微尝试一下实际的数据存储:
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)
}
原来 Root 开头不能是 child-less,至少据我所知。添加 child 后,我可以删除 place-holder 值,树似乎(大部分)按我的意愿工作。另一件事是,每当数据发生变化时,我都必须重新加载数据并重新显示大纲:
outlineView.reloadData()
outlineView.setNeedsDisplay()
没有它,什么都没有。我仍然没有正确更新数据(请参阅 Warren 的回答下方的评论),但我快到了。
显而易见,一个NSTreeController
管理一棵object的树,所有这些都需要回答以下三个questions/requests。
- 你是叶子吗,也就是说你没有children? =
leafKeyPath
- 如果你不是一片叶子,你有几片children? =
countKeyPath
- 给我你的children! =
childrenKeyPath
在 IB 中或以编程方式设置它们很简单。一组相当标准的属性分别是。
isLeaf
childCount
children
但这完全是任意的,可以是回答这些问题的任何一组属性。
我通常会设置一个类似于 TreeNode
的协议,并使我所有的 object 都遵守它。
@objc protocol TreeNode:class {
var isLeaf:Bool { get }
var childCount:Int { get }
var children:[TreeNode] { get }
}
对于 Device
object,您用 isLeaf
和 children
回答了 3 个问题中的 2 个,但没有回答 childCount 问题。
您设备的 children 是 Service
object,它们会回答 none,这就是您出现异常的部分原因。
因此,要修复您的代码,一个可能的解决方案是...
服务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 []
}
}
设备object
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
}
}
从 MVC 的角度来看,这是一件可怕的事情,但对于这个答案来说很方便。根 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
}
}
所以您需要做的就是设置您的 treeController 的内容。假设您的 ViewController
中有一个 IBOutlet
。
class ViewController: NSViewController, TreeNode {
@IBOutlet var treeController:NSTreeController!
@IBOutlet var outlineView:NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
treeController.content = self
}
现在每次添加设备或添加服务时,只需在 outlineView
上调用 reloadItem
(您还需要一个插座)
@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)
}
这就是基础知识,应该可以帮助您入门,但是 NSOutlineView
和 NSTreeController
的文档有更多信息。
编辑
除了上述内容之外,您还需要将大纲视图绑定到树控制器。
首先确保您的大纲视图处于查看模式。
接下来将 table 列绑定到树控制器上的 arrangedObjects。
最后将文本单元格绑定到相关的键路径。在您的例子中,它是 name。 object值 是对单元格中 object 的引用。