Building a Scratch Pad with Cocoa
Pages: 1, 2
Alternatives
Let's add some more functionality to this little application. We'll set things up so that when we press a key, say “c”, the view will be cleared of any previous drawing. To do this, we will implement keyDown, as well as reorganize our existing code some. This involves moving the initialization code for path into PadView’s init method so that init looks like this:
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
path = [[NSBezierPath bezierPath] retain];
}
return self;
}
In this approach, we are adopting the policy of creating a path object at application launch and never destroying it. Rather than releasing and re-instantiating path, we will remove all points from the path to clear it, as we will see in keyDown’s implementation.
MouseDown: looks like the following:
- (void)mouseDown:(NSEvent *)theEvent
{
NSPoint loc = [theEvent locationInWindow];
loc.x -= [self frame].origin.x;
loc.y -= [self frame].origin.y;
[path moveToPoint:loc];
}
Now we want to override NSResponder’s keyDown method in the following way:
- (void)keyDown:(NSEvent *)theEvent
{
NSString *keyChar = [theEvent characters];
if ( [keyChar isEqualToString:@”c”] ) {
[path removeAllPoints];
[self setNeedsDisplay:YES];
}
}
In a key event, we can use the NSEvent method characters to return a string corresponding to the key pressed. We then compare this to some string that represents the action to be executed, and codify this logic in an if statement. In this case, I choose “c” -- for clear -- to execute code that clears the path of any elements that it contains.
|
|
But we’re not done yet. One last thing we have to do is override the NSResponder method acceptsFirstResponder in the following way:
- (BOOL)acceptsFirstResponder
{
return YES;
}
This method tells anyone who is interested that our NSView subclass will accept being the first responder. If we want keyDown to work here, we need to do this. What exactly does it mean for something to be first responder, and why does it matter here? Let’s look into this matter more.
More responder discussion
|
Also in Programming with Cocoa |
Cocoa’s event-handling system is based around what is known as a responder chain. As the name suggests, the responder chain is essentially a list of responder objects that are eligible to potentially handle events.
When an event is generated, it is passed to the responder object at the top of the list. If this responder is able to handle the event, then it does, otherwise it passes the buck to the next responder in line on the chain. If this next object can handle the event, then it does. If it can’t then the event message continues on up the chain until a responder is found that can handle the event. Generally, the active window is the first responder in the chain, and when a mouse event occurs the window passes the event to the view beneath the mouse cursor, which gives the view the opportunity to respond appropriately, if it can.
With key events, however, the situation is slightly different. Put quite simply, objects that aren’t at the top of the responder list -- those that aren’t first responder -- can respond to mouse events as discussed, but cannot respond to key events. To enable an object to respond to key events, we must indicate that it is capable of doing so by returning YES in the overridden acceptsFirstResponder method (by default NO is returned).
This is exactly what we did above to let PadView have the chance to respond to key events. Considering the number of key events that are generated when one types, it makes perfect sense to limit the number of objects that are considered for response to key events.
For a more detailed discussion of the responder chain, check out the class documentation for NSResponder; for a more conceptual discussion of this concept, and why it is used, check out the classic book on object-oriented design, Design Patterns by Gamma et al.; specifically look at the Chain of Responsibility pattern.
At this point you should be able to compile and doodle away! You can download my version of the project here. This little application has lots of potential for simple extensions.
One thing you could do is to add a color well that lets you change the color of the line. If you do this, I anticipate that you’ll soon want to draw many lines with lots of different colors. Trying to implement this will lead you into the wall that a color applies to an entire bezier path. We can’t make different elements different colors. Anyway, have some fun with this and see what you can come up with, and I will see you in the next column!
Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.
Read more Programming With Cocoa columns.
Return to the Mac DevCenter.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 27 of 27.
-
so did anyone create a workable drawing app
2002-09-23 09:52:36 anonymous2 [Reply | View]
so did anyone create a workable pressure
drawing app from all of this. I realy miss using
my tablet for sketching since I no longer have
Os9 .. -
RE: tabled pressure
2003-12-13 17:05:16 anonymous2 [Reply | View]
NSEvent got a float named pressure:
NSEvent
- (float)pressure
Returns a value between 0.0 and 1.0 indicating the pressure applied to the input device (used for appropriate devices). For devices that aren't pressure-sensitive, the value is either 0.0 or 1.0. Raises anNSInternalInconsistencyException if sent to a non-mouse event.
-
keyDown method doesn't execute before mouseDown
2002-08-31 18:21:53 whiteman [Reply | View]
In your scratchPad program the keyDown method will not execute until there is a mouse clicked in the view. (Try adding a NSLog() function call to keyDown and pressing a before and after you make your first drawing.) This does not matter in your program where the overridden keyDown method has no effect unless the user has clicked and dragged the mouse in the View. However, I am trying to use this code in a different app. How do you initialize the view so that it will accept key down events before a mouse has been clicked in the View.
AW
-
Error in Scratch Pad
2002-04-09 05:38:50 cheeseb [Reply | View]
In the override of the keyDown: method in Scratch Pad, you neglect to call super if some key other than "c" was pressed. When you implement keyDown: in a subview, you have to send a [super keyDown:theEvent] message in all cases that you don't handle yourself, so that some responder higher up the responder chain can have a crack at the event. Ultimately, if nobody responds, this will enable the application to beep, as it normally does when you press a key to which no view responds.
This is a great series. Keep 'em coming!
-
How to use scrolling in a NSView Subclass
2001-12-31 07:07:13 zeus [Reply | View]
I try to make a very simple sofware, and in that sofware I need to scroll the view. Here is my code:
-(void)mouseDown:(NSEvent *) theEvent{
NSPoint loc = [theEvent locationInWindow];
loc.x -= [self frame].origin.x;
loc.y -= [self frame].origin.y;
[self scrollPoint:loc];
[self setNeedsDisplay:YES];
}
I'm using a methode scrollPoint:aPoint from the NSView Class but i can't hardly get a scroll. I'm sure that i'm going into this method because when I ran my soft in debugger mode I get the right coordinates of the point "loc".
Does anyone have an idea ?
-
How to use scrolling in a NSView Subclass
2001-12-31 07:06:24 zeus [Reply | View]
I tri to make a very simple sofware, and in that sofware I need to scroll the view. Here is my code:
-(void)mouseDown:(NSEvent *) theEvent{
NSPoint loc = [theEvent locationInWindow];
loc.x -= [self frame].origin.x;
loc.y -= [self frame].origin.y;
[self scrollPoint:loc];
[self setNeedsDisplay:YES];
}
I'm using a methode scrollPoint:aPoint from the NSView Class but i can't hardly get a scroll. I'm sure that i'm going into this method because when I ran my soft in debugger mode I get the right coordinates of the point "loc".
Does anyone have an idea ? -
I'm going to try to say what's going on
2002-01-01 19:35:03 psheldon [Reply | View]
Here's what I think is going on. If I'm right, I agree with you, I'm confused as you why it doesn't work.
loc, an NSPoint gets assigned, at first, the location of the mousedown. Then, with a c assignment, you take that location relative to the origin and use that for loc. Then scrollPoint, a method of NSView, is supposed to scroll the view. After, setNeedsDisplay, forces a redraw of the view.
It seems that if all that I said above is true, this thing you made should work.
One candidate to make it work would have you go through reading all the methods to see where this thing falls short of the above expectations. That candidate course of action, however, doesn't break the problem into chunks.
A question, however, came out of the blue for me. WIth mousedown so engaged what is the source of the drawing that you are scrolling? If you have the drawing happening in the NSView method (is it drawrect?) then that happens after setNeedsDisplay and so the scrolling can't move it.
I'm thinking that it's possible that scrolling moves screenbits and you don't use setNeedsDisplay to force a draw after scrolling . Is that so?
There's a chunk to find out !
Maybe you could make an additional button and boolean and have a multiuse mousedown and have some sense of conceptual victory. The button and boolean would have two states, one for mousedown draws and other for it scrolls.
Well, I'm sorry, I don't get much more coherent today, New Year's Day. I'm exhausted. I don't dare experiment with my ideas on this today.
Soon (Friday) I go off a week to visit my mother without internet access. -
I'm going to try to say what's going on
2002-01-03 08:02:33 zeus [Reply | View]
As you say I'm drawing in a drawRect method but could you give me something concrete because I'm almost lost.
And an other thing : I cannot access the NSView.m file that would let me see where the things are going wrong.
In any case thanks for the help.
-
Good Clear Articles
2001-12-29 10:10:25 mahongue [Reply | View]
These articles are a great complement to the Learning Cocoa book. I especially liked the last three on graphics, as that is what I will use the most. I have almost got my Mandelbrot set drawn.
My question is how to plot individual points into a view. I could define a rect using NSMakeRect as (x,y,x+1,y+1), and color in that pixel, but this seems to be a lot of extra code just to plot an NSPoint. Is there a command that takes an NSPoint and draws it in the default color?
Thank you for any help.
Alexander
-
small steps on my own to three color padview
2001-12-05 08:52:23 psheldon [Reply | View]
I made a red green and blue color button and connected them to actions that chose currentPath amongst 3 different colored Bezierpaths.
Here is the relevant code I added to PadView.m (obvious variables and headers in PadView.h) :
- (IBAction)SetColorPath:(NSString *)NSstr //wouldn't allow me (void *) for return type, not connected to button
{
BOOL test;
test=[NSstr isEqualToString:@"red"];
if (test) pathCurrent=pathRed;
test=[NSstr isEqualToString:@"green"];
if (test) pathCurrent=pathGreen;
test=[NSstr isEqualToString:@"blue"];
if (test) pathCurrent=pathBlue;
}
- (IBAction)ChooseBlue:(id)sender
{
[self SetColorPath:@"blue"];
}
- (IBAction)ChooseGreen:(id)sender
{
[self SetColorPath:@"green"];
}
- (IBAction)ChooseRed:(id)sender
{
[self SetColorPath:@"red"];
}
I don't know whether my code was that tight for further generalization . That tightness might emerge on attempting to generalize.
I could imagine each mouse down could generate a new path and put in an NSMutableArray that could be inspired by canibalizing sketch project for relevant methods. Once that tested out without bugs,
one might have a new colored path button asking for a color well to attach a color to a new path in the array.
Somehow, as the code got more complex, it should have helper methods to make it more surveyable. I don't know what the warning in response to my putting (void *) output to SetColorPath meant, so my confidence at making things surveyable has gotten bitten.
;-) -
small steps on my own to three color padview
2001-12-05 21:49:03 Michael Beam |
[Reply | View]
I think your inability to make the return type (void *) comes from the fact that IBAction is actually type void. (void *) is the datatype for a generic pointer, which is something where void is nothing. Weird. Anyway, good stuff.
I enable multiple path support in a version of this little app i wrote a while back by doing exactly what you said with using a mutable array. Basically, it worked by creating a new path and appending it to the mutable array when ever the mouse went down. All mouse drag events appended to the most recently added path in the array. Then in the drawRect method i just enumerated the array, sending a stroke message to each array element. While it would be slow, you could enable color support in this by having each element of this mutable array be an NSDictionary that contains a key-value pair for the path object, and a key-value pair for the color. Thus, in each loop through the enumeration you could set the color to draw in based on the information stored in the dictionary, and then stroke the path. Anyway, have fun!
Mike -
NSBezier subclass for ColoredPath
2001-12-08 23:48:12 psheldon [Reply | View]
Ran off to party and perhaps let go of too much code to the thread. Maybe someone can speed read it and see what I was trying to do. Not that they should finish my assumed homework for me, but could my own structs serve as an addanobject for an NSMutableArray and how do I birth a struct other than on a stack? There isn't a NEW, is there, in objective C?
I think that a subclass could have an NSColor would inherit a constructor and not have the dot problem because I wouldn't be using a struct. Nor would I try to do something crazy like retain an address on a stack, though I don't particularly know why it is crazy, just suspect it so. I guess I would add an accessor method for the color of the ColoredPath.
I heard someone at an Apple seminar say that objects were much better than structs. He didn't formally declare why but just seemed to assure from experience. Maybe some more talk than his would make me see the trades of using structs rather than a subclass. -
NSBezier subclass creation set with no get accessor
2001-12-09 07:06:48 psheldon [Reply | View]
I would make a method to create a ColorPath with an NSColor value which would assign an internal NSColor value and retain it within the object instantiation initialization which would also perform the inherited function. Then, I would override the stroke function to include the NSColor set for the current environment.
I am not clear with the "fuzzy typing" whether I could make a subclass of ColorPath be ColorPaths because of my narrow focus on the methods of NSBezierclass. Apple didn't do this in sketch, so I think it would be too much of a stretch for me to try to do. -
works but with warning (short code eg.)
2001-12-10 09:24:48 psheldon [Reply | View]
ColorBezier.m:20: warning: instance variable `theColor' accessed in class method
+ (ColorBezier*)initWithColor:(NSColor*)color {
self = [[super bezierPath] retain];
if (self) {
theColor = color;
}
return self;
}
-
got rid of warning with accessor and less self
2001-12-11 10:15:44 psheldon [Reply | View]
I really don't know the meaning of "self" in newtonscript and objective C and need some years of pain, probably, to teach me the intuition and, beyond, perhaps to formalize into words. I think it is the left argument in the square brackets at first, be it class or instance.
When I tried to make a new version that used a ColorWell, all my plans went haywire, as I doubted I had understood memory managment deep in my bones. I threw in retains and releases and perhaps too many retains knowing that when I quit, it would clean house, maybe. Still no change of color. I think the click in the color well ran the mousedown method and I got messages about not having a point for a line as soon as I tried to draw. I got rid of those error messages, without understanding them really, by insuring that I last clicked on a button I called "Well" by putting code assigning a new path in the action associated with that button in padView.
So, having gotten rid of an error message, I went on to see that my color didn't change because my ColorBezier subclass of NSBezierpath didn't change. So, perhaps I don't know how to think in assigning currentPath because of this pointer retain and release stuff not having sunk in.
Perhaps the button and colorwell actions may illustrate my confusion rather than all my code :
- (IBAction)ChooseWell:(id)sender
{
[pathCurrent release];
pathCurrent = [ColorBezier initWithColor: colorCurrent];
[pathCurrent retain];
[ColorPaths addObject: pathCurrent];
}
- (IBAction)colorWellAction:(id)sender
{
float *locRed,*locGreen,*locBlue,*locAlpha;
[colorCurrent release];
colorCurrent = [sender color];
[colorCurrent retain];
//[colorCurrent getRed:locRed green:locGreen blue:locBlue alpha:locAlpha];
[colorCurrent set];
//[colorCurrent getRed:locRed green:locGreen blue:locBlue alpha:locAlpha];
}
-
structs for ColorPath in padview
2001-12-08 16:02:05 psheldon [Reply | View]
I worry that I am building a struct on a stack and trying to retain it in NSMutableArray.
With this half baked code I got an error message :
PadView.m:93: parse error before `.'
PadView.m:102: parse error before `.'
PadView.m:103: parse error before `ColorPath'
PadView.m:104: warning: passing arg 1 of `addObject:' from incompatible pointer type
Here's full code :
//
// PadView.h
// ScratchPad
//
// Created by psheldon on Fri Nov 30 2001.
// Copyright (c) 2001 __MyCompanyName__. All rights reserved.
//
#import <AppKit/AppKit.h>
typedef struct ColorPath {
NSColor *color;
NSBezierPath *path;
} ColorPath;
@interface PadView : NSView {
NSBezierPath *pathRed;
NSBezierPath *pathGreen;
NSBezierPath *pathBlue;
NSBezierPath *pathCurrent;
NSMutableArray *ColorPaths;
}
- (IBAction)SetColorPath:(NSString *)NSstr;
- (void)ChooseBlue:(id)sender;
- (void)ChooseGreen:(id)sender;
- (void)ChooseRed:(id)sender;
@end
//
// PadView.m
// ScratchPad
//
// Created by psheldon on Fri Nov 30 2001.
// Copyright (c) 2001 __MyCompanyName__. All rights reserved.
//
#import "PadView.h"
@implementation PadView
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
//convenience constructor autoreleases so need retain for PadView
ColorPaths = [[NSMutableArray alloc] init];
pathRed = [[NSBezierPath bezierPath] retain];
pathGreen = [[NSBezierPath bezierPath] retain];
pathBlue = [[NSBezierPath bezierPath] retain];
pathCurrent = pathRed;
}
return self;
}
- (void)drawRect:(NSRect)rect {
// Drawing code here.
[[NSColor redColor] set];
[pathRed stroke];
[[NSColor greenColor] set];
[pathGreen stroke];
[[NSColor blueColor] set];
[pathBlue stroke];
}
- (void)mouseDown:(NSEvent *)theEvent
{
//mouse location with window to view coordinate change
NSPoint loc = [theEvent locationInWindow];
loc.x -= [self frame].origin.x;
loc.y -= [self frame].origin.y;
//convenience constructor autoreleases so need retain for PadView
//path = [[NSBezierPath bezierPath] retain];
//put pencil down at point
[pathCurrent moveToPoint:loc];
}
- (void)mouseDragged:(NSEvent *)theEvent
{
//mouse location with window to view coordinate change
NSPoint loc = [theEvent locationInWindow];
loc.x -= [self frame].origin.x;
loc.y -= [self frame].origin.y;
//move pencil to dragged to point
[pathCurrent lineToPoint:loc];
//padview needs to redisplay with new path information
[self setNeedsDisplay:YES];
}
- (void)mouseUp:(NSEvent *)theEvent
{
//[path release];
}
- (void)keyDown:(NSEvent *)theEvent
{
NSString *keyChar = [theEvent characters];
if ( [keyChar isEqualToString:@"c"] )
{
[pathRed removeAllPoints];
[pathGreen removeAllPoints];
[pathBlue removeAllPoints];
[self setNeedsDisplay:YES];
}
}
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (IBAction)SetColorPath:(NSString *)NSstr //wouldn't allow me (void *) for return type, not connected to button
{
BOOL test;
NSBezierPath *NewPath;
NSColor *NewColor;
ColorPath *NewColorPath;
NewPath = [[NSBezierPath bezierPath] retain];
ColorPath.path=NewPath;
test=[NSstr isEqualToString:@"red"];
if (test) NewColor = [[NSColor redColor] retain];
test=[NSstr isEqualToString:@"green"];
if (test) NewColor = [[NSColor greenColor] retain];
test=[NSstr isEqualToString:@"blue"];
if (test) NewColor = [[NSColor blueColor] retain];
ColorPath.color=NewColor;
pathCurrent=ColorPath.path;
[ColorPaths addObject:NewColorPath];
}
- (void)ChooseBlue:(id)sender
{
[self SetColorPath:@"blue"];
}
- (void)ChooseGreen:(id)sender
{
[self SetColorPath:@"green"];
}
- (void)ChooseRed:(id)sender
{
[self SetColorPath:@"red"];
}
@end
-
Memory leak?
2001-12-04 09:16:39 retro [Reply | View]
I notice in the final version, that you retain the path but you never release it. Is this a problem? I know that it is good practice to always balance retains with releases, and it seems that there should be a dealloc method for this subclass in order to release the memory.
What will happen to this memory if you simply quit the program? Is there some kind of global memory sweep that happens at the time of program termination? Just curious on this one. -
Memory leak?
2001-12-04 23:04:11 Michael Beam |
[Reply | View]
You're right, i should have put a dealloc method in the view class that would release the path object. Good eye! As for your second question, yes, memory used by an application, leaks and all, it released when said application quits.
-
warning: cannot find class (factory) method.
2001-12-01 09:52:39 larryvp [Reply | View]
I get the following 2 errors:
warning: cannot find class (factory) method.
warning: return type for 'path' defaults to id
for the following line in the initWithFrame method:
path = [[NSBezierPath path] retain];
I tried to download the source project to see what I did wrong but when stuffit trys to uncompress it I get an error that says:
The file "ScratchPad.sit" does not appear to be compressed or encoded. It is advised that you obtain further information about the contents of this file from the sender or provider of the file.
I've tried repeatedly but still get the same error.
Many thanks for any suggestions!
Thanks for a great column!
-
warning: cannot find class (factory) method.
2001-12-01 23:04:41 thee_ice [Reply | View]
I found that if you replaced it with
path = [[NSBezierPath bezierPath] retain];
that it works fine, hope that helps. -
That did the trick! Thanks!
2001-12-02 09:07:04 larryvp [Reply | View]
That did the trick! Thanks! -
That did the trick! Thanks!
2001-12-02 10:21:56 Michael Beam |
[Reply | View]
That's right, it should be bezierPath, not path. It will get corrected on monday. Hope you enjoy the article!
Mike
-
apostrophe become capital O with tilde
2001-11-30 21:00:20 psheldon [Reply | View]
I have Acrobat PDFWriter and using netscape communicator 4.75 in os 9 clicked on print button to get rid of web page pagination and have a single page to print through this driver into a pdf file. I seemed (erroneously) to have a font deficit in both os 9.1 and os x Acrobat Reader that substitutes this capital O with tilde for the apostrophe that can be seen on the web page.
Then again, I thought it might be my old netscape version that was at fault. It takes two for interapplication miscommunication.
I tried Internet Explorer 5 in os 9 and the apostrophe was depicted correctly in the pdf file though the save as ... dialogue box was screwed up and even though I typed in a name, the pdf file got renamed desktop.
So, pdf writers beware and take your choices to get your prices and prizes.
;-)
Happy the article is in!
Happy graduation in two weeks.







But I never receive a mouseMoved event. So my co-ordinate update never changes.
Does anyone know how to get this working ?