Learning Cocoa with Objective-C/Single-Window Applications/Custom Views

From WikiContent

< Learning Cocoa with Objective-C | Single-Window Applications
Revision as of 12:56, 7 March 2008 by Docbook2Wiki (Talk)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search
Learning Cocoa with Objective-C

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.

Contents

Custom View Creation Steps

When you make a custom subclass of NSView and want to perform custom drawing and handle events, the following procedure applies:

  1. In Interface Builder, define a subclass of NSView, then generate header and implementation files.
  2. Drag a Custom View object from the Views palette onto a window, and resize it.
  3. 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.
  4. If needed, override the designated initializer (initWithFrame: ) to perform any custom initialization.
  5. Implement the drawRect: method to draw.

We will walk you through the steps outlined here to show you how to create customized subclasses of NSView for your applications.

Create a Custom View

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 ~/LearningCocoafolder.

Open the Main Nib File

Begin by opening the application's main nib file in Interface Builder.

  1. In Project Builder's Groups & Files pane, click on the disclosure triangle next to Resources to reveal the MainMenu.nib file.
  2. 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.

Define a Subclass of NSView

To define a class that will implement the custom functionality of our view, we need to define a subclass of the NSView class.

  1. Click on the Classes tab of the MainMenu.nib window.
  2. 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 NSObjectNSResponderNSView.
  3. 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.)

    Figure 7-1. Creating the MyView subclass of NSView

    Creating the MyView subclass of NSView

  4. 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.)
  5. Interface Builder then displays a dialog box.
  6. Verify that the checkboxes next to MyView.h and MyView.mare selected in the Create column.
  7. Verify that the checkbox next to Red Square is selected in the Target column.
  8. Click the Choose button to create the files.

Add a Custom View to the Main Window

Next, we need to create a place for our custom view to draw.

  1. 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.

    Figure 7-2. Drag a CustomView object from the Cocoa-Containers window into the project window

    Drag a CustomView object from the Cocoa-Containers window into the project window

  2. With the CustomView object still selected, choose the Custom Class pane of the Show Info window (Tools → Show Info, or Shift-[[Image:Learning Cocoa with Objective-C_I_3_tt232.png|]]-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.

Figure 7-3. Setting the class for the custom view

Setting the class for the custom view

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 [[Image:Learning Cocoa with Objective-C_I_3_tt234.png|]]-S), and return to Project Builder by clicking on its icon in the Dock.

Implement the Drawing Method

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.

  1. Open the MyView.m implementation file in Project Builder by clicking on the filename in the Other Sources folder.
  2. Edit it to match Example 7-1.

    Example 7-1. Simple drawRect method

    #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:

    1. 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.
    2. 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.
    3. Calls the NSRectFill function, defined by the AppKit framework, and tells it to fill the bounds of the view.
  3. Save the project (File → Save, or [[Image:Learning Cocoa with Objective-C_I_3_tt235.png|]]-S).
  4. Build and run the project (Build → Build and Run, or [[Image:Learning Cocoa with Objective-C_I_3_tt236.png|]]-R). You should see a window containing a red square, as shown in Figure 7-4.

    Figure 7-4. Red Square displayed

    Red Square displayed


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.

  1. Quit the Red Square application ([[Image:Learning Cocoa with Objective-C_I_3_tt238.png|]]-Q) before moving on to the next step.

Tip

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.

Autosizing of Views

To ensure that our view occupies the whole window, no matter how it is resized:

  1. Bring the MainMenu.nib file to the foreground in Interface Builder.
  2. Select the MyView component in the interface.
  3. 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.

Figure 7-5. Setting the Autosizing behavior of a view

