macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button Programming With Cocoa

Building a Scratch Pad with Cocoa

11/30/2001

Today we continue learning about Cocoa graphics with a look at how NSView objects can respond to mouse and keyboard events. This is useful for letting users interact with objects onscreen and can be used in drawing applications.

This discussion will center around two important Application Kit classes -- NSEvent and NSResponder -- and how they work together to form the backbone of the Application Kit's event-handling system. To illustrate these principles, we'll build a simple scratch-pad application that allows us to scribble lines in a window using the mouse.

The usual setup

Before getting into the meat of our discussion, let’s get the application framework set up. In Project Builder, create a new project called ScratchPad. Add to your project an Objective-C NSView subclass as we’ve done previously and name it PadView (and PadView.h). Now go into Interface Builder and import the import PadView.h into the nib file and assign it to a CustomView container as we’ve learned before. Now we're ready to roll.

Understanding NSResponder

The way Cocoa handles user-input events, such as mouse clicks and keystrokes, is through the class NSResponder. NSResponder is an abstract class, that is, a class that defines methods for its children to implement, but provides no implementation itself. As you know, the ability to respond to and handle user-input events is key to any operating system, and NSResponder plays an appropriately large role in the Application Kit.

The Application Kit’s three core application classes -- NSApplication, NSWindow, and NSView -- all inherit from NSResponder. For us, the this means that any subclass of NSView is also an NSResponder, and thus has the intrinsic ability to respond to user-input events. All we have to do is decide which events our view should respond to, and what actions should be taken in response to those events. We do this by overriding in our subclass those abstract methods defined in NSResponder.

To get an idea of the kinds of events we can respond to, let's enumerate some of the methods defined in NSResponder. NSResponder declares these following methods relating to mouse events:

  • -mouseDown:
  • -mouseUp:
  • -mouseDragged:
  • -mouseMoved:

-mouseDown: is invoked whenever the primary mouse button is pressed, and –mouseUp: is invoked when that button is released. –mouseDragged is called when the mouse is moved with the main button pressed (standard dragging), and –mouseMoved: is invoked when the mouse moves.

NSResponder also defines methods analogous to –mouseDown:, -mouseUp:, and –mouseDragged: that are prefixed with rightMouse… and otherMouse… rather than mouse…. As you can imagine, these methods are invoked in response to events from multi-button mice. NSResponder also declares two methods to respond to raw keystrokes. They are keyDown: and keyUp:, which are invoked when a user presses and releases a key, respectively.

We know we have to override these methods to enable a view to respond to events, but still need to discuss an important part of this implementation. That part is the class NSEvent, which is the data type of the argument to all of these NSResponder methods.

So when an NSResponder message is sent to a responder object, included with that message is an NSEvent object that encapsulates detailed information about the event that just took place -- details such as the coordinates of the mouse location in the active window when the event transpired, which key was pressed, or when a mouse click happened in time. The NSEvent class documentation goes into all the gory detail about the information we can extract from an event object.

The coding

To see how this all fits together in actual code, let's use our simple scratch-pad application to draw a freeform line that follows the movement of the mouse dragging. This application will involve overriding the methods –mouseDown:, –mouseDragged:, and –mouseUp:.

When the user clicks in the view, a new bezier-path object is created. As the user drags, lines will append the path at the current point, and end at the point given in lineToPoint:. Once the user releases the mouse, the line will be set the way it is. When this process is repeated, the previous line is destroyed and a new one is drawn.

In addition to the two skeleton methods given to us by Project Builder -- init and drawRect: -- we will override the methods mouseDown:, mouseDragged:, and mouseUp:. We also will have an NSBezierPath object-instance variable that will be the path we draw. Add this instance variable declaration to the PadView.h file:

NSBezierPath *path;

In PadView.m, we don’t need to modify the initialization method at all, and drawRect: requires only the simplest coding:


- (void)drawRect:(NSRect)rect
{
    [NSColor redColor] set];
    [path stroke];
}

