Painting – Multi-finger Paint

Exclusive offer: get 50% off this eBook here
Mac Application Development by Example: Beginner's Guide

Mac Application Development by Example: Beginner's Guide — Save 50%

A comprehensive and practical guide, for absolute beginners, to developing your own App for Mac OS X book and ebook.

$26.99    $13.50
by Robert Wiebe | March 2013 | Beginner's Guides

This article will walk us through the steps needed to create a bitmapped painting App that uses the multi-touch track pad to allow the App user to paint with multiple fingers.

In this article by Robert Wiebe, the author of Mac Application Development by Example Beginner's Guide, we shall learn the following:

  • What is multi-touch?

  • Implementing a custom view

  • Receiving multi-touch events

  • Managing the mouse cursor

  • Drawing using the 2D drawing APIs

  • Receiving keyboard events

  • Receiving gesture events

(For more resources related to this topic, see here.)

What is multi-touch?

The genesis of multi-touch on Mac OS X was the ability to perform two finger scrolling on a trackpad. The technology was further refined on mobile touch screen devices such as the iPod Touch, iPhone, and iPad. And it has also matured on the Mac OS X platform to allow the use of multi-touch or magic trackpad combined with one or more fingers and a motion to interact with the computer. Gestures are intuitive and allow us to control what is on the screen with fluid motions. Some of the things that we can do using multi-touch are as follows:

  • Two finger scrolling: This is done by placing two fingers on the trackpad and dragging in a line

  • Tap or pinch to zoom : This is done by tapping once with a single finger, or by placing two fingers on the trackpad and dragging them closer to each other

  • Swipe to navigate: This is done by placing one or more fingers on the trackpad and quickly dragging in any direction followed by lifting all the fingers

  • Rotate : This is done by placing two fingers on the trackpad and turning them in a circular motion while keeping them on the trackpad

But these gestures just touch the surface of what is possible with multi-touch hardware. The magic trackpad can detect and track all 10 of our fingers with ease. There are plenty of new things that can be done with multi-touch — we are just waiting for someone to invent them.

Implementing a custom view

Multi-touch events are sent to the NSView objects. So before we can invent that great new multi-touch thing, we first need to understand how to implement a custom view.

Essentially, a custom view is a subclass of NSView that overrides some of the behavior of the NSView object. Primarily, it will override the drawRect: method and some of the event handling methods.

Time for action — creating a GUI with a custom view

