Programming Mac OS X with Cocoa for Beginners/Graphics - Drawing with Quartz

Previous Page: A more ambitious application | Next Page: Document-based applications

We have already seen how to create a subclass of NSView that gives us the ability to draw in a window. Before we get into the hard work of implementing our drawing program, Wikidraw, we'll need to have a better understanding of graphics, and how to use the built-in vector graphics API, to do our bidding.

First let's recap the principles of views and how they draw. When we subclass NSView, we supply a method called drawRect: which Cocoa will call whenever it needs us to draw the content of our view. Any code we place here is executed at that time, so if we place graphics commands here, we can draw in the window. Anything we draw will be confined automatically to the boundary (the bounds) of our view. Within the view, we have our own coordinate system, so we can draw without regard to where our view is within the window - everything is drawn relative to the view's bounds. Normally, coordinates are arranged so that x increases from left to right, and y increases from bottom to top, but by implementing isFlipped, returning YES, we can invert the y axis which will usually be more comfortable for programmers.

The coordinate system within a view starts at 0,0 for the top, left (assuming it is flipped), and increases by 1 for each pixel. However, coordinates are expressed as floating point values, so it's perfectly reasonable to specify a point 2.5 pixels across or 3.5612 pixels down if you want. We are not limited to integer coordinates, in other words. The view will render appropriately, rasterising our graphics using antialiasing so that these fractional pixel positions do have the proper outcome on a screen.

NSBezierPath

edit

A key object for drawing any shapes in Cocoa is NSBezierPath. Its name is a little misleading, since while it is able to render bezier paths, it can also handle a variety of shapes including rectangles, circles, lines and polygons. Think of NSBezierPath as a list of coordinate points that can be joined up by lines, or optionally by curved bezier paths. We can build a list of points, adding them to the path object. We can then draw the outline of the path formed (called stroking) or fill the interior of a closed shape (called filling). A stroked path can have any width we choose. We can either use any colour we choose, or with more effort, a gradient or patterned fill.

Earlier we used a utility function, NSFrameRect, to draw the bounds of the view used in HelloWorld. Behind the scenes, NSFrameRect simply makes a temporary NSBezierPath object with a rectangular shape, then strokes the path. In Wikidraw, we will be using NSBezierPath as our workhorse to draw each shape we create.

Before we do that though, let's just try out a few simple shapes with NSBezierPath to get an idea of how it works. We'll temporarily add some code to WKDDrawView's drawRect: method. Later we'll remove it and put the real code there. To make this easier, we'll put our test code into a method called testDraw, and call it from drawRect. Add the following code to WKDDrawView.m:

static NSPoint tp[] = { 10, 10, 400, 100, 300, 200, 10, 150 };

- (void)	testDraw
{
	NSBezierPath* path = [NSBezierPath bezierPath];
	
	[path moveToPoint:tp[0]];
	[path lineToPoint:tp[1]];
	[path lineToPoint:tp[2]];
	[path lineToPoint:tp[3]];
	[path closePath];	
	[[NSColor yellowColor] set];
	[path fill];
	
	[[NSColor blackColor] set];
	[path stroke];
}

To make supplying a set of points easier, we just create a static array with alternating x and y values. We can then refer to these points as tp[0], tp[1] and so on. We create an NSBezierPath object called 'path'. We use a class factory method to return an empty object, which is autoreleased, so we don't need to retain or release it, just use it and forget it.

We form the path using the moveToPoint:/lineToPoint: methods, finally closing the shape using closePath. Then we set yellow as the drawing colour and ask the path to fill itself. Then we set black and ask the path to stroke itself.

Next we need to call our method from drawRect: so it gets drawn when the view is refreshed. We also need to do one other thing. Though we have signalled that our view is opaque, we do need to erase the part of the background we are refreshing to avoid problems when we scroll. The 'rect' parameter passed to drawRect: is the area that is to be redrawn, so we set the colour white and erase this rect using the utility NSRectFill. Then we call our testDraw method.

 - (void)	drawRect:(NSRect) rect
 {
	[[NSColor whiteColor] set];
	NSRectFill( rect );

	[self testDraw];
 }

Add this code then Build and Go. Verify that the shape is drawn and that it works correctly when the view is scrolled. Now you have a basic mechanism for experimenting with NSBezierPath, you might like to explore some of its other methods, such as curveToPoint:controlPoint1:controlPoint2: which is how bezier curves are specified. You can also set rectangles and ovals in one go. Try altering the stroke width. Look at the documentation for NSBezierPath to see what else it can do.

Previous Page: A more ambitious application | Next Page: Document-based applications