Cocoa’s default set of
controls covers most common UI needs, but it can’t
cover everything. For example, you might want to create a drawing
application and need a view that can have lines and other shapes
drawn into it. Or, you might want to create a custom graph of stock
data over time. Whenever you have these kind of needs, you will need
to create a subclass of
NSView
: a custom view.
A custom view is responsible for drawing content into, and handling events that occur within, its bounds —the rectangular region given to it by its superview. You can use any of Cocoa’s drawing tools to draw content into the view. In this chapter, we’ll work through a couple of basic custom-view examples to show you how everything works. Then, in the next chapter, you’ll build on what you learn in this chapter to create a custom view to respond to user events.
When you make a custom subclass of
NSView
and want to perform custom drawing and
handle events, the following procedure applies:
In Interface Builder, define a subclass of NSView
,
then generate header and implementation files.
Drag a Custom View object from the Views palette onto a window, and resize it.
With the Custom View object still selected, choose the Custom Class panel of the Info window, and select the custom class. Connect any outlets and actions.
If needed, override the designated initializer
(initWithFrame:
) to perform any custom
initialization.
We will walk you through the steps outlined here to show you how to
create customized subclasses of NSView
for your
applications.
To start working with views, we will make a custom view. In Project Builder, create a new Cocoa Application project (File → New Project → Application → Cocoa Application) named “Red Square”, and save it in your ~/LearningCocoa folder.
Begin by opening the application’s main nib file in Interface Builder.
In Project Builder’s Groups & Files pane, click on the disclosure triangle next to Resources to reveal the MainMenu.nib file.
Double-click on the nib file to open it in Interface Builder.
A default menu bar and window will appear when the nib file is opened.
To define a class that will implement the custom functionality of our
view, we need to define a subclass of the
NSView
class.
Click on the Classes tab of the MainMenu.nib window.
Find the NSView
class in the hierarchy of
available classes. (You may need to scroll through the browser to
find it.) Its complete path in the hierarchy is
NSObject
→
NSResponder
→
NSView
.
Control-click on NSView, and select Subclass NSView from the pop-up
menu to create a new subclass named MyView
, as
shown in Figure 7-1. (You can also hit Return with
NSView
selected to create a subclass
automatically.)
Generate the source files for MyView
from the
Classes menu (Classes → Create Files for MyView).
(You can also Control-click on MyView
and select
Create Files for MyView from the Context menu.)
Interface Builder then displays a dialog box.
Verify that the checkboxes next to MyView.h and MyView.m are selected in the Create column.
Verify that the checkbox next to Red Square is selected in the Target column.
Click the Choose button to create the files.
Next, we need to create a place for our custom view to draw.
Drag a CustomView object from the Cocoa-Containers window (as shown in Figure 7-2) into the main window, and resize it to occupy the entire window.
With the CustomView object still selected, choose the Custom Class pane of the Show Info window (Tools → Show Info, or Shift-
-I), and select the
MyView
custom class. The name of the view will
change to MyView
to confirm this change, as shown
in Figure 7-3.
The nib now has enough information to create an instance of the
MyView
class and to assign it to an area of the
window. Save the nib file (File → Save, or
-S), and return to Project Builder by clicking on its icon in the Dock.
To draw into the view, we only need to implement the
drawRect:
method of our MyView
class. We’re
just going to fill the view with a red square.
Open the MyView.m implementation file in Project Builder by clicking on the filename in the Other Sources folder.
Edit it to match Example 7-1.
#import "MyView.h" @implementation MyView - (void)drawRect:(NSRect) rect // a { [[NSColor redColor] set]; // b NSRectFill([self bounds]); // c } @end
The code we added in Example 7-1 does the following things:
Adds a method declaration for the drawRect:
method. This method is called by the display
method of the NSView
class and takes a single C
structure (or struct),
NSRect
. This parameter is provided so that you can
just draw the part of the view that needs it—critical when
dealing with large, complex views that take time to redraw. In some
cases—and this is one of
them—redrawing the entire view
won’t really be a performance drag.
Sets the color that Cocoa uses for subsequent drawing operations.
Here we use a convenience method of the NSColor
class to get a red color.
Calls the NSRectFill
function, defined by the
AppKit framework, and tells it to fill the bounds of the view.
Save the project (File → Save, or
-S).
Build and run the project (Build → Build and Run, or
-R). You should see a window containing a red square, as shown in Figure 7-4.
Our view looks like it works just fine. However, we have a slight problem. Resize the window and observe how the red square is anchored to the bottom-left corner of the window and moves down as the window is stretched and resized. Ideally, we’d like to have the square fill the window no matter how the user resizes it.
Why does the view stay anchored to the lower left-hand corner of the window? The answer lies in the fact that Cocoa’s coordinate system starts at the lower-left corner. This behavior takes programmers who are used to a coordinate systembased on the upper-right corner (such as Carbon-based applications) a bit of time to adjust to.
To ensure that our view occupies the whole window, no matter how it is resized:
Bring the MainMenu.nib file to the foreground in Interface Builder.
Select the MyView
component in the interface.
Select Size from the pull-down menu of the Show Info window, and click once on the vertical and horizontal lines so that they appear to have springs in them, as shown in Figure 7-5.
Setting the Autosizing to these settings means that the view will grow and shrink as necessary to keep the distance from the edges of its parent view (the content view of the window) constant. Think of the springs as making the inside of the view “springy” so that it can stretch in size, while the straight lines ensure that the distance between the view and the edge of its container remains constant. If you wanted the view to remain a constant size in the middle of the window’s content view, you could turn the straight lines to springs and vice versa.
Save the nib file (
-S).
Switch back to Project Builder, and build and run (
-R) the project. Now when you run the program, you can resize the window to any size you want, and the red square expands or contracts as needed.
At this point, we’re done with the Red Square application. Close the project in both Interface Builder and Project Builder before moving on to the next section.
Before the NSView
class’s display
method, or one of
its variants, invokes a drawRect:
method on a
NSView
subclass, there is a bit of work that is
performed behind the scenes.
Core Graphics (CG) calls are needed to set
up Quartz with information
about the view, including the graphics context in which it draws, the
coordinate system and clipping paths it uses, and other graphics
state information. The NSView
method that does
this is
lockFocus
. There is a companion method that undoes
the effects of lockFocus
, called
unlockFocus
.
Focusing modifies the graphics state by doing the following:
Making the view’s window the current graphics context
Creating a clipping path around the view’s frame
Making the CG coordinate system match the view’s coordinate system
To produce proper results, all drawing code invoked by a view must be
bracketed by invocations of these methods. The
display
method, and its variants, of the
NSView
class perform these duties automatically,
so don’t worry about locking focus in the
drawRect
method. However, if you define some
methods that need to draw in a view without going through the
display
methods, you must first send a
lockFocus
message to the view in which you are
drawing before performing any drawing; then you can send the
unlockFocus
message as soon as you are done.
Only one view at a time can have focus. If focus is already locked
onto another view when the lockFocus
method is
invoked, the previous view’s lock is put onto a
stack, so focus can be restored to it when the lock of the current
view is released with the unlockFocus
message.
To continue working with drawing into
views, we will create an application that renders a string into a
custom NSView
subclass. In Project Builder, create
a new Cocoa Application project (File → New Project
→ Application → Cocoa Application)
named “String View”, and save it in
your ~/LearningCocoa folder.
As in the Red Square application, a new custom view class needs to be created. Follow the same directions as before to accomplish the following tasks:
Open the MainMenu.nib file.
Define a subclass of NSView
named
MyView
.
Generate the source files for MyView
.
Add a CustomView to the main window.
Assign the MyView
class to the CustomView from the
Custom Class pull-down menu of the Info window.
Set the Autosizing attributes of the view so that the view fills the window when the window is resized.
Save the nib file, and return to Project Builder.
Once again, we implement the drawRect:
method of
our MyView
class.
Open the MyView.m implementation file in Project Builder, and edit it to match Example 7-2.
#import "MyView.h" @implementation MyView - (void)drawRect:(NSRect)rect { NSRect bounds = [self bounds]; // a NSString * hello = @"Hello World!"; // b NSMutableDictionary * attribs = [NSMutableDictionary dictionary]; // c [[NSColor whiteColor] set]; // d NSRectFill(bounds); // e [hello drawAtPoint:NSMakePoint((bounds.size.width/2), // f (bounds.size.height/2)) withAttributes:attribs]; } @end
The code we added implements the same drawRect:
method that was overridden in the Red Square application (Example 7-1) and does the following things:
Gets an NSRect
structure containing the bounds of
the view.
Initializes an NSString
containing the string that
we want to draw into the view.
Creates an empty dictionary object (a Cocoa collection object like
those we covered in Chapter 4) that will be needed
for the drawAtPoint:withAttributes:
method in line
f.
Sets the active drawing color to white.
Calls the NSRectFill
function to fill the view.
This will paint the entire view white.
Calls the drawAtPoint:withAttributes:
method on
the hello
string. This draws the string at a point
that is half the width and half the height of the view. We give this
method an empty attributes argument to tell the system not do
anything special when the string is drawn.
Save the project (
-S).
Build and run the project (
-R). You should see the string drawn in the window, as shown in Figure 7-6.
Note that the string isn’t perfectly centered in the view. This is because the drawing point that the string uses to draw itself onto the view is at the lower-left hand corner of the bounding box of the string. This follows the same logic as the screen, window, and view coordinate systems.
Quit the String View application (
-Q).
You’ll notice that when we drew our “Hello World!” string, it was drawn with a small Helvetica font. You’ll often want to draw strings in other fonts and sizes. Do this by setting attributes that will be used when drawing a string. We’ll talk much more about string attributes in Chapter 11. For now, we just use the attributes needed to set the font and color of our string.
Modify the drawRect:
method in
MyView.m to match Example 7-3:
- (void)drawRect:(NSRect)rect { NSRect bounds = [self bounds]; NSString * hello = @"Hello World!"; NSMutableDictionary * attribs = [NSMutableDictionary dictionary]; [attribs setObject:[NSFont fontWithName:@"Times" size:24] // a forKey:NSFontAttributeName]; [attribs setObject:[NSColor redColor] // b forKey:NSForegroundColorAttributeName]; [[NSColor whiteColor] set]; NSRectFill(bounds); [hello drawAtPoint:NSMakePoint((bounds.size.width/2), (bounds.size.height/2)) withAttributes:attribs]; }
The code we added in Example 7-3 does the following things:
Obtains a font object for the Times font with a size of 24 points and
sets it into the attribs
dictionary.
Obtains a red color object and sets it into the
attribs
dictionary.
Build and run (
-R) the application. You should see the string drawn into the window with our attributes, as seen in Figure 7-7.
We’re now done with the String View application. Close the project in both Project Builder and Interface Builder before moving on.
Next in our exploration of drawing into
views, we are going draw some lines into a custom
NSView
subclass. In Project Builder, create a new
Cocoa Application project (File → New Project
→ Application → Cocoa Application)
named “Line View”, and save it in
your ~/LearningCocoa folder.
As before, a new custom view class needs to be created. Perform the following tasks:
Open the MainMenu.nib file.
Define a subclass of NSView
named
MyView
.
Generate the source files for MyView
.
Add a custom view to the main window.
Assign the MyView
class to the CustomView.
Set the Autosizing attributes of the view so that the view fills the window when the window is resized.
Save the nib file, and return to Project Builder.
Once again, we will implement the drawRect:
method
of our MyView
class. This time we will use the
NSPoint
structure to keep track of the various
points of the view between which we want to draw lines.
The NSPoint
structure is defined by the Foundation
Kit as the following:
typedef struct _NSPoint { float x; float y; } NSPoint;
Open the MyView.m implementation file in Project Builder, and edit it to match Example 7-4.
#import "MyView.h" @implementation MyView - (void)drawRect:(NSRect)rect { NSRect bounds = [self bounds]; NSPoint bottom = NSMakePoint((bounds.size.width/2.0), 0); // a NSPoint top = NSMakePoint((bounds.size.width/2.0), bounds.size.height); // b NSPoint left = NSMakePoint(0, (bounds.size.height/2.0)); // c NSPoint right = NSMakePoint(bounds.size.width, (bounds.size.height/2.0)); // d [[NSColor whiteColor] set]; [NSBezierPath fillRect:bounds]; // e [[NSColor blackColor] set]; [NSBezierPath strokeRect:bounds]; // f [NSBezierPath strokeLineFromPoint:top toPoint:bottom]; // g [NSBezierPath strokeLineFromPoint:right toPoint:left]; // h } @end
The code we added in Example 7-4 does the following things:
Creates an NSPoint
halfway along the bottom of the
view
Creates an NSPoint
halfway along the top of the
view
Creates an NSPoint
halfway up the left side of the
view
Creates an NSPoint
halfway up the right side of
the view
Draws a path that encompasses the entire view and fills that path with the current drawing color (white)
Draws a path that encompasses the entire view and draws a line along that path in the current drawing color (black)
Draws a path from the NSPoint
along the top of the
view to the NSPoint
along the bottom of the view
Draws a path from the NSPoint
along the right side
of the view to the NSPoint
along the left side of
the view
Save the project (
-S).
Build and run the application (
-R). You should see the lines drawn in the view as shown in Figure 7-8.
Now quit the Line View application (
-Q) before going on to the next example.
To finish the chapter, we’re going to modify the MyView.m used in the Line View application and draw an oval path in the view. To accomplish this task, you need to add one line to the MyView.m file, as shown in Example 7-5.
#import "MyView.h"
@implementation MyView
- (void)drawRect:(NSRect)rect
{
NSRect bounds = [self bounds];
NSPoint bottom = NSMakePoint((bounds.size.width/2.0), 0);
NSPoint top = NSMakePoint((bounds.size.width/2.0), bounds.size.height);
NSPoint left = NSMakePoint(0, (bounds.size.height/2.0));
NSPoint right = NSMakePoint(bounds.size.width, (bounds.size.height/2.0));
[[NSColor whiteColor] set];
[NSBezierPath fillRect:bounds];
[[NSColor blackColor] set];
[NSBezierPath strokeRect:bounds];
[NSBezierPath strokeLineFromPoint:top toPoint:bottom];
[NSBezierPath strokeLineFromPoint:right toPoint:left];
[[NSBezierPath bezierPathWithOvalInRect:bounds] stroke];
}
@end
The single line of code creates a oval path the size of the bounds of
the view, then draws it using the
stroke
method. Save the project
(
-S), then build and run the application (
-R). You should see something that looks like Figure 7-9.
Define the red color in Red Square by using the
colorWithCalibratedRed:green:blue:alpha:
method.
Draw the string from String View into the Line View project, noticing where the string is drawn.
Vary the width of the lines drawn by using the
setDefaultLineWidth:
method of
NSBezierPath
.
Use Project Builder’s
“Find” feature to look up
occurrences of NSBezierPath
in your project; then
use it to find the occurrences of NSBezierPath
in
the AppKit headers.