表格

在這裡製做一個計帳簿程式, 主要是可以記錄時間, 項目及金額. 對文字編輯器有興趣的, 可以參考Ink. 在這裡使用 NSTableView. 這是一個比較複雜的圖形介面, 但 GNUstep 已經處理好許多細節了.

有關 NSTableView, 可參考 Getting Started With NSTableView

使用開啟 Document.gorm. 在視窗中加入一個表格, 並將大小拖放至與視窗同大.

圖形 13.7. 在視窗中加入表格

在視窗中加入表格
在視窗中加入表格

勾選 "Horizontal" scroller. 這在目前不是很重要.

圖形 13.8. NSTableView 屬性

NSTableView 屬性

查看 NSTableView 的 Size 屬性. 點選方框中的直線, 會變成彈簧狀, 表示會隨視窗大小改變.

圖形 13.9. 改變表格自動調整大小屬性

改變表格自動調整大小屬性

方框代表 NSTableView. 直線表示其相對距離不改變, 彈簧表示其距離隨視窗大小改變. 在方框外的線表示 NSTableView 與其 superview 的距離, 在這裡 NSTableView 的 superview 即為視窗本身. 方框內的線則為 NSTableView 的大小. 在這裡, 表格的大小將隨視窗的大小而有改變.

點兩下表格中的 column 可以改變其標題. 在這裡不太重要. 接下來會在程式碼中改變表格的介面.

在 Document 類別中加入一個 tableView 的 outlet.

圖形 13.10. 增加 outlet

增加 outlet

將 NSOwner, 即 Document 類別, 做為表格的代理者 (delegate) 及資料來源 (data source).

圖形 13.11. 設定表格的代理者及資料來源

設定表格的代理者及資料來源
設定表格的代理者及資料來源
設定表格的代理者及資料來源

將 NSOwner 中的 tableView 連結到表格上.

圖形 13.12. 連結 outlet 至表格

連結 outlet 至表格
連結 outlet 至表格
連結 outlet 至表格

將 Document.gorm 存檔後離開.

將新的 outlet 加入 Document.h.

Document.h:

#import <AppKit/AppKit.h>
#import <AppKit/NSDocument.h>

@interface Document : NSDocument
{
   id tableView;
}
@end

NSTableView 顯示資料的方式是, 首先尋問其資料來源有多少資料, 並一一尋問每個欄位的資料. 因此在資料來源中有兩個必需實作的 methods.

Document.m:

- (int) numberOfRowsInTableView: (NSTableView *) view
{
   return 5;
}

- (id) tableView: (NSTableView *) view
       objectValueForTableColumn: (NSTableColumn *) column
       row: (int) row
{
   return [NSString stringWithFormat: @"column %@ row %d", [column identifier], row]; 
}

-numberOfRowsInTableView: 傳回有多少列的資料, -tableView:objectValueForTableColumn:Row: 則一一傳回每個欄位的資料. 欄位由其所在的行 (column) 及列 (row) 所定義. 每行有其專用的 NSTableColumn. 每個 NSTableColumn 有其專用的辨識字串, 因為行的順序可能會被使用者所改變.

這個程式目前已經能執行了. 基本上只是顯示五列的資料. NSTableView 可以顯示任何的物件, 包含圖檔. 接下來要製做有真正功能的程式.

到目前為止的程式碼在此: Table-1-src.tar.gz

首先將 NSTableView 的介面調整一下. NSTableView 是由一系例 NSTableColumn 所組成. 在 Gorm 中拖拉出來的表格已有兩個內定的 NSTableColumn, 還需要額外的一個.

Document.m:

- (void) windowControllerDidLoadNib: (NSWindowController *) controller
{
   NSTableColumn *column;

取得所有 NSTableColumn

   NSArray *columns = [tableView tableColumns];

改變 NSTableColumn 的屬性

   column = [columns objectAtIndex: 0];
   [column setWidth: 100];
   [column setEditable: NO];
   [column setResizable: YES];
   [column setIdentifier: @"date"];
   [[column headerCell] setStringValue: @"Date"];

   column = [columns objectAtIndex: 1];
   [column setWidth: 100];
   [column setEditable: NO];
   [column setResizable: YES];
   [column setIdentifier: @"item"];
   [[column headerCell] setStringValue: @"Item"];

製做一個新的 NSTableColumn, 並加入 NSTableView 中

   column = [[NSTableColumn alloc] initWithIdentifier: @"amount"];
   [column setWidth: 100];
   [column setEditable: NO];
   [column setResizable: YES];
   [[column headerCell] setStringValue: @"Amount"];
   [tableView addTableColumn: column];
   RELEASE(column);

調整 NSTableColumn 的大小.

   [tableView sizeLastColumnToFit];
   [tableView setAutoresizesAllColumnsToFit: YES];
}

