MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Programming With Cocoa

Introduction to Cocoa Graphics

10/19/2001

Since the beginning of this column, I have primarily discussed how to use Cocoa for the purposes of making applications with user interfaces. I've also talked about the fundamentals of object-oriented programming (OOP), as well as how the most commonly used Cocoa classes are used in applications. Today I'm going to shift our focus from those subjects to the area of drawing and 2D graphics in Cocoa. I'll start out "small" today by discussing the basics of how to draw simple shapes in a window.

In future columns, I'll get into drawing more complicated shapes, implementing animations, as well as working with raw image data. Before that, however, let's start with the basics.

Rectangles, sizes, and points

Rectangles, sizes, and points are represented by three Cocoa data types that are actually nothing more than C structs. They are respectively NSRect, NSSize, and NSPoint.

NSPoint (or more simply, a "point") is the most fundamental of these three data types, so we will talk about it first. A point in Cocoa is a variable that stores a coordinate pair, that is the x and y values of a location on the coordinate plane of our drawing surface. We will discuss the drawing surface in more detail later on, but suffice it to say that the drawing surface coordinate plane is just a Cartesian coordinate plane that we've all learned about in geometry. The origin (0,0) of the plane is in the lower left corner of your drawing surface, and positive x-values extend to the right, while up is positive in the y-direction. In this system, one unit of distance is equivalent to one pixel on your screen; keep that in mind when visualizing sizes of objects that you will draw onscreen.

So what about points? How does Cocoa represent them? NSPoint is formally a C structure of the following constitution:

typedef struct _NSPoint {
    float x;
    float y;
} NSPoint;

If you're unfamiliar with the intricacies of C, the typedef statement simply defines a new data type -- NSPoint -- that is equivalent to the more fundamental data type struct _NSPoint. So whenever we talk about NSPoint, we are talking about the structure shown above. If you are unfamiliar with C structs, then you might do well to go back to your preferred C reference and read up on it a bit.

To initialize an NSPoint variable, we use the Foundation function NSMakePoint, which takes two arguments, the x and y coordinates of the point, and returns an NSPoint variable initialized to those arguments. The following line of code shows how we use this:

NSPoint p = NSMakePoint(10, 45);

To fully leverage these variables (NSPoint, NSRect, and NSSize), you must know how to access the structure members. In other words, given an NSPoint variable, how do we determine the x-coordinate value or the y-coordinate value of that point? The answer is the period operator. The period operator refers to the how we access structure members using the structVariable.memberName syntax.

For example, if we wanted to retrieve the x and y coordinates from an NSPoint structure variable, we would do the following:

float xCoord = p.x;
float yCoord = p.y;

Where given the point above, xCoord would take the value "10", and yCoord would have the value "45". With NSPoints out of the way, let's move on to NSRect and NSSize, the other two data types that we will frequently interact with.

NSRect

NSRect (or more simply, "rect") is another custom data type defined in the Foundation framework. A rect defines a rectangular region in the drawing area. It's not a shape, but it could define the boundary of a shape, the boundary of a window, the boundary of a drawing view, an area to fill with a color or anything else of that nature. Rects are determined by an origin point, and a size, which is simply a width and height. The formal definition of NSRect, as documented in the Foundation reference, is the following:

typedef struct _NSRect {
    NSPoint origin;
    NSSize size;
} NSRect;

The origin member is an NSPoint object that indicates the bottom left corner of the rectangle. NSSize is simply a structure data type that contains values for the height and width of the rect as its members. NSSize has the following formal definition:

typedef struct _NSSize {
    float width;
    float height;
} NSSize;

To create a rect, we use the Foundation function NSMakeRect, which takes four arguments: the x- and y-coordinates of the origin, the width, and the height. If we wanted to create an NSRect variable, we would do the following:

NSRect r = NSMakeRect(10, 10, 100, 150);

Comment on this articleYou don't have to be Degas to draw in Cocoa. Have you tried using NSRect, NSSize, and NSPoint? How did it go?
Post your comments