Setting the Autosizing behavior of a view

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.

  1. Save the nib file ([[Image:Learning Cocoa with Objective-C_I_3_tt240.png|]] -S).
  2. Switch back to Project Builder, and build and run ([[Image:Learning Cocoa with Objective-C_I_3_tt241.png|]]-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.

Drawing into a View: Behind the Scenes

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.

Draw Strings into a View

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 ~/LearningCocoafolder.

Create a Custom View

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:

  1. Open the MainMenu.nib file.
  2. Define a subclass of NSView named MyView.
  3. Generate the source files for MyView.
  4. Add a CustomView to the main window.
  5. Assign the MyView class to the CustomView from the Custom Class pull-down menu of the Info window.
  6. Set the Autosizing attributes of the view so that the view fills the window when the window is resized.
  7. Save the nib file, and return to Project Builder.

Implement the Drawing Method

Once again, we implement the drawRect: method of our MyView class.

  1. Open the MyView.mimplementation file in Project Builder, and edit it to match Example 7-2.

    Example 7-2. Drawing a string into a view

    #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:

    1. Gets an NSRect structure containing the bounds of the view.
    2. Initializes an NSString containing the string that we want to draw into the view.
    3. 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.
    4. Sets the active drawing color to white.
    5. Calls the NSRectFill function to fill the view. This will paint the entire view white.
    6. 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.
  2. Save the project ([[Image:Learning Cocoa with Objective-C_I_3_tt243.png|]] -S).
  3. Build and run the project ([[Image:Learning Cocoa with Objective-C_I_3_tt244.png|]]-R). You should see the string drawn in the window, as shown in Figure 7-6.

    Figure 7-6. A Hello World string drawn into a view

    A Hello World string drawn into a view

    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.

  4. Quit the String View application ([[Image:Learning Cocoa with Objective-C_I_3_tt246.png|]] -Q).

Drawing Strings with Attributes

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.

  1. Modify the drawRect: method in MyView.m to match Example 7-3:

    Example 7-3. Setting font attributes

    - (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:

    1. Obtains a font object for the Times font with a size of 24 points and sets it into the attribs dictionary.
    2. Obtains a red color object and sets it into the attribs dictionary.
  2. Build and run ([[Image:Learning Cocoa with Objective-C_I_3_tt247.png|]]-R) the application. You should see the string drawn into the window with our attributes, as seen in Figure 7-7.

Figure 7-7. Drawing a string with attributes

Drawing a string with attributes

We're now done with the String View application. Close the project in both Project Builder and Interface Builder before moving on.

Draw Paths into a View

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 ~/LearningCocoafolder.

Create a Custom View

As before, a new custom view class needs to be created. Perform the following tasks:

  1. Open the MainMenu.nib file.
  2. Define a subclass of NSView named MyView.
  3. Generate the source files for MyView.
  4. Add a custom view to the main window.
  5. Assign the MyView class to the CustomView.
  6. Set the Autosizing attributes of the view so that the view fills the window when the window is resized.
  7. Save the nib file, and return to Project Builder.

Implement the Drawing Method

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.

Note

The NSPoint structure is defined by the Foundation Kit as the following:

typedef struct _NSPoint {
    float x;
    float y;
} NSPoint;
  1. Open the MyView.mimplementation file in Project Builder, and edit it to match Example 7-4.

    Example 7-4. Drawing a string into a view

    #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:

    1. Creates an NSPoint halfway along the bottom of the view
    2. Creates an NSPoint halfway along the top of the view
    3. Creates an NSPoint halfway up the left side of the view
    4. Creates an NSPoint halfway up the right side of the view
    5. Draws a path that encompasses the entire view and fills that path with the current drawing color (white)
    6. Draws a path that encompasses the entire view and draws a line along that path in the current drawing color (black)
    7. Draws a path from the NSPoint along the top of the view to the NSPoint along the bottom of the view
    8. Draws a path from the NSPoint along the right side of the view to the NSPoint along the left side of the view
  2. Save the project ([[Image:Learning Cocoa with Objective-C_I_3_tt250.png|]] -S).
  3. Build and run the application ([[Image:Learning Cocoa with Objective-C_I_3_tt251.png|]]-R). You should see the lines drawn in the view as shown in Figure 7-8.

Figure 7-8. The Line View application in action

The Line View application in action

Now quit the Line View application ([[Image:Learning Cocoa with Objective-C_I_3_tt253.png|]] -Q) before going on to the next example.

Draw an Oval Path

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.

Example 7-5. Drawing an oval path

#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 ([[Image:Learning Cocoa with Objective-C_I_3_tt254.png|]]-S), then build and run the application ([[Image:Learning Cocoa with Objective-C_I_3_tt255.png|]]-R). You should see something that looks like Figure 7-9.

Figure 7-9. Line View with an oval path drawn

Line View with an oval path drawn

Exercises

  1. Define the red color in Red Square by using the colorWithCalibratedRed:green:blue:alpha: method.
  2. Draw the string from String View into the Line View project, noticing where the string is drawn.
  3. Vary the width of the lines drawn by using the setDefaultLineWidth: method of NSBezierPath.
  4. 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.
Personal tools