4.2 - Document Editor#
We now have the skeleton of Money.app. I want it be a
spreadsheet-like application to track the expense.
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
If you are interested in text editors, Ink.app is a good example.
Creating the table view#
Use Gorm to open Document.gorm. Add a table view into the window. Try to resize it until it fit the whole window.
Figure 4-38. Add table into window
Check the “Horizontal” scroller.
Figure 4-39. Attributes of NSTableView
Look at the Size panel in the inspector of NSTableView
. Click the line in
the Autosizing box to make them springs.
Figure 4-40. 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 4-41. Add outlet for table view
Set NSOwner as the data source and delegate of the NSTableView
. I’ll
explain the data source later.
Figure 4-42. Connect data source and delegate of table view
Connect the outlet tableView
to NSTableView
.
Figure 4-43. Connect outlet to table view
Save the Gorm file and quit Gorm.
Basic data source#
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
works is that when it needs to display, it will ask
its data source to provide the data it needs. So we need to implement
two methods to provide NSTableView
the data it need:
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];
}
The method -numberOfRowsInTableView:
returns how many rows NSTableView
should display – in this case, we’ll display 5 rows. The method
-tableView:objectValueForTableColumn:row:
returns the value shown in a certain cell in the table.
Now, this application is ready to run, even though it does nothing but
display 5 rows of “column 0 row 0”. This is merely a demonstration of how 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 sources later on.
Here is the source code: Table-1-src.tar.gz
Configuring the table view#
Let’s work on the interface first. NSTableView
is a collection of
NSTableColumn
s. I want three columns for the date, item and amount. By default, there are two columns. Therefore, I need to add a
NSTableColumn
into our NSTableView
.
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];
}
We adjust the interface of our NSTableView
in the method
-windowControllerDidLoadNib
:, which guarantees that the Gorm file is
loaded. This is similar to -awakeFromNib
. First, we get the existing
columns and change their properties. Second, we create a new NSTableColumn
and add it into our NSTableView
. Finally, we adjust the layout of our
NSTableView
. This way, we can programatically adjust our NSTableView
without using Gorm to adjust it. Run this application
again, and you will see the new column.
Note
At the time this tutorial was originnally written, Gorm did not allow you to configure NSTableView
.
An important property of NSTableColumn
is its identifier. Each
NSTableColumn
has an unique identifier to distinguish them.
The identifier can be any object, but it’s usually an NSString
. The
identifier does not have to be the same as the header of the column, but
should being the same for easier management. So we access the
NSTableColumn
via its identifier. Many GNUstep objects have identifiers.
Functional data source#
Now that we’ve finished the interface, we can set up a data source. The data source is an
object which provides the data for NSTableView
. Therefore, the data source is
the model in the MVC (Model-View-Controller)
paradigm. Depending on
the behavior of NSTableView
, we need to implement the proper methods in
the data source of NSTableView
. We already implemented those methods, but they give the useless “column 0 row 0” messages, instead of useful data from our “Money Document”.
The data for NSTableView
can be considered as an NSArray
of
NSDictionary
s. 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
. For more information about the usage of
NSMutableArray
, read 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]];
}
}
We create the instance of NSMutableArray
in the method -init
, and release it
in -dealloc
, which will destroy it if no other object needs it. In the method -numberOfRowsInTableView:
, we return one plus the amount of records because we want it to display an extra empty row for the user to add new records. Hence, in the
method -tableView:objectValueForTableColumn:row:
, I have to check
whether the row the NSTableView
requests is larger than the number of actual records. If so, it is a request for data to show in the empty row. Therefore, we just return an empty string (@""
). We are using an array of dictionaries to make the key
of the dictionary the same as the identifier of the NSTableColumn
. So I can
get the object directly by knowing the identifier of NSTableColumn
. If
you are not using an NSDictionary
for each row, you can consider
Key Value Coding
(KVC),
which offers a 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.
Data input#
Now, we’ll add the functionary of data input. First, we have to set the
NSTableColumn
to 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];
}
When the user double-clicks a cell, the user can edit the contents of the cell. When the user finishes, the table view will send -tableView:setObjectValue:forTableColumn:row:
to the data source.
Document.m:
- (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, we need to take care of the special situation where user input in the last
empty row. Since it is not in the records, I need to add a new
dictionary item to the records, to represent a new row. Whenever the user inputs the data, it will be
store into records according its row and the identifier of the column. And
the key in the dictionary is the same as the identifier of the NSTableColumn
.
Hence I can retrieve the data according to the identifier of the 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 it is to make a real document-based application without worrying about the management of multiple documents and windows.