Now that we’ve thought about GraphPaper a bit, let’s get on with the work of building the application.
We need to make one change to Evaluator so that it can recognize more than a single expression on a line — that’s important, because GraphPaper will be sending (x,y) pairs such as (3,2*3+1) to Evaluator. (If we sent only y values, we might get confused.)
The easy way to do this is to make Evaluator recognize two
expressions separated by a comma and terminated with a newline.
Because we built Evaluator with lex
and
yacc
, this change is easy to make and is
confined to a single file, grammar.y
.
Launch Project Builder, choose File → New Project, and double-click “Cocoa Application”.
Type “GraphPaper” in the Project
Name field, hit the Tab key so that the project is saved in the
folder ~/GraphPaper
, and then click Finish.
Create a second target called Evaluator in the GraphPaper project by choosing PB’s Project → New Target menu command, double-clicking Tool at the bottom of the list in the resulting sheet, typing “Evaluator”, and clicking Finish in the second sheet in the project window.
Click the Files vertical tab in PB’s main window and disclose the Resources group.
Activate the Finder, open your ~/MathPaper
folder, single-click grammar.y,
and then
Command-click rules.l
to select these two files.
We’ll add them to the GraphPaper project in the next
step.
Drag the two-file selection from the Finder and drop it on the Resources group in PB’s Groups & Files pane. In the resulting sheet, click the checkbox next to “Copy items into destination group’s folder”, make sure the Evaluator target is checked, and finally click Add.
In PB’s editor, insert the six lines shown here in
bold into grammar.y
:
stat : expr ' ' { printf("%10g ", $1); printingError = 0; fflush(stdout); }| expr ',' expr ' ' { printf("%g,%g ", $1, $3); printingError = 0; fflush(stdout); } ;
These changes enable us to send to Evaluator two expressions on the same line, separated by a comma. Evaluator will evaluate each expression and print the results, separated by a comma, on a single line.
Select Evaluator in the pop-up menu at the top center of
PB’s main window, then click the build and run
button to build the Evaluator target. Save
grammar.y
before building.
Test the new running Evaluator process in PB’s Run pane by typing the expressions shown here in bold. The nonbold lines are output from Evaluator.
0,2*0+1 0,11,2*1+1 1,32,2*2+1 2,5
In this example, we typed the seven-character string “0,2*0+1” and Evaluator responded with “0,1”. Note that the upgraded Evaluator can now handle the numeric algebraic pairs that we plan to send it later.
Quit Evaluator by clicking the Stop button in PB’s toolbar.
Still in PB, add the Evaluator file to the GraphPaper target. To do
this, select the GraphPaper target in PB’s pop-up
menu, choose Project → Add Files, and add Evaluator
(in the build
folder) to the GraphPaper target.
In this section, we will put in place the underlying framework for the GraphPaper application:
Using a graphics program, create a 128 x 128 bit application icon for GraphPaper. Our amateurish attempt is shown here.
You might also create the 48 x 48,
32 x 32, and 16 x 16 icon sizes to use in
GraphPaper’s .icns
file.
Launch IconComposer, create a GraphPaper.icns
icons file using the icon(s) that you created in the previous step,
and save this file in the ~/GraphPaper
project
folder.
Drag the GraphPaper.icns
icon from the Finder
and drop it in the Resources section of PB’s Groups
& Files pane. Add the GraphPaper.icns
file
to your GraphPaper target.
Click PB’s Targets vertical tab and select the GraphPaper target. Click the Application Settings tab and establish the application settings, as specified in Table 16-2.
Table 16-2. Application settings for GraphPaper
Setting name |
Value |
---|---|
Executable |
GraphPaper |
Identifier |
GraphPaper |
Type |
AAPL |
Signature |
GRFP |
Version |
1.0 |
Display Name |
GraphPaper graphical calculator |
Get-Info String |
GraphPaper |
Short version |
GraphPaper |
Icon file |
GraphPaper |
Principal class |
NSApplication |
Main nib file |
MainMenu |
Single-click the InfoPlist.strings
filename
(under Resources) in PB’s Groups & Files pane.
Edit the copyright messages in PB as appropriate.
Double-click the MainMenu.nib
filename (under
Resources) in PB’s Groups & Files pane. IB will
automatically launch and display the default
MainMenu.nib
interface created by PB.
Modify the GraphPaper menus, changing “NewApplication” to “GraphPaper” in four places in the application menu. Also, rename “MyApp” in the Help menu.
Earlier, we mentioned that a class called GraphView will be used to display function graphs. As you might expect, GraphView will be a subclass of Cocoa’s NSView class. GraphView will need outlets to point to most of the on-screen objects, as well as action methods to start and stop the graphing.
Select NSView in the MainMenu.nib
window and
choose Classes→
Subclass NSView to create a new
subclass. Rename the new subclass
“GraphView”.
Type Command-1 to open the GraphView Class Info dialog. Add the following outlets and action methods to GraphView:
Outlets Action Methods
graphButton graph:
xminCell stopGraph:
xmaxCell
xstepCell
yminCell
ymaxCell
formulaField
Choose Classes → Create Files for GraphView to create the files for the GraphView class and insert them into the GraphPaper target.
Next we’ll set up GraphPaper’s main window, as shown in Figure 16-2.
Still in IB, change the main window’s title from “Window” to “GraphPaper” in the Window Info dialog.
Resize the window so that it is about two inches tall and four inches wide.
Drag a CustomView icon from IB’s Cocoa-Views palette and drop it in the GraphPaper window. Enlarge it and position it as shown in Figure 16-2.
Change the class of the CustomView to GraphView in the Info dialog.
Drag an NSForm object from IB’s Cocoa-Views palette and drop it in the right side of the GraphPaper window.
Option-drag the bottom-center handle of the NSForm object to create three more NSFormCells, for a total of five NSFormCells.
Change the labels on the NSFormCells to “xmin”, “xmax”, “xstep”, “ymin”, and “ymax”, as shown in Figure 16-2. (Use the Tab key to move quickly from one NSFormCell to the next.)
Enter the numbers “0.0”, “10.0”, “0.1”, “-1.0”, and “1.0” in the five text areas of the NSForm, as shown in Figure 16-2. We’ll use these initial graphing parameters to show the user a good-looking graph at launch time.
Select the entire NSForm matrix and change the text to be right-aligned in the Text Alignment box in the NSForm Info dialog.
Drag a SystemFont Text icon from IB’s Cocoa-Views palette and drop it in the lower-left corner of the GraphPaper window. Change the text to “y(x)=”, as shown in Figure 16-2, and make it larger (16 point) using IB’s Font dialog (Command-T). This SystemFont Text icon represents an NSTextField object with attributes such as uneditable, etc.
Drag an NSTextField icon from IB’s Cocoa-Views palette and drop it at the right of the “y(x)=” in the GraphPaper window.
Make the text in the NSTextField larger (16 point) using IB’s Font dialog. Make the NSTextField wider as well.
Enter a function that has an interesting graph in the white
NSTextField. We’ll use sin(3*x)
,
which will produce an interesting graph at launch time.
Drag an NSButton object from IB’s Cocoa-Views palette and drop it in the GraphPaper window below the NSForm, as shown in Figure 16-2. Change the title on the NSButton object to “Graph” and make the text size 16 point. Use the blue guidelines to align the on-screen objects.
Connect the seven GraphView outlets to the appropriate
on-screen objects. For example, Control-drag from the GraphView
on-screen instance to the Graph button and double-click the
graphButton
outlet. Similarly, connect the
xmaxCell
outlet to the NSFormCell labeled
“xmax”, the
xminCell
outlet to the NSFormCell labeled
“xmin”, and so on. The
formulaField
outlet should be connected to the
NSTextField object containing the formula (see the GraphView
Connections Info dialog at the left of Figure 16-3).
Note that all of the outlet connections are listed at the bottom of
the Info dialog.
Later in this chapter, we’ll use the
graphButton
outlet to temporarily change the title
on the button from “Graph” to
“Stop” while GraphPaper is drawing
a graph.
Connect the Graph button to the on-screen GraphView instance. Make it send the graph: action message to GraphView. See the corresponding NSButton Info dialog in Figure 16-3.
We won’t use the stopGraph: action as part of the connections we set up in IB, but we will connect the Graph button to stopGraph: programmatically. One tiny advantage of adding stopGraph: to the GraphView class in IB is that IB’s Create Files command will create the skeleton code for the method, which saves a bit of typing. Another advantage is that stopGraph: will be visible while you’re working in IB.
Save the MainMenu.nib
file.
GraphView will be the most complicated
class that we build in this book, so we’ll go over
it in pieces. When learning any new class, the best place to start is
with the interface file. In this case, that file is
GraphView.h
.
Back in PB, insert the lines shown here in bold into
GraphView.h
:
#import <Cocoa/Cocoa.h> @interface GraphView : NSView { IBOutlet id formulaField; IBOutlet id graphButton; IBOutlet id xmaxCell; IBOutlet id xminCell; IBOutlet id xstepCell; IBOutlet id ymaxCell; IBOutlet id yminCell; // These five variables are the same as those in MathPaper NSPipe *toPipe; NSPipe *fromPipe; NSFileHandle *toEvaluator; NSFileHandle *fromEvaluator; NSTask *evaluator; NSMutableString *fromBuf; // These hold the contents of the NSForm // double xmin; These three will be public variables // double xmax; See the @public directive below // double xstep; double ymin; double ymax; // Display list NSMutableArray *displayList; BOOL first; // Getting the first point? NSPoint lastPt; // Last point received // Communication with stuffer thread BOOL stop_sending; BOOL sending; BOOL receiving; @public // For use by stuffer thread BOOL graphing; char *formula; int toFd; double xmin, xmax, xstep; } - (IBAction)graph:(id)sender; - (IBAction)stopGraph:(id)sender;- (void)doStop:(int)which; - (void)getFormAndScaleView; - (void)addGraphElement:(id)element; - (void)clear; - (void)sendData; @end #define STOP_SENDER 1 #define STOP_RECEIVER 2 #define GRAPH_TAG 1 #define AXES_TAG 2 #define LABEL_TAG 3
The first seven id
statements declare the outlets
we set up and connected in IB. The remaining instance variables are a
little more complicated. Here is a brief description of what they do:
toPipe
, fromPipe
, toEvaluator
, fromEvaluator
, evaluator
These variables all have the same functions as the corresponding variables in the MathPaper application. The MathPaper variables were initially defined in Chapter 11.
fromBuf
It’s possible to get a variable amount of
information back from Evaluator (including a partial line), so
it’s necessary to buffer
Evaluator’s content. We’ll use
fromBuf
, an NSString instance variable, as the
buffer.
The next group of instance variables holds a copy of the graphing parameters that are read from the NSForm. We use instance variables to store the graphing parameters so that they can be referenced by both the main thread and the stuffer thread.
xmin
, xmax
These two variables determine the horizontal scale of the graph that is drawn.
xstep
This variable determines the step increment in the horizontal (x) direction, used for drawing the graph.
ymin
, ymax
These two variables determine the vertical scale of the graph that is drawn.
The next group of variables is used for holding and maintaining the display list:
displayList
This is the actual display list itself, implemented with an NSMutableArray.
first
This boolean variable is set before the first pair is received from Evaluator. It enables the GraphView object to distinguish between the first pair of coordinates returned and the others.
lastPt
The (x,y) coordinate pair of the last point read from Evaluator. This
variable is valid only if first=NO
. It is used to
construct the line segment from the last point received to the
current point.
The last group of instance variables is used for communication between the main thread and the stuffer thread. Because of the design of the GraphView class, it won’t be necessary to use an NSLock.
stop_sending
When set to YES, this boolean variable forces the stuffer thread to exit.
sending
This boolean variable is set to YES just before the stuffer thread starts up. When the stuffer thread is finished, it will send the termination code (999) to Evaluator and resets this variable to NO.
receiving
This boolean variable is set to YES just before the stuffer thread starts up. When the main thread receives the termination code (999) from Evaluator, it resets this variable to NO.
The @public
declarations mean that the four
instance variables (graphing
,
formula
, etc.) will be visible everywhere,
including to the stuffer thread. We’ll discuss the
new methods declared in GraphView.h
as we
progress through this chapter. Finally, the
#define
statements set up the tags that we will
use for various parts of the GraphView class.
Now let’s look at the
GraphView class implementation code in
GraphView.m
. The first part of the file requires
another #import
directive:
The new Segment class will be used to create line segments to draw pieces of the graph. We’ll create the Segment class later in this chapter.
The first method we’ll discuss in our GraphView
class definition is initWithFrame:
, the view’s designated
initializer. This method will set up the connection to Evaluator and
will initialize the displayList
and
fromBuf
instance variables.
Insert the following initWithFrame:
method into GraphView.m
:
- initWithFrame:(NSRect)frame { NSString *path; [super initWithFrame:frame]; displayList = [ [NSMutableArray alloc] init]; fromBuf = [ [NSMutableString alloc] init]; // What follows is largely from MathPaper path = [ [NSBundle mainBundle] pathForResource:@"Evaluator" ofType:@""]; if (!path) { NSLog(@"%@: Cannot find Evaluator", [self description]); } else { toPipe = [NSPipe pipe]; fromPipe = [NSPipe pipe]; toEvaluator = [toPipe fileHandleForWriting]; fromEvaluator = [fromPipe fileHandleForReading]; evaluator = [ [NSTask alloc] init] retain; [evaluator setLaunchPath:path]; [evaluator setStandardOutput:fromPipe]; [evaluator setStandardInput:toPipe]; [evaluator launch]; [ [NSNotificationCenter defaultCenter] addObserver:self selector:@selector(gotData:) name:NSFileHandleReadCompletionNotification object:fromEvaluator ]; [fromEvaluator readInBackgroundAndNotify]; } // The notification below causes the getFormAndScaleView // method to be invoked whenever this view is resized [ [NSNotificationCenter defaultCenter] addObserver:self selector:@selector(getFormAndScaleView) name:NSViewFrameDidChangeNotification object:self]; return self; }
The initWithFrame:
method starts by creating the
displayList
and fromBuf
objects. Then it creates Evaluator, using code that is largely
borrowed from MathPaper (see Chapter 11). Finally,
it makes GraphView a receiver of notifications of the
NSViewFrameDidChangeNotification type. This notification ensures that
the GraphView will be sent a getFormAndScaleView message if its on-screen
view area changes size. By doing this, we avoid having to make
GraphView a delegate of the NSWindow in which it resides.
Recall
that the data stuffer thread sends to Evaluator a series of
expressions that looks like this (for y=2*x+1
):
0, 2*0+1 1, 2*1+1 2, 2*2+1
And Evaluator sends back a series of numbers that looks like this:
0, 1 1, 3 2, 5
The GraphView object uses those pairs of numbers to construct a graph. For this to happen, we must create a display list — a list of objects that will be used to describe the drawing of a graph.
Our display list will be implemented with a series of objects that
adopt a new formal
protocol
that we’ll call
GraphViewElement. We say that an
Objective-C class
adopts a protocol if it implements all the
methods in that protocol. A formal protocol is a group of methods
declared between the
@protocol
and @end
directives. A formal protocol is adopted in code by listing its name
between
angle brackets in a class
declaration, as we’ll see later.
The methods in our GraphViewElement protocol are described in Table 16-3.
Table 16-3. GraphViewElement protocol methods
Method |
Purpose |
---|---|
- (int)tag |
Returns the object’s tag (used later) |
- (void)setTag:(int)aTag |
Sets the object’s tag |
- (void)stroke |
Draws the object |
- (NSRect)bounds |
Returns the element’s bounding box |
- (void)setColor:(NSColor *)aColor |
Sets the element’s color |
- (NSColor *)color |
Returns the object’s color |
The GraphView class will maintain a list of
objects that respond to this protocol in the
displayList
mutable array. The following GraphView
methods will be used to implement this display list functionality:
Insert the GraphViewElement protocol into the
GraphView.h
file, after the
@end
directive that ends the GraphView interface:
@protocol GraphViewElement - (int)tag; - (void)setTag:(int)aTag; - (void)stroke; - (NSRect)bounds; - (void)setColor:(NSColor *)aColor; - (NSColor *)color; @end
Placing this protocol definition in the file
GraphView.h
informs the GraphView class about
the declarations for each of these methods.
Insert the following clear method
into the GraphView.m
file, after the
@implementation
directive but before the
@end
directive:
// Display list maintenance - (void)clear { [displayList removeAllObjects]; [self setNeedsDisplay:YES]; }
This clear method removes all of the objects from the displayList and then sends a message to itself indicating that the entire GraphView needs to be redisplayed.
Insert the following addGraphElement:
method into
GraphView.m
:
- (void)addGraphElement:(id)element { [displayList addObject:element]; [self setNeedsDisplayInRect:[element bounds]]; }
This method adds the element object argument to the display list, then invokes NSView’s setNeedsDisplayInRect: method to tell the GraphView’s superclass that the region within the bounding box of the added element needs to be redrawn.
As with PolygonView in the previous chapter, GraphView will use Quartz and the NSView architecture to provide all of the scaling that we need to draw our mathematical functions.
The only thing that our program needs to do is provide information for the required scaling. This will be done by the method getFormAndScaleView, which will read the current parameters from the on-screen window’s form, set up the GraphView’s instance variables, and then scale the GraphView’s bounds to the appropriate size.
Insert the following getFormAndScaleView
method into
GraphView.m
:
- (void)getFormAndScaleView { xmin = [xminCell doubleValue]; xmax = [xmaxCell doubleValue]; xstep = [xstepCell doubleValue]; ymin = [yminCell doubleValue]; ymax = [ymaxCell doubleValue]; [self setBounds:(NSMakeRect(xmin, ymin, xmax-xmin, ymax-ymin) ) ]; [self setNeedsDisplay:YES]; }
You might think that the drawRect: method, which we show in the next step, would be the workhorse of the GraphView class. After all, this method does all of the work of actually drawing the graph, right? But in fact, this method is very simple in GraphView. First it initializes the background color to white, then it determines an appropriate line width for drawing the graph and sets the current line width accordingly. (The default line width is 1 point, but because we will be rescaling the coordinate system of this NSView to match that of our graph, we need to calculate the “true” size of 1 point in our scaled coordinate system.) Finally, the drawRect: method iterates through all of the objects in the display list, determines whether or not they intersect the area that is being redrawn, and draws them if they do.
Insert the
following isOpaque and drawRect: methods into
GraphView.m
:
-(BOOL)isOpaque { return YES; } // Because GraphView is opaque -(void)drawRect:(NSRect )rect { id obj=nil; NSEnumerator *en; NSSize sz; [ [NSColor whiteColor] set]; NSRectFill(rect); sz = [self convertSize:NSMakeSize(1,1) fromView:nil]; [NSBezierPath setDefaultLineWidth:MAX(sz.width,sz.height)]; en = [displayList objectEnumerator]; while (obj = [en nextObject]) { if (NSIntersectsRect(rect,[obj bounds]) ) { [obj stroke]; } } }
Note that for the first time we are using the drawRect: argument rect
for
more than simply drawing a rectangle: we use it to determine the
intersection of GraphView and the object (line segment) to be drawn.
The drawRect: method that we
constructed for the PolygonView class in the previous chapter drew
the entire polygon every time the method was invoked. That was okay
because drawing the polygon involved very few drawing operations, but
when drawing complex images, it’s wasteful to redraw
the entire image — especially if you need to redraw only a tiny
sliver of the image.
We’ll use drawRect:’s
rect
argument to help us determine which part of
the screen to redraw. rect
is passed as an
argument to the Cocoa function NSIntersectsRect( )
, which provides a handy way to determine if
rect
and the new area to be drawn intersect.
NSIntersectsRect( )
is
one of
the many Cocoa utility rectangle functions. Similar functions will
tell you if one rectangle contains another rectangle or a specified
point, or if two rectangles are the same. The function
NSUnionRect( )
will compute the smallest rectangle
large enough to contain two other rectangles, while the function
NSIntersectionRect( )
will compute the region of
overlap.
That’s it for the drawRect: method. This simple method is not only optimized to redraw the absolute minimum amount of the graph that’s ever required; it will also handle printing, faxing, and generating PDF files.
The part of GraphView that sets up and uses the data stuffer thread consists of the following four methods:
Sets up global variables and starts the data stuffer thread.
Lets a user interrupt the graph currently being drawn.
The data stuffer method that will be executed in a separate thread by the NSThread class.
The common logic for stopping the graph and resetting the GUI. This method is invoked regardless of whether the graph stops normally or by user intervention.
Insert the lines shown here in bold into the graph: action method in
GraphView.m
:
- (IBAction)graph:(id)sender { // Set instance variables from the form [self getFormAndScaleView]; // Check the parameters of the graph if (xmax < xmin || ymax < ymin) { NSRunAlertPanel( nil, @"Invalid min/max combination", @"OK", nil, nil); return; } if ( xstep <= 0 ) { NSRunAlertPanel(0, @"The step size must be positive", @"OK", nil, nil); return; } [self clear]; first = YES; stop_sending = NO; sending = YES; receiving = YES; [graphButton setTitle:@"Stop"]; [graphButton setAction:@selector(stopGraph:)]; [NSThread detachNewThreadSelector:@selector(sendData) toTarget:self withObject:nil]; }
The graph: action method first
validates the values entered by the user for the graphing parameters
xmin
, xmax
,
ymin
, ymax
, and
xstep
. If these values are not acceptable, an
alert panel is displayed and the method returns.
If the values are acceptable, the display list is cleared and the
state variables are initialized. The statement
first=YES
sets the first instance variable so that
the method that builds the graph will know that a new graph is being
created. The statement stop_sending=NO
resets the
instance variable that is used to control the stuffer thread. The
sending=YES
and receiving=YES
statements set toggles that will be used in the doStop: and gotData: methods described a bit later.
The setTitle: message changes the title of the on-screen button from “Graph” to “Stop”. The related statement that follows changes the action method associated with the button from graph: to stopGraph:. If the user clicks the button when its title is “Stop”, the stopGraph: message is sent to the GraphView. This is the way to rewire (change the connection in) an application while it is running. You can’t do that in IB.
The last line in the graph: action method sends a message to the NSThread class to detach the stuffer thread. Although we haven’t seen it yet, the thread starts up with the sendData message being sent to the same GraphView object that was previously running. The trick here, of course, is that the sendData method executes simultaneously with the rest of the GraphView object.
Insert the following sendData method
into GraphView.m
:
- (void)sendData { NSAutoreleasePool *threadPool = [ [NSAutoreleasePool alloc] init]; NSString *formula; double x; int i; formula = [formulaField stringValue]; for (x=xmin; stop_sending==NO && x<=xmax; x+=xstep) { NSMutableString *fsend = [NSMutableString stringWithString:@"x,"]; NSString *xString = [NSString stringWithFormat:@"%g",x]; [fsend appendString:formula]; [fsend appendString:@" "]; // Now go through the formula and change every 'x' to a '%g' for (i=[fsend length]-1; i>=0; i--) { if ([fsend characterAtIndex:i] == 'x') { [fsend replaceCharactersInRange:NSMakeRange(i,1) withString:xString]; } } // Send this to the other side [toEvaluator writeData: [fsend dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES] ]; } // Now send through the termination code [toEvaluator writeData:[@"999 " dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES] ]; [self doStop:STOP_SENDER]; // Release the pool before the thread exits [threadPool release]; }
The sendData method implements the entire stuffer thread, so it is understandably complicated (we hope that you’ll find it understandable as well!). The first thing this thread does is set up its own NSAutoreleasePool. Each thread must have its own autorelease pool: it would do no good to have one thread’s releasing another thread’s data!
After the autorelease pool is set up, the sendData method makes a copy of the formula
that is presently in the GraphView’s
formulaField
. It then sets up a loop that will
step the variable x
from xmin
to xmax
by xstep
(recall that
these instance variables were set up by the message [self getFormAndScaleView] in the graph: method. Each time through the loop, the
method creates a new NSMutableString that contains the formula that
is to be solved. The x
variables are then replaced
with the current value of x
. This algebraic
formula is then sent to Evaluator.
When the loop finishes, the data stuffer sends the number 999 to Evaluator. This number is used as a flag to indicate that no more data is coming through the pipe. The procedure that constructs the graph will look for a 999 on a line by itself and will use that flag as its way of knowing that the graph is finished. The specific digits 999 really don’t matter: what’s important is that Evaluator is sent a line of data with one expression and no comma.
When the whole process is finished, the doStop:
method is invoked with the argument
STOP_SENDER
to indicate that the stuffer has
finished. Finally, the autorelease pool is released, which causes all
of the temporary strings that were created to be freed.
The stopGraph: method stops a running graph. It is invoked when the user clicks the Stop button (“Stop” replaces “Graph” as the button’s title only when a graph is being drawn).
As part of its main loop, the data stuffer monitors the status of the
stop_sending
Boolean variable. When this variable
is set to YES, the data stuffer immediately stops what it is doing
and sends the termination code 999 to Evaluator.
The doStop: method is invoked twice, once when the stuffer stops sending, and again when Evaluator receives the stuffer’s termination code, which is the last line that the stuffer sends prior to terminating.
Insert the following doStop: method
into GraphView.m
:
- (void)doStop:(int)which { switch (which) { case STOP_SENDER: sending = NO; break; case STOP_RECEIVER: receiving = NO; break; } if (sending==NO && receiving==NO) { // Reinitialize [graphButton setTitle:@"Graph"]; [graphButton setAction:@selector(graph:)]; [graphButton setEnabled:YES]; } if (sending==NO && receiving==YES) { // Wait for results data [graphButton setEnabled:FALSE]; [graphButton setTitle:@"Waiting..."]; } if (sending==YES && receiving==NO) { // A problem NSLog(@"Synchronization error"); } }
This doStop: method controls the
on-screen Graph button. When the user first clicks the Graph button,
its label changes from “Graph” to
“Stop”. Pressing the button when it
is labeled “Stop” causes the
stop_sending
flag to be set, as discussed earlier.
But after the stuffer flag has finished sending its data, neither
“Graph” nor
“Stop” is really an appropriate
setting for this button. Instead, there is a third mode: the button
displays “Waiting . . . " and is
disabled. At this point, the application is simply waiting for
Evaluator to process all of the information that it has been sent and
to return the calculated results.
Now it’s time to implement the method that receives data from Evaluator and constructs line segments that make up the graph.
Insert the following gotData:
method into
GraphView.m
:
- (void)gotData:(NSNotification *)not { NSData *data; NSString *str; NSPoint pt; int num; NSString *line=0; data = [ [not userInfo] objectForKey:NSFileHandleNotificationDataItem]; str = [ [NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; // Add the data to the end of the text buffer [fromBuf appendString:str]; // Register to get the notification again [fromEvaluator readInBackgroundAndNotify]; // Now, process all complete lines we have do { NSRange r1; r1 = [fromBuf rangeOfString:@" "]; if (r1.length<1) break; line = [fromBuf substringToIndex:r1.location]; [fromBuf replaceCharactersInRange:NSMakeRange(0,r1.location+1) withString:@""]; num = sscanf( [line cString], "%f, %f", &pt.x, &pt.y); if (num!=2) { [self doStop:STOP_RECEIVER]; return; } if (!first && !stop_sending) { Segment *seg = [ [ [Segment alloc] initFrom:lastPt to:pt ] autorelease]; [seg setTag:GRAPH_TAG]; [self addGraphElement:seg]; } first = NO; // No longer first lastPt = pt; // Remember this point } while (line); // End of data }
This method is invoked whenever new data is available. Its main complication is that it needs to break the block of data it receives into individual lines. Each of these lines is then used to create a Segment object, and these objects are then added to the display list with the addGraphElement: method.
The reason for the line-by-line buffering is that Evaluator might send more than one line of data to the GraphView object before it is scheduled to read the data (because the data is being generated by a different execution thread). It might also send an incomplete line, due to blocking on the pipe. The GraphView object therefore needs to buffer the data that it receives and then read it out one line at a time.
Note that the addBufToGraph:
method ignores the data it receives if
the stop_sending
instance variable is set to YES.
This means that after the user clicks the Stop button all of the rest
of the data in the pipeline is ignored, giving the application a nice
snappy response time.
The gotData:
method uses the sscanf( )
function to turn the line of text from
Evaluator back into numbers. If num!=2
, then there
were not two numbers separated by a comma to read; in this case, the
method invokes the stopGraph: method
and the graph stops.
If this data pair is the first data pair, the execution drops down to
the last four lines. These lines set the instance variables
lastx
and lasty
to be the
coordinates of the current point, then unsets the first variable and
returns.
On all data pairs other than the first, the middle section of this
method gets executed. This conditional code first creates a Segment
object (described in the next section) with endpoints at
(lastPt.x,
lastPt.y)
and
(pt.x,
pt.y)
and adds this
segment to the display list.
This method also sets the tag of the segment to
GRAPH_TAG
. We’ll use tags later
to distinguish between different objects stored inside the display
list.
Although a GraphView object constructs graphs, it relies upon a Segment object to actually draw the lines that make up the graph. The GraphView object invokes two Segment instance methods:
Returns a rectangle bounding the Segment’s line
Causes the Segment object to draw itself in the current view
By using a separate class that interacts with the GraphView class
according to a well-defined protocol, we open up the possibility of
adding new objects to the graph with very little work. To make the
Segment class even more general, it supports a tag
internal variable (which we’ll use later, when we
add more types of objects).
Choose PB’s File → New File command, select Cocoa → Objective-C class, and click Next.
Name the file Segment.m
. Leave the checkbox
checked so that Segment.h
is also created in the
~/GraphPaper
folder, GraphPaper project, and
GraphPaper target. Click Finish.
Edit the Segment.h
file so that it looks like
the following:
#import <Cocoa/Cocoa.h> #import "GraphView.h" @interface Segment:NSObject <GraphViewElement> { NSPoint start; NSPoint end; NSColor *color; int tag; } - initFrom:(NSPoint)start to:(NSPoint)end; - (NSPoint) segmentCenter; @end
The @interface
directive with the angle brackets
(<>) tells the Objective-C compiler that Segment is a subclass
of NSObject that follows the GraphViewElement protocol, and thus that
it must implement the six methods declared previously in that
protocol.
Edit the Segment.m
file so that it looks like
the following:
#import "Segment.h" @implementation Segment - initFrom:(NSPoint)theStart to:(NSPoint)theEnd { [super init]; // Init the NSObject superclass start = theStart; end = theEnd; color = [ [NSColor blackColor] retain]; return self; } - (void)dealloc { [color release]; // Release what you retain [super dealloc]; // and dealloc the superclass } // Accessor methods - (int)tag { return tag; } - (void)setTag:(int)aTag { tag = aTag; } - (void)setColor:(NSColor *)aColor { [color release]; color = [aColor retain]; } - (NSColor *)color { return color; } // Methods that derive information for the caller - (NSRect)bounds { return NSMakeRect( MIN(start.x,end.x), MIN(start.y,end.y), fabs(start.x-end.x) + FLT_MIN, fabs(start.y-end.y) + FLT_MIN ); } - (NSPoint)segmentCenter { return NSMakePoint((start.x+end.x)/2.0, (start.y+end.y)/2.0); } - (void)stroke { [color set]; [NSBezierPath strokeLineFromPoint:start toPoint:end]; } @end
The Segment class implementation is fairly straightforward. Notice
that there is no bounds
instance variable;
instead, we calculate each segment’s bounding box on
demand from other instance variables and return what was calculated.
This is known as data hiding — an
object’s internal representation of data does not
have to be the same representation that is used by its accessor
methods.
We use FLT_MIN
in the bounds
method so that lines that are vertical or horizontal will
still have a width or height that is non-zero.
FLT_MIN
is the smallest floating-point number that
the IEEE floating-point package can represent. By adding
FLT_MIN
to the calculated width and height, we
guarantee that these values will not be zero. If they are computed to
be a number that is larger than FLT_MIN
— for
example, the number 5 — adding FLT_MIN
will
have no significant effect, as it’s a very tiny
value.
Now that we’ve built the interface, made the connections, and implemented all the classes, we’re finally ready to make and test GraphPaper.
Build and run GraphPaper. Save all files first.
With GraphPaper running, click the Graph button.
You’ll see the graph of
y=sin(3*x)
over the x
range
[0,10]
, as shown in Figure 16-4.
Because xstep=0.1
, 100 line segments (steps) made
up the graph of sin(3*x)
.
Change the value of xstep
to
0.001
and click the Graph button again. This time,
the graph of sin(3*x)
will be displayed slowly,
and the Graph button title will change to
“Stop” and then a dimmed
“Waiting . . . “. Try clicking the
Stop button.
Try graphing another function, such as x*cos(4*x)
,
with a different step and ranges.
Try entering a negative value for xstep
and click
the Graph button. An alert should show up. Try entering
min
values greater than the max
values, and you should get another alert.
Quit GraphPaper.
In Chapter 18, we’ll clean up the GraphPaper application a bit and make it respond properly to resizing, and we’ll arrange for the (x,y) coordinates of each point to be displayed as the mouse is moved over the graph. We’ll finish off this chapter by showing how to add two different objects to GraphPaper’s display list.
3.143.17.27