Grid Effects
Another cool thing about Cocoa tables is the ability to completely change how the grid is drawn. The article shows how to write some generic code that displays thin, thick or double line for any row or column of a table. As an example, we’ll be developing a table that looks like a pen-and-paper accounting journal.

Setup
We’re going to need a way to tell the table how to draw the grid. In Cocoa the most logical way to do this is to ask the datasource how to draw each row and column in a similar manner to how an NSTableView asks its NSTableDataSource for the data in each cell. This allows the data to reside in one class and to have a similar execution path to retrieve the data.
Open the header file for your table view and add the following code:
@interface NSObject(AGridEffectDataSource)
// Returns a structure indicating how to draw the top and bottom grid
// lines for the specified row.
- (RowBorders)tableView:(NSTableView*)tableView bordersForRow:(int)rowIndex;
// Returns a structure indicating how to draw the left and right grid
// lines for the specified column.
- (ColumnBorders)tableView:(NSTableView*)tableView
bordersForTableColumn:(NSTableColumn*)tableColumn;
@end
The code above is known as an informal protocol. It differs from a formal protocol in that classes which implement the protocol are not required to implement every function in the protocol. The downside is that an informal protocol receives no run-time or compile-time support. This particular informal protocol says that any object whose root is NSObject (every Obj-C object in a standard Cocoa environment) may implement either or both of the functions listed.
Notice that the return types for the functions listed in the AGridEffectDataSource protocol are for data structures that don’t exist yet. Let’s make those now:
// The styles in which a grid line may be drawn...
typedef enum
{
kCellBorderNone = 0,
kCellBorderSingle,
kCellBorderDouble,
kCellBorderThick
} CellBorderStyle;
// RowBorders specifies the style and color of the top and bottom edges
// of a row. Both topColor and bottomColor should be autoreleased
// objects. If either color is NULL, the default grid color will be
// used (assuming the line is drawn at all).
typedef struct
{
CellBorderStyle top;
CellBorderStyle bottom;
NSColor* topColor;
NSColor* bottomColor;
} RowBorders;
// ColumnBorders specifies the style and color of the left and right
// edges of a column. Both leftColor and rightColor should be
// autoreleased objects. If either color is NULL, the default grid
// color will be used (assuming the line is drawn at all).
typedef struct
{
CellBorderStyle left;
CellBorderStyle right;
NSColor* leftColor;
NSColor* rightColor;
} ColumnBorders;
Now its time for the routines that will draw these borders. Add the following two function declarations to your table view interface declaration:
- (void)drawColumnBorders:(ColumnBorders)borders
forRect:(NSRect)columnRect inClipRect:(NSRect)clipRect;
- (void)drawRowBorders:(RowBorders)borders forRect:(NSRect)rowRect
inClipRect:(NSRect)clipRect;
Below is the code listing for these functions. As with most drawing routines, these functions consist of a large number of similar, but unique drawing commands. Though there would be methods of factoring these functions into smaller components, they would reduce the clarity of the code.
Only three things really bear mentioning about this code.
- (void)drawColumnBorders:(ColumnBorders)borders
forRect:(NSRect)columnRect inClipRect:(NSRect)clipRect
{
//
// Left side
//
// Set the color for drawing on the left side
if (NULL == borders.leftColor)
[[NSColor gridColor] set];
else
[borders.leftColor set];
// Draw the border on the left side
switch (borders.left)
{
case kCellBorderSingle:
{
// Draw a single line down the left side of the column rect,
// clipping it to the clipRect.
NSRect drawRect = columnRect;
drawRect.origin.x -= 1;
drawRect.size.width = 1;
drawRect = NSIntersectionRect(drawRect, clipRect);
NSRectFill(drawRect);
}
break;
case kCellBorderDouble:
{
// Calculate a single line to either side of the column border
NSRect lineOne = columnRect;
lineOne.size.width = 1;
NSRect lineTwo = lineOne;
lineTwo.origin.x -= 2;
// Clip and draw the lines
lineOne = NSIntersectionRect(lineOne, clipRect);
NSRectFill(lineOne);
lineTwo = NSIntersectionRect(lineTwo, clipRect);
NSRectFill(lineTwo);
}
break;
case kCellBorderThick:
{
// Draw a two pixel line centered on the boundary. Because of
// Quartz we can use fractional line positions.
NSRect drawRect = columnRect;
drawRect.origin.x -= 0.5;
drawRect.size.width = 2;
drawRect = NSIntersectionRect(drawRect, clipRect);
NSRectFill(drawRect);
}
break;
default:
// Default case is empty, but still used to prevent compiler
// warning.
break;
}
//
// Right side
//
// Set the color for drawing on the right side
if (NULL == borders.rightColor)
[[NSColor gridColor] set];
else
[borders.rightColor set];
// Draw the border on the right side
switch (borders.right)
{
case kCellBorderSingle:
{
// Draw a single line down the left side of the column rect,
// clipping it to the clipRect.
NSRect drawRect = columnRect;
drawRect.origin.x += drawRect.size.width - 1;
drawRect.size.width = 1;
drawRect = NSIntersectionRect(drawRect, clipRect);
NSRectFill(drawRect);
}
break;
case kCellBorderDouble:
{
// Calculate a single line to either side of the column border
NSRect lineOne = columnRect;
lineOne.origin.x += lineOne.size.width - 1;
lineOne.size.width = 1;
NSRect lineTwo = lineOne;
lineTwo.origin.x -= 2;
// Clip and draw the lines
lineOne = NSIntersectionRect(lineOne, clipRect);
NSRectFill(lineOne);
lineTwo = NSIntersectionRect(lineTwo, clipRect);
NSRectFill(lineTwo);
}
break;
case kCellBorderThick:
{
// Draw a two pixel line centered on the boundary. Because of
// Quartz we can use fractional line positions.
NSRect drawRect = columnRect;
drawRect.origin.x += drawRect.size.width - 1.5;
drawRect.size.width = 2;
drawRect = NSIntersectionRect(drawRect, clipRect);
NSRectFill(drawRect);
}
break;
default:
// Default case is empty, but still used to prevent compiler
// warning.
break;
}
}
- (void)drawRowBorders:(RowBorders)borders forRect:(NSRect)rowRect
inClipRect:(NSRect)clipRect
{
//
// Top side
//
// Set the color for drawing on the top side
if (NULL == borders.topColor)
[[NSColor gridColor] set];
else
[borders.topColor set];
// Draw the border on the top side
switch (borders.top)
{
case kCellBorderSingle:
{
// Draw a single line down the left side of the column rect,
// clipping it to the clipRect.
NSRect drawRect = rowRect;
drawRect.size.height = 1;
drawRect = NSIntersectionRect(drawRect, clipRect);
NSRectFill(drawRect);
}
break;
case kCellBorderDouble:
{
// Calculate a single line to either side of the column border
NSRect lineOne = rowRect;
lineOne.size.height = 1;
NSRect lineTwo = lineOne;
lineTwo.origin.y -= 2;
// Clip and draw the lines
lineOne = NSIntersectionRect(lineOne, clipRect);
NSRectFill(lineOne);
lineTwo = NSIntersectionRect(lineTwo, clipRect);
NSRectFill(lineTwo);
}
break;
case kCellBorderThick:
{
// Draw a two pixel line centered on the boundary. Because of
// Quartz we can use fractional line positions.
NSRect drawRect = rowRect;
drawRect.origin.y -= 0.5;
drawRect.size.height = 2;
drawRect = NSIntersectionRect(drawRect, clipRect);
NSRectFill(drawRect);
}
break;
default:
// Default case is empty, but still used to prevent compiler
// warning.
break;
}
//
// Bottom side
//
// Set the color for drawing on the bottom side
if (NULL == borders.bottomColor)
[[NSColor gridColor] set];
else
[borders.bottomColor set];
// Draw the border on the bottom side
switch (borders.bottom)
{
case kCellBorderSingle:
{
// Draw a single line down the left side of the column rect,
// clipping it to the clipRect.
NSRect drawRect = rowRect;
drawRect.origin.y += drawRect.size.height - 1;
drawRect.size.height = 1;
drawRect = NSIntersectionRect(drawRect, clipRect);
NSRectFill(drawRect);
}
break;
case kCellBorderDouble:
{
// Calculate a single line to either side of the column border
NSRect lineOne = rowRect;
lineOne.origin.y += lineOne.size.height - 1;
lineOne.size.height = 1;
NSRect lineTwo = lineOne;
lineTwo.origin.y -= 2;
// Clip and draw the lines
lineOne = NSIntersectionRect(lineOne, clipRect);
NSRectFill(lineOne);
lineTwo = NSIntersectionRect(lineTwo, clipRect);
NSRectFill(lineTwo);
}
break;
case kCellBorderThick:
{
// Draw a two pixel line centered on the boundary. Because of
// Quartz we can use fractional line positions.
NSRect drawRect = rowRect;
drawRect.origin.y += drawRect.size.height - 1.5;
drawRect.size.height = 2;
drawRect = NSIntersectionRect(drawRect, clipRect);
NSRectFill(drawRect);
}
break;
default:
// Default case is empty, but still used to prevent compiler
// warning.
break;
}
}
Now we’ve got a way to ask the datasource which rows and columns have grid lines and a way to draw the lines. Let’s add the method that will actually perform these operations at run-time. The function below does five major tasks.
- (void)drawGridInClipRect:(NSRect)clipRect
{
// Determine the range of columns and rows intersected by the clip
// rect
NSRange columns = [self columnsInRect:clipRect];
NSRange rows = [self rowsInRect:clipRect];
// Make sure the row border function is implemented in the
// datasource
if ([[self dataSource] respondsToSelector:
@selector(tableView:bordersForRow:)])
{
// Loop through the rows, drawing the border for each
int row;
for (row = rows.location; row < rows.location + rows.length;
++row)
{
// Get the border's rectangle
NSRect rowRect = [self rectOfRow:row];
// Get the borders
RowBorders borders = [[self dataSource] tableView:self
bordersForRow:row];
// Draw the border
[self drawRowBorders:borders forRect:rowRect
inClipRect:clipRect];
}
}
// Grids often look best if the column lines end with the last row.
// This will happen automatically if the table needs to be scrolled,
// but if all the data can fit in one window, the column rectangle
// reported by rectOfColumn will include the area that has no rows.
// To correct this, we'll calculate the final position of the last
// row.
NSRect lastRowRect =
[self rectOfRow:rows.location + rows.length - 1];
float maxHeight = lastRowRect.origin.y + lastRowRect.size.height;
// Make sure the column border function is implemented in the
// datasource
if ([[self dataSource] respondsToSelector:
@selector(tableView:bordersForTableColumn:)])
{
// Loop through the columns, drawing the border for each
int column;
for (column = columns.location;
column < columns.location + columns.length; ++column)
{
// Get the rectangle and table column to pass to the
// datasource
NSRect columnRect = [self rectOfColumn:column];
NSTableColumn* tableColumn = [[self tableColumns]
objectAtIndex:column];
// Get the borders for the table column from the datasource
ColumnBorders borders = [[self dataSource] tableView:self
bordersForTableColumn:tableColumn];
// Draw the border, accounting for the fact that we don't want
// to draw outside the row area
columnRect.size.height = maxHeight;
[self drawColumnBorders:borders forRect:columnRect
inClipRect:clipRect];
}
}
}
There’s a couple more details to take care of in regards to the table behavior. The first is that Interface Builder creates secondary columns that draw their own background over the table. The second is that with double lines used frequently in the table, its best to increase the spacing between cells a little. And lastly, we’d like the row number to appear in a medium green instead of black to more closely simulate a paper tablet. The following code in
awakeFromNib accomplishes these three objectives:- (void)awakeFromNib
{
// Interface Builder creates the secondary columns of the table view
// with cells that draw their background color. Since the
// background color will draw over top of the table background, tell
// the columns not to draw the background.
NSEnumerator* enumerator = [[self tableColumns] objectEnumerator];
id column;
while (column = [enumerator nextObject])
[[column dataCell] setDrawsBackground:NO];
[self setDrawsGrid:YES];
// Set the intercell spacing a little wider then the default so that
// the values aren't crunched with the lines
[self setIntercellSpacing:NSMakeSize(7.0, 6.0)];
// Set the text color of the first column to a medium green
[[[self tableColumnWithIdentifier:@"row"] dataCell]
setTextColor:[NSColor colorWithCalibratedRed:0.0 green:0.85546875
blue:0.4296875 alpha:1.0]];
}
The custom table view is now all set to draw an accounting spreadsheet! Below is the source for the AFakeDataSource class that we created to produce the table shown at the beginning of the article. Because this is a sample, we populate the data directly in the code rather then loading it from XML, SQL or some proprietary data format.
FakeDataSource.h:
// Copyright © 2003 Art & Logic, Inc. All rights reserved.
// $Id: index.php,v 1.1 2005/09/20 22:42:17 jdueck Exp $
@interface AFakeDataSource : NSObject
{
// An array of dictionaries that holds the data displayed in the
// table. Normally this data structure would be created by reading
// values from disk or replaced by functions accessing some kind of
// database. In this sample, we'll simply create the data right in
// the code.
NSArray* fData;
// This time we'll cache the color used to draw green lines in the
// table.
NSColor* fGreenColor;
}
@end
FakeDataSource.m
// Copyright © 2003 Art & Logic, Inc. All rights reserved.
// $Id: index.php,v 1.1 2005/09/20 22:42:17 jdueck Exp $
#import "FakeDataSource.h"
#import "GridEffectTableView.h.h"
@implementation AFakeDataSource
- (void)awakeFromNib
{
// Create the color used to draw non-accented lines in the table
fGreenColor = [[NSColor colorWithCalibratedRed:0.0 green:0.85546875
blue:0.4296875 alpha:1.0] retain];
// Fill an array with the data used to populate the table. Each
// element of the array corresponds to a row in the table. The
// elements consist of dictionaries whose keys are the column
// identifiers of each column in the table. Values of @"" in the
// data structure indicate an empty spot in the table.
fData = [[NSArray arrayWithObjects:
[NSDictionary dictionaryWithObjectsAndKeys:
[NSDate dateWithNaturalLanguageString:@"1/2/03"], @"date",
@"Accounts Receivable", @"account",
[NSNumber numberWithInt:1], @"ref",
[NSNumber numberWithInt:210000], @"debit",
@"", @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
@"", @"date",
@" Revenue", @"account",
@"", @"ref",
@"", @"debit",
[NSNumber numberWithInt:210000], @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
[NSDate dateWithNaturalLanguageString:@"1/3/03"], @"date",
@"Computer", @"account",
[NSNumber numberWithInt:2], @"ref",
[NSNumber numberWithInt:5000], @"debit",
@"", @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
@"", @"date",
@" Accounts Payable", @"account",
@"", @"ref",
@"", @"debit",
[NSNumber numberWithInt:5000], @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
[NSDate dateWithNaturalLanguageString:@"1/21/03"], @"date",
@"Cash", @"account",
[NSNumber numberWithInt:3], @"ref",
[NSNumber numberWithInt:195000], @"debit",
@"", @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
@"", @"date",
@" Accounts Receivable", @"account",
@"", @"ref",
@"", @"debit",
[NSNumber numberWithInt:195000], @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
[NSDate dateWithNaturalLanguageString:@"1/22/03"], @"date",
@"Accounts Payable", @"account",
[NSNumber numberWithInt:4], @"ref",
[NSNumber numberWithInt:5000], @"debit",
@"", @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
@"", @"date",
@" Cash", @"account",
@"", @"ref",
@"", @"debit",
[NSNumber numberWithInt:5000], @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
[NSDate dateWithNaturalLanguageString:@"1/31/03"], @"date",
@"Rent Expense", @"account",
[NSNumber numberWithInt:5], @"ref",
[NSNumber numberWithInt:12000], @"debit",
@"", @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
@"", @"date",
@" Cash", @"account",
@"", @"ref",
@"", @"debit",
[NSNumber numberWithInt:12000], @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
[NSDate dateWithNaturalLanguageString:@"1/31/03"], @"date",
@"Wage Expense", @"account",
[NSNumber numberWithInt:6], @"ref",
[NSNumber numberWithInt:110000], @"debit",
@"", @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
@"", @"date",
@" Cash", @"account",
@"", @"ref",
@"", @"debit",
[NSNumber numberWithInt:110000], @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
[NSDate dateWithNaturalLanguageString:@"2/3/03"], @"date",
@"Legal Expense", @"account",
[NSNumber numberWithInt:7], @"ref",
[NSNumber numberWithInt:10000], @"debit",
@"", @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
@"", @"date",
@" Cash", @"account",
@"", @"ref",
@"", @"debit",
[NSNumber numberWithInt:10000], @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
[NSDate dateWithNaturalLanguageString:@"2/5/03"], @"date",
@"Cash", @"account",
[NSNumber numberWithInt:8], @"ref",
[NSNumber numberWithInt:50000], @"debit",
@"", @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
@"", @"date",
@" Note Payable", @"account",
@"", @"ref",
@"", @"debit",
[NSNumber numberWithInt:50000], @"credit",
NULL],
[NSDictionary dictionaryWithObjectsAndKeys:
@"", @"date",
@"Totals", @"account",
@"", @"ref",
[NSNumber numberWithInt:597000], @"debit",
[NSNumber numberWithInt:597000], @"credit",
NULL],
NULL] retain];
}
- (void)dealloc
{
[fData release];
[fGreenColor release];
[super dealloc];
}
- (int)numberOfRowsInTableView:(NSTableView*)tableView
{
return [fData count];
}
- (id)tableView:(NSTableView*)tableView
objectValueForTableColumn:(NSTableColumn*)tableColumn row:(int)rowIndex
{
id result = NULL;
// The value of the 'row' column is the 1-based index of the row.
// The other values come from the data array.
if ([[tableColumn identifier] isEqualToString:@"row"])
result = [NSNumber numberWithInt:rowIndex + 1];
else
{
result = [[fData objectAtIndex:rowIndex]
objectForKey:[tableColumn identifier]];
}
return result;
}
- (RowBorders)tableView:(NSTableView*)tableView bordersForRow:(int)rowIndex
{
RowBorders borders = { kCellBorderNone, kCellBorderSingle,
NULL, NULL };
// Every other row will have a green single line separating it from
// the next. This corresponds to the line between two matching
// entries in the journal
if (0 == rowIndex % 2)
borders.bottomColor = fGreenColor;
// The second to last line and the last line are separated by a
// double line to indicate 'total.' The last line has a thick line
// after it.
if (rowIndex == [fData count] - 2)
borders.bottom = kCellBorderDouble;
else if (rowIndex == [fData count] - 1)
{
borders.bottom = kCellBorderThick;
borders.bottomColor = NULL;
}
return borders;
}
- (ColumnBorders)tableView:(NSTableView*)tableView
bordersForTableColumn:(NSTableColumn*)tableColumn
{
ColumnBorders borders = { kCellBorderDouble, kCellBorderNone,
NULL, NULL };
// Don't draw any line on the far left hand side of the table, and
// draw a green single line between the account name and the
// reference number.
if ([[tableColumn identifier] isEqualToString:@"row"])
borders.left = kCellBorderNone;
else if ([[tableColumn identifier] isEqualToString:@"ref"])
{
borders.left = kCellBorderSingle;
borders.leftColor = fGreenColor;
}
return borders;
}
@end