All we did in drawRect was set the color to draw in and we told the path to draw itself. Move along folks, nothing to see here.

Now we add three methods to this skeletal implementation:

- (void)mouseDown:(NSEvent *)theEvent
{
}

- (void)mouseDragged:(NSEvent *)theEvent
{
}

- (void)mouseUp:(NSEvent *)theEvent
{
}

We assign path to a new instance of NSBezierPath in mouseDown:. When the mouse button is pressed, we append lines to the path during mouseDragged:, and when we release the mouse button, we release the object we assigned path to. Out of these three methods, mouseDragged is the only place we will tell the view to redraw itself, as that is the only method that changes path.

Let's take a look at how we code these methods. For the first of these methods, I used the following implementation:


- (void)mouseDown:(NSEvent *)theEvent
{
    NSPoint loc = [theEvent locationInWindow];
    loc.x -= [self frame].origin.x;
    loc.y -= [self frame].origin.y;
    
    path = [[NSBezierPath bezierPath] retain];
    [path moveToPoint:loc];
}

There are several important things to notice about this code. In the first part, all we're doing is determining the coordinate in our view’s coordinate system where the mouse was clicked. We can obtain the event location of the mouse click in the window’s coordinate system by sending a locationInWindow message to theEvent, which returns a point. Because this method returns points in the window’s coordinate system, we have to jimmy around with those points to be representative of locations in the view’s coordinate system.

So we subtract from the x and y coordinates of this point the x and y coordinates of PadView’s origin. The origin, as we see in the code above, is accessed by sending a frame message to self, which returns the NSRect that defines the boundaries of PadView. Because PadView is contained within the window, its origin is given in the window’s coordinate system. Thus by subtracting the origin coordinates from the event-location coordinates, we can obtain the location of the event relative to PadView.

In the second part of mouseDown:, we move our imaginary pencil in path to the point of the event location because we want our line to start where we first clicked the mouse. Notice that we sent a retain message to the object returned by bezierPath. This is because bezierPath is a convenience constructor, and path would have been autoreleased if we had not retained it -- we need path to hang around for our other methods.

The next method, mouseDragged, makes use of the same transformation of the mouse event location we discussed above:

 
- (void)mouseDragged:(NSEvent *)theEvent
{
    NSPoint loc = [theEvent locationInWindow];
    loc.x -= [self frame].origin.x;
    loc.y -= [self frame].origin.y;
    
    [path lineToPoint:loc];
    [self setNeedsDisplay:YES];
}

Comment on this articleHow goes the drawing? Let's talk about your experiences with this tutorial and anything new you may have discovered.
Post your comments

In the second part of this method, we simply send a lineToPoint: message to path, which appends a line from the last point of path to the point passed in the argument. Recall how complex bezier paths are constructed from our last column: The last point used in the path construction is treated as the so-called current point, and new path elements originate from the current point and extend to the point given in the argument.

The last line of this message is something we haven’t discussed yet. This is how we force the view to redraw itself, by sending it a setNeedsDisply: message, rather than directly invoking drawRect. We do this to reflect the changes we made to path in mouseDragged in the drawing onscreen:.

Now we finish up with mouseUp:. The purpose of mouseUp is to simply release the previously retained path object -- nothing more than keeping our memory clean (which I desperately need this late in the semester and this close to graduation). Note that we didn’t tell PadView to redraw itself here. If we had done that after releasing path, we would have a cleared screen.

The way we set up our retains and releases results in a particular behavior. This behavior is that the previous path drawn will be cleared the next time we click and drag the mouse. You can juggle things around a bit to never clear the path and to always add elements to the path, or you might come up with something else.

One downside to our implementation is that our drawing will be cleared if we resize the window (assuming the size of the view is tied to the size of the window) or perform some other system operation that tells the view to redraw itself. One solution for this would be to release the previous path in mouseDown just before we create a new path. Next we'll discuss an alternative implementation.

Pages: 1, 2

Next Pagearrow