By now we should be familiar with creating new Xcode projects so some of the steps here are very high level. Let's get started!

  1. Create a new Xcode project with Automatic Reference Counting enabled and these options enabled as follows:

    Option

    Value

    Product Name

    Multi-Finger Paint

    Company Identifier

    com.yourdomain

    Class Prefix

    Your initials

  2. After Xcode creates the new project, design an icon and drag it in to the App Icon field on the TARGET Summary.

  3. Remember to set the Organization in the Project Document section of the File inspector.

  4. Click on the filename MainMenu.xib in the project navigator.

  5. Select the Multi-Finger Paint window and in the Size inspector change its Width and Height to 700 and 600 respectively.

  6. Enable both the Minimum Size and Maximum Size Constraints values.

  7. From the Object Library , drag a custom view into the window.

  8. In the Size inspector , change the Width and Height of the custom view to 400 and 300 respectively.

  9. Center the window using the guides that appear.

  10. From the File menu, select New>, then select the File…option.

  11. Select the Mac OS X Cocoa Objective-C class template and click on the Next button.

  12. Name the class BTSFingerView and select subclass of NSView.

    It is very important that the subclass is NSView. If we make a mistake and select the wrong subclass, our App won't work.

  13. Click on the button titled Create to create the .h and .m files.

  14. Click on the filename BTSFingerView.m and look at it carefully. It should look similar to the following code:

    // // BTSFingerView.m // Multi-Finger Paint // // Created by rwiebe on 12-05-23. // Copyright (c) 2012 BurningThumb Software. All rights reserved. // #import "BTSFingerView.h" @implementation BTSFingerView - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { // Initialization code here. } return self; } - (void)drawRect:(NSRect)dirtyRect { // Drawing code here. } @end

  15. By default, custom views do not receive events (keyboard, mouse, trackpad, and so on) but we need our custom view to receive events. To ensure our custom view will receive events, add the following code to the BTSFingerView.m file to accept the first responder:

    /* ** - (BOOL) acceptsFirstResponder ** ** Make sure the view will receive ** events. ** ** Input: none ** ** Output: YES to accept, NO to reject */ - (BOOL) acceptsFirstResponder { return YES; }

  16. And, still in the BTSFingerView.m file, modify the initWithFrame method to allow the view to accept touch events from the trackpad as follows:

    - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { // Initialization code here. // Accept trackpad events [self setAcceptsTouchEvents: YES]; } return self; }

  17. Once we are sure our custom view will receive events, we can start the process of drawing its content. This is done in the drawRect: method. Add the following code to the drawRect: method to clear it with a transparent color and draw a focus ring if the view is first responder:

    /* ** - (void)drawRect:(NSRect)dirtyRect ** ** Draw the view content ** ** Input: dirtyRect - the rectangle to draw ** ** Output: none */ - (void)drawRect:(NSRect)dirtyRect { // Drawing code here. // Preserve the graphics content // so that other things we draw // don't get focus rings [NSGraphicsContext saveGraphicsState]; // color the background transparent [[NSColor clearColor] set]; // If this view has accepted first responder // it should draw the focus ring if ([[self window] firstResponder] == self) { NSSetFocusRingStyle(NSFocusRingAbove); } // Fill the view with fully transparent // color so that we can see through it // to whatever is below [[NSBezierPath bezierPathWithRect:[self bounds]] fill]; // Restore the graphics content // so that other things we draw // don't get focus rings [NSGraphicsContext restoreGraphicsState]; }

  18. Next, we need to go back into the .xib file, and select our custom view, and then select the Identity Inspector where we will see that in the section titled Custom Class, the Class field contains NSView as the class.

  19. Finally, to connect this object to our new custom view program code, we need to change the Class to BTSFingerView as shown in the following screenshot:

What just happened?

We created our Xcode project and implemented a custom NSView object that will receive events. When we run the project we notice that the focus ring is drawn so that we can be confident the view has accepted the firstResponder status.

How to receive multi-touch events

Because our custom view accepts first responder, the Mac OS will automatically send events to it. We can override the methods that process the events that we want to handle in our view. Specifically, we can override the following events and process them to handle multi-touch events in our custom view:

  • - (void)touchesBeganWithEvent:(NSEvent *)event

  • - (void)touchesMovedWithEvent:(NSEvent *)event

  • - (void)touchesEndedWithEvent:(NSEvent *)event

  • - (void)touchesCancelledWithEvent:(NSEvent *)event

Time for action — drawing our fingers

When the multi-touch or magic trackpad is touched, our custom view methods will be invoked and we will be able to draw the placement of our fingers on the trackpad in our custom view.

  1. In Xcode, click on the filename BTSFingerView.h in the project navigator and add the following highlighted property:

    // // BTSFingerView.h // Multi-Finger Paint // // Created by rwiebe on 12-05-23. // Copyright (c) 2012 BurningThumb Software. All rights reserved. // #import <Cocoa/Cocoa.h> @interface BTSFingerView : NSView // A reference to the object that will // store the currently active touches @property (strong) NSMutableDictionary *m_activeTouches; @end

  2. In Xcode, click on the file BTSFingerView.m in the project navigator and add the following program code to synthesize the property:

    // // BTSFingerView.m // Multi-Finger Paint // // Created by rwiebe on 12-05-23. // Copyright (c) 2012 BurningThumb Software. All rights reserved. // #import "BTSFingerView.h" @implementation BTSFingerView // Synthesize the object that will // store the currently active touches @synthesize m_activeTouches;

  3. Add the following code to the initWithFrame: method in the BTSFingerView.m file to create the dictionary object that will be used to store the active touch objects:

    - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { // Initialization code here. // Create the mutable dictionary that // will hold the list of currently active // touch events m_activeTouches = [[NSMutableDictionary alloc] init]; } return self; }

  4. Add the following code to the BTSFingerView.m file to add BeganWith touch events to the dictionary of active touches:

    /** ** - (void)touchesBeganWithEvent:(NSEvent *)event ** ** Invoked when a finger touches the trackpad ** ** Input: event - the touch event ** ** Output: none */ - (void)touchesBeganWithEvent:(NSEvent *)event { // Get the set of began touches NSSet *l_touches = [event touchesMatchingPhase:NSTouchPhaseBegan inView:self]; // For each began touch, add the touch // to the active touches dictionary // using its identity as the key for (NSTouch *l_touch in l_touches) { [m_activeTouches setObject:l_touch forKey:l_touch. identity]; } // Redisplay the view [self setNeedsDisplay:YES]; }

  5. Add the following code to the BTSFingerView.m file to add moved touch events to the dictionary of active touches:

    /** ** - (void)touchesMovedWithEvent:(NSEvent *)event ** ** Invoked when a finger moves on the trackpad ** ** Input: event - the touch event ** ** Output: none */ - (void)touchesMovedWithEvent:(NSEvent *)event { // Get the set of move touches NSSet *l_touches = [event touchesMatchingPhase:NSTouchPhaseMoved inView:self]; // For each move touch, update the touch // in the active touches dictionary // using its identity as the key for (NSTouch *l_touch in l_touches) { // Update the touch only if it is found // in the active touches dictionary if ([m_activeTouches objectForKey:l_touch.identity]) { [m_activeTouches setObject:l_touch forKey:l_touch.identity]; } } // Redisplay the view [self setNeedsDisplay:YES]; }

  6. Add the following code to the BTSFingerView.m file to remove the touch from the dictionary of active touches when the touch ends:

    /** ** - (void)touchesEndedWithEvent:(NSEvent *)event ** ** Invoked when a finger lifts off the trackpad ** ** Input: event - the touch event ** ** Output: none */ - (void)touchesEndedWithEvent:(NSEvent *)event { // Get the set of ended touches NSSet *l_touches = [event touchesMatchingPhase:NSTouchPhaseEnded inView:self]; // For each ended touch, remove the touch // from the active touches dictionary // using its identity as the key for (NSTouch *l_touch in l_touches) { [m_activeTouches removeObjectForKey:l_touch.identity]; } // Redisplay the view [self setNeedsDisplay:YES]; }

  7. Add the following code to the BTSFingerView.m file to remove the touch from the dictionary of active touches when the touch is cancelled:

    /** ** - (void)touchesCancelledWithEvent:(NSEvent *)event ** ** Invoked when a touch is cancelled ** ** Input: event - the touch event ** ** Output: none */ - (void)touchesCancelledWithEvent:(NSEvent *)event { // Get the set of cancelled touches NSSet *l_touches = [event touchesMatchingPhase:NSTouchPhaseCancelled inView:self]; // For each cancelled touch, remove the touch // from the active touches dictionary // using its identity as the key for (NSTouch *l_touch in l_touches) { [m_activeTouches removeObjectForKey:l_touch.identity]; } // Redisplay the view [self setNeedsDisplay:YES]; }

  8. When we touch the trackpad we are going to draw a "finger cursor" in our custom view. We need to decide how big we want that cursor to be and the color that we want the cursor to be. Then we can add a series of #define to the file named BTSFingerView.h to define that value:

    // Define the size of the cursor that // will be drawn in the view for each // finger on the trackpad #define D_FINGER_CURSOR_SIZE 20 // Define the color values that will // be used for the finger cursor #define D_FINGER_CURSOR_RED 1.0 #define D_FINGER_CURSOR_GREEN 0.0 #define D_FINGER_CURSOR_BLUE 0.0 #define D_FINGER_CURSOR_ALPHA 0.5

  9. Now we can add the program code to our drawRect: implementation that will draw the finger cursors in the custom view.

    // For each active touch for (NSTouch *l_touch in m_activeTouches.allValues) { // Create a rectangle reference to hold the // location of the cursor NSRect l_cursor; // Determine where the touch point NSPoint l_touchNP = [l_touch normalizedPosition]; // Calculate the pixel position of the touch point l_touchNP.x = l_touchNP.x * [self bounds].size.width; l_touchNP.y = l_touchNP.y * [self bounds].size.height; // Calculate the rectangle around the cursor l_cursor.origin.x = l_touchNP.x - (D_FINGER_CURSOR_SIZE / 2); l_cursor.origin.y = l_touchNP.y - (D_FINGER_CURSOR_SIZE / 2); l_cursor.size.width = D_FINGER_CURSOR_SIZE; l_cursor.size.height = D_FINGER_CURSOR_SIZE; // Set the color of the cursor [[NSColor colorWithDeviceRed: D_FINGER_CURSOR_RED green: D_FINGER_CURSOR_GREEN blue: D_FINGER_CURSOR_BLUE alpha: D_FINGER_CURSOR_ALPHA] set]; // Draw the cursor as a circle [[NSBezierPath bezierPathWithOvalInRect: l_cursor] fill]; }

What just happened?

We implemented the methods required to keep track of the touches and to draw the location of the touches in our custom view. If we run the App now, and move the mouse pointer over the view area, and then touch the trackpad, we will see red circles that track our fingers being drawn in the view as shown in the following screenshot:

What is an NSBezierPath?

A Bezier Path consists of straight and curved line segments that can be used to draw recognizable shapes. In our program code, we use Bezier Paths to draw a rectangle and a circle but a Bezier Path can be used to draw many other shapes.

How to manage the mouse cursor

One of the interesting things about the trackpad and the mouse is the association between a single finger touch and the movement of the mouse cursor. Essentially, Mac OS X treats a single finger movement as if it was a mouse movement. The problem with this is that when we move just a single finger on the trackpad, the mouse cursor will move away from our NSView causing it to lose focus so that when we lift our finger we need to move the mouse cursor back to our NSView to receive touch events.

Time for action — detaching the mouse cursor from the mouse hardware

The solution to this problem is to detach the mouse cursor from the mouse hardware (typically called capturing the mouse) whenever a touch event is active so that the cursor is not moved by touch events. In addition, since a "stuck" mouse cursor may be cause for concern to our App user, we can hide the mouse cursor when touches are active.

  1. In Xcode, click on the file named BTSFingerView.h in the project navigator and add the following flag to the interface:

    @interface BTSFingerView : NSView { // Define a flag so that touch methods can behave // differently depending on the visibility of // the mouse cursor BOOL m_cursorIsHidden; }

  2. In Xcode, click on the file named BTSFingerView.m in the project navigator.

  3. Add the following code to the beginning of the touchesBeganWithEvent: method to detach and hide the mouse cursor when a touch begins. We only want to do this one time so it is guarded by a BOOL flag and an if statement to make sure we don't do it for every touch that begins.

    - (void)touchesBeganWithEvent:(NSEvent *)event { // If the mouse cursor is not already hidden, if (NO == m_cursorIsHidden) { // Detach the mouse cursor from the mouse // hardware so that moving the mouse (or a // single finger) will not move the cursor CGAssociateMouseAndMouseCursorPosition(false); // Hide the mouse cursor [NSCursor hide]; // Remember that we detached and hid the // mouse cursor m_cursorIsHidden = YES; }

  4. Add the following code to the end of the touchesEndedWithEvent: method to attach and unhide the mouse cursor when all touches end. We use a BOOL flag to remember the state of the cursor so that the touchesBeganWithEvent: method will re-hide it when the next touch begins.

    // If there are no remaining active touches if (0 == [m_activeTouches count]) { // Attach the mouse cursor to the mouse // hardware so that moving the mouse (or a // single finger) will move the cursor CGAssociateMouseAndMouseCursorPosition(true); // Show the mouse cursor [NSCursor unhide]; // Remember that we attached and unhid the // mouse cursor so that the next touch that // begins will detach and hide it m_cursorIsHidden = NO; } // Redisplay the view [self setNeedsDisplay:YES]; }

  5. Add the following code to the end of the touchesCancelledWithEvent: method to attach and unhide the mouse cursor when all touches end. We use a BOOL flag to remember the state of the cursor so that the touchesBeganWithEvent: method will re-hide it when the next touch begins.

    // If there are no remaining active touches if (0 == [m_activeTouches count]) { // Attach the mouse cursor to the mouse // hardware so that moving the mouse (or a // single finger) will move the cursor CGAssociateMouseAndMouseCursorPosition(true); // Show the mouse cursor [NSCursor unhide]; // Remember that we attached and unhid the // mouse cursor so that the next touch that // begins will detach and hide it m_cursorIsHidden = NO; } // Redisplay the view [self setNeedsDisplay:YES]; }

  6. While we are looking at the movement of the mouse, we also notice that the focus ring for our custom view is being drawn regardless of whether or not the mouse cursor is over our view. Since touch events will only be sent to our view if the mouse cursor is over it, we want to change the program code so that the focus ring only appears when the mouse cursor is over the custom view. This is something we can do with another BOOL flag. Add the following code to the file to define a BOOL flag that will allow us to determine if the mouse cursor is over our custom view:

    // Define a flag so that view methods can behave // differently depending on the position of the // mouse cursor BOOL m_mouseIsInFingerView;

  7. In the file named BTSFingerView.m, add the following code to create a tracking rectangle that matches the bounds of our custom view. Once the tracking rectangle is active, the methods mouseEntered: and mouseExited: will be automatically invoked as the mouse cursor enters and exits our custom view.

    /** ** - (void)viewDidMoveToWindow ** ** Informs the receiver that it has been added to ** a new view hierarchy. ** ** We need to make sure the view window is valid ** and when it is, we can add the tracking rect ** ** Once the tracking rect is added the mouseEntered: ** and mouseExited: events will be sent to our view ** */ - (void)viewDidMoveToWindow { // Is the views window valid if ([self window] != nil) { // Add a tracking rect such that the // mouseEntered; and mouseExited: methods // will be automatically invoked [self addTrackingRect:[self bounds] owner:self userData:NULL assumeInside:NO]; } }

  8. In the file named BTSFingerView.m, add the following code to implement the mouseEntered: and mouseExited: methods. In those methods, we set the BOOL flag so that the drawRect: method knows whether or not to draw the focus ring.

    /** ** - (void)mouseEntered: ** ** Informs the receiver that the mouse cursor ** entered a tracking rectangle ** ** Since we only have a single tracking rect ** we know the mouse is over our custom view ** */ - (void)mouseEntered:(NSEvent *)theEvent { // Set the flag so that other methods know // the mouse cursor is over our view m_mouseIsInFingerView = YES; // Redraw the view so that the focus ring // will appear [self setNeedsDisplay:YES]; } /** ** - (void)mouseExited: ** ** Informs the receiver that the mouse cursor ** exited a tracking rectangle ** ** Since we only have a single tracking rect ** we know the mouse is not over our custom view ** */ - (void)mouseExited:(NSEvent *)theEvent { // Set the flag so that other methods know // the mouse cursor is not over our view m_mouseIsInFingerView = NO; // Redraw the view so that the focus ring // will not appear [self setNeedsDisplay:YES]; }

  9. Finally, in the drawRect: method, change the program code that draws the focus ring to only do so if the mouse cursor is in the tracking rectangle:

    // If this view has accepted first responder // it should draw the focus ring but only if // the mouse cursor is over this view if ( ([[self window] firstResponder] == self) && (YES == m_mouseIsInFingerView) ) { NSSetFocusRingStyle(NSFocusRingAbove); }

What just happened?

We implemented the program code that will prevent the mouse cursor from moving out of our custom view when touch events are active. In doing so we noticed that our focus ring behavior could be improved. Therefore we added additional program code to ensure the focus ring is visible only when the mouse pointer is over our view.

Performing 2D drawing in a custom view

Mac OS X provides a number of ways to perform drawing. The methods provided range from very simple methods to very complex methods. For our multi-finger painting program we are going to use the core graphics APIs designed to draw a path. We are going to collect each stroke as a series of points and construct a path from those points so that we can draw the stroke.

Each active touch event will have a corresponding active stroke object that needs to be drawn in our custom view. When a stroke is finished, and the App user lifts the finger, we are going to send the finished stroke to another custom view so that it is drawn only one time and not each time fingers move. The optimization of using the second view will ensure our finger tracking is not slowed down too much by drawing.

Before we can begin drawing, we need to create two new objects that will be used to store individual points and strokes. The program code for these two objects is not shown but the objects are included in the Multi-Finger Paint Xcode project. The two objects are as follows:

  • BTSPoint

  • BTSStroke

The BTSPoint object is a wrapper for an NSPoint structure. The NSPoint structure needs to be wrapped in an object so that it can be stored in an NSArray object. It has a single instance variable:

NSPoint m_point;

It implements the following methods which allows it to be initialized: return the point (x and y), return just the x value, or return just the y value. For more information on the object, we can read the source code file in the project:

- (id) initWithNSPoint:(NSPoint)a_point; - (NSPoint) point; - (CGFloat)x; - (CGFloat)y;

The BTSStroke object is a wrapper for an array of BTSPoint objects, a color, and a stroke width. It is used to store strokes that are drawn in our custom NSView. It has the following instance variables and properties:

float m_red; float m_green; float m_blue; float m_alpha; float m_width; @property (strong) NSMutableArray *m_points;

It implements the following methods which allows it to be initialized: a new point to be added, return the array of points, return any of the color components, and return the stroke width. For more information on the object, we can read the source code file in the project:

- (id) initWithWidth:(float)a_width red:(float)a_red green:(float)a_green blue:(float)a_blue alpha:(float)a_alpha; - (void) addPoint:(BTSPoint *)a_point; - (NSMutableArray *) points; - (float)red; - (float)green; - (float)blue; - (float)alpha; - (float)width;

Mac Application Development by Example: Beginner's Guide A comprehensive and practical guide, for absolute beginners, to developing your own App for Mac OS X book and ebook.
Published: December 2012
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:

Time for action — drawing the active strokes

We can now look back at our project and implement the program code needed to draw the strokes that are active. There are two classes that are included in the sample code project but that are not discussed in detail here. While they are not essential to understanding multi-touch it is recommended that we examine the code, for completeness. We will need the files that implement these classes included in our project so that our multi-touch paint program can run.

  1. In Xcode, examine the program code in the files named BTSPoint.h, BTSPoint.m, BTSStroke.h, and BTSStroke.m so that we are familiar with these two objects that will be used during the painting process to create colored strokes in our custom NSView object.

  2. Create new files that match these files so that the project we are working on has these objects implemented.

  3. We are going to need to allow the App user to select the color for each finger to paint. To do this, we need to add a new custom view to our .xib file and add the NSColorWell objects to that view for each finger that we want to support. The subview and image wells should look like the following screenshot:

    The order of the subviews is important. Make sure to add them from left to right, starting with the black color well and ending with the white color well.

  4. Add the following program code in the BTSFingerView.h file to define the objects we will use to reference the finder colors in our implementation:

    // A reference to the custom view object that will // contain the NSColorWell subviews @property (strong) IBOutlet NSView *m_colorWellView; // A reference to the array of NSColorWell objects @property (strong) NSArray *m_colorWells;

  5. Add the following program code in the BTSFingerView.m file to synthesize the objects we will use to reference the finder colors in our implementation:

    // Synthesize the object that will // store the color well information @synthesize m_colorWellView; @synthesize m_colorWells;

  6. In the .xib file, right-click on the Finger View and drag to connect it to the new Custom View using the Outlets named m_colorWellView as shown in the following screenshot:

     

  7. In BTSFingerView.m, implement the awakeFromNib method and include the program code to get the array of color well subviews:

    // This method is automatically invoked // when the view is created -(void) awakeFromNib { // Get the color well views // into an array m_colorWells = [m_colorWellView subviews]; }

  8. In BTSFingerView.m, change the touchesBeganWithEvent: method to ignore any touches for which there is no color defined, by adding the following program code:

    - (void)touchesBeganWithEvent:(NSEvent *)event { // Ignore touches for which there is no color if ([m_activeTouches count] > ([m_colorWells count] - 1)) { return; }

  9. In the BTSFingerView.h file add a new @property for the array of active strokes. This array will contain a BTSStroke object for each finger that is touching the trackpad.

    // A reference to the array of Active Strokes @property (strong) NSMutableDictionary *m_activeStrokes;

  10. In the BTSFingerViewContoller.m file, synthesize the m_activeStrokes property as follows:

    // Synthesize the object that will // store the currently active stokes @synthesize m_activeStrokes;

  11. Add the following, highlighted, program code to the initWithFrame: method in the BTSFingerView.m file to create the dictionary object that will be used to store the stroke touch objects:

    - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { // Initialization code here. // Create the mutable dictionary that // will hold the list of currently active // touch events m_activeTouches = [[NSMutableDictionary alloc] init]; // Create the mutable dictionary that // will hold the list of currently active // strokes m_activeStrokes = [[NSMutableDictionary alloc] init]; // Accept trackpad events [self setAcceptsTouchEvents: YES]; } return self; }

  12. Add the program code to the touchesBeganWithEvent: method to create a new BTSStroke object that contains a single BTSPoint object at the location of the touch in our custom NSView:

    for (NSTouch *l_touch in l_touches) { [m_activeTouches setObject:l_touch forKey:l_touch. identity]; // Get the color for the stroke NSColor *l_color = [[m_colorWells objectAtIndex: [m_activeTouches count] - 1] color]; // Create a new stroke object with // the color BTSStroke *l_stoke = [[BTSStroke alloc] initWithWidth:2.0 /* BAD MAGIC NUMBER */ red:[l_color redComponent] green:[l_color greenComponent] blue:[l_color blueComponent] alpha:[l_color alphaComponent]]; // Add the stroke to the array of active strokes [m_activeStrokes setObject:l_stoke forKey:l_touch.identity]; // Create a new point at the location of the // finger touch. This is done by getting the // normalized position (between 0 and 10 and // calculating the position in the view bounds NSPoint l_touchNP = [l_touch normalizedPosition]; l_touchNP.x = l_touchNP.x * self.bounds.size.width; l_touchNP.y = l_touchNP.y * self.bounds.size.height; BTSPoint * l_point = [[BTSPoint alloc] initWithNSPoint:l_touchNP]; // Add the point to the stroke [l_stoke addPoint:l_point]; }

  13. Add the program code to the touchesMovedWithEvent: method to add more BTSPoint objects to the BTSStroke as tracked fingers move on the trackpad:

    for (NSTouch *l_touch in l_touches) { // Update the touch only if it is found // in the active touches dictionary if ([m_activeTouches objectForKey:l_touch.identity]) { [m_activeTouches setObject:l_touch forKey:l_touch.identity]; // Retrieve the stroke for this touch BTSStroke *l_Line = [m_activeStrokes objectForKey:l_ touch.identity]; // Create a new point at the location of the // finger touch. This is done by getting the // normalized position (between 0 and 10 and // calculating the position in the view // bounds NSPoint l_touchNP = [l_touch normalizedPosition]; l_touchNP.x = l_touchNP.x * self.bounds.size.width; l_touchNP.y = l_touchNP.y * self.bounds.size.height; BTSPoint * l_point = [[BTSPoint alloc]initWithNSPoint:l_touchNP]; // Add the point to the stroke [l_Line addPoint:l_point]; } }

  14. Add the program code to the drawRect: method to perform the actual drawing of the active strokes. This code uses core graphics to create a path for each stroke and render it in the view. The first part of the code fills the rect with the selected transparent color, so that we can see through this view to the one below, with a focus ring.

    // Fill the view with fully transparent // color so that we can see through it // to whatever is below [[NSBezierPath bezierPathWithRect:dirtyRect] fill];

  15. Because we wanted a focus ring just around the drawing area, we need to restore the graphics context to the state it was in previously so that the focus ring is not drawn around the remaining objects.

    // Restore the graphics content // so that other things we draw // don't get focus rings [NSGraphicsContext restoreGraphicsState]; // Get the current graphics context NSGraphicsContext * l_GraphicsContext = [NSGraphicsContext currentContext];

  16. Then we can get the graphics port and draw the active strokes using the correct color for the stroke. First, we do some basic initialization of variables that we will use for drawing:

    // Get the low level Core Graphics context // from the high level NSGraphicsContext // so that we can use Core Graphics to // draw CGContextRef l_CGContextRef = (CGContextRef) [l_GraphicsContext graphicsPort]; // We will need to reference the array of // points in each store NSMutableArray *l_points; // We will need a reference to individual // points BTSPoint *l_point; // We will need to know how many points // are in each stroke NSUInteger l_pointCount; // We will need a reference to the // first point in each stroke BTSPoint * l_firsttPoint;

  17. Once we have declared our variable references, we loop through all of our active strokes as follows:

    // For all of the active strokes for (BTSStroke *l_stroke in m_activeStrokes.allValues ) {

  18. We need to set the stroke width and stroke color for each stroke as follows:

    // Set the stroke width for line // drawing CGContextSetLineWidth(l_CGContextRef, [l_stroke width]); // Set the color for line drawing CGContextSetRGBStrokeColor(l_CGContextRef, [l_stroke red], [l_stroke green], [l_stroke blue], [l_stroke alpha]);

  19. Then, we need to get all the points and create a path from the points:

    // Get the array of points l_points = [l_stroke points]; // Get the number of points l_pointCount = [l_points count]; // Get the first point l_firsttPoint = [l_points objectAtIndex:0]; // Create a new path CGContextBeginPath(l_CGContextRef); // Move to the first point of the stroke CGContextMoveToPoint(l_CGContextRef, [l_firsttPoint x], [l_firsttPoint y]); // For the remaining points for (NSUInteger i = 1; i < l_pointCount; i++) { // note the index starts at 1 // Get the SECOND point l_point = [l_points objectAtIndex:i]; // Add a line segment to the stroke CGContextAddLineToPoint(l_CGContextRef, [l_point x], [l_point y]); }

  20. Once we have created a path from the list of points, we simply draw the path and the stroke will appear in our view.

    // Draw the path CGContextDrawPath(l_CGContextRef,kCGPathStroke); }

  21. In both the touchesEndedWithEvent: and touchesCancelledWithEvent: methods, change the code so that when the activeTouch is removed the activeStroke is also removed as follows:

    // For each ended touch, remove the touch // from the active touches dictionary // using its identity as the key // Also remove the active stroke for // the touch for (NSTouch *l_touch in l_touches) { [m_activeTouches removeObjectForKey:l_touch.identity]; [m_activeStrokes removeObjectForKey:l_touch.identity]; }

What just happened?

We implemented the drawing code so that when fingers touch the trackpad, lines will be drawn to follow their motion. The line color will be selected from an image well based on when the finger touched the trackpad. When a finger is lifted from the trackpad the line disappears.

Saving strokes

Currently when we lift our finger from the trackpad, the stroke that we draw disappears. We need some way to save that stroke. One way we can do this is to create another custom view and hand the stroke to that view just before we remove it from the list of active strokes.

Time for action — saving the strokes

Similar to how we created a custom view for our finger touches, we can create a second custom view for completed strokes.

  1. Create a new file in Xcode, using the Mac OS X Cocoa Objective-C class template, that is a subclass of NSView, and call it BTStrokeView.

  2. In the .xib file, click on the Finger View object and select Duplicate from the Edit menu to make a copy of the Finger View object.

  3. Select the new Finger View , and in the Identity inspector , change its Class to BTSStrokeView .

  4. In the Objects hierarchy, drag the Finger View object into the original Stroke View object to make it a subview of the original object. The new Objects hierarchy will look as shown in the following screenshot:

  5. In the file named BTSStrokeView.h, add a @property that will be used to reference the mutable array that will hold the saved BTSStroke objects. We are also going to need a method to add strokes to the m_Strokes array so we can define the interface for that method as well.

    #import <Foundation/Foundation.h> #import "BTSStroke.h" @interface BTSStrokeView : NSView // A reference to the array of Saved Strokes @property (strong) NSMutableArray *m_Strokes; /** ** - (void) addStroke: (BTSStroke *)a_stroke ** ** Add a stroke to the saved array of ** BTSStroke objects ** ** Input: A BTSStroke object ** ** Output: none */ - (void) addStroke: (BTSStroke *)a_stroke; @end

  6. In the file named BTSStrokeView.m, add the program code to synthesize the m_Strokes as follows:

    #import "BTSStrokeView.h" @implementation BTSStrokeView // Synthesize the object that will // store the saved stokes @synthesize m_Strokes;

  7. In the file named BTSStrokeView.m, add the program code to implement the addStroke: method: /p>

    - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { // Initialization code here. // Initialize and allocate the array m_Strokes = [[NSMutableArray alloc] init]; } return self; } /** ** - (void) addLine: (BTSStroke *)a_line ** ** Add a stroke to the saved array of ** BTSStroke objects ** ** Input: A BTSStroke object ** ** Output: none */ - (void) addStroke: (BTSStroke *)a_stroke { // Add the stroke to the array [m_Strokes addObject: a_stroke]; }

  8. In the file named BTSStrokeView.m , add the program code to implement the drawRect: method. This method is almost identical to the method used in the Finger View except that instead of a transparent background, it draws a white background and instead of iterating (looping) over the dictionary values, it iterates over the array values. The complete method is shown in the following code snippet but the two highlighted sections are the ones that are different from the previous method:

    - (void)drawRect:(NSRect)dirtyRect { // Drawing code here. // color the background white [[NSColor whiteColor] set]; // Fill the view with fully white color [[NSBezierPath bezierPathWithRect:dirtyRect] fill]; // Get the current graphics context NSGraphicsContext * l_GraphicsContext = [NSGraphicsContext currentContext]; // Get the low level Core Graphics context // from the high level NSGraphicsContext // so that we can use Core Graphics to // draw CGContextRef l_CGContextRef = (CGContextRef) [l_GraphicsContext graphicsPort]; // We will need to reference the array of // points in each store NSMutableArray *l_points; // We will need a reference to individual // points BTSPoint *l_point; // We will need to know how many points // are in each stroke NSUInteger l_pointCount; // We will need a reference to the // first point in each stroke BTSPoint * l_firsttPoint; // For all of the saved strokes for (BTSStroke *l_stroke in m_Strokes ) { // Set the stroke width for line // drawing CGContextSetLineWidth(l_CGContextRef, [l_stroke width]); // Set the color for line drawing CGContextSetRGBStrokeColor(l_CGContextRef, [l_stroke red], [l_stroke green], [l_stroke blue], [l_stroke alpha]); // Get the array of points l_points = [l_stroke points]; // Get the number of points l_pointCount = [l_points count]; // Get the first point l_firsttPoint = [l_points objectAtIndex:0]; // Create a new path CGContextBeginPath(l_CGContextRef); // Move to the first point of the stroke CGContextMoveToPoint(l_CGContextRef, [l_firsttPoint x], [l_firsttPoint y]); // For the remaining points for (NSUInteger i = 1; i < l_pointCount; i++) { // note the index starts at 1 // Get the SECOND point l_point = [l_points objectAtIndex:i]; // Add a line segment to the stroke CGContextAddLineToPoint(l_CGContextRef, [l_point x], [l_point y]); } // Draw the path CGContextDrawPath(l_CGContextRef,kCGPathStroke); } }

  9. In the file named BTSFingerView.h, add a #define that we will use to add strokes to the Stroke View only if they contain at least two points:

    // Define the minimum number of // points that can be in a stroke #define D_MIN_POINTS 2

  10. In the file named BTSFingerView.m, modify the touchesEndedWithEvent: method to add a stroke to the Stroke View , and mark the Stroke View as it needs to be redisplayed, just before removing it from the active strokes dictionary. This is done by getting the superview of the Finger View , which will be the Stroke View , and invoking the addStroke: method on that view.

    // For each ended touch, remove the touch // from the active touches dictionary // using its identity as the key // Also remove the active stroke for // the touch // Get a reference to the // stroke view object BTSStrokeView *l_strokeView = (BTSStrokeView *)[self superview]; for (NSTouch *l_touch in l_touches) { // If there is an active touch if ([m_activeTouches objectForKey:l_touch.identity]) { // Get the active stroke for the touch BTSStroke *l_stroke = [m_activeStrokes objectForKey:l_touch.identity]; // If the stroke has at least 2 points // in it add it to the stroke view // object if (l_stroke.m_points.count > D_MIN_POINTS) { [l_strokeView addStroke: l_stroke]; [l_strokeView setNeedsDisplay:YES]; } // Remove the active touch [m_activeTouches removeObjectForKey:l_touch.identity]; // Remove the active stroke [m_activeStrokes removeObjectForKey:l_touch.identity]; } }

What just happened?

We implemented our BTSStrokeView object and made our BTSFingerView object a subview of the BTSStrokeView object. The BTSStrokeView keeps track of all the strokes that we want to be drawn but that are no longer active. Now we can use Multi-Finger Paint to draw a complete picture as follows:

Have a go hero — implementing a Pen Down checkbox

Sometimes, we are going to want to use the touchpad to perform gestures. Because gestures would result in lines potentially being draw where we don't want them we can add a checkbox to our GUI that is used to determine if lines are to be drawn during touches.

Go ahead and implement the Pen Down checkbox:

  1. In the .xib file, create a checkbox to the Multi-Finger Paint window, and title it Pen Down .

  2. In the Attributes Inspector make sure the checkbox state is Off.

  3. In the BTSFingerView.h file, add an IBOutlet @property for the NSButton and call it m_penDownCheckbox.

  4. In the BTSFingerView.m file, synthesize the m_penDownCheckbox property.

  5. In the BTSFingerView.m file, change the touchesBeganWithEvent: implementation to add an active line only if the m_penDownCheckbox state is equal to NSOnState.

  6. In the BTSFingerView.m file change the touchesMovedWithEvent: implementation to add points to an active line only if the m_penDownCheckbox state is equal to NSOnState.

Once these steps are complete, we can use the mouse to check and uncheck the Pen Down GUI item so that we can control whether or not lines are drawn. But because we disconnect the mouse hardware from the mouse cursor when a finger is on the trackpad, it would be really nice to be able to toggle the checkbox using a keyboard event.

  1. In the file named BTSFingerView.h, implement a new method with the prototype:

    - (void)keyDown:(NSEvent *)event

  2. The event has a member named characters that returns an NSString , add code to examine the characters to see if the first character is equal to the Space bar , @" ".

  3. If the first character is equal to @" ", then add code that examines the state of m_penDownCheckbox and if it is equal to NSOnState, set it to NSOffState, otherwise set it to NSOnState.

  4. If the first character is not equal to @" ", pass the event to the super class to be handled.

How to receive gesture events

Gestures are combinations of touches and just like touch events, our view will automatically receive gesture events. The gesture events that we can receive are as follows:

  • - (void)magnifyWithEvent:(NSEvent *) event

  • - (void)rotateWithEvent:(NSEvent *)event

  • - (void)swipeWithEvent:(NSEvent *)event

  • - (void)beginGestureWithEvent:(NSEvent *)event

  • - (void)endGestureWithEvent:(NSEvent *)event

Time for action — handling rotate gestures

When a gesture event is received by our view, we can take whatever action we would like to handle the event. We decide which gestures we want to handle and what we want them to do.

  1. In Xcode, click on the file named BTSFingerView.m in the project navigator and add the following method to handle a rotateWithEvent: gesture:

    /** ** (void)rotateWithEvent:(NSEvent *)event ** ** Invoked when two fingers make a rotating ** gesture on the trackpad ** ** Input: event - the gesture event ** ** Output: none */ - (void)rotateWithEvent:(NSEvent *)event { // If the pen is not down if (NSOffState == m_penDownCheckbox.state) { // Rotate the super view // By the amount of rotation in the // event [self.superview setFrameCenterRotation: [self.superview frameCenterRotation] + [event rotation]]; } }

What just happened?

We implemented an event handler for the rotateWithEvent: gesture that rotates the superview. Because our view is contained in the superview, our view will also rotate as shown in the following screenshot:

Have a go hero — implementing swipe to clear

We can use other gestures to do more complex things. For example, we could use a four-finger swipe to erase our picture.

Go ahead and implement the swipe to clear gesture:

  1. In the BTSStrokeView.h file, add a method to the interface called clear that returns nothing and has no arguments.

  2. In the BTSStrokeView.m file implement the clear method. It needs to remove all the BTSStroke objects from the m_strokes NSMutableArray and send the setNeedsDisplay message with a value of YES to the view.

  3. In the BTSFingerView.m file add a method using the swipe prototype - (void)swipeWithEvent:(NSEvent *)event. It needs to check to make sure the pen is not down and then remove all the objects from m_activeTouches and m_activeStrokes. It can, optionally, re-attach the mouse hardware to the mouse cursor and unhide the mouse. Finally, it should send the clear method to the superview.

Once these steps are complete we can use the four-finger swipe gesture to clear the painting from the view.

Summary

In this article, we have implemented an application that uses the multi-touch trackpad to paint with our fingers. We have also implemented several multi-touch gestures to clear the painting from our view.

Specifically, we covered what is multi-touch, how to implement custom views and subviews, how to select colors from a color well and draw strokes in a custom view, how to manage the mouse cursor, and how to receive keyboard, touch, and gesture events and respond to them.

Now that we have spent some time looking at the cool multi-touch and gesture event handling and how to draw in a view, we are going to shift gears and look at another cool input device, the iSight camera.

Resources for Article :


Further resources on this subject:


Mac Application Development by Example: Beginner's Guide A comprehensive and practical guide, for absolute beginners, to developing your own App for Mac OS X book and ebook.
Published: December 2012
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:

About the Author :


Robert Wiebe

Robert Wiebe was born in 1961. He has more than 30 years experience designing, implementing, and testing software. He wrote his first App in 1979, as a high school student, using 6502 assembler code on an Ohio Scientific C2-4P computer with 8k RAM. More recently, he has focused on developing games and utilities for Mac OS X.

His interests include a vintage computer collection which includes many pre-IBM PC era microcomputers; Apple Macintosh computers starting with the SE/30 running Mac OS 7 through to the Macbook Pro running Mac OS X that he uses today.

He has developed many popular Mac OS X Apps including ShredIt X, NetShred X, Music Man, iVCD, and many more.

He has experience developing software in a number of industries, including mining, finance, and communications. He has worked for a number of employers including Motorola as a Senior Systems Architect developing two-way wireless data systems and Infowave Software as the Software Development Manager for their Imaging Division. After working for other people's companies, he founded his own companies, Mireth Technology and Burningthumb Software, which are his primary interests today.

He is also the author of Unity iOS Essential book (ISBN 978-1-849691-82-6).

Books From Packt


TextMate How-To
TextMate How-To

iWork for Mac OS X Cookbook
iWork for Mac OS X Cookbook

Xcode 4 iOS Development Beginner's Guide
Xcode 4 iOS Development Beginner's Guide

Xcode 4 Cookbook
Xcode 4 Cookbook

iPhone Applications Tune-Up
iPhone Applications Tune-Up

iOS 5 Essentials
iOS 5 Essentials

Instant New iPad Features in iOS 6 How-to
Instant New iPad Features in iOS 6 How-to

iPad Enterprise Application Development BluePrints
iPad Enterprise Application Development BluePrints


Your rating: None Average: 1 (1 vote)

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
z
d
M
y
g
S
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software