在 -windowControllerDidLoadNib: 中調整表格的介面, 因為在這時 Gorm 檔已經被載入了, -windowControllerDidLoadNib: 的作用類似 -awakeFromNib. 重新編譯並執行這個範例, 便可見到新的欄位.

NSTableColumn 最重要的就是其辨識字串 (identifier). 每個 NSTableColumn 都有獨一無二的 identifier. 根據這個 identifier 可以得知其所在的 NSTableColumn. Identifier 不一定需要是字串, 只是習慣上使用字串. 在 GNUstep 中很多物件都使用 identifier 來做為區分物件的方法.

使用者介面完成了, 接著就是資料來源的部份. 在 MVC 架構 中, NSTableView 為 View, 資料來源即可 Model.

習慣上, 每一筆 NSTableView 的資料可以被在 NSDictionary. NSDictionary 的 key 可以使用與 NSTableColumn 的 identifier 相同字串, 好方便存取. 而所有資料則放在 NSArray 中. 如此每一列資料就相當放 NSArray 中的每一個 NSDictionary. 因此, 在這裡使用 NSMutableArray 來做為 NSTableView 的資料來源.

Document.h:

#import <AppKit/AppKit.h>
#import <AppKit/NSDocument.h>

@interface Document : NSDocument
{
   id tableView;
   NSMutableArray *records;
}
@end

有關 NSMutalbeArray 的使用可參考 Basic GNUstep Base Library Classes.

為了方便使用者輸入, 會多一個空白列以方便使用者輸入. 先實做顯示資料的方法.

Document.m:

- (id) init
{
   self = [super init];
   records = [NSMutableArray new];
   return self;
}

- (void) dealloc
{
   RELEASE(records);
   [super dealloc];
}

- (int) numberOfRowsInTableView: (NSTableView *) view
{
   return [records count] + 1;
}

- (id) tableView: (NSTableView *) view
       objectValueForTableColumn: (NSTableColumn *) column 
       row: (int) row
{
   if (row >= [records count])
      {
         return @""; 
      }
   else
      {
         return [[records objectAtIndex: row] objectForKey: [column identifier]];
      }
}

因為要多一個空白列, 在 -numberOfRowsInTableView: 中的傳回值加一. 在 -tableView:objectValueForTableColumn:row: 中就要特別處理多出來的空白標. 傳回空白字串. 最重要的是 NSTableColumn 的 identifier 與 NSDictionary 中的 key 相等, 如此直接使用 NSTableColumn 的 identifier 做為 NSDictionary 的 key 來讀取資料即可, 非常方便. 如果不使用 NSDictionary 做為每一筆資料的資料結構, 可以考慮使用 Key Value Coding (KVC) 這個程式技術, 與 NSDictionary 有異曲同功之妙.

接著加入輸入的功能. 首先讓 NSTableColumn 可以接受輸入.

- (void) windowControllerDidLoadNib: (NSWindowController *) controller
{
   NSTableColumn *column;
   NSArray *columns = [tableView tableColumns];

   column = [columns objectAtIndex: 0];
   [column setWidth: 100];
   [column setEditable: YES];
   [column setResizable: YES];
   [column setIdentifier: @"date"];
   [[column headerCell] setStringValue: @"Date"];

   column = [columns objectAtIndex: 1];
   [column setWidth: 100];
   [column setEditable: YES];
   [column setResizable: YES];
   [column setIdentifier: @"item"];
   [[column headerCell] setStringValue: @"Item"];

   column = [[NSTableColumn alloc] initWithIdentifier: @"amount"];
   [column setWidth: 100];
   [column setEditable: YES];
   [column setResizable: YES];
   [[column headerCell] setStringValue: @"Amount"];
   [tableView addTableColumn: column];
   RELEASE(column);

   [tableView sizeLastColumnToFit];
   [tableView setAutoresizesAllColumnsToFit: YES];
}

當使用者在欄位中點兩下時, 即可輸入新的數值與字串.

資料來源使用 -tableView:setObjectValue:forTableColumn:row: 來儲存資料, 其概念與顯示資料正好相反, 一讀一寫.

Document.h:

- (void) tableView: (NSTableView *) view
         setObjectValue: (id) object
         forTableColumn: (NSTableColumn *) column
         row: (int) row
{
   if (row >= [records count])
      {
         [records addObject: [NSMutableDictionary new]];
      }
   [[records objectAtIndex: row] setObject: object
                                    forKey: [column identifier]];
   [tableView reloadData];
}

在此特別處理空白列的問題. 如果是輸入在空白列, 表示是新的一筆資料, 因此要先加入新的 NSDictionary. 接著依照所處的 NSTableColumn 及列 (row) 將資料加入. 最後使用 -reloadData 更新 NSTableView 的顯示畫面.

現在便可以自行輸入資料了. 程式碼在此: Table-2-src.tar.gz. 在 GNUstep 的文件程式架構中, 只要專心在處理文件的本身就好, 而不需要耽心管理文件的問題. 這就是使用 GNUstep 的方便之處.