Chapter 15. Images and Mouse Events

In Chapter 14, you drew lines connecting random points. A more interesting application would have been to write some sort of a drawing application. To write this sort of application, you will need to be able to get and handle mouse events.

NSResponder

NSView inherits from NSResponder. All of the event-handling methods are declared in NSResponder. We will discuss keyboard events in Chapter 16. For now, we are interested just in mouse events. NSResponder declares these methods:

- (void)mouseDown:(NSEvent *)theEvent;
- (void)rightMouseDown:(NSEvent *)theEvent;
- (void)otherMouseDown:(NSEvent *)theEvent;
- (void)mouseUp:(NSEvent *)theEvent;
- (void)rightMouseUp:(NSEvent *)theEvent;
- (void)otherMouseUp:(NSEvent *)theEvent;
- (void)mouseDragged:(NSEvent *)theEvent;
- (void)scrollWheel:(NSEvent *)theEvent;
- (void)rightMouseDragged:(NSEvent *)theEvent;
- (void)otherMouseDragged:(NSEvent *)theEvent;

Notice that the argument is always an NSEvent object.

NSEvent

An event object has all the information about what the user did to trigger the event. When you are dealing with mouse events, you might be interested in the following methods:

  • - (NSPoint)locationInWindow
    
  • This method returns the location of the mouse event.

    - (unsigned int)modifierFlags
    
  • The integer tells you which modifier keys the user is holding down on the keyboard. This enables the programmer to tell a control-click from a shift-click, for example. The code would look like this:

    - (void)mouseDown:(NSEvent *)e
    {
        unsigned int flags;
        flags = [e modifierFlags];
        if (flags & NSControlKeyMask) {
             ...handle control click...
        }
        if (flags & NSShiftKeyMask) {
             ...handle shift click...
        }
    }
    

Here are the constants that you will AND (&) against the modifier flags:

     NSAlphaShiftKeyMask

     NSShiftKeyMask

     NSControlKeyMask

     NSAlternateKeyMask

     NSCommandKeyMask

     NSNumericPadKeyMask

     NSHelpKeyMask

     NSFunctionKeyMask

- (NSTimeInterval)timestamp

This method gives the time interval in seconds between the time the machine booted and the time of the event. NSTimeInterval is a double.

- (NSWindow *)window

This method returns the window associated with the event.

- (int)clickCount

Was it a single-, double-, or triple-click?

- (float)pressure

If the user is using an input device that gives pressure (a tablet, for example), this method returns the pressure. It is between 0 and 1.

- (float)deltaX;
- (float)deltaY;
- (float)deltaZ;

These methods give the change in the position of the mouse or scroll wheel.

Getting Mouse Events

To get mouse events, you need to override the mouse event methods in StretchView.m:

- (void)mouseDown:(NSEvent *)event
{
    NSLog(@"mouseDown: %d", [event clickCount]);
}
- (void)mouseDragged:(NSEvent *)event
{
    NSLog(@"mouseDragged:");
}
- (void)mouseUp:(NSEvent *)event
{
    NSLog(@"mouseUp:");
}

Build and run your application. Try double-clicking, and check the click count. Note that the first click is sent and then the second click. The first click has a click count of 1; the second click has a click count of 2.

Using NSOpenPanel

It would be fun to composite an image onto the view, but first you need to create a controller object that will read the image data from a file. This is a good opportunity to learn how to use NSOpenPanel. Note that the RaiseMan application used the NSOpenPanel, but it was done automatically by the NSDocument class. Here you will use the NSOpenPanel explicitly. Figure 15.1 shows what your application will look like while the open panel is active. Figure 15.2 shows what it will look like once the user has chosen an image. The slider at the bottom of the window will control how opaque the image is.

NSOpenPanel

Figure 15.1. NSOpenPanel

Completed Application

Figure 15.2. Completed Application

Figure 15.3 shows the object diagram.

Object Diagram

Figure 15.3. Object Diagram

Change the Nib File

Open the nib file and create a new subclass of NSObject called AppController. Create two outlets called stretchView (of type StretchView) and slider (of type NSSlider). Create two actions: open: and fade: (Figure 15.4). Create files for the AppController class.

Create AppController Class

Figure 15.4. Create AppController Class

In the Classes menu, use Instantiate AppController to create an instance of your new class (Figure 15.5).

Create an Instance of AppController

Figure 15.5. Create an Instance of AppController

Control-drag from the AppController to the StretchView and connect the stretchView outlet (Figure 15.6).

Connect the stretchView Outlet

Figure 15.6. Connect the stretchView Outlet

Drop a slider on the window. In the inspector, set its range from 0 to 1. Also, check the box labeled Continuously send action while sliding. This slider will control how opaque the image is (Figure 15.7).

Inspect the Slider

Figure 15.7. Inspect the Slider

Connect the slider outlet to the instance of NSSlider (Figure 15.8).

Connect the Slider Outlet

Figure 15.8. Connect the Slider Outlet

Set the target of the slider to be the AppController. Its action will be fade: (Figure 15.9).

Set the Action of the Slider

Figure 15.9. Set the Action of the Slider

Look at the main menu in your nib. Open the File menu and delete all menu items except Open. Control-drag to connect the menu item to the AppController's open: action (Figure 15.10). Save the file.

Connect the Menu Item

Figure 15.10. Connect the Menu Item

awakeFromNib Versus init

When your nib file is loaded, three things will happen (in this order) to your AppController object (and any other object in the nib file):

  1. It will be allocated and sent the message init. (Views will be sent initWithFrame:.)

  2. Its outlets will be set.

  3. It will be sent awakeFromNib.

Note that init is sent before the outlets of the object are set. Thus, you cannot send messages to any of the other objects from the nib file in the init method. You can, however, send them messages in the awakeFromNib method.

Edit the Code

Edit AppController.h and add the following line:

#import <Cocoa/Cocoa.h>
@class StretchView;

Edit AppController.m:

#import "AppController.h"
#import "StretchView.h"

@implementation AppController

- (void)awakeFromNib
{
    // Make sure the slider and the stretch view
    // agree on the initial opacity
    [slider setFloatValue:0.5];
    [stretchView setOpacity:0.5];
}
- (IBAction)fade:(id)sender
{
    // The sender is the slider
    [stretchView setOpacity:[sender floatValue]];
}

- (void)openPanelDidEnd:(NSOpenPanel *)openPanel
             returnCode:(int)returnCode
            contextInfo:(void *)x
{
    NSString *path;
    NSImage *image;

    // Did they choose "Open"?
    if (returnCode == NSOKButton) {
        path = [openPanel filename];
        image = [[NSImage alloc] initWithContentsOfFile:path];
        [stretchView setImage:image];
        [image release];
    }
}

- (IBAction)open:(id)sender
{
    NSOpenPanel *panel = [NSOpenPanel openPanel];

    // Run the open panel
    [panel beginSheetForDirectory:nil
                             file:nil
                            types:[NSImage imageFileTypes]
                   modalForWindow:[stretchView window]
                    modalDelegate:self
                   didEndSelector:
        @selector(openPanelDidEnd:returnCode:contextInfo:)
                      contextInfo:NULL];
}

@end

Notice the line where you start the sheet. This is a very handy method:

- (void)beginSheetForDirectory:(NSString *)path
                           file:(NSString *)name
                          types:(NSArray *)types
                modalForWindow:(NSWindow *)docWindow
                 modalDelegate:(id)delegate
                didEndSelector:(SEL)didEndSelector
                   contextInfo:(void *)contextInfo

This method brings up an open panel as a sheet attached to the docWindow. The didEndSelector should have the following signature:

- (void)openPanelDidEnd:(NSWindow *)sheet
             returnCode:(int)returnCode
            contextInfo:(void *)contextInfo;

It should be implemented in the modal delegate. The path is the place where the file browser will open initially. The name is the name of the file that will be chosen initially. Both the path and the name may be nil.

Composite an Image onto Your View

You will also need to change StretchView so that it uses the opacity and image. First, declare variables and methods in your StretchView.h file:

#import <Cocoa/Cocoa.h>

@interface StretchView : NSView
{
    NSBezierPath *path;
    NSImage *image;
    float opacity;
}
- (void)setImage:(NSImage *)newImage;
- (void)setOpacity:(float)x;
- (NSPoint)randomPoint;

@end

Now implement these methods in your StretchView.m file:

- (void)setImage:(NSImage *)newImage
{
    [newImage retain];
    [image release];
    image = newImage;
    [self setNeedsDisplay:YES];
}
- (void)setOpacity:(float)x
{
    opacity = x;
    [self setNeedsDisplay:YES];
}

At the end of each of the methods, you inform the view that it needs to redraw itself.

Also in StretchView.m, you need to add compositing of the image to the drawRect: method:

- (void)drawRect:(NSRect)rect
{
    NSRect bounds = [self bounds];
    [[NSColor greenColor] set];
    [NSBezierPath fillRect:bounds];
    [[NSColor whiteColor] set];
    [path stroke];
    if (image) {
        NSRect imageRect;
        NSRect drawingRect;
        imageRect.origin = NSZeroPoint;
        imageRect.size = [image size];
        drawingRect = imageRect;
        [image drawInRect:drawingRect
                fromRect:imageRect
                operation:NSCompositeSourceOver
                fraction:opacity];
    }
}

Notice that the drawInRect:fromRect:operation:fraction: method composites the image onto the view. The fraction determines the image's opacity.

In the name of tidiness, be sure to release the image in your view's dealloc method:

- (void)dealloc
{
    [image release];
    [path release];
    [super dealloc];
}

Build and run your application. You will find a few images in /Developer/ Examples/AppKit/Sketch . When you open an image, it will appear in the lower-left corner of your StretchView object.

The View's Coordinate System

The final bit of fun comes from being able to choose the location and dimensions of the image based on the user's dragging. The mouse down will indicate one corner of the rectangle where the image will appear, and the mouse up will indicate the opposite corner. The final application will look something like Figure 15.11.

Completed Application

Figure 15.11. Completed Application

Each view has its own coordinate system. By default, (0, 0) is in the lower-left corner. This is consistent with PDF and PostScript. You can change the coordinate system of the view if you wish. You can move the origin, change the scale, or rotate the coordinates. The window also has a coordinate system.

If you have two views, a and b, and you need to translate an NSPoint p from b's coordinate system to a's coordinate system, it would look like this:

NSPoint q = [a convertPoint:p fromView:b];

If b is nil, the point is converted from the window's coordinate system.

Mouse events have their locations in the window's coordinate system, so you will nearly always have to convert the point to the local coordinate system. You are going to create variables to hold onto the corners of the rectangle where the image will be drawn.

Add these instance variables to StretchView.h:

    NSPoint downPoint;
    NSPoint currentPoint;

downPoint will be the location of the mouseDown:. currentPoint will be updated by mouseDragged: and mouseUp:.

Edit the mouse event-handling methods to update downPoint and currentPoint:

- (void)mouseDown:(NSEvent *)event
{
    NSPoint p = [event locationInWindow];
    downPoint = [self convertPoint:p fromView:nil];
    currentPoint = downPoint;
    [self setNeedsDisplay:YES];
}

- (void)mouseDragged:(NSEvent *)event
{
    NSPoint p = [event locationInWindow];
    currentPoint = [self convertPoint:p fromView:nil];
    [self setNeedsDisplay:YES];
}

- (void)mouseUp:(NSEvent *)event
{
    NSPoint p = [event locationInWindow];
    currentPoint = [self convertPoint:p fromView:nil];
    [self setNeedsDisplay:YES];
}

Add a method to calculate the rectangle based on the two points:

- (NSRect)currentRect
{
    float minX = MIN(downPoint.x, currentPoint.x);
    float maxX = MAX(downPoint.x, currentPoint.x);
    float minY = MIN(downPoint.y, currentPoint.y);
    float maxY = MAX(downPoint.y, currentPoint.y);

    return NSMakeRect(minX, minY, maxX-minX, maxY-minY);
}

(I don't know why, but many people mistype that last method. Look at yours once more before going on. If you get it wrong, the results are disappointing.)

Declare the currentRect method in StretchView.h.

So that the user will see something even if he has not dragged, initialize downPoint and currentPoint in the setImage: method:

- (void)setImage:(NSImage *)newImage
{
    [newImage retain];
    [image release];
    image = newImage;
    NSSize imageSize;
    imageSize = [newImage size];
    downPoint = NSZeroPoint;
    currentPoint.x = downPoint.x + imageSize.width;
    currentPoint.y = downPoint.y + imageSize.height
    [self setNeedsDisplay:YES];
}

In the drawRect: method, composite the image inside the rectangle:

- (void)drawRect:(NSRect)rect
{
    NSRectbounds = [selfbounds];
    [[NSColor greenColor] set];
    [NSBezierPath fillRect:[bounds]];
    [[NSColor whiteColor] set];
    [path stroke];
    if (image) {
        NSRect imageRect;
        NSRect drawingRect;
        imageRect.origin.x = NSZeroPoint;
        imageRect.size = [image size];
        drawingRect = [self currentRect];
        [image drawInRect:drawingRect
                 fromRect:imageRect
                operation:NSCompositeSourceOver
                 fraction:opacity];
    }
}

Build and run your application. Notice that the view doesn't scroll when you drag past the edge. It would be nice if the scroll view would move to allow users to see where they have dragged to, a technique known as autoscrolling. In the next section, you will add autoscrolling to your application.

Autoscrolling

To add autoscrolling to your application, you will send the message autoscroll: to the clip view when the user drags. You will include the event as an argument. Open StretchView.m and add the following line to the mouseDragged: method:

- (void)mouseDragged:(NSEvent *)event
{
    NSPoint p = [event locationInWindow];
    currentPoint = [self convertPoint:p fromView:nil];
    [[self superview] autoscroll:event];
    [self setNeedsDisplay:YES];
}

Build and run your application.

Notice that autoscrolling happens only as you drag. For smoother autoscrolling, most developers will create a timer that sends the view the autoscroll: method periodically while the user is dragging. Timers are discussed in Chapter 21.

For the More Curious: NSImage

In most cases, it suffices to read in an image, resize it, and composite it onto a view, as you did in this exercise.

An NSImage object has an array of representations. For example, your image might be a drawing of a cow. That drawing can be in PDF, a color bitmap, and a black-and-white bitmap. Each of these versions is an instance of a subclass of NSImageRep. You can add representations to and remove representations from your image. When you sit down to rewrite Adobe Photoshop, you will be manipulating the image representations.

Here is a list of the subclasses of NSImageRep:

  • NSBitmapImageRep

  • NSEPSImageRep

  • NSPICTImageRep

  • NSCachedImageRep

  • NSCustomImageRep

  • NSPDFImageRep

Although NSImageRep has only five subclasses, it is important to note that NSImage knows how to read approximately two dozen types of image files, including all of the common formats: PICT, GIF, JPG, PNG, PDF, BMP, TIFF, and so on.

Challenge

Create a new document-based application that allows the user to draw ovals in arbitrary locations and sizes. NSBezierPath has the following method:

+ (NSBezierPath *)bezierPathWithOvalInRect:(NSRect)rect;

If you are feeling ambitious, add the ability to save and read files.

If you are feeling extra ambitious, add undo capabilities.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset