Miscellaneous Display Attributes

Formatting the Tabular Data

Another customizable aspect of the NSTableView display process is in data formatting. Whenever it receives a data object for a specific row and column, the NSTableView renders the data in human-readable form by sending a description message to the object. The object then returns a non-localized and unformatted textual description of itself as an NSString.

You can assign an NSFormatter object to the NSTableView data cell in order to display
its data in a specific format. One way to accomplish this is to use Interface Builder
to drag-and-drop one of two NSFormatter objects from the Cocoa palette onto a specific
NSTableColumn. You can then configure the NSFormatter object through the Inspector
window, as shown in Figure 14. Choose Show Inspector from the Tools menu to display
the Inspector window.

Using Interface Builder to assign an NSFormatter to your NSTableView object is sufficient if all you need is the functionality built into the NSNumberFormatter and NSDateFormatter objects, which is what the palette provides. However, for more sophisticated formatting needs, you will need to subclass the NSFormatter class and override the following two methods:

- (NSString *)stringForObjectValue:(id)anObj

Convert the data contained in anObj to the appropriate human-readable representation as an NSString.

Preferably, the class type used by anObj is the same one used in the stringForObjectValue: message.

This method is only required if the NSTableView object is displaying editable data.

Creating a custom NSFormatter object allows you to control the data format to a degree
not possible with either an NSNumberFormatter or an NSDateFormatter object. For
example, your custom NSFormatter can take an NSNumber object and convert its numerical
value as a hexadecimal string. Another example is to have your NSFormatter take
a file size (in bytes) and convert it to kilobytes (Listing 4).

Listing 4. An NSFormatter class that converts between bytes and kilobytes.

The Table Delegation Process

The NSTableView class uses delegation to provide control over some of its events. The generated messages are often sent to the controller object designated as the delegate target for the table view. Some messages are used to track changes in a table event. Others are used to validate a specific event by responding with a YES or NO value. Not all delegate messages, however, are supported in versions of MacOS X older than 10.4. To ensure backward compatibility, use the following #if...#endif directive block to encapsulate the newer messages.

Interface Builder makes it easy to bind a delegate target to the table view. Simply follow the same directions for binding a data-source target to the table view; except this time, choose the delegate option from the Inspector panel (Figure 10). The delegate target can also be bound manually by sending a [setDelegate:] message to the table view. For example, sending [aTable setDelegate:aDelegate]; binds the controller, aDelegate, as the delegate target of aTable. Note that multiple table views can use the same delegate target. It is up to the target to determine which view generated the message. In some of the following code examples, this is done by sending a [tag] message to each aTable parameter. aTable responds by returning an NSNumber containing the unique tag value assigned to the table view through the Inspector panel (Figure 3).

Figure 11. Binding a delegate target to the table view.

To best demonstrate their functionality, most of the NSTableView delegate messages are categorized in terms of five table view events. Also, code fragments for each delegate message are provided in this article. For detailed descriptions about each NSTableView delegate messages, consult one the official Apple documents listed at the end of this article. Also review the TableDemo source file, DemoDelegate.m, for additional code examples for each message.

Figure 11 is the sequence diagram of two table resize events. In the first scenario, the user first resizes the NSWindow view, aWindow, either by dragging its grow icon or by choosing Zoom from the Window menu. If aDelegate is assigned to be the delegate target, aWindow sends a [windowWillResize:toSize:] message informing the delegate of the change in window size. Note that this delegate message is generated only by NSWindow. Neither NSView nor NSPanel generates the aforementioned message.

Afterwards, aWindow passes the resize event to its NSTableView object, aTable, which then checks if its columnAutoResizingStyle flag is not set to NSTableViewNoColumnResizing. If it is not, aTable sends a [tableViewColumnDidResize:] message to aDelegate. This informs aDelegate the table column subviews that have been resized. The order in which each subview is resized depends largely on the setting used by columnAutoresizingStyle flag of aTable.

In the second resize scenario, the user manually resizes a table column by placing the cursor over a column header border. If the NSTableView flag, allowColumnResizing, is set to YES, aTable changes the cursor image from an arrow icon to a drag icon. It then sends a [tableView:mouseDownInHeaderOfTableColumn:] message to aDelegate after a mouse-down signal. The message informs aDelegate which column subview is located on the lefthand side of the cursor during the resize event.

