A Simple Drawing Sample in Quartz 2Dby Scott Thompson
The previous article in this series examined some of the basic differences between the Quartz 2D and QuickDraw drawing models. It also covered a few of the fundamental ideas that make up the Quartz 2D drawing model, like the virtual coordinate space and the painting algorithm.
This article uses a bit of sample code to point out how some of those ideas actually look when used inside an application. Along the way we'll explore the creation of CGContexts, look at the painting algorithm in more depth, talk about how paths are constructed, see an example of how Quartz 2D handles colors, and look an some examples of how the Quartz 2D coordinate system can be manipulated to make drawing easier.
Sample Code--Drawing a Texas Flag
Long ago my family hosted a foreign exchange student from Denmark. He arrived not long after my family had purchased our first computer, an Apple II+. I had done some BASIC programming and was playing with code that drew low-resolution graphics on the computer. The exchange student was also interested in computers, and we took the time to figure out how to get the computer to draw the Danish flag. Once we had succeeded (the country's flag is not terribly complicated), we dragged out an atlas that had pictures of lots of different flags in it and tried to see how many of those we could replicate.
Our code sample for this article builds upon that fine tradition by drawing the flag of the state of Texas using Quartz 2D. The Texas flag was chosen for two reasons. First, it is pretty easy to draw. As you'll see, in spite of its simplicity it also lets us explore quite a few different areas of the Quartz 2D drawing system. The second reason, of course, is that this article was written in Texas. For the morbidly curious, and in order to make the drawing accurate, I actually looked up the standards for the Texas flag as spelled out in chapter 3100 of the Texas Government Code. You can see a picture of the flag and find information about it, its dimensions, its colors, as well as other interesting tidbits within those laws at the About Texas web site.
In drawing the flag, we're going to demonstrate a few of the common things that any program might do to draw with Quartz 2D:
- Obtain the
CGContextRefwe will draw in.
- Mark off areas of the coordinate plane we want to color using paths.
- Ask Quartz 2D to fill those areas with color.
- Use coordinate transformations to simplify things where appropriate.
A Brief Digression on API Issues
Before we get into the details of the sample code, it may be worth taking a moment to stop and talk about some API issues in Core Graphics and Quartz 2D in particular. The routines that make up Quartz 2D, like the rest of the Core Graphics routines, are found in the Core Graphics framework. The Core Graphics framework, in turn, is found inside the Application Services umbrella framework. An application will usually link to the Application Services framework alongside Carbon. (If you don't know what a framework is, I strongly recommend you explore the System Overview at Apple's web site. Don't stop at just the section on frameworks, either. The entire book is full of valuable information.)
The constants, routines, and other code elements that make up the Core Graphics incorporate the two-letter prefix CG into their name (for example,
CGImageCreate, and so on), which should make them easier to spot.
The Core Graphics API, like many modern APIs, categorizes its calls in terms of objects and has rules for managing the lifetime of those objects. In particular, any routine that creates a Core Graphics object--for example,
CGBitmapContextCreate--will have a corresponding routine for releasing that object back to the system: in this instance,
CGContextRelease. Our sample won't be creating any Core Graphics objects directly, and we won't have to concern ourselves too much with the life cycle of the objects we work with. The object ownership idioms are important nevertheless, and you can find more information on the topic in Chapter 2 of Drawing With Quartz 2D.
The object-oriented nature of the API has implications beyond memory management. In QuickDraw, the operating system maintains a reference to a current GrafPort, and any drawing commands are executed using that port. In contrast, Core Graphics issues commands to a particular instance of a some object when your application calls a C API. The first argument of most of those routines, of course, is the object to which the command is sent. For example, the first argument to routines that talk to a CGContext is a
CGContextRef. However, you may be wondering: if we don't create the CGContext we interact with, where does it come from?
Getting a CGContext
CGContextRef represents the object that is the link between your application and a graphics device. In that respect, it is a rough analog to the QuickDraw
CGrafPtr. Your application can obtain different types of CGContexts from different sources depending on the type of device those contexts communicate with. Since they all descend from the same root class, however, they will all accept the same drawing commands and do their best to translate those commands into something meaningful on the destination device. The following table summarizes some of the most popular ways your application might receive, or create, an CGContext:
|HIView||Using Carbon Events, you can attach a handler for the
|QuickDraw Ports||The routine
|Carbon Printing||The routine
|Offscreen Drawing||One way to get an offscreen graphics context is to attach a CGContext to an offscreen GWorld (see QuickDraw Ports above). If you want to avoid QuickDraw altogether, however, you can get a CGContext for offscreen drawing from
|Metafiles||Just as PICT is the metafile format for QuickDraw, PDF is the metafile format for recording Quartz 2D commands. You can create a CGContext for a Quartz 2D PDF metafile by using
|NSView||The CGContext for the currently focused view can be obtained from
For our code sample, we are going to be using an HIView. We're going to override the
kEventControlDraw event and get a CGContext from that event. Since the CGContext has a coordinate system of infinite extent, we're going to use the bounds of the HIView to limit the size of our drawing. Other than that--and one other concession we have to make because we're using an HIView, which we'll get to in a moment--there is nothing specific to HIViews in our drawing code. The same code could just as easily draw our flag in an NSView, send it to the page of a PDF file, or print it.
The small concession that we must make, which is specific to the way the system passes an HIView's graphics context to an application, is a feature designed to make the transition of QuickDraw programmers easier. Recall from the previous discussion that Quartz 2D can orient the coordinate axes of a CGContext arbitrarily with respect to the coordinate axes of the destination device. A common tactic when drawing with Quartz 2D is to use this fact to make drawing easier. The HIView system does just that.
In most cases, when the system passes a CGContext to your application, the origin of that context's coordinate system coincides with in the lower-left corner of the device's coordinate system. From that starting point, the convention is to have the positive y-axis extend up the left side of the device and the positive x-axis point off to the right (Figure 1a). Compare this with the default QuickDraw coordinate system, which has the origin in the upper-left corner of the device and the y-axis extending downward (Figure 1b):
|Figure 1a. Default Quartz 2D axes||Figure 1b. Default QuickDraw axes|
Apple created the HIView system as a more powerful replacement for the classic Control Manager, and it wants to make the transition from the Control Manager to HIView as simple as possible. To make it easier to incorporate legacy QuickDraw drawing code into a view, HIViews follow a convention in which the CGContext that the operating system passes to your application orients its coordinate axes the same way the QuickDraw coordinate system does. Recall from our prior discussion that for bitmapped devices, the system scales the CGContext's initial coordinate system so that one unit on the CGContext's coordinate axis covers the same distance as one pixel in the bitmap. That means that by default, the operating system passes an HIView a CGContext whose coordinate system matches the QuickDraw coordinate system!
By and large, this setup is advantageous for programmers trying to reimplement custom QuickDraw controls using the HIView system. Depending on the needs of the HIView in question, however, this unconventional arrangement of the axes can lead to unexpected behavior. One of the most frequently encountered problems crops up when an application tries to draw an image, or ATSUI text, in an HIView using Quartz 2D. The application will obtain a
CGImageRef from some source and then call
CGContextDrawImage to draw that image.
Quartz 2D draws the image oriented to the coordinate system such that "up" on the image is the same direction as the positive y-axis (Figure 2a). In the default HIView coordinate system, however, the positive y-axis travels down the window. That means that images (or text) drawn through Core Graphics wind up on the screen upside down relative to the window (Figure 2b). In the case of images, the Human Interface Toolbox provides a convenience routine to handle this common error directly. The
HIViewDrawCGImage routine will reorient the coordinate axes, draw the image, and return the coordinate axes to their previous state, all in one routine call.
|Figure 2a. Image drawn with axes in Quartz 2D orientation||Figure 2b. Image drawn with axes in QuickDraw orientation|
For this sake of this sample, we will try to insulate our drawing code from the peculiarities of HIView as much as we are able. As part of achieving that goal, we will write our drawing routines so that they assume the coordinates are set up in the conventional Quartz 2D orientation. To do that, one of the first steps we will take after getting the CGContext for our HIView is to rearrange the coordinate system to suit us. That involves moving the origin to the lower-left corner of the view and flipping the y-axis around so that it points upward on the window. The code that the sample uses to accomplish this is:
// Reorient the coordinate system so that the // origin is in the lower left corner of // the view and the y axis points "up" on the // window. CGContextSaveGState(contextToDraw); CGContextTranslateCTM( contextToDraw, 0, flagViewBounds.size.height); CGContextScaleCTM(contextToDraw, 1.0, -1.0); ... Drawing commands here ... CGContextRestoreGState(contextToDraw);
Listing 1. Reorienting an HIView's coordinate system
You could undo this transformation simply by repeating the translation and scale commands (a fact you can verify yourself by applying logic similar to that in the discussion above). However, if your transformation is complex, undoing it can be difficult and prone to error. Listing 1 demonstrates a much better technique. The routine calls
CGContextRestoreGState form a pair. As you may remember, both the CGContext and the QuickDraw GrafPort maintain a certain amount of information about how to interpret drawing commands.
For example, a QuickDraw port has a foreground color, a background color, a clipping region, a pen size, and a several other fields in its graphics state. Quartz 2D graphics contexts have similar state items such as the stroke color, the fill color, and the text drawing mode. One of the most popular fields in a CGContext's graphics state is the Current Transformation Matrix, or CTM. The CTM is that part of the drawing state responsible for keeping track of the way that the context's coordinate system maps to the device's coordinate system. From the API perspective we can think of coordinate transformations as translations, scales, and rotations, but Quartz 2D represents all of its coordinate transformations as 3-by-3 matrices and combines and applies them using matrix mathematics.
Quartz 2D will let you get at the matrices themselves, but the specifics of that math don't concern us yet. It may nevertheless be helpful to realize that when you issue calls like
CGContextTranslateCTM, you're not actually moving the origin; rather, you are changing the value of some numbers in the CTM. This means that the coordinate transformation routines are fairly inexpensive operations.
CGContextSaveGState routine does, in essence, is to push the fields of the current graphics state onto a stack. From that point, your code can change the graphics state freely to accomplish whatever drawing tasks it may need to perform. When you call
CGContextRestoreGState, Quartz 2D pops all the fields it had saved to the stack and restores them to their previous values. By placing our CTM calls inside the scope of the save and restore pair, we insulate other code that doesn't need those changes. We call
CGContextSaveGState to save the current CTM (and therefore the current origin and y-axis directions). We then change the coordinate system to suit our needs, and call our drawing code. Once we're done we politely restore the CTM back the way we found it for any drawing code that follows.
CGContextTranslateCTM routine moves the origin to the lower-left corner of the view. To get from the top left to the bottom left, we have to move the origin down the window. Recall that at the time we call this routine, the positive y-axis is already pointing down the window, so the new location we want for the origin is
flagViewBounds.size.height units away along the positive y-axis. We don't want to move the axis horizontally, so the x-coordinate of our translation is 0.
CGContextScaleCTM command we use here is not very intuitive at first. Normally you would use
CGContextScaleCTM to change the scale of the Quartz 2D coordinate axes from their initial scale of 1 unit as 1 point (or, for bitmap contexts, 1 unit as 1 pixel). If you wanted your drawing to be twice the size (2X, or 200 percent), for example, you might use the command:
CTContextScaleCTM(contextToDraw, 2.0, 2.0);
and from that point on, every unit you specify in the context's coordinate system would cover twice the distance on the device that it did before. In our code, we're using a scale factor of 1.0 to the x-axis. Scaling an axis by 1X (or 100 percent) actually means that you're keeping everything exactly the same that it was before. Oddly, however, when scaling the y-coordinate axis, the code uses a scale factor of -1.0. The net effect of this strange scale factor is that the current coordinate system gets "flipped" across the x-axis. During the flip, the positive and negative y-axes simply trade directions without otherwise being affected.
A common mistake people sometimes make when using this trick is scale the x-axis by 0 instead of 1.0. Scaling an axis by 0 doesn't really make much sense, but the system is happy to try to do it for you. In scaling, if you want the x-axis to remain as it is, you use 1.0. In translations, to keep the origin at the same x position, use 0.