Mac開發基礎18-NSTableView(一)

Mr.陳發表於2024-08-06

NSTableView 是 macOS 應用程式中用於顯示和管理資料表格的控制元件。它提供了豐富的 API 和高度自定義的能力,使得開發者可以精細地控制表格的顯示和行為。本文將詳細介紹 NSTableView 的常見 API 和一些基礎技巧,並深入探討其相關知識。

1. 基本使用

建立和初始化

Objective-C

#import <Cocoa/Cocoa.h>

// 建立並初始化一個 NSTableView 例項
NSTableView *tableView = [[NSTableView alloc] initWithFrame:NSMakeRect(0, 0, 400, 300)];

// 建立並初始化一個 NSTableColumn 例項
NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:@"ColumnIdentifier"];
[column setWidth:300]; // 設定列寬

// 為表格檢視新增列
[tableView addTableColumn:column];

// 設定表格頭標題
[[column headerCell] setStringValue:@"Header Title"];

Swift

import Cocoa

// 建立並初始化一個 NSTableView 例項
let tableView = NSTableView(frame: NSRect(x: 0, y: 0, width: 400, height: 300))

// 建立並初始化一個 NSTableColumn 例項
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("ColumnIdentifier"))
column.width = 300 // 設定列寬

// 為表格檢視新增列
tableView.addTableColumn(column)

// 設定表格頭標題
column.headerCell.title = "Header Title"

資料來源和委託

NSTableView 依賴資料來源 (NSTableViewDataSource) 和委託 (NSTableViewDelegate) 來提供資料和處理使用者互動事件。

Objective-C

// 設定資料來源和委託
[tableView setDataSource:self];
[tableView setDelegate:self];
// 實現資料來源協議
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
    return _dataArray.count; // 返回資料來源中的行數
}

- (nullable id)tableView:(NSTableView *)tableView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row {
    return _dataArray[row]; // 返回行資料
}
// 實現委託協議
- (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row {
    NSTextField *textField = [tableView makeViewWithIdentifier:tableColumn.identifier owner:self];
    if (!textField) {
        textField = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, tableColumn.width, 20)];
        textField.identifier = tableColumn.identifier;
    }
    textField.stringValue = _dataArray[row];
    return textField;
}

Swift

// 設定資料來源和委託
tableView.dataSource = self
tableView.delegate = self
// 實現資料來源協議
func numberOfRows(in tableView: NSTableView) -> Int {
    return dataArray.count // 返回資料來源中的行數
}

func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
    return dataArray[row] // 返回行資料
}
// 實現委託協議
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
    let identifier = tableColumn?.identifier ?? NSUserInterfaceItemIdentifier("")
    var textField = tableView.makeView(withIdentifier: identifier, owner: self) as? NSTextField
    if textField == nil {
        textField = NSTextField(frame: NSRect(x: 0, y: 0, width: tableColumn?.width ?? 100, height: 20))
        textField?.identifier = identifier
    }
    textField?.stringValue = dataArray[row]
    return textField
}

2. 編輯和選擇

允許選擇行

Objective-C

[tableView setAllowsSelection:YES];  // 允許選擇行

Swift

tableView.allowsSelection = true  // 允許選擇行

允許編輯單元格

Objective-C

- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
    return YES;  // 允許編輯單元格
}

Swift

func tableView(_ tableView: NSTableView, shouldEdit tableColumn: NSTableColumn?, row: Int) -> Bool {
    return true  // 允許編輯單元格
}

處理行選擇事件

Objective-C

- (void)tableViewSelectionDidChange:(NSNotification *)notification {
    NSInteger selectedRow = [tableView selectedRow];
    if (selectedRow != -1) {
        NSLog(@"Selected row: %ld", (long)selectedRow);  // 處理行選擇事件
    }
}

Swift

func tableViewSelectionDidChange(_ notification: Notification) {
    let selectedRow = tableView.selectedRow
    if selectedRow != -1 {
        print("Selected row: \(selectedRow)")  // 處理行選擇事件
    }
}

3. 高階用法

自定義單元格

可以透過建立自定義 NSTableCellView 來實現複雜的單元格佈局。

Objective-C

@interface CustomTableCellView : NSTableCellView
@property (nonatomic, strong) NSTextField *customTextField;
@end

@implementation CustomTableCellView

- (instancetype)initWithFrame:(NSRect)frameRect {
    self = [super initWithFrame:frameRect];
    if (self) {
        _customTextField = [[NSTextField alloc] initWithFrame:frameRect];
        [self addSubview:_customTextField];
    }
    return self;
}

@end
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
    CustomTableCellView *cellView = [tableView makeViewWithIdentifier:@"CustomCell" owner:self];
    if (!cellView) {
        cellView = [[CustomTableCellView alloc] initWithFrame:NSMakeRect(0, 0, tableColumn.width, 20)];
        cellView.identifier = @"CustomCell";
    }
    cellView.customTextField.stringValue = _dataArray[row];
    return cellView;
}

