Cocoa NSTableView, NSTextFieldCell, NSImage, ATImageAndTextCell tutorial

71: Display an NSTextfieldCell containing text and an image within a NSTableView

Problem: We want to display a table of rows consisting of two lines of text and an image. NSTableView looks like the ideal display context but there is no specific NSCell type dedicated to this kind of task. For reasons which will become apparent later, refer to the two lines of text as parent and child respectively. The parent text is to be editable and means are to be provided for the user to delete a selected row, insert a new row at a selected row or at table end

NSTableView using NSTextFieldCell, NSImage, NSCell, ATImageTextCell exaple

Example NSTableView using NSTextFieldCell, NSImage and ATImageAndTextCell

Answer: We make use of Apple's ATImageTextCell Class that is to be found in Apple's AnimatedTableView example.

The key idea is straightforward. Subclass NSTextViewCell as the class ATImageTextCell (Apple example: ATImageTextCell.h, ATImageTextCell.m). This class will intercept the call to NSTextViewCell's drawInteriorWithFrame:inView: method and use it to do four things. First to calculate the sizes and locations of the NSRects which will contain the image and the two lines of text. Then to create an NSImageCell to contain the image and draw it, an NSTextFieldCell to contain and draw the child text, and finally to draw the parent text.

One thing to remember is that the same ImageAndText object is used to display each of the text and image pairs. Essentially one can think of ImageAndText being moved through successive rows drawing as it goes.

In Interface Builder declare your control to be both the NSTableView's delegate and data source.

As delegate declare methods for:

  • the obligatory pair of -(NSInteger)numberOfRowsInTableView:,
    and -(id)tableView:objectValueForTableColumn:row:

  • Then the method for putting the data into the cell
    -(void)tableView:willDisplayCell:forTableColumn:row:

  • and that for putting the returned edited text back into the database
    -(void)tableView:setObjectValue:forTableColumn:row:

  • For reasons I have yet to determine one also needs to declare
    - (NSCell *)tableView:dataCellForTableColumn:row:

A curiosity

There are occasions on which one needs to tell NSTableView to end editing. For instance, if one has clicked on the text in one of the rows and started editing and then without doing anything else one clicks on the AddAtSelectedRow key, it is necesary to tell NSTableView to stop editing or the edit will appear in the newly inserted row.

The only approach I've so far found to work was presented in http://www.stone.com/The_Cocoa_Files/Takes_All_Sorts.html.

This consists of returning focus to the NSTableView window. See the addAtSelectedRow method below. This preserves the original edit. To abort the edit completely use the NSTableView method abortEditing.

Source code