On the next mouse-up signal, aTable sends two [tableViewColumnDidResize:] messages to aDelegate. The first message is for the column subview that was resized, while the second is for the subview affected by the resize. However, if the user only resized the rightmost subview, aTable sends only a single [tableViewColumnDidResize:] message for that subview.

Figure 12. Table column resize event.

The [tableView:mouseDownInHeaderOfTableColumn:] message (Listing 5) determines which column header has been clicked on by the user. It provides two input parameters: aTable, the table view that generated the message; and aCol, the column subview that owns the header. After identifying which table generated the message, the delegate sends an [identifier] message to aCol thus determining which column subview was selected by the user. Notice that both aCol and aTable use a different NSObject class for their respective unique identifiers.

The [tableViewColumnDidResize:] message (Listing 6) determines which column subview has been resized. It provides a single input parameter, aSig, which is an NSNotification object containing additional details about the message. To determine which table view generated the message, an [object] message is first sent to aSig to retrieve the NSTableView reference. Then, a [tag] message is send to the reference to retrieve the table's unique identifier.

To determine which column subview has been resized, a [userInfo] message is first sent to aSig. This returns an NSDictionary object containing additional information about the notification signal. Then, an [objectForKey:] message is sent to the dictionary with a key value of @"NSTableColumn". The dictionary responds by returning an NSTableColumn reference. Finally, an [identifier] message is sent to the reference thus retrieving the subview's unique identifier.

The second table event is a column reorder event, whose sequence diagram is shown in Figure 12. This event occurs when the user drags a column subview to a different location on the table. Each time a user selects a column subview by clicking on its header, aTable sends a [tableView:mouseDownInHeaderOfTableColumn:] message to aDelegate. Then, the moment the user starts dragging, aTable checks the status of its allowsColumnReordering flag. If the flag is set to YES, aTable displays a translucent image of the column subview being dragged. Also, if the dragged image moves at least halfway across another column subview, aTable repositions that subview in the opposite direction of the drag movement.

Once the user finishes the drag, thus generating a mouse-up signal, aTable checks to see if the column subview has indeed been moved. If so, it sends a [tableViewColumnDidMove:] message to aDelegate. It also sends a [tableView:didDragTableColumn:] message signifying the end of the reorder event. However, if the column subview did not move, only the [tableView:didDragTableColumn:] message is sent to aDelegate.

Figure 13. Table column reorder event.

Two new delegate messages are generated during the column reorder event. The first one, [tableViewColumnDidMove:], (Listing 7) determines the old and new locations of the column subview. Like [tableViewColumnDidResize:], this message provides a single NSNotification parameter, aSig. The same code used in Listing 6 to determine the table view that generated the message as well as the column subview that was moved is also applicable here. Additionally, the old and new locations of the column subview is determined by also sending an [objectForKey:] message to the userInfo dictionary. To determine the old column index, send the message with a key value of @"NSOldColumn". To determine the new column index, send the same message but with a key value of @"NSNewColumn". Either one will return an NSNumber containing the specified index.

The second message, [tableView:didDragTableColumn:], determines which column subview has been moved by the user. As shown in Figure 12, this message is sent whether or not the column subview was moved to a new location. The message shares the same input parameters as [tableView:mouseDownInHeaderOfTableColumn:]. Because of this, the code fragment shown in Listing 5 can be used to handle this message with minimal modifications.

The next possible table event is the column selection event; its sequence diagram is shown in Figure 13. Like in the previous events, aTable first sends a [tableView:mouseDownInHeaderOfTableColumn:] message to aDelegate indicating which column header has been clicked. Then, on the next mouse-up signal within the same header, aTable checks the status of its allowsColumnSelection flag. If it is set to YES, aTable sends at most four additional messages to aDelegate in the following order:

a [selectionShouldChangeInTableView:] message asking if the table selection focus should be allowed to change. aDelegate responds by returning a YES to allow the change; otherwise, it returns a NO.

a [tableView:shouldSelectTableColumn:] message asking if the table column should be selected. Again, aDelegate responds with either a YES or NO. This is sent only after aDelegate has responded with a YES to the [selectionShouldChangeInTableView:] message.