This creates a rectangle with an origin at the point (10,10), with a width of 100, and a height of 150. So the top right corner of the rectangle would be at the point (110, 160).

Because the members of NSRect are themselves structure variables, we must use the period operator twice to drill down and access the most fundamental pieces of information about the rect. For example, to access the value of the x-coordinate of the rect we created above, we would do the following:

float xOrigin = r.origin.x;

If we wanted to know the height of a rect, we would do something similar:

float width = r.size.width;

With a handle on the fundamental data types that we will use in Cocoa drawing, let's get on to see how to draw shapes.

The canvas and the brush

Learning CocoaLearning Cocoa
By Apple Computer, Inc.
Table of Contents
Index
Sample Chapter
Full Description
Read Online -- Safari

A useful way of envisioning how Cocoa drawing works is to think of how you would draw anything else. You'll need a surface to draw onto and something to draw with -- in other words, a canvas and a brush. In Cocoa, the canvas is the class NSView, and the brush is the class NSBezierPath. Let's first look at how we set up our canvas in Project Builder and Interface Builder, and then we'll get to the fun part of actually drawing.

Setting up the canvas

As I said above, Cocoa's canvas is the class NSView. NSView is a class that provides a mechanism for defining an area on the screen to draw to, as well as all of the machinery behind drawing. NSView is a beast of a class, and to go into a detailed description of the inner workings of this beast would be an exercise in duplicating the excellent documentation provided by Apple, and would make this column entirely too long. So I refer you to the NSView class documentation for a detailed understanding of the workings of this class, and here we will talk about how to use it.

We never directly interact with an instance of NSView -- it is an abstract class. Rather, we subclass NSView and add all of our drawing code to the subclass, and interact with the subclass. Project Builder provides a means for creating a skeletal NSView subclass.

The first thing we need to do is to create a new project. Call your new project CocoaDrawing. After the new project opens up, go to the File menu and select "New File". In the New File dialog select "Objective-C NSView Subclass" as the type of file you want to create. Click on Next, and name your new file CocoaDrawing.m. Make sure that CocoaDrawing.h is set to be created as well. Click Finish, and you have your NSView subclass file.

The next step is to let our application know about our subclass of NSView. This is done within Interface Builder (IB). Open Interface Builder by double-clicking on the file MainMenu.nib in your Project. With Interface Builder open, we do a neat little trick of adding our new class, CocoaDrawing, to those classes that IB knows about. We do this by dragging the file CocoaDrawing.h from our project window onto the IB Instances pane.

Screen shot.
Dragging the CocoaDrawing class header from Project Builder into Interface Builder.

This will add our new class, CocoaDrawing, to the list in the Classes pane as shown in the image below. This process of adding a class to IB can be done with any class you create in Project Builder. In fact, this is the reason for all of the IBOutlet and IBAction tags we see in our IB-generated code, so IB can make available any indicated outlets and actions.

Screen shot.
CocoaDrawing added to the list of classes in InterfaceBuilder.

The next step is to drag a CustomView container onto our application's window. From the Cocoa Containers Palette, drag a Custom View object onto the window. With CustomView selected, open the Show Info panel, and under Attributes you should see CocoaDrawing in the class list. Select it and the name of the view will change to CocoaDrawing, and the contents of this view will now be controlled by the code in CocoaDrawing. We're now set and ready to learn about the Cocoa paintbrush. Save your work in IB, return to Project Builder, and move onto the next section.

Screen shot.
Adding a view container to your application's window.

Screen shot.
Changing the CustomView from an NSView to our NSView subclass, CocoaDrawing.

The brush

Our brush in Cocoa is the AppKit class NSBezierPath (also know as bezier path). Like NSView, NSBezierPath is a class jam-packed with all sorts of neat behaviors. Unfortunately, we'll only scratch the surface of today, but we will hit much more of it later on.

Bezier paths are objects that define shapes as a series line and curve segments. For example, a rectangle could be represented as a bezier path with four straight-line segments. With bezier paths, you can make shapes with arbitrary complexity. The procedure for drawing a shape to a view is to first create a path, and then either fill the path with a color, or draw it as an outline. We'll see examples of both shortly.

