Table

I have the skeleton of document-base application already. I want it be a spreadsheet-like application to track the expense. If you are interested in text editors, Ink.app in GNUstep CVS is a pretty good one. It involves the text system of GNUstep, which is a big topic. Spreadsheet-like applications need a table. NSTableView is a good start. NSTableView is a more complicated user interface than NSButton, NSTextField, etc. So do NSBrowser, NSOutlineView, NSMatrix, etc. GNUstep does a great job to make it very easy to use. I'll try to explain it step by step.

Here is a related article: Getting Started With NSTableView

Use Gorm to open Document.gorm. Add a table view into the window. Try to resize it until it fit the whole window.

Figure 13.7. Add table into window

Add table into window
Add table into window

Check the "Horizontal" scroller. Actually it doesn't matter for now.

Figure 13.8. Attributes of NSTableView

Attributes of NSTableView

Look at the "Size" in the inspector of NSTableView. Click the line in the box to make them the spring.

Figure 13.9. Set resize attribute of table view

Set resize attribute of table view

The box represent the NSTableView. The straight line or spring represent the distance relationship. Line outside the box is the distance between NSTableView and its superview. It is the window in this case. The line inside the box is the size of the NSTableView. Straight line means the distance is fixed, the spring means it is resizable. In this case, when window is resized, since the distance between NSTableView and window is fixed, NSTableView will be resized according to the window. That's the behavior I want.

You can change the title of the column by double-click on it. But it is not necessary for now. You will find that it is still hard to control the interface of NSTableView from Gorm. I'll do that programmingly. Therefore, I need a outlet connected to this NSTableView from NSOwner.

Add an outlet, tableView, in the class "Document".

Figure 13.10. Add outlet for table view

Add outlet for table view

Set NSOwner as the data source and delegate of the NSTableView. I'll explain the data source later.

Figure 13.11. Connect data source and delegate of table view

Connect data source and delegate of table view
Connect data source and delegate of table view
Connect data source and delegate of table view

Connect the outlet "tableView" to NSTableView.

Figure 13.12. Connect outlet to table view

Connect outlet to table view
Connect outlet to table view
Connect outlet to table view

Save the Gorm file and quit Gorm.

Add the new outlet in Document.h.

Document.h:

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

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

The way NSTableView work is that when it need to display, it will ask its data source to provide the data it needs. So I need to implement some methods to provide NSTableView the data it need. There are necessary two 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]; 
}

Method -numberOfRowsInTableView: will ask how many rows NSTableView should display. I set it to 5. Method -tableView:objectValueForTableColumn:Row: will ask the object for a given column and row. I return a string with the identifier of column and the number of row.

Now, this application is ready to run, even though it does nothing but display the data of 5 rows. This is merely a demo how the NSTableView works. I provide the number of rows, and the object in a given column and row. As long as these two kinds of data are provided, the NSTableView can display anything, even a image in the cell. I'll talk about more details about data source later on.

Here is the source code: Table-1-src.tar.gz

Let's work on the interface first. NSTableView is a collection of NSTableColumn. I want three columns for date, item and amount. There are two default columns in the Gorm. Therefore, I need to add a NSTableColumn into it.

Document.m:

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

   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"];

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

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

I adjust the interface of NSTableView in method -windowControllerDidLoadNib:, which quarantees that the Gorm file is loaded. This is similar to -awakeFromNib. Firstly, I get the existing columns, change their property. Secondly I create a new NSTableColumn and add it into NSTableView. Finally, I adjust the layout of NSTableView. By this way, I can arrange the user interface programmingly even though Gorm is not fully functional yet. Run this application again, and you will see the new column.

An important thing of NSTableColumn is the "identifier". Every NSTableColumn has an unique "identifier" to distinguish them. "Identifier" can be any object, but used to being NSString. The identifier is not necessary to be the same as the header of column, but used to being the same for easilier management. So we access the NSTableColumn via its identifier. You can find the "identifier" in many GNUstep objects.

The interface is done. Go back for the data source. Data source is an object which provide the data for NSTableView. Therefore, data source is the model of NSTableView in MVC paradigm. Depending on the behavior of NSTableView, I need to implement the proper methods in the data source of NSTableView. I already show the methods for display. But they are useless. Now, I will make them real functional.

The data for NSTableView can be considered as an NSArray of NSDictionary. The object in each index of NSArray corresponds to each row of NSTableView. And the object of each NSDictionary with a given key corresponds to each NSTableColumn with a given identifier. That's the simplest way to build the model for NSTableView. Therefore, I add an NSMutableArray in Document class.

Document.h:

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

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

The "records" will store the data of NSTableView. About the usage of NSMutableArray, read Basic GNUstep Base Library Classes.

I want there are always an empty row so that user can add data into the table. Before I add the function of adding data, look at the new methods for display.

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]];
      }
}

I create the instance of NSMutableArray in method -init, and release it in -dealloc. In the method -numberOfRowsInTableView:, I return one more number because I want it to display an extra empty row. Hence, in the method -tableView:objectValueForTableColumn:row:, I have to check whether the row the NSTableView request is large than the actuall number of data. If so, it is request the empty row. I just return an empty string "". The trick to use a NSArray of NSDictionary is to make the key of NSDictionary the same as the identifier of NSTableColumn. So I can get the object directly by knowing the identifier of NSTableColumn. If you are not using the NSDictionary for each row, you can consider the Key Value Coding (KVC), which offer similar way to get the right object. Otherwise, you have to use if-else to get the right object. The advantage of NSDictionary (or KVC) will be more clear for data input.

Now, I'll add the function of data input. Firstly, I have to set the NSTableColumn editable.

- (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];
}

Once the users double-click each cell in the table, they can input the data.

The way data source receive the data is in the method -tableView:setObjectValue:forTableColumn:row:, which is the opposite of the method for display.

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];
}

Again, I need to take care the situation when user input in the last empty row. Since it is not in the records, I need to add a new NSMutableDictionary first. When ever user input the data, it will be store into records according its row and the identifier of column. And the key of NSDictionary is the same as the identifier of NSTableColumn. Hence I can retrieve the data according to the identifier of column. Finally I ask the NSTableView to reload the data in order to reflect the change of data source.

Now you can play around this application and input the data. Here is the source code: Table-2-src.tar.gz. This example shows how easy I can make a real document-base application without worry about the management of multiple documents and windows.