a [tableViewSelectionIsChanging:] message indicating that the table selection focus is about to change. Again, it is only sent after aDelegate responded with a YES to the [tableView:shouldSelectTableColumn:] message.

a [tableViewSelectionDidChanged:] message indicating that the table selection focus has successfully changed.

At the end of the sequence, aTable sends a [tableView:didClickedTableColumn:] message to aDelegate indicating the end of the selection event. Like its predecessor, the [tableView:mouseDownInHeaderOfTableColumn:] message, it informs aDelegate that a column header has been clicked on by the user. However, whereas the predecessor is sent after a mouse-down signal, the [tableView:didClickedTableColumn:] message is sent after a mouse-up signal. It is also sent regardless of whether or not all of the four messages listed previously were generated and sent by aTable.

Figure 14. Table column selection event.

Five additional delegate messages are generated during the column selection event. The first message, [selectionShouldChangeInTableView:] (Listing 8), asks the delegate if the change in selection should be allowed. It provides a single input parameter, aTable, indicating the source of the message. The delegate determines the table row and column being selected by respectively sending a [selectedRow] and a [selectedColumn] message to aTable. aTable responds by returning the indices corresponding to the selected row and column. In the case of a column selection event, however, aTable always returns a 0 for the row and a -1 for the column. Either way, the delegate validates the change request and returns the appropriate BOOL value.

The second message, [tableView:shouldSelectTableColumn:] (Listing 9), asks the delegate if the specified table column should be selected. It provides the same input parameters as the [tableView:mouseDownInHeaderOfTableColumn:] message. The delegate also uses the same approach shown in Listing 5 to determine the source of the message and the column subview being selected. It then verifies the data collection associated with the subview and returns the appropriate BOOL value.

The next two messages, [tableViewSelectionIsChanging:] and [tableViewSelectionDidChanged:], inform the delegate about the change in selection focus. The first one tells the delegate that the selection focus is about to change whereas the second one tells that the focus has successfully changed. Both messages uses the same input parameter, aSig, as [tableViewColumnDidResize:]. Because of this similarity, the same code fragment shown in Listing 6 can be used to handle the two messages with minimal modification.

The fifth and final message generated in the column selection event is [tableView:didClickedTableColumn:]. This message tells the delegate which column subview had the selection focus after the mouse-up signal. It also shares the same input parameters as the [tableView:mouseDownInHeaderOfTableColumn:] message. Hence, the same code fragment shown in Listing 5 can be used to handle this message.

Another possible table event is a table row selection. Figure 14 shows the sequence diagram of that event. When the user clicks on a table row to select it, aTable first sends a [selectionShouldChangeInTableView:] message to aDelegate, which then responds with either a YES or NO value. If aDelegate responds with a YES, aTable then sends a [tableView:shouldSelectRow:] message to aDelegate, which again responds with either a YES or NO value. Finally, on the next mouse-up signal, aTable sends a [tableViewSelectionDidChanged:] message to aDelegate signalling the end of the sequence.

The above sequence, however, is correct if and only if the allowsMultipleSelection flag of aTable is set to NO, meaning that the user can only select one tale row at the time. Setting that flag to YES enables the user can select more than one table row. Then each time the user adds another row to the selection, aTable sends the [selectionShouldChangeInTableView:] message 12 times after the [tableViewIsChanging:] message.

Another special case is when the allowsEmptySelection flag of aTable is set to NO. This means that aTable automatically selects the first valid table row after receiving an awakeFromNib message. It will also send a single [tableViewSelectionDidChanged:] message informing aDelegate about the selection. Afterwards, other row selections follows the sequence described above.

Figure 15. Table row selection event.

Most of the delegate messages used by the table row selection are the same ones used in the table column selection. The only new message introduced by this event is the [tableView:shouldSelectRow:] message (Listing 10). This message allows the delegate to validate the row selection in progress. It provides two input parameters: aTable, the reference to the table view that generated the message; and aRow, the index of the selected table row as a signed integer. The delegate returns a YES to allow the row selection; otherwise, it returns a NO.

