Extending the Display List

GraphView’s display list and drawRect: method can easily be extended to draw objects other than graphs in the on-screen GraphView. In this section, we’ll add axes and labels to the graph.

Adding Axes

Adding X and Y axes to GraphView is quite simple, because we already have the Segment class to draw the line. All we need to do to draw the axes is to create a new method that draws the axes and then arrange for it to be invoked by GraphView’s graph: method.

  1. Insert the following addAxesFrom:to: method declaration into GraphView.h:

                            - (void)addAxesFrom:(NSPoint)pt1 to:(NSPoint)pt2;
  2. Insert the following addAxesFrom:to: method implementation into GraphView.m:

                            - (void)addAxesFrom:(NSPoint)pt1 to:(NSPoint)pt2
                            {
                                Segment *seg = [ [ [Segment alloc] initFrom:pt1 to:pt2] autorelease];
                                [seg setTag:AXES_TAG];
                                [self addGraphElement:seg];
                            }
  3. Insert the lines shown here in bold into the graph: method in GraphView.m:

    - (IBAction)graph:(id)sender
    {
        ...
        [self clear];
    
        // Display the axes    [self addAxesFrom:NSMakePoint(xmin,0.0) to:NSMakePoint(xmax,0.0)];
                                [self addAxesFrom:NSMakePoint(0.0,ymin) to:NSMakePoint(0.0,ymax)];
                            ...
  4. Build and run GraphPaper. Save all files first.

  5. To see both the axes in GraphPaper, enter the values shown in Figure 16-5.

GraphPaper with axes

Figure 16-5. GraphPaper with axes

  1. Try other functions and graphing parameters, and then Quit GraphPaper.

When the first data point comes through, the new statements in the graph: method create two Segment objects that correspond to the X and Y axes. The tags for each of these objects are set to 1. The axis lines are added to the display list, and then the graph is displayed. The window in Figure 16-5 shows what the graph looks like with axes. We set the tag to AXES_TAG so that we can distinguish these segments from the segments used to draw the graph itself. This distinction will be important in Chapter 18, when we want to make the graph segments (but not the axes) sensitive to mouseovers.

Adding Labeling

In addition to axes, we can add a function label fairly easily by creating a Label class that responds to the same bounds and stroke methods as the Segment class. Because Objective-C uses dynamic binding — where messages are resolved when they are sent, rather than when the program is compiled — we won’t need to make any changes to GraphView’s drawRect: method. Label objects will be stored in the display list, along with the Segment objects. Label objects must have their own initialization methods, however, because line segments and text labels need to be set up in different ways.

As we’ll see, drawing text also represents an interesting problem: the Quartz fonts assume that they are drawing on a grid with a square scale — that is, they assume that the scale on the X axis is the same as the scale on the Y axis. It’s possible to override this assumption by providing a two-dimensional transformation matrix when creating the font. However, it’s easier to change the scale of the GraphView while the label is being drawn and then change it back after. Such manipulations are actually remarkably easy, and they can be done in a manner that is completely transparent to the GraphView object itself.

  1. Create new Label class files (Label.h, Label.m), as you did earlier for the Segment class files, and add them to the GraphPaper project. (Recall how we did it before: choose PB’s File New File command, select Cocoa Objective-C class, etc.).

  2. Make the code in the Label.h file look like the following code:

                            #import <Cocoa/Cocoa.h>
                            #import "GraphView.h"
                            
    @interface Label:NSObject <GraphViewElement>
                            {
                            
        NSRect bounds;
                                NSMutableAttributedString *text;
                                NSColor             *color;
                                int                  tag;
                                NSFont              *font;
                                NSMutableDictionary *dict;
                            }
                            
    - initRect:(NSRect)bounds text:(NSString *)aText size:(float)aSize;
                            @end

Notice that this interface is very similar to the interface used by Segment. In particular, it adopts the GraphViewElement protocol. The main difference is that the two objects use different initialization methods, as of course they must. The Label class implementation is a bit more complicated than the Segment class implementation. It starts out with the initRect:text:size: method, which we show in the next step.

  1. Make the code in the Label.m file look like the following (incomplete) code:

                            #import "Label.h"
                            
    @implementation Label
                            - initRect:(NSRect)aBounds text:(NSString *)aText size:(float)aSize 
                            {
                                [super init];
                                bounds = aBounds;
                            
        font  = [NSFont fontWithName:@"Times-Roman" size:aSize];
                                dict  = [ [NSMutableDictionary alloc] init];
                                [dict setObject:font forKey:NSFontAttributeName];
                            
        text  = [ [NSMutableAttributedString alloc]
                                           initWithString:aText attributes:dict];
                                [self  setColor:[NSColor blackColor] ];
                                return self;
                            }

This initRect:text:size: method sets the bounds of the Label object. It then creates an NSMutableAttributedString that will draw centered text in Times Roman in the requested point size.

Because the Label class uses the alloc method to create the NSMutableDictionary and the NSMutableAttributedString, these objects must be released when the Label object itself is freed. This is done by the dealloc method.

  1. Insert the following dealloc method into Label.m:

                            - (void)dealloc
                            {
                                [dict release]
                                [text release];
                                [super dealloc];
                            }

The following setColor: , bounds , tag , and setTag: accessor methods provide access to these values of the Label class from outside. Notice that the Label class’s implementation of setColor: is completely different from the Segment class’s. In particular, Label’s setColor: method adds the color to the attribute dictionary and then reapplies the attribute to the attributed string. Then, because of a bug in the AppKit (present in Cocoa 10.1), this implementation reapplies the NSCenterTextAlignment to the string.

Tip

This is an example of a case where the structure of the AppKit makes it easy to work around some bugs or limitations in the existing Cocoa release.

  1. Insert the following five methods into Label.m:

                            - (NSRect)bounds         { return bounds;}
                            - (int)tag               { return tag;}
                            - (void)setTag:(int)aTag { tag=aTag;}
                            
    - (void)setColor:(NSColor *)aColor
                            {
                                [dict setObject:aColor forKey:NSForegroundColorAttributeName];
                                [text setAttributes:dict range:NSMakeRange(0,[text length])];
                                // Now reapply the alignment because of a Cocoa bug
                                [text setAlignment:NSCenterTextAlignment
                                             range:NSMakeRange(0,[text length])];    
                            }
                            
    - (NSColor *)color {return color;}

Finally, the Label class implements a stroke method to generate the Quartz commands necessary to display the label.

  1. Insert the following stroke method and @end directive into Label.m:

                            // This works, but it requires a subview
                            - (void)stroke
                            {
                            
        NSView *fv = [NSView focusView];
                                NSView *tempView;
                            
        tempView = [ [NSView alloc] initWithFrame:bounds];
                            
        [fv addSubview:tempView];
                            
        // Scale the tempView to screen coordinates
                                [tempView setBounds:
                                          [tempView convertRect:[self bounds] toView:nil] ];
                            
        [tempView lockFocus];
                                [color set];
                                [text drawInRect:[tempView bounds] ];
                                [tempView unlockFocus];
                                [tempView removeFromSuperviewWithoutNeedingDisplay];
                            }
                            
    @end

This method is pretty wacky, and we had hoped to replace it with a better one before this book was published. The problem here is that the GraphView usually has a nonsquare transformation, but Quartz provides few mechanisms for drawing fonts with nonsquare transformations. To get around this limitation, this method first creates a new NSView in the area in the GraphView where the label is to be drawn. This view is then scaled so that its coordinate system matches that of the NSWindow it contains. We then lock focus on this view, draw the label, unlock the focus, and remove the view from the superview using a method that prevents the superview from being redisplayed. This works and is reasonably fast, but it would be better to use the NSAffineTransformation and NSGraphicsContext classes. So far, though, we haven’t been able to get them to work. If you figure out how, please send us email, and we’ll post the solution on the O’Reilly web site.

Using the Label Class

To use this new class, we’ll need to make several changes to GraphView.m. First we need to import the new Label class into the GraphView class, and then we need to make another modification to the graph: method.

  1. Insert the #import statement shown here in bold into GraphView.m:

    #import "GraphView.h"
    #import "Segment.h"#import "Label.h"
    
    @implementation GraphView
  2. Insert the new statements shown here in bold into the graph: method in GraphView.m:

    ...
    // Add the axes
    [self addAxesFrom:NSMakePoint(xmin,0.0) to:NSMakePoint(xmax,0.0)];
    [self addAxesFrom:NSMakePoint(0.0,ymin) to:NSMakePoint(0.0,ymax)];
    // Add a label
                            {
                                Label *label = [ [Label alloc]
                                initRect:NSMakeRect(xmin, ymin, xmax-xmin, (ymax-ymin)*.2)
                                    text:[formulaField stringValue]
                                    size:24.0];
                            
        [label autorelease];
                                [label setTag:LABEL_TAG];
                                [self addGraphElement:label];
                            }
                            ...

Notice that we inserted a new block of code so that we can have a local variable called label that is limited in scope to these statements.

  1. Build and run GraphPaper. Save all files first.

  2. Enter a function and ranges, as shown in Figure 16-1 at the beginning of this chapter, and then click the Graph button to see the function label.

  3. Play around with GraphPaper and then quit.

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

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