Swift

class CustomTableCellView: NSTableCellView {
    var customTextField: NSTextField?
    
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        customTextField = NSTextField(frame: frameRect)
        if let customTextField = customTextField {
            addSubview(customTextField)
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
    var cellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("CustomCell"), owner: self) as? CustomTableCellView
    if cellView == nil {
        cellView = CustomTableCellView(frame: NSRect(x: 0, y: 0, width: tableColumn?.width ?? 100, height: 20))
        cellView?.identifier = NSUserInterfaceItemIdentifier("CustomCell")
    }
    cellView?.customTextField?.stringValue = dataArray[row]
    return cellView
}

表頭的自定義檢視

可以透過提供自定義表頭檢視來替換預設的表頭檢視。

Objective-C

- (NSTableHeaderView *)tableView:(NSTableView *)tableView viewForHeaderInSection:(NSInteger)section {
    CustomHeaderView *headerView = [tableView makeViewWithIdentifier:@"CustomHeader" owner:self];
    if (!headerView) {
        headerView = [[CustomHeaderView alloc] initWithFrame:NSMakeRect(0, 0, tableView.frame.size.width, 30)];
        headerView.identifier = @"CustomHeader";
    }
    headerView.title = @"Custom Header";
    return headerView;
}

Swift

func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
    var headerView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("CustomHeader"), owner: self) as? CustomHeaderView
    if headerView == nil {
        headerView = CustomHeaderView(frame: NSRect(x: 0, y: 0, width: tableView.frame.size.width, height: 30))
        headerView?.identifier = NSUserInterfaceItemIdentifier("CustomHeader")
    }
    headerView?.title = "Custom Header"
    return headerView
}

處理拖放操作

可以透過實現 tableView(_:writeRowsWith:to:)tableView(_:acceptDrop:row:dropOperation:) 方法來支援拖放操作。

Objective-C

- (BOOL)tableView:(NSTableView *)tableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard {
    // 允許拖動的行,並將相關資料寫入到剪貼簿
    [pboard declareTypes:@[NSPasteboardTypeString] owner:self];
    [pboard setString:[NSString stringWithFormat:@"%ld", rowIndexes.firstIndex] forType:NSPasteboardTypeString];
    return YES;
}

- (NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id<NSDraggingInfo>)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)dropOperation {
    return NSDragOperationMove; // 指示允許移動操作
}

- (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id<NSDraggingInfo>)info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)dropOperation {
    NSPasteboard *pboard = [info draggingPasteboard];
    NSString *rowString = [pboard stringForType:NSPasteboardTypeString];
    NSInteger draggedRow = [rowString integerValue];
    
    // 交換資料來源中的行順序
    id draggedObject = _dataArray[draggedRow];
    [_dataArray removeObjectAtIndex:draggedRow];
    [_dataArray insertObject:draggedObject atIndex:row];
    [tableView reloadData];
    
    return YES;
}

Swift

func tableView(_ tableView: NSTableView, writeRowsWith rowIndexes: IndexSet, to pboard: NSPasteboard) -> Bool {
    // 允許拖動的行,並將相關資料寫入到剪貼簿
    pboard.declareTypes([.string], owner: self)
    pboard.setString("\(rowIndexes.first ?? 0)", forType: .string)
    return true
}

func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation {
    return .move // 指示允許移動操作
}

func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool {
    guard let rowString = info.draggingPasteboard.string(forType: .string),
          let draggedRow = Int(rowString) else {
        return false
    }
    
    // 交換資料來源中的行順序
    let draggedObject = dataArray[draggedRow]
    dataArray.remove(at: draggedRow)
    dataArray.insert(draggedObject, at: row)
    tableView.reloadData()
    
    return true
}

4. 深入探討

NSTableView 的內部機制

NSTableView 是建立在 NSView 之上的,它透過 NSTableColumn 管理列資訊,並使用 rowHeight 屬性控制行高。每個單元格作為 NSView 存在,可以是一個簡單的 NSTextField,也可以是自定義的 NSTableCellView

資料快取和複用機制

NSTableView 使用了一種類似於 UITableView 的資料快取和複用機制。透過 dequeueReusableCell(withIdentifier:) 方法,可以重用已經建立的單元格檢視,提高效能。

效能最佳化策略

  1. 避免過多建立檢視:儘量重用現有檢視,減少不必要的檢視建立。
  2. 接受部分更新:使用 reloadRows(atIndexes:) 方法更新特定行的資料,而不是 reloadData
  3. 非同步資料載入:對於大型資料集,可以使用非同步載入來提高效能。例如,分頁載入或後臺載入資料。

總結

NSTableView 是一個強大且靈活的表格控制元件,透過了解其常見 API 和基礎技巧,可以實現基本的表格展示和互動需求。

相關文章