Professional Documents
Culture Documents
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.
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.
Here's an example of appending a path to an existing path. The path being added is
an arc:
NSRect rect;
rect.origin = origin;
rect.size.width = 128;
rect.size.height = 128;
NSBezierPath * path;
path = [NSBezierPath bezierPathWithRect:rect];
[path setLineWidth:4];
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.
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.
// copy the rect for the top ellipse and adjust the y axis
NSRect rect4 = rect3;
rect4.origin.y = NSMaxY(mainOval) - (qt + dotRd);
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.
[[NSGraphicsContext currentContext]
setImageInterpolation: NSImageInterpolationHigh];
NSPoint viewCenter;
viewCenter.x = viewSize.width * 0.50;
viewCenter.y = viewSize.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.
[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];
// 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.
[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 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.
- (void)createCompositeImage {
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.
// -createCompositeImage continued...
NSImage * compositeImage;
compositeImage = [[NSImage alloc] initWithSize:compositeSize];
[compositeImage lockFocus];
int i;
NSImage * image;
NSString * file;
[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.
// 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.