表格是非常重要和复杂的一个控件,本节会用大量篇幅来把表格这东西力求讲清楚。
基本设置
表格结构
表格是 OS X 组件中为数不多采用了MVC设计模式来实现的控件,即tableView–dataSource–Delegate,这种分层架构给处理数据带来了极大的便利性。先了解下表格的组成,如下:
- NSTableView:由NSTableHeaderView + NSTableRowView组成;
- NSScrollView:NSScrollView包装了NSTableView,它和NSTableView构成了最外围的对象,NSTableRowView 的视图由NSScrollView来管理;
- NSTableHeaderView:为表头,由一组 NSTableHeaderCell 组成;
- NSTableRowView:表示内容区,由一组 NSTableCellView组成;
- NSTableViewDataSource:由多个NSTableColumn(即列视图)来定义;NSTableRowView 的数据由NSTableViewDataSource来定义,NSTableViewDataSource定义了一系列的显示回调方法;
- NSTableViewDelegate:NSTableRowView 的代理由NSTableViewDelegate来定义,它提供了数据加载时的回调方法;
以下是UI的层级结构:
table 属性
- content mode:设置table的cell模式,推荐使用view-based,cell-based是一种老设计;
- columns:表示表格有多少列;
- header:是否显示表头;
- horizontal grid和 vertical grid:是否显示表格线;
- background color:单元格背景色;
- selection:是否允许行多选;
column 属性
- title:标题;
- state:是否可以编辑列名称;
- identifer:列的唯一标识符,用于在数据源和代理回调中使用;
- Table Cell View 的identifer:用于数据量大时可从缓存中获取可复用的cell单元视图,用于性能优化;
设置表格外观
主要是颜色等外观样式
@IBOutlet weak var tableView: NSTableView!
func tableStyleConfig() {
//表格网格线设置
self.tableView.gridStyleMask = [NSTableView.GridLineStyle.dashedHorizontalGridLineMask,NSTableView.GridLineStyle.solidVerticalGridLineMask]
//表格背景
self.tableView.backgroundColor = NSColor.white
//背景颜色交替
self.tableView.usesAlternatingRowBackgroundColors = true
//表格行选中样式
self.tableView.selectionHighlightStyle = .regular
}
表格数据绑定
以下是三种表格数据绑定方法,推荐第二种方式。
Cell-Based表格数据
-
首先修改 Table View 的Content Mode 为Cell Based;
-
选择column,修改identifier,与程序代码中数据定义相匹配(参考下边代码示例);
-
可以往单元格中添加不同的 cell,注意是cell而不是控件,这样就可以实现表格中的个性化展示了,如下图:
-
设置TableView的Deletegate和DataSource为默认的View Controller,拖动下图红框到UI导航的View Controller上面;
也可以通过代码设置self.tableView.delegate = self
和self.tableView.dataSource = self
-
定义表格数据,下面的updateData方法是自定义的,需要调用:
//表格数据,这个datas属性会在协议实现时再加载使用
var datas = [NSDictionary]()
func updateData() {
self.datas = [
["name":"john","address":"USA","gender":"male","married":(1)],
["name":"mary","address":"China","gender":"female","married":(0)],
["name":"park","address":"Japan","gender":"male","married":(0)],
["name":"Daba","address":"Russia","gender":"female","married":(1)],
]
}
- 扩展协议,加载数据
extension ViewController: NSTableViewDataSource {
//返回表格数据行数
func numberOfRows(in tableView: NSTableView) -> Int {
return self.datas.count
}
//正常只读显示
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
let data = self.datas[row]
//表格列的标识
let key = tableColumn?.identifier
//单元格数据
let value = data[key!]
return value
}
}
View-Based表格数据(推荐)
- 首先修改 Table View 的Content Mode 为View Based;
- 选择column,修改identifier,与数据相匹配;(同上)
- 可以往单元格中添加不同的控件,注意是控件而不是cell,这样就可以实现表格中的个性化展示了:
- 设置TableView的Deletegate和DataSource为默认的View Controller;(同上)
- 定义表格数据(同上)
- 扩展协议,加载数据,要实现两个协议
extension ViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return self.datas.count
}
}
extension ViewController: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let data = self.datas[row]
//表格列的标识
let key = (tableColumn?.identifier)!
//单元格数据
let value = data[key]
//根据表格列的标识,创建单元视图
let view = tableView.makeView(withIdentifier: key, owner: self)
let subviews = view?.subviews
if (subviews?.count)!<=0 {
return nil
}
if key.rawValue == "name" || key.rawValue == "address" {
let textField = subviews?[0] as! NSTextField
if value != nil {
textField.stringValue = value as! String
}
}
if key.rawValue == "gender" {
let comboField = subviews?[0] as! NSComboBox
if value != nil {
comboField.stringValue = value as! String
}
}
if key.rawValue == "married" {
let checkBoxField = subviews?[0] as! NSButton
checkBoxField.state = NSControl.StateValue(rawValue: 0)
if (value != nil) {
checkBoxField.state = NSControl.StateValue(rawValue: 1)
}
}
return view
}
}
Bindings 表格数据
- 添加ArrayControl 控件,然后绑定到View Controller的一个变量中;
实现代码如下:
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
updateData()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
dynamic var datas = [NSDictionary]()
func updateData() {
self.datas = [
["name":"john","address":"USA"],
["name":"mary","address":"China"],
["name":"park","address":"Japan"],
["name":"Daba","address":"Russia"],
]
}
}
- 添加一个NSTableView控件,设置tableColumn的identifier和上面的datas名称一样;
- 表格列绑定到Array Controller,Model Key Path可以省略;
- 单元格绑定到Model key path,选顶中的objectValue表示当前的行数据;
动态编辑表格
协议实现
表格的操作,需要实现NSTableViewDataSource
协议方法,否则无法保存数据:
//动态编辑表格使用
func tableView(_ tableView: NSTableView, setObjectValue object: Any?, for tableColumn: NSTableColumn?, row: Int) {
let data = self.datas[row]
//表格列的标识
let key = tableColumn?.identifier
let editData = NSMutableDictionary.init(dictionary: data)
editData[key!] = object
self.datas[row] = editData
}
添加和删除行
@IBAction func addTableRow(_ sender: NSButton) {
let data = NSMutableDictionary()
data["name"] = ""
data["address"] = ""
//增加数据到datas数据区
self.datas.append(data)
//刷新表数据
self.tableView.reloadData()
//定位光标到新添加的行
self.tableView.editColumn(0, row: self.datas.count-1 , with: nil, select: true)
}
@IBAction func deleteTableRow(_ sender: NSButton) {
//表格当前选择的行
let row = self.tableView.selectedRow
//如果row小于0表示没有选择行
if row<0 {
return
}
//从数据区删除选择的行的数据
self.datas.remove(at: row)
//刷新表数据
self.tableView.reloadData()
}
表格行拖放
- 注册一个自定义事件;
- 实现 NSTableViewDataSource 把表格行号数据拷贝到剪切板对象中;
- 实现 NSTableViewDataSource 把表格行号数据复制到表格对象中;
//注册拖放事件,事件为一自定义的值
let kTableViewDragDataTypeName = "TableViewDragDataTypeName"
self.tableView.register(forDraggedTypes: [kTableViewDragDataTypeName])
实现协议方法
func tableView(_ tableView: NSTableView, writeRowsWith rowIndexes: IndexSet, to pboard: NSPasteboard) -> Bool {
//将表格行号拷贝到剪切板对象中
let zNSIndexSetData = NSKeyedArchiver.archivedData(withRootObject: rowIndexes);
pboard.declareTypes([kTableViewDragDataTypeName], owner: self)
pboard.setData(zNSIndexSetData, forType: kTableViewDragDataTypeName)
return true
}
func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableViewDropOperation) -> Bool {
let pboard = info.draggingPasteboard()
let rowData = pboard.data(forType: kTableViewDragDataTypeName)
let rowIndexes = NSKeyedUnarchiver.unarchiveObject(with: rowData!) as! NSIndexSet
let dragRow = rowIndexes.firstIndex
let temp = self.datas[row]
self.datas[row] = self.datas[dragRow]
self.datas[dragRow] = temp
tableView.reloadData()
return true
}
表格事件
定义表格常见的一些操作事件
数据选取
以下是表格行和列的选取方法,采用(X,Y)坐标系的方式:
let row = self.selectedRow
let col = self.selectedColumn
行单击事件
需要实现 NSTableViewDelegate 协议中的方法,不需要设置绑定
func tableViewSelectionDidChange(_ notification: Notification){
let tableView = notification.object as! NSTableView
let row = tableView.selectedRow
print("selection row \(row)")
}
行双击事件
动态添加事件。
//表格双击事件
self.tableView.doubleAction = #selector(ViewController.doubleAction(_:))
@IBAction func doubleAction(_ sender: AnyObject ) {
let row = self.tableView?.selectedRow
print("double selection row \(row!)")
}
表格右键菜单
- 拖动一个NSMenu到设计面板;
- 绑定menu对象到ViewController中;
- 实现NSMenuDelegate协议
@IBOutlet weak var tableMenu: NSMenu!
//表格菜单
self.tableView.menu = self.tableMenu
self.tableView.menu?.delegate = self
实现协议
//表格上下文菜单协议
extension ViewController: NSMenuDelegate {
func menuNeedsUpdate(_ menu: NSMenu) {
menu.removeAllItems()
NSLog("menu clicked !")
}
}
不同菜单的写法
import Cocoa
class MyTableView: NSTableView {
var menu1: NSMenu?
var menu2: NSMenu?
override func menu(for event: NSEvent) -> NSMenu? {
let row = self.selectedRow
let col = self.selectedColumn
if (row == 0 && col == 1) {
return self.menu1
}
return self.menu2
}
}
表格排序
点击列头时出现排序升降箭头指示,其算法也可以自定义,在表格初始化后,可调用一次以下几个方法之一;
//表格排序
func tableSortConfig() {
for tableColumn in self.tableView.tableColumns {
//升序排序
let sortRules = NSSortDescriptor(key: tableColumn.identifier.rawValue, ascending: true)
tableColumn.sortDescriptorPrototype = sortRules
}
}
func tableSortConfig2() {
for tableColumn in self.tableView.tableColumns {
//升序排序 使用字符串标准的比较函数
let sortRules = NSSortDescriptor(key: tableColumn.identifier.rawValue, ascending: true, selector: #selector(NSString.localizedStandardCompare(_:)))
tableColumn.sortDescriptorPrototype = sortRules
}
}
func tableSortConfig3() {
for tableColumn in self.tableView.tableColumns {
//升序排序
let sortRules = NSSortDescriptor(key: tableColumn.identifier.rawValue, ascending: true, comparator:{ s1,s2 in
let str1 = s1 as! String
let str2 = s2 as! String
if str1 > str2 {return .orderedAscending}
if str1 < str2 {return .orderedDescending}
return .orderedSame
}
)
tableColumn.sortDescriptorPrototype = sortRules
}
}
编码实现
其实就是按步骤创建下列元素。
下面的代码需要完成以下界面效果
创建表格元素
基本元素定义
//1、定义表格对象和滚动条对象,最外层对象
let tableView = NSTableView()
let tableScrollView = NSScrollView()
//2、定义存储表格数据的变量
var datas = [NSDictionary]()
其组装过程大概如下:
//3、组装表格
override func viewDidLoad() {
super.viewDidLoad()
//创建表格列
self.tableViewConfig()
//配置滚动条视图
self.tableScrollViewConfig()
//设置滚动条自动布局
self.autoLayoutConfig()
//加载更新数据
self.updateData()
}
创建表头
func tableViewConfig() {
self.tableView.focusRingType = .none
//self.tableView.autoresizesSubviews = true
self.tableView.delegate = self
self.tableView.dataSource = self
let column1 = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "name"))
column1.title = "name"
column1.width = 80
column1.maxWidth = 100
column1.minWidth = 50
self.tableView.addTableColumn(column1)
let column2 = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "address"))
column2.title = "address"
column2.width = 80
column2.maxWidth = 100
column2.minWidth = 50
self.tableView.addTableColumn(column2)
}
设置滚动条样式
这块可有可无
func tableScrollViewConfig() {
self.tableScrollView.hasVerticalScroller = true
self.tableScrollView.hasVerticalScroller = false
self.tableScrollView.focusRingType = .none
self.tableScrollView.autohidesScrollers = true
self.tableScrollView.borderType = .bezelBorder
self.tableScrollView.translatesAutoresizingMaskIntoConstraints = false
self.tableScrollView.documentView = self.tableView
self.view.addSubview(self.tableScrollView)
}
设置布局
此处建议,使用了自动化布局窗口辅助,此处使用的是Masony,但如果用swiftui需要引入三方包
func autoLayoutConfig() {
let topAnchor = self.tableScrollView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0)
let bottomAnchor = self.tableScrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0)
let leftAnchor = self.tableScrollView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0)
let rightAnchor = self.tableScrollView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0)
NSLayoutConstraint.activate([topAnchor, bottomAnchor, leftAnchor, rightAnchor])
}
初始化数据
更新数据同时,需要实现表格代理协议
func updateData() {
self.datas = [
["name":"john","address":"USA"],
["name":"mary","address":"China"],
["name":"park","address":"Japan"],
["name":"Daba","address":"Russia"],
]
self.tableView.reloadData()
}
实现NSTableViewDelegate协议
这里有很多方法可以按需定义
extension ViewController: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let data = self.datas[row]
//表格列的标识
let key = (tableColumn?.identifier)!
//单元格数据
let value = data[key]
//根据表格列的标识,创建单元视图
var view = tableView.makeView(withIdentifier: key, owner: self)
if view == nil {
let cellView = NSTableCellView()
cellView.identifier = identifier;
view = cellView
let textField = NSTextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.isBezeled = false
textField.drawsBackground = false
cellView.addSubview(textField)
let topAnchor = textField.topAnchor.constraint(equalTo: cellView.topAnchor, constant: 0)
let bottomAnchor = textField.bottomAnchor.constraint(equalTo: cellView.bottomAnchor, constant: 0)
let leftAnchor = textField.leftAnchor.constraint(equalTo: cellView.leftAnchor, constant: 0)
let rightAnchor = textField.rightAnchor.constraint(equalTo: cellView.rightAnchor, constant: 0)
NSLayoutConstraint.activate([topAnchor,bottomAnchor,leftAnchor, rightAnchor])
}
let subviews = view?.subviews
if (subviews?.count)!<=0 {
return nil
}
let textField = subviews?[0] as! NSTextField
if value != nil {
textField.stringValue = value as! String
}
return view
}
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
return 30
}
}