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];