http://juliuspaintings.co.uk/cgi-bin/paint_css/animatedPaint/070-NSTableView-ImageAndTextCell.pl
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
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
-(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 inhttp://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
//
// 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
NSSizeimageSize;
NSRectimageFrame;
//NSRecttextFrame;
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");
returnself.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