All drawing code that we write will be done in the method drawRect:. For those of you with experience with Java 2D graphics, drawRect: is analogous to the method paint(). The argument of this method is the rectangle in which the drawing is to be done.

Normally we will never call drawRect: directly. Rather, whenever something changes with our view that requires the contents of the view to be redrawn, that process will tell the view that it needs to be redrawn. Today we won't have any need or reason to manually tell the view to redraw itself, but that will come in a later column.

What we'll work on today is simply how to draw rectangles and ellipses, which are actually almost identical in code. The two methods we will concern ourselves are the convenience constructors +bezierPathWithRect: and +bezierPathWithOvalInRect:. Let's dive right into our first example of drawing and see how it works.

In the drawRect: method, we will add the following code:

- (void)drawRect:(NSRect)rect
{
  NSRect r = NSMakeRect(10, 10, 50, 60);
  NSBezierPath *bp = [NSBezierPath bezierPathWithRect:r];
  NSColor *color = [NSColor blueColor];
  [color set];
  [bp fill];
}

In the first line, we defined the rectangle which would soon become our bezier path. Following that, we created a bezier path object that simply follows the perimeter of the rectangle. In the next line, we created a color that we would fill the rectangle with, and following that, made the current color of subsequent drawing using the -set method of NSColor. The message [color set] tells the graphics engine in the background that any drawing operations will be done with the color blue (as indicated by the object color). Finally, we send a fill message to the bezier path that fills the path with the color we set. This snippet of code should produce the following output:

Screenshot.

Alternatively, we could have made the last line [bp stroke], which would draw a line along the path, as shown below:

Screenshot.

Now, if we changed the bezier path creation line of code (the second line) to the following:

NSBezierPath *bp = [NSBezierPath bexierPathWithOvalInRect:r];

The shape drawn to the screen would be an ellipse that fits into the specified rectangle, as is shown in the image below:

Screenshot.

Another way we can draw a rectangle is using the Foundation function NSRectFill, which takes a rectangle as its argument. The color of the fill operation performed by this function is the current color of the graphics environment. For example, we could change the first example to circumvent the use of NSBezierPath by using the following code:

- (void)drawRect:(NSRect)rect
{
    NSRect r = NSMakeRect(10, 10, 50, 60);
    NSColor *color = [NSColor blueColor];
    [color set];
    NSRectFill(r);
}

NSRectFill is also a convenient way of coloring the background of a view. We can do this by setting a color as the background color, and then passing the drawRect argument variable rect to NSRectFill. Reverting back to our original example, we can make the background black in the following way:

- (void)drawRect:(NSRect)rect
{
    NSRect r;
    NSBezierPath *bp;

    [[NSColor blackColor] set];
    NSRectFill(rect);

    r = NSMakeRect(10, 10, 50, 60);
    bp = [NSBezierPath bezierPathWithRect:r];
    [[NSColor blueColor] set];
    [bp fill];
}

In this example, I eliminated the color variable by sending a set message directly to the object returned by [NSColor blueColor]. This code produces the output shown below.

Screen shot.

This method of coloring the background of the view works because when the method drawRect: is automatically invoked by the view, the boundaries of the view are passed as the rect argument. Another note about the way drawing works -- the background painting code must come before any other drawing commands. This is because objects that are drawn to the view are drawn over anything that was previously there.

End

So that's essentially how you can create shapes using the Cocoa drawing classes! It's pretty simple -- there's not too much to it. In the next column, we will continue with our graphics and drawing discussion by learning how to make more complex paths. Until then, have fun with what you've learned today, experiment, and play around with your ideas. I leave you with a small app that draws a collection of randomly generated rectangles and ellipses to the screen, as is shown in the image below. It's a simple app that doesn't use much else than what we've seen today in terms of drawing code. It can be downloaded here See you next time!

Screenshot.

Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.


Read more Programming With Cocoa columns.

Return to the Mac DevCenter.

Copyright © 2009 O'Reilly Media, Inc.