Intro to Quartz II
This tutorial is the second entry in the
Introduction to Quartz series.
Intro to Quartz Part I describes ideas that are essential
to understand before reading this tutorial. Even if you've read it, you may
want to review first.
This tutorial explains how to create and use custom views, how to construct paths
using NSBezierPath, and touches on a variety of topics related to using
images with the NSImage class.
This
tutorial is written and illustrated by
Scott Stevenson
Copyright © 2006 Scott Stevenson
Custom Views
A custom view is a subclass of NSView which serves a "blank canvas" for your
drawing code. Once you have created the view class, you add drawing code
using rects, paths and images in the drawRect method.
To create a new view class in your project, choose File > New File in Xcode and select
Objective-C NSView subclass
After the class file is created, you drag the header file to
the NIB window in Interface Builder so it knows about the class.
Now the class has been added to the NIB file and can be
used in a custom view.
Using a Custom View
Once the view has been added to the NIB file, open the Interface Builder
palette and drag the CustomView icon out onto your application window.
While the custom view is still selected, bring up the inspector and
choose MyView from the Custom Class inspector dropdown.
The custom view now uses the MyView class. You should also select
Size from the inspector dropdown at this point
to configure the sizing to your liking.
The view will not display anything until drawing code is added to
drawRect method in the MyView class.
In the above screenshot, we added a 210x210 rect and filled it using
a blue NSColor.
We implemented isFlipped to return YES so that
the coordinates start in the upper-left instead of the
bottom-left.
Paths
Rects can be used for basic graphics, but paths allow for
much more complex shapes. The NSBezierPath class is used to
create paths in Cocoa.
Paths are mutable, so they can be changed or combined at any
time. Path objects can draw themselves into a view. Here are
some examples:
Paths can contain curved lines, arcs and rectanges.
Each of these examples were drawn to the view by sending the
stroke message to the path object.
To create a curved line, you provide a start point, and end point,
and two "control" points which act as gravity on the line shape.
Control points are not needed for simple straight lines.
Filled Paths
Paths can be filled in addition to being stroked. The following examples
first use the fill method with a white NSColor set, then
stroke the path with a gray color set.
The paths are filled before being stroked so that the lines are visible.
Creating Paths
The following examples show how to create paths. All of the code
assumes a flipped view with coordinates starting in the upper-left.
NSBezierPath * path = [NSBezierPath bezierPath];
[path setLineWidth: 4];
NSPoint startPoint = { 21, 21 };
NSPoint endPoint = { 128,128 };
[path moveToPoint: startPoint];
[path curveToPoint: endPoint
controlPoint1: NSMakePoint ( 128, 21 )
controlPoint2: NSMakePoint ( 21,128 )];
[[NSColor whiteColor] set];
[path fill];
[[NSColor grayColor] set];
[path stroke];
Here's an example of appending a path to an existing
path. The path being added is an arc:
NSBezierPath * path = [NSBezierPath bezierPath];
[path setLineWidth:4];
NSPoint center = { 128,128 };
[path moveToPoint: center];
[path appendBezierPathWithArcWithCenter: center
radius: 64
startAngle: 0
endAngle: 321];
[[NSColor whiteColor] set];
[path fill];
[[NSColor grayColor] set];
[path stroke];
It's also easy to create a bezier path from a basic rect:
NSPoint origin = { 21,21 };
NSRect rect;
rect.origin = origin;
rect.size.width = 128;
rect.size.height = 128;
NSBezierPath * path;
path = [NSBezierPath bezierPathWithRect:rect];
[path setLineWidth:4];
[[NSColor whiteColor] set];
[path fill];
[[NSColor grayColor] set];
[path stroke];
Complex Path Construction
The following examples incrementally build and draw several paths to
construct the final image. All of the code goes into the custom
view's drawRect method.
First, we start by creating a basic oval in the center of the view.
// setup basic size and color properties
float dm = 81 * 2;
float rd = dm * 0.50;
float qt = dm * 0.25;
NSColor * white = [NSColor whiteColor];
NSColor * black = [NSColor blackColor];
NSBezierPath *path1, *path2, *path3, *path4;
// find the center of the view
float center = [self bounds].size.width * 0.50;
float middle = [self bounds].size.height * 0.50;
// create a rect in the center
NSPoint origin = { center - rd, middle - rd };
NSRect mainOval = { origin.x, origin.y, dm, dm };
// create a oval bezier path using the rect
path1 = [NSBezierPath bezierPathWithOvalInRect:mainOval];
[path1 setLineWidth:2.18];
// draw the path
[white set];[path1 fill];
[black set];[path1 stroke];
Next, we define a path to overlay on the first one. The path
is built in three parts, by appending three different arc paths
to the original object.
Note how we build the ellipse in the top half by negating the clockwise
option.
// overlay a new path to draw the right side
path2 = [NSBezierPath bezierPath];
// arc from the center to construct right side
NSPoint mainOvalCenter = { center, middle };
[path2 appendBezierPathWithArcWithCenter: mainOvalCenter
radius: rd
startAngle: 90
endAngle: 270
clockwise: YES];
// add a half-size arc at the top: counter clockwise
NSPoint curveOneCenter = { center, origin.y+qt };
[path2 appendBezierPathWithArcWithCenter: curveOneCenter
radius: qt
startAngle: 270
endAngle: 90
clockwise: NO];
// add a half-size arc in the bottom half
NSPoint curveTwoCenter = { center, origin.y+rd+qt };
[path2 appendBezierPathWithArcWithCenter: curveTwoCenter
radius: qt
startAngle: 270
endAngle: 90
clockwise: YES];
// fill the path on the right side with black
[black set];[path2 fill];
Finally, we build the paths for the two internal ellipses.
// calculate the size for each ellipses
float dotDm = ( qt * 0.618 );
float dotRd = ( dotDm * 0.5 );
// create a rect at the center of the top section
NSRect rect3 = NSMakeRect ( center - dotRd,
origin.y + (qt - dotRd),
dotDm,
dotDm );
// create an oval with the rect and fill it with black
path3 = [NSBezierPath bezierPathWithOvalInRect: rect3];
[black set];[path3 fill];
// copy the rect for the top ellipse and adjust the y axis
NSRect rect4 = rect3;
rect4.origin.y = NSMaxY(mainOval) - (qt + dotRd);
// create an oval with the rect and fill it with white
path4 = [NSBezierPath bezierPathWithOvalInRect: rect4];
[white set];[path4 fill];
The final result in our custom view.
Images
Cocoa's basic image class is
NSImage. An image can draw
itself into a view at various sizes and levels of opacity,
or can be written to disk.