The fifth and final table event is a table cell edit; its sequence diagram is shown in Figure 15. This event occurs when the user double-clicks on a table cell thus initiating an inline editing session. aTable first sends a [selectionShouldChangeInTableView:] message to aDelegate. aDelegate confirms the change in selection by responding with a YES value. Then, aTable sends a [tableView:shouldSelectRow:] message, which aDelegate also responds with a YES value confirming the row selection. Finally, aTable sends a [tableViewSelectionIsChanging:] and a [tableViewSelectionDidChanged:] message informing aDelegate of the change in row selection. It then sends a [tableView:shouldEditTableColumn:row:] message, which ends the event sequence.

However, if the allowsEmptySelection flag is set to NO, aTable skips the first four delegate messages and sends only the [tableView:shouldEditTableColumn:row:] message to aDelegate when the user double-clicks on the automatically selected row. Also, if the user double-clicks on a different row, the same sequence of five messages shown in Figure 15 are sent to aDelegate.

Figure 16. Table cell edit event.

The first four messages during the cell edit event are exactly the same ones generated during a table row selection event. However, only the cell edit event generates the [tableView:shouldEditTableColumn:row:] message (Listing 11). This message informs the delegate that a table data cell is about to be edited. It provides three input parameters that the delegate can use to determine the table cell being edited. If the delegate wants the inline-editing to proceed, it returns a YES value. Otherwise, it returns a NO, which aborts the edit session while keeping the selection focus on the double-clicked row.

Finally, the NSTableView class generates three other delegate messages, none of which fits in the five event categories featured here. The first message, [tableView:toolTipForCell:rec:tableColumn:row:mouseLocation:], is sent to the delegate when the cursor remains over a specific table data cell for a predetermined time (Listing 12). The delegate then returns an NSString object containing the message to be displayed by the table view as a tooltip.

The message provides six input parameters, only four of which are of any general significance. The first one, aTable, refers to the table view that generated the message. The aCol parameter refers to the column subview located underneath the cursor. The aRow parameter is the index of the table row underneath the cursor. Finally, the fourth parameter, aCell, refers to the table cell located underneath the cursor at the time of the message. Sending either an [intValue], [stringValue], or [objectValue] message to aCell returns the data contained by that cell.

The other two delegate messages, [tableView:heightOfRow:] and [tableView:willDisplayCell:forTableColumn:row:], are sent to the delegate during the data-source process (Figure 3). They provide the delegate some degree of control over the display of table data. The first message, [tableView:heightOfRow:] (Listing 13), is sent right before the data-source message, [tableView:objectValueForColumn:row:]. It enables the delegate to vary the row height so as to better fit the tabular data. The delegate then returns the new height value as a float.

The message provides two input parameters to the delegate. aTable refers to the table view that generated the message while aRow is the index of the table row about to be updated. Since the message itself is sent before any tabular data is available for display, the delegate has to directly query the data-source controller for the data corresponding to the specified row. In the example shown, the row data is provided by the data-source controller as an NSDictionary object through the [getDataForRow:] accessor. The delegate invokes this accessor and uses the returned data to determine the appropriate row height.

The second delegate message, [tableView:willDisplayCell:forTableColumn:row] (Listing 14), is sent to the delegate right after [tableView:objectValueForColumn:row:]. It provides the same input parameters as [tableView:toolTipForCell:rect:tableColumn:row:mouseLocation:], minus those that are graphics-related. The message allows the delegate to customize the display attributes for each data cell. For instance, higher-priority data items would be displayed in bold-red fonts, while lower-priority ones in plain-blue fonts. However, it should not be used to directly alter the actual table data, especially if data-editing is supported. For such situations, use an NSFormatter object instead to customize the data format.

Final Thoughts

The NSTableView class is a very versatile and feature-rich member of the Application Framework. We have only touched the bare basics on how to use this view to display tabular data in a Cocoa application. But we were able to demonstrate how to effectively implement a controller that would serve as a data source for the NSTableView class. We learned how to configure various visual aspects of the view using the appropriate modifier message.

We also learned how this view delegates various aspects of its display process to a separate controller. We were able to demonstrate how to implement either inline editing (through a data source) or panel editing using a separate controller. We demonstrated how to effectively bind a custom NSFormatter object to the NSTableView data cell, thus enabling the view to automatically format its tabular data to our own liking.

References

The following list of references are the official developer documentation used to write this article. The latest revisions of these documents can also be viewed online by visiting the Apple Developer website at http://developer.apple.com/documentation/Cocoa/Reference/ApplicationKit.