// MyData.h #import <Cocoa/Cocoa.h> @interface MyData : NSObject { NSString * nsStringParentText; NSString * nsStringChildText; NSImage * nsParentImageObj; } @property (assign) NSString * nsStringParentText; @property (assign) NSString * nsStringChildText; @property (assign) NSImage * nsParentImageObj; - (id) initWithParentImagePath:(NSString *)pParentImagePath parentText:(NSString *)pParentText childText:(NSString *)pChildText; @end // // MyData.m #import "MyData.h" @implementation MyData @synthesize nsStringParentText; @synthesize nsStringChildText; @synthesize nsParentImageObj; - (id) initWithParentImagePath:(NSString *)pParentImagePath parentText:(NSString *)pParentText childText:(NSString *)pChildText { if (! (self = [super init])) { NSLog(@"*Error* MyData initWithImagePathString"); return self; } // end if self.nsStringParentText = pParentText; self.nsStringChildText = pChildText; self.nsParentImageObj = [[NSImage alloc] initWithContentsOfFile:pParentImagePath]; return self; } // end initWithParentImagePath @end // // MyParentImageText.h // JJG modification of ATImageTextCell.h // #import <Cocoa/Cocoa.h> @interface MyParentImageText : NSTextFieldCell { NSString * nsStringParentText; NSString * nsStringChildText; NSImage * nsParentImageObj; NSImageCell * nsImageCellObj; NSTextFieldCell * nsChildTextFieldObj; } @property (assign) NSString * nsStringParentText; @property (assign) NSString * nsStringChildText; @property (assign) NSImage * nsParentImageObj; @property (assign) NSImageCell * nsImageCellObj; @property (assign) NSTextFieldCell * nsChildTextFieldObj; - (void)createCells; - (void)setImage:(NSImage *)image; - (void)ensureChildCellCreated; - (NSRect)parentImageFrameForInteriorFrame:(NSRect)frame; - (NSRect)parentTitleFrameForInteriorFrame:(NSRect)frame; - (NSRect)childCellFrameForInteriorFrame:(NSRect)frame; @end // // MyParentImageText.m // // JJG modification of ATImageTextCell.h // #import "MyParentImageText.h" #define IMAGE_INSET 8.0 #define ASPECT_RATIO 1.6 #define TITLE_HEIGHT 17.0 #define FILL_COLOR_RECT_SIZE 25.0 #define INSET_FROM_IMAGE_TO_TEXT 4.0 @implementation MyParentImageText @synthesize nsStringParentText; @synthesize nsStringChildText; @synthesize nsParentImageObj; @synthesize nsImageCellObj; @synthesize nsChildTextFieldObj; - (void)createCells { //NSLog(@"createCells"); [self setImage:self.nsParentImageObj]; [self ensureChildCellCreated]; self.title = self.nsStringParentText; self.nsChildTextFieldObj.title = self.nsStringChildText; } // end createCells // only create one NSImageCell object - (void)setImage:(NSImage *)image { if (self.nsImageCellObj == nil) { self.nsImageCellObj = [[NSImageCell alloc] init]; [self.nsImageCellObj setControlView:self.controlView]; [self.nsImageCellObj setBackgroundStyle:self.backgroundStyle]; } self.nsImageCellObj.image = image; } // end setImage // only create one myChildImageTextObj object - (void)ensureChildCellCreated { if (self.nsChildTextFieldObj == nil) { self.nsChildTextFieldObj = [[NSTextFieldCell alloc] init]; [self.nsChildTextFieldObj setControlView:self.controlView]; [self.nsChildTextFieldObj setBackgroundStyle:self.backgroundStyle]; [self.nsChildTextFieldObj setTextColor:[NSColor grayColor]]; [self.nsChildTextFieldObj setEditable:NO]; } // end if } // end ensureChildCellCreated // called by super - (void)drawInteriorWithFrame:(NSRect)frame inView:(NSView *)controlView { if (self.nsImageCellObj) { NSRect imageFrame = [self parentImageFrameForInteriorFrame:frame]; [self.nsImageCellObj drawWithFrame:imageFrame inView:controlView]; } if (nsChildTextFieldObj) { NSRect childTextFrame = [self childCellFrameForInteriorFrame:frame]; [nsChildTextFieldObj drawWithFrame:childTextFrame inView:controlView]; } NSRect titleFrame = [self parentTitleFrameForInteriorFrame:frame]; [super drawInteriorWithFrame:titleFrame inView:controlView]; } // end drawInteriorWithFrame // NSCell over-ride // Not being called - (void)editWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject event:(NSEvent *)theEvent { aRect = [self parentTitleFrameForInteriorFrame:aRect]; [super editWithFrame:aRect inView:controlView editor:textObj delegate:anObject event:theEvent]; } - (void)selectWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject start:(NSInteger)selStart length:(NSInteger)selLength { aRect = [self parentTitleFrameForInteriorFrame:aRect]; [super selectWithFrame:aRect inView:controlView editor:textObj delegate:anObject start:selStart length:selLength]; } // private methods for creating the NSRects in which // the Cell elements will be displayed - (NSRect)parentImageFrameForInteriorFrame:(NSRect)frame { NSRect result = frame; // Inset the top result.origin.y += IMAGE_INSET; result.size.height -= 2*IMAGE_INSET; // Inset the left result.origin.x += IMAGE_INSET; // Make the width match the aspect ratio based on the height result.size.width = ceil(result.size.height * ASPECT_RATIO); return result; } // end parentImageFrameForInteriorFrame - (NSRect)parentTitleFrameForInteriorFrame:(NSRect)frame { NSRect imageFrame = [self parentImageFrameForInteriorFrame:frame]; NSRect result = frame; // Move our inset to the left of the image frame result.origin.x = NSMaxX(imageFrame) + INSET_FROM_IMAGE_TO_TEXT; // Go as wide as we can result.size.width = NSMaxX(frame) - NSMinX(result); // Move the title above the Y centerline of the image. NSSize naturalSize = [super cellSize]; result.origin.y = floor(NSMidY(imageFrame) - naturalSize.height - INSET_FROM_IMAGE_TO_TEXT); result.size.height = naturalSize.height; return result; } // end parentTitleFrameForInteriorFrame - (NSRect)childCellFrameForInteriorFrame:(NSRect)frame { NSRect imageFrame = [self parentImageFrameForInteriorFrame:frame]; NSRect result = frame; // Move our inset to the left of the image frame result.origin.x = NSMaxX(imageFrame) + INSET_FROM_IMAGE_TO_TEXT; result.size.width = NSMaxX(frame) - NSMinX(result); result.size.height = FILL_COLOR_RECT_SIZE; result.origin.y = floor(NSMidY(imageFrame)); return result; } // end childCellFrameForInteriorFrame - (id)copyWithZone:(NSZone *)zone { MyParentImageText *zCopyOfMe = [super copyWithZone:zone]; if (zCopyOfMe != nil) { // Retain or copy all our ivars zCopyOfMe->nsStringParentText = [self.nsStringParentText copyWithZone:zone]; zCopyOfMe->nsStringChildText = [self.nsStringChildText copyWithZone:zone]; zCopyOfMe->nsParentImageObj = [self.nsParentImageObj copyWithZone:zone]; zCopyOfMe->nsImageCellObj = [self.nsImageCellObj copyWithZone:zone]; zCopyOfMe->nsChildTextFieldObj= [self.nsChildTextFieldObj copyWithZone:zone]; } return zCopyOfMe; } // end copyWithZone @end // // MyControl.h // #import <Cocoa/Cocoa.h> @class MyParentImageText; // Example of NSTextView: // text and Image and child NSTextView with image and text @interface MyControl : NSObject { NSMutableArray * nsMutaryOfMyData; MyParentImageText * myParentImageTextObj; IBOutlet NSTableView * nsTableViewObj; } @property (assign) NSMutableArray * nsMutaryOfMyData; @property (assign) MyParentImageText * myParentImageTextObj; @property (assign) IBOutlet NSTableView * nsTableViewObj; - (IBAction)tableViewSelected:(id)sender; - (IBAction)addAtSelectedRow:(id)pId; - (IBAction)addToEndOfTable:(id)pId; - (IBAction)removeCellAtSelectedRow:(id)sender; @end // // MyControl.m // #import "MyControl.h" #import "MyData.h" #import "MyParentImageText.h" @implementation MyControl @synthesize nsMutaryOfMyData; @synthesize myParentImageTextObj; @synthesize nsTableViewObj; - (void) awakeFromNib { self.nsMutaryOfMyData = [[NSMutableArray alloc]init]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithParentImagePath:@"../../../Lucia.tif" parentText:@"Lucia, dog, peacock and Galapagos turtle" childText:@"watercolour and body colour"]]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithParentImagePath:@"../../../BellaSeals.tif" parentText:@"Bella's Dream (detail): Bella, Lucia and the seals" childText:@"watercolour and body colour"]]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithParentImagePath:@"../../../BellaPanda.tif" parentText:@"Bella's Dream (detail): Bella, Lucia and Panda" childText:@"watercolour and body colour"]]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithParentImagePath:@"../../../landBogay.tif" parentText:@"View of Bogay" childText:@"Oil on canvass"]]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithParentImagePath:@"../../../landRiverRoeSpring.tif" parentText:@"The River Roe in Spring" childText:@"Oil on canvass"]]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithParentImagePath:@"../../../oliveHarvest.tif" parentText:@"The Olive Harvest" childText:@"Ceramic tiles"]]; self.myParentImageTextObj = [[MyParentImageText alloc] init]; self.myParentImageTextObj.nsStringParentText = [[self.nsMutaryOfMyData objectAtIndex:0]nsStringParentText]; self.myParentImageTextObj.nsStringChildText = [[self.nsMutaryOfMyData objectAtIndex:0]nsStringChildText]; self.myParentImageTextObj.nsParentImageObj = [[self.nsMutaryOfMyData objectAtIndex:0]nsParentImageObj]; [self.myParentImageTextObj setEditable: YES]; NSTableColumn* zTableColumnObj = [[self.nsTableViewObj tableColumns] objectAtIndex:0]; [zTableColumnObj setDataCell: self.myParentImageTextObj]; } // end awakeFromNib // NSTableView delegate methods - (NSInteger)numberOfRowsInTableView:(NSTableView *)pTableView { return [nsMutaryOfMyData count]; } // end numberOfRowsInTableView - (id)tableView:(NSTableView *)pTableView objectValueForTableColumn:(NSTableColumn *)pTableColumn row:(int)pRow { //return @"fred"; // any simple displayable object will do? // NO: this stops one inserting the word "fred" MyData * zMyDataObj = [self.nsMutaryOfMyData objectAtIndex:pRow]; return zMyDataObj.nsStringParentText; } // end tableView:objectValueForTableColumn:tableColumn // this is the delegate method that allows you to put data into your cell - (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)pRow { MyData * zMyDataObj = [self.nsMutaryOfMyData objectAtIndex:pRow]; MyParentImageText * zMyCell = (MyParentImageText *)cell; zMyCell.nsStringParentText = zMyDataObj.nsStringParentText; zMyCell.nsStringChildText = zMyDataObj.nsStringChildText; zMyCell.nsParentImageObj = zMyDataObj.nsParentImageObj; // we now need to get zMyCell to crete the other cells. // ultimately this will be done by the data setter methods but for now // it is useful to keep the mechanism clearly visible [zMyCell createCells]; } // end tableView:willDisplayCell:forTableColumn:row: // if this is not here we crash - (NSCell *)tableView:(NSTableView *)pTableView dataCellForTableColumn:(NSTableColumn *)pTableColumn row:(NSInteger)pRow { return self.myParentImageTextObj; } // end tableView:dataCellForTableColumn:row: // this is the routine that returns cell data // (an edited string) back after editing - (void)tableView:(NSTableView *)aTableView setObjectValue:anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)pRow { MyData * zMyDataObj = [self.nsMutaryOfMyData objectAtIndex:pRow]; NSLog(@"setObjectValue string = %@",(NSString *)anObject); zMyDataObj.nsStringParentText = (NSString *)anObject; } // end tableView:setObjectValue:forTableColumn:row: - (IBAction)tableViewSelected:(id)sender { NSLog(@"the user clicked row %d",[self.nsTableViewObj selectedRow]); } // end tableViewSelected - (IBAction)addAtSelectedRow:(id)pId { [[self.nsTableViewObj window] makeFirstResponder:[self.nsTableViewObj window]]; NSInteger zSelectedRow = [self.nsTableViewObj selectedRow]; if ( zSelectedRow < 0) { return; } // end if // crash if out of bounds NSParameterAssert(zSelectedRow < [self.nsMutaryOfMyData count]); [nsMutaryOfMyData insertObject:[[MyData alloc] initWithParentImagePath:@"../../../anghiari.tif" parentText:@"Copy after Ruben's copy after Leonardo: The Battle of Anghiari.. And here is some extra text to see how we get on with very lengthy things" childText:@"Oil on canvass"] atIndex:zSelectedRow]; [self.nsTableViewObj noteNumberOfRowsChanged]; [self.nsTableViewObj reloadData]; } // end addAtSelectedRow - (IBAction)addToEndOfTable:(id)pId { [nsMutaryOfMyData addObject:[[MyData alloc] initWithParentImagePath:@"../../../BellaElephants.tif" parentText:@"Bella's Dream (detail): Bella and Elephants" childText:@"Watercolour with body colour"]]; [self.nsTableViewObj noteNumberOfRowsChanged]; [self.nsTableViewObj reloadData]; } // end addToEndOfTable - (IBAction)removeCellAtSelectedRow:(id)sender { if ([self.nsTableViewObj selectedRow] < 0 || [self.nsTableViewObj selectedRow] >= [nsMutaryOfMyData count]) { return; } // end if [nsMutaryOfMyData removeObjectAtIndex:[self.nsTableViewObj selectedRow]]; [self.nsTableViewObj noteNumberOfRowsChanged]; [self.nsTableViewObj reloadData]; } // end removeCellAtSelectedRow @end

Bindings

Control as NSTableView Data Source and Delegate plus connections to buttons

Control as NSTableView Data Source and Delegate plus connections to buttons

Location of image files relative to the xcode source code

Location of image files relative to the xcode source code

Example source code



Please send me your comments

If you include your e-mail I may reply!  

Page last modified: 18:58 Sunday 12th. May 2013

Julius Guzy

Paintings & Drawings

  • Link to painting of Star of Bethlehem flowers

animatedPaint