Image objects can be created from files on disk, loaded from
data in memory, imported from the pasteboard, or drawn on
the fly.
Each NSImage instance manages a series of
NSImageRep objects
which are versions of the original image data suited to
different contexts.
NSImage will automatically choose a representation when you draw into
a view, but you can also use an image rep directly. NSBitmapImageRep
is perhaps the most common.
Create and Display Images
Creating an NSImage from a file on disk and drawing it into a view
is as simple as calling initWithContentsOfFile followed by
drawInRect. To make things look nice, we'll center the
image in the view and draw a border around it.
First, we need to use NSImageInterpolationHigh to
to get smooth bitmap scaling, then calculate the origin of the rect
to draw the image in.
[[NSGraphicsContext currentContext]
setImageInterpolation: NSImageInterpolationHigh];
NSSize viewSize = [self bounds].size;
NSSize imageSize = { 250, 156 };
NSPoint viewCenter;
viewCenter.x = viewSize.width * 0.50;
viewCenter.y = viewSize.height * 0.50;
NSPoint imageOrigin = viewCenter;
imageOrigin.x -= imageSize.width * 0.50;
imageOrigin.y -= imageSize.height * 0.50;
NSRect destRect;
destRect.origin = imageOrigin;
destRect.size = imageSize;
We load the image from disk and flip it since our view
uses a flipped coordinate system. Once it's loaded, we
draw it to the destination rect, than draw a border
around the same rect.
NSString * file = @"/Library/Desktop Pictures/Plants/Leaf Curl.jpg";
NSImage * image = [[NSImage alloc] initWithContentsOfFile:file];
[image setFlipped:YES];
[image drawInRect: destRect
fromRect: NSZeroRect
operation: NSCompositeSourceOver
fraction: 1.0];
NSBezierPath * path = [NSBezierPath bezierPathWithRect:destRect];
[path setLineWidth:3];
[[NSColor whiteColor] set];
[path stroke];
[image release];
Here's the final result drawn into the custom view.
In this example, the image is loaded from disk right before
being drawn to the view. This makes the code easier to follow,
but it's very inefficient and makes window resizing very slow.
A better approach is to load the image from disk once and store
it in an instance variable for drawing later. One of the
following examples demonstrates that.
Overlay Images with Transparency
Cocoa can easily draw images as partially transparent, as seen in the
following example. First, we need to calculate the rects that
we want to draw into, centering the images in the view.
[[NSGraphicsContext currentContext]
setImageInterpolation: NSImageInterpolationHigh];
NSSize viewSize = [self bounds].size;
NSPoint viewCenter;
viewCenter.x = viewSize.width * 0.50;
viewCenter.y = viewSize.height * 0.50;
NSSize large = { 250, 156 };
NSSize small = { 125, 78 };
NSPoint origin = viewCenter;
origin.x -= (large.width+small.width) * 0.5;
origin.y -= large.height * 0.5;
// calculate the main image rect
NSRect largeRect;
largeRect.origin = origin;
largeRect.size = large;
// move to the right and resize
NSRect smallRect1 = largeRect;
smallRect1.origin.x += large.width;
smallRect1.size = small;
// move down
NSRect smallRect2 = smallRect1;
smallRect2.origin.y += small.height;
Next we need to load the images from disk and flip them to
match our coordinate system.
NSString * file1 = @"/Library/Desktop Pictures/Plants/Purple Frond.jpg";
NSString * file2 = @"/Library/Desktop Pictures/Plants/Agave.jpg";
NSImage * image1 = [[NSImage alloc] initWithContentsOfFile:file1];
NSImage * image2 = [[NSImage alloc] initWithContentsOfFile:file2];
[image1 setFlipped:YES];
[image2 setFlipped:YES];
Now each image draws itself into the view twice: once in the main large rect, and
once in a smaller rect. A single image object can draw itself any
number of times.
The images are drawn on top of each other in the large rect, with 80%
and 60% opacity, respectively. After that, each image draws itself into a
separate smaller rect, fully opaque.
[image1 drawInRect: largeRect
fromRect: NSZeroRect
operation: NSCompositeSourceOver
fraction: 0.8];
[image2 drawInRect: largeRect
fromRect: NSZeroRect
operation: NSCompositeSourceOver
fraction: 0.6];
[image1 drawInRect: smallRect1
fromRect: NSZeroRect
operation: NSCompositeSourceOver
fraction: 1.0];
[image2 drawInRect: smallRect2
fromRect: NSZeroRect
operation: NSCompositeSourceOver
fraction: 1.0];
Finally, we draw borders around the images.
[[NSColor whiteColor] set];
NSBezierPath *path = [NSBezierPath bezierPathWithRect: largeRect];
[path appendBezierPathWithRect: smallRect1];
[path appendBezierPathWithRect: smallRect2];
[path setLineWidth:2.5];
[path stroke];
[image1 release];
[image2 release];
Here's the final result.
Resizing the window is still slow in this example, because we're still
loading the image from disk frequently. The next example focuses
on some optimization techniques.
Draw Directly to an Image
Hint: This technique is also useful if your
application needs to generate images dynamically, or superimpose content
on top of an existing image. The results can be written to a file.
We can optimize drawing multiple images to the screen by combining them in into
one image, then draw the composite version to the view.
This is called drawing into an "offscreen image" or "offscreen buffer",
because the pixels are collected for later use instead of being displayed
on the screen immediately.
To do this, we need to create an instance variable in the view named
compositeImage, and
add a method to generate the image. The generated image will be used in drawRect.
Below is the first part of the code for createCompositeImage.
First, we set up an array of image file names, and calculate the total size
needed to hold all of them.
- (void)createCompositeImage {
NSString * folder = @"/Library/Desktop Pictures/";
NSArray * files = [NSArray arrayWithObjects:
@"Plants/Petals.jpg",
@"Nature/Flowing Rock.jpg",
@"Plants/Maple.jpg", nil];
// make a size big enough to hold all images
int count = [files count];
NSRect rect = { 0,0, 150, 94};
NSSize compositeSize;
compositeSize.width = (rect.size.width * count);
compositeSize.height = rect.size.height;

Now we'll create the compositeImage object and draw into it using the
lockFocus
method. Once lockFocus is called, all drawing goes directly into
the image object until
unlockFocus is called.
Locking focus creates a new graphics context and also creates a
new
coordinate system where (0,0) is the upper-left corner of the
image, rather than the view itself. For clarity, it's best to indent
the code between locking and unlocking focus.
The code below assumes standard accessors for compositeImage exist.
// -createCompositeImage continued...
NSImage * compositeImage;
compositeImage = [[NSImage alloc] initWithSize:compositeSize];
[compositeImage lockFocus];
// this image has its own graphics context, so
// we need to specify high interpolation again
[[NSGraphicsContext currentContext]
setImageInterpolation: NSImageInterpolationHigh];
int i;
NSImage * image;
NSString * file;
for ( i = 0; i < count; i++ )
{
// load the image from disk and draw it
// into the composite image
file = [folder stringByAppendingString:
[files objectAtIndex:i]];
image = [[NSImage alloc] initWithContentsOfFile:file];
[image setFlipped:YES];
[image drawInRect: rect
fromRect: NSZeroRect
operation: NSCompositeSourceOver
fraction: 1.0];
[image release]; image = nil;
rect.origin.x += rect.size.width; // move right
}
[compositeImage unlockFocus];
[self setCompositeImage:compositeImage];
[compositeImage release];
} // end -createCompositeImage
Now we'll move on to drawing the composite image into the view.
Display the Composite Image
The composite image is drawn into the view in the NSView drawRect method,
as shown below.
- (void) drawRect: (NSRect)aRect {
NSImage * compositeImage = [self compositeImage];
if ( !compositeImage )
{
// if the image is nil, we need to create it
[self createCompositeImage];
compositeImage = [self compositeImage];
}
// find the center of the view
NSSize viewSize = [self bounds].size;
NSPoint viewCenter;
viewCenter.x = viewSize.width * 0.50;
viewCenter.y = viewSize.height * 0.50;
// calculate image rect to draw the image to
NSSize size = [compositeImage size];
NSRect rect;
rect.origin.x = viewCenter.x - (size.width * 0.50);
rect.origin.y = viewCenter.y - (size.height * 0.50);
rect.size = size;
// draw composite image
[compositeImage drawInRect: rect
fromRect: NSZeroRect
operation: NSCompositeSourceOver
fraction: 1.0];
// draw border
NSBezierPath * path = [NSBezierPath bezierPathWithRect:rect];
[path setLineWidth:2.5];
[[NSColor whiteColor] set];
[path stroke];
} // end of -drawRect
Here's our final result. You'll notice that resizing the window in this
example is much faster than in the previous examples because we're only
loading the image data once.
It's only necessary to draw into an offscreen image if you want to create
a combination of multiple images. Individual images can just be set
as instance variables and drawn as is.
Wrap Up
We've covered some intermediate Quartz topics here. If you'd like to see more
tutorials like this, please make a donation below. The source code from this
tutorial is included in the following zip file.
Please consider the download your
thank you gift for making a donation.
It also contains a few small extras, including many more comments and an example
of using Bindings and the Objective-C runtime to dynamically select a method to
run.
Show me a garden that's bursting into life.