Monday, March 30, 2009

Drawing an NSCell from a flipped NSView to a bitmap context


Recently I found myself in need of a custom drawn list view. It's purpose would be to display 1 column and a bunch of rows, similarly to the way NSTableView does, except my view needed more flexibility in terms of the way it shapes and displays items. I thought about subclassing NSTableView, but after careful consideration I've realized that would be an overkill and went with subclassing NSView instead. To make my control efficient, I used the same approach as Apple did with NSTableView - each row is drawn by the same NSCell instance. This way I didn't have to waste memory creating a corresponding NSCell object for each item in the list views content. This was all fairly basic. I added some simple methods, did a little math and presto - custom control a 'la cocoa.


I came across a small issue when implementing drag and drop and I thought the solution might be of interest to some. My custom list views isFlipped method returns YES. This allows the NSScrollView that owns my control to work intuitively - from top to bottom (it's also much more intuitive for me). I do realize there are other ways of accomplishing this, but this just felt like it required the least hassle. All was fine in Cocoa land until I wanted to draw my NSCells to a bitmap and use it as a drag and drop image. There were two goals I wanted to accomplish:

  • Draw the NSCell into a bitmap context without changing anything in it's drawing method - (void)drawInteriorWithFrame:(NSRect)theCellFrame inView:(NSView *)theControlView
  • Draw a few cells and then arrange them into a nice image suitable for drag and drop. This made it unsuitable for me to use any of the NSViews standard methods for drawing to a bitmap context.


The cell I want to draw to the bitmap context is displayed below:


So the standard way to go about drawing to a bitmap context is:

//creating the rectangle that defines the bounds of our bitmap image
NSRect offscreenRect = NSMakeRect(0.0, 0.0, 100, 20);
NSBitmapImageRep* offscreenRep = nil;

//creating the bitmap image
offscreenRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil
pixelsWide:offscreenRect.size.width
pixelsHigh:offscreenRect.size.height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bitmapFormat:0
bytesPerRow:(4 * offscreenRect.size.width)
bitsPerPixel:32];

[NSGraphicsContext saveGraphicsState];

//setting the current context to a bitmap context
[NSGraphicsContext setCurrentContext:[NSGraphicsContext
graphicsContextWithBitmapImageRep:offscreenRep]];

//do some drawing

[NSGraphicsContext restoreGraphicsState];

//creating an NSImage setting whatever we drew above to be it's representation
NSImage *image = [[[NSImage alloc] init] autorelease];
[image addRepresentation:offscreenRep];

//this allows us to create an image thats content is transparent (see the 'fraction') parameter below
NSImage *dragImage = [[[NSImage alloc] initWithSize:[image size]] autorelease];
[dragImage lockFocus];
[image compositeToPoint:NSMakePoint(0, 0) operation:NSCompositeSourceOver fraction:0.5];
[dragImage unlockFocus];


Unfortunately, this left me with:


If you compare that to the original, you will notice that the text views got switched. This was of course, unacceptable.

I started googling and found a bunch of blogs stating that using an NSAffineTransform would solve the problem. So I added:

NSAffineTransform* xform = [NSAffineTransform transform];
[xform translateXBy:0.0 yBy:offscreenRect.size.height];
[xform scaleXBy:1.0 yBy:-1.0];
[xform concat];


I really hoped this method would work, because it was the only solution that seemed, at the time, reasonable. It produced the image you see below:


This time the text views were in their proper places, but the text was flipped. Having wasted some time trying to find a solution I decided to give my own wacky idea a go. When using a "ported" graphics context one can set the context as flipped. I decided to create a CGContextRef, use it as a ported and flipped NSGraphicsContext and draw to it like so:

//create a CGContextRef so that we can later port it and make it flipped
CGContextRef context = CGBitmapContextCreate (bitmapData,
offscreenRect.size.width,
offscreenRect.size.height,
8,
bitmapBytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast);

[NSGraphicsContext saveGraphicsState];
//here we port the context and make it flipped
[NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:YES]];

NSAffineTransform* xform = [NSAffineTransform transform];
[xform translateXBy:0.0 yBy:offscreenRect.size.height];
[xform scaleXBy:1.0 yBy:-1.0];
[xform concat];

//perform drawing

//create a CGImageRef from the CGContextRef
CGImageRef myImage = CGBitmapContextCreateImage (context);

//port the CGImageRef to an NSBitmapImageRep
NSBitmapImageRep* offscreenRep = [[[NSBitmapImageRep alloc] initWithCGImage:myImage] autorelease];

[NSGraphicsContext restoreGraphicsState];

//create the image and set it's representation to what was drawn above
NSImage *image = [[[NSImage alloc] init] autorelease];
[image addRepresentation:offscreenRep];

//this allows us to create an image thats content is transparent (see the 'fraction') parameter below
NSImage *dragImage = [[[NSImage alloc] initWithSize:[image size]] autorelease];
[dragImage lockFocus];
[image compositeToPoint:NSMakePoint(0, 0) operation:NSCompositeSourceOver fraction:0.5];
[dragImage unlockFocus];


As a result I got exactly what I wanted: