Cocoa- Using NSTrackingArea


This is short tutorial on how to use Cocoa’s NSTrackingArea to get a hold of useful mouse events such as mouseEntered and mouseExited, so lets jump right into it.

We’ll be starting off with an NSView-subclass, such as the following (which is identical to the ones Xcode automatically creates):


    @implementation CJTutorialView

    - (id)initWithFrame:(NSRect)frame
    {
        self = [super initWithFrame:frame];
        return self;
    }

    - (void)dealloc
    {
        [super dealloc];
    }

    - (void)drawRect:(NSRect)dirtyRect
    {
        // Drawing code here.
    }

    @end

And the corresponding header:


    @interface CJTutorialView : NSView {
    @private
        NSTrackingArea * trackingArea;
    }

    -(void)resizeFrameBy:(int)value;

    @end
    

First of all, we are going to implement drawRect to fill the view with a color, so we can see what we are doing:


    - (void)drawRect:(NSRect)dirtyRect
    {
        [[NSColor redColor] set];
        NSRectFill([self bounds]);
    }
    

This code is pretty self-explanatory, at least if you’ve already done some drawing in Cocoa at all. If you haven’t you should check out the Introduction to Cocoa Drawing.

There is a nifty little method called updateTrackingAreas in NSView, which is invoked automatically whenever the view’s geometry changes in a way that its tracking areas need to be updated. Sounds like what we need, so lets implement it. First of all, we will need to create the NSTrackingArea instance:


    int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
    trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds]
                                            options:opts
                                            owner:self
                                            userInfo:nil];
                                            

The first parameter, an NSRect, specifies the actual area to be tracked. I use [self bounds] instead of [self frame] here, because the latter is in the superview’s coordinate system, which is probably not what you want (if you are unfamiliar with the difference between a view’s frame and bounds, look into the View Programming Guide).

The second parameter (options) specifies various aspects of the tracking area behavior. In our case we want to get notified whenever the mouse enters or leaves our NSTrackingArea (NSTrackingMouseEnteredAndExited), and the tracking should be active even if our window is not focused (NSTrackingActiveAlways). For more options just take a look at the NSTrackingArea Class Reference.

The third parameter (owner) specifies where the event messages should be sent to, in our case it is our example view which we reference with self.

The fourth and last parameter can specify some extra data which will be passed with every event (except for mouse move events, probably because of the frequency in which they are triggered), but we don’t need this feature now, so we can just pass nil.

Now we want to add the NSTrackingArea to our NSView:


    [self addTrackingArea:trackingArea];
    

Note that an NSView can have multiple tracking areas and you cannot update the old ones, so if you need to update a tracking area, remove the old one and just add a new one!

So before creating a new tracking area, we will want to check if one already exists, and if thats the case, remove it:


    if(trackingArea != nil) {
        [self removeTrackingArea:trackingArea];
        [trackingArea release];
    }
    

Don’t forget to release your old tracking areas, or you’ll end up with a nice memory leak ;) Now lets stuff all that code into our updateTrackingAreas method:


    -(void)updateTrackingAreas
    {
        if(trackingArea != nil) {
            [self removeTrackingArea:trackingArea];
            [trackingArea release];
        }

        int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
        trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds]
                                                options:opts
                                                owner:self
                                                userInfo:nil];
        [self addTrackingArea:trackingArea];
    }
    

To see if our previously written code works, we also need the respective mouse events, that is, mouseEntered and mouseExited:


    -(void)mouseEntered:(NSEvent *)theEvent
    {
        [self resizeFrameBy:+100];
    }

    -(void)mouseExited:(NSEvent *)theEvent
    {
        [self resizeFrameBy:-100];
    }
    

Those are small and simple, whenever the mouse enters our view we want to increase the view’s size by 100, and when we leave we reduce it again. The code for resizeFrameBy is the following:


    -(void)resizeFrameBy:(int)value
    {
        NSRect frame = [self frame];
        [self setFrame:CGRectMake(frame.origin.x,
                                      frame.origin.x,
                                      frame.size.width + value,
                                      frame.size.height + value
                                    )];
    }
    

Thats it already. All we need to do now is create an instance of the view and add it somewhere, and see the NSTrackingArea magic do its job. I did this directly inside the applicationDidFinishLaunching method of the app delegate:


    CJTutorialView * view = [[CJTutorialView alloc] init];
    [view setFrame:CGRectMake(100, 100, 100, 100)];
    [[window contentView] addSubview:view];

For the full, runnable source code check out the tutorial repository on GitHub.