Cocoa NSTableView, NSTextFieldCell, NSCell, ImageAndTextCell tutorial

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

Problem: We want to display a table of rows of text and image pairs. NSTableView looks like the ideal display context but there is no specific NSCell type dedicated to this kind of task. The text is to be editable and the means provided for the user to delete a selected row, insert a new row at a selected row or at table end

NSTableView using NSTextFieldCell, NSCell, ImageAndTextCell exaple

Example NSTableView using NSTextFieldCell and ImageAndTextCell

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

The key idea is straightforward. Subclass NSTextViewCell as the class ImageAndText (Apple example: ImageAndTextCell.h, ImageAndTextCell.m). This class will intercept the call to NSTextViewCell's drawWithFrame:inView: method and use it to draw the image before passing control on to super' drawWithFrame:inView: which will draw the 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

  • and that for putting the returned edited text back into the database

  • 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

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

// // ImageAndTextCell.h // // Copyright 2006, Apple. All rights reserved. // JJG: edited to remove memory management statements // and for this example inessential code. #import <Cocoa/Cocoa.h> @interface ImageAndTextCell : NSTextFieldCell { @private NSImage *nsImageObj; } @property (assign) NSImage *nsImageObj; - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView; //- (NSSize)cellSize; @end // ImageAndTextCell.m // Copyright 2006, Apple Computer, Inc., all rights reserved. // // Subclass of NSTextFieldCell which can display text // and an image simultaneously. // JJG: edited to remove memory management statements // and for this example inessential code. #import "ImageAndTextCell.h" @implementation ImageAndTextCell @synthesize nsImageObj; - (id)copyWithZone:(NSZone *)zone { ImageAndTextCell *zCell = (ImageAndTextCell *)[super copyWithZone:zone]; zCell.nsImageObj = self.nsImageObj; return zCell; } // end copyWithZone // over-ride NSCell selectWithFrame : // called when frame is selected for editing - (void)selectWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject start:(NSInteger)selStart length:(NSInteger)selLength { NSLog(@"My Cell: selectWithFrame"); NSRect textFrame, imageFrame; NSDivideRect (aRect, &imageFrame,&textFrame, 3+[nsImageObj size].width,NSMinXEdge); [super selectWithFrame: textFrame inView: controlView editor:textObj delegate:anObject start:selStart length:selLength]; } // draw the image on the left hand side of the NSTextFieldCell - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { if (nsImageObj == nil) { [super drawWithFrame:cellFrame inView:controlView]; return; } // end if NSSize imageSize; NSRect imageFrame; //NSRect textFrame; imageSize = [nsImageObj size]; NSDivideRect(cellFrame,&imageFrame,&cellFrame, 3+imageSize.width,NSMinXEdge); if ([self drawsBackground]){ [[self backgroundColor] set]; NSRectFill(imageFrame); } // end if imageFrame.origin.x += 3; imageFrame.size = imageSize; if ([controlView isFlipped]) { imageFrame.origin.y += ceil((cellFrame.size.height+ imageFrame.size.height)/2); } else { imageFrame.origin.y += ceil((cellFrame.size.height- imageFrame.size.height)/2); } // end if [nsImageObj compositeToPoint:imageFrame.origin operation:NSCompositeSourceOver]; [super drawWithFrame:cellFrame inView:controlView]; //[super drawWithFrame:textFrame inView:controlView]; } @end // // MyController.h // TableViewExample #import //@class MyNSCell; @class ImageAndTextCell; @interface MyController : NSObject { NSMutableArray * nsMutaryOfMyData; ImageAndTextCell * myImageAndTextCelObj; IBOutlet NSTableView * nsTableViewObj; } @property (assign) NSMutableArray * nsMutaryOfMyData; @property (assign) ImageAndTextCell * myImageAndTextCelObj; @property (assign) IBOutlet NSTableView * nsTableViewObj; //- (IBAction)tableViewSelected:(id)sender; - (IBAction)addAtSelectedRow:(id)pId; - (IBAction)addToEndOfTable:(id)pId; - (IBAction)removeCellAtSelectedRow:(id)sender; @end // // MyController.m // TableViewExample #import "MyController.h" #import "MyData.h" #import "ImageAndTextCell.h" @implementation MyController @synthesize nsMutaryOfMyData; @synthesize nsTableViewObj; @synthesize myImageAndTextCelObj; //@synthesize nsIntSelectedRow; // first step // cashe the images in MyData // second step : get the add abd delete going - (void) awakeFromNib { // self.nsIntSelectedRow = -1; self.nsMutaryOfMyData = [[NSMutableArray alloc]init]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithImagePathString:@"../../../Lucia.tif" text:@"Lucia, dog, peacock and Galapagos turtle"]]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithImagePathString:@"../../../BellaSeals.tif" text:@"Bella's Dream (detail): Bella, Lucia and seals"]]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithImagePathString:@"../../../BellaPanda.tif" text:@"Bella's Dream (detail): Bella, Lucia and Panda"]]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithImagePathString:@"../../../danceOfLife.tif" text:@"Stuff That Stars Are Made Of (detail): Dance of Life"]]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithImagePathString:@"../../../landBogay.tif" text:@"View of Bogay"]]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithImagePathString:@"../../../landRiverRoeSpring.tif" text:@"The River Roe in Spring"]]; [self.nsMutaryOfMyData addObject:[[MyData alloc] initWithImagePathString:@"../../../oliveHarvest.tif" text:@"The Olive Harvest"]]; self.myImageAndTextCelObj = [[ImageAndTextCell alloc] init]; self.myImageAndTextCelObj.image = [self.nsMutaryOfMyData objectAtIndex:0]nsImageObj]; [self.myImageAndTextCelObj setEditable: YES]; NSTableColumn* zTableColumnObj = [[self.nsTableViewObj tableColumns] objectAtIndex:0]; [zTableColumnObj setDataCell: self.myImageAndTextCelObj]; } // end awakeFromNib // these are called by the table(s) - (NSInteger)numberOfRowsInTableView:(NSTableView *)pTableView { return [nsMutaryOfMyData count]; } // end numberOfRowsInTableView - (id)tableView:(NSTableView *)pTableView objectValueForTableColumn:(NSTableColumn *)pTableColumn row:(int)pRow { MyData * zMyDataObj = [self.nsMutaryOfMyData objectAtIndex:pRow]; return zMyDataObj.nsStrText; // Note if returned string is same as that typed into the cell // then no update takes place // e.g. returned string="fred", cell = "hello world", // user selects the word "world" and types "fred": no change takes place. } // 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 { //NSLog(@"willDisplayCell"); MyData * zMyDataObj = [self.nsMutaryOfMyData objectAtIndex:pRow]; ImageAndTextCell * zMyCell = (ImageAndTextCell *)cell; zMyCell.nsImageObj = zMyDataObj.nsImageObj; [zMyCell setTitle:zMyDataObj.nsStrText]; } // end tableView:willDisplayCell:forTableColumn: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.nsStrText = (NSString *)anObject; } // end tableView:setObjectValue:forTableColumn:row: // if this is not here we crash - called whenever mouseOver - (NSCell *)tableView:(NSTableView *)pTableView dataCellForTableColumn:(NSTableColumn *)pTableColumn row:(NSInteger)pRow { //NSLog(@"dataCellForTableColumn"); return self.myImageAndTextCelObj; } // end tableView:dataCellForTableColumn:row: //- (IBAction)tableViewSelected:(id)sender { // NSLog(@"the user just clicked on row %d", // [self.nsTableViewObj selectedRow]); //} // end tableViewSelected - (IBAction)addAtSelectedRow:(id)pId { NSLog(@"addAtSelectedRow"); // this ends the editing if an edit was begun // just before the addAtSelectedRow button was clicked. [[self.nsTableViewObjwindow]makeFirstResponder:[self.nsTableViewObjwindow]]; NSInteger zSelectedRow = [self.nsTableViewObj selectedRow]; if ( zSelectedRow < 0) { return; } // end if NSParameterAssert(zSelectedRow < [self.nsMutaryOfMyData count]); // crash [nsMutaryOfMyData insertObject:[[MyData alloc] initWithImagePathString:@"../../../anghiari.tif" text:@"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"] atIndex:zSelectedRow]; [self.nsTableViewObj noteNumberOfRowsChanged]; [self.nsTableViewObj reloadData]; } // end addAtSelectedRow - (IBAction)addToEndOfTable:(id)pId { NSLog(@"addToEndOfTable"); [nsMutaryOfMyData addObject:[[MyData alloc] initWithImagePathString:@"../../../BellaElephants.tif" text:@"Bella's Dream (detail): Bella and Elephants"]]; [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 // // MyData.h // TableViewExample #import @interface MyData : NSObject { NSString * nsStrText; NSImage * nsImageObj; } @property (assign) NSString * nsStrText; @property (assign) NSImage * nsImageObj; - (id) initWithImagePathString:(NSString *)pImagePath text:(NSString *)pText; @end // // MyData.m // TableViewExample // #import "MyData.h" @implementation MyData @synthesize nsStrText; @synthesize nsImageObj; - (id) initWithImagePathString:(NSString *)pImagePath text:(NSString *)pText { if (! (self = [super init])) { NSLog(@"*Error* MyData initWithImagePathString"); return self; } // end if self.nsStrText = pText; self.nsImageObj = [[NSImage alloc] initWithContentsOfFile:pImagePath]; return self; } // end initWithImagePathString @end


Control as NSTableView Data Source and Delegate plus connections to buttons

Control as NSTableView Data Source and Delegate plus connections to buttons

NSTableView Connections: make control the Data Source and Delegate

NSTableView Connections: make control the Data Source and Delegate

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 the Cities of Babel destroying our Garden of Eden