MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Programming With Cocoa

Working With Bitmap Images; Document-Based Application Redux

by Michael Beam
04/19/2002

One of the most useful and feature-rich aspects of Cocoa is its document-based application architecture. It's been awhile since we covered this ground - back when we first learned how to build Cocoa applications with the SimpleTextEditor application. However, that visit was a superficial glance at the surface that doesn't do justice to the capabilities provided by this part of Cocoa.

Today's column is the first in a series that takes a closer look at a number of previously-neglected aspects of Cocoa's document-based application feature set. The application we will build as a platform for exploring these concepts is an image viewer (and later an editor) application called ImageApp.

Subsequent columns will add image-exporting functionality, printing, undo, and a host of other features. Since we're constructing a graphics application, we'll also learn a great deal more about Cocoa graphics, in particular its image-handling classes. These classes include NSImage, NSImageView, NSImageRep and its subclasses, and more.

To kick things off, let's cover the mass of conceptual information that will provide a backdrop for our programming.

Document-based Application Refresher

In the document-based application architecture, three Application Kit classes are prime. They are NSDocument, NSDocumentController, and NSWindowController. Each of these classes has a specific role in the overall architecture of the document-based application, which follows the principles of the Model-View-Controller (MVC) pattern.

Recall that in the MVC pattern the application's functionality is split amongst several classes. The prototypical MVC pattern has three classes -- a class that models the data (the model), a class that displays the data (the view), and a class that is a mediator between the model and view (the controller). In past applications we learned how to implement this pattern by creating a controller class and using standard Cocoa classes as our model and view classes.

The document-based application architecture embraces the MVC pattern in its triad of classes and extends it somewhat by creating provisions for two controller classes. One controller class controls and manages the data model. This is the model-controller. The other controller serves the traditional role of controlling the user interface -- the view. This is the view-controller.

Related Reading

Learning Cocoa
By Apple Computer, Inc.

In Cocoa, NSDocument is the model-controller, and NSWindowController is the view-controller. Another way of looking at this is that NSDocument owns and manages objects representing the document's data model, which in our case will simply be an instance of NSImage. Part of the job of being the owner of the data objects is knowing how to load and save the persistent data, which is part of what NSDocument is all about.

NSWindowController as the view-controller is the owner and controller over all objects that make up the user interface, and in particular, controls how the document's contents are displayed to the user. Completing the pattern are the non-document-related classes, such as NSImage as the data model and NSImageView acting as the primary view object.

NSDocumentController doesn't really fit into the MVC pattern, which is fine, as it has a different purpose in the application from managing and presenting data to the user. Quite simply, NSDocumentController manages documents -- it knows how to create new documents (as an object, not the data of a document), how to open documents, and other application-level activities related to documents. This includes a knowledge of the types of files that the application can open (a viewer role), and that the application can modify and save (an editor role). Normally developers don't need to subclass NSDocumentController.

This is much different than the way we originally started out building document-based applications. Remember the simple SimpleTextEditor you built last year? That application was constructed around one class, MyDocument, which contained all of the code to manage and control the user interface, as well as the data. Not only was it the model-controller, but it was also a view-controller. In light of the recent discussion of MVC, this was bad design.

Fortunately SimpleTextEditor remained a small application (at least as far as this column goes), and we didn't run into tricky design situations that resulted from poor foresight and planning. Clearly, if we were to modularize our code according to the principles of good object-oriented design, and the dictates of the MVC, we would want to separate the code that controls the interface from the code that manages the data model. This becomes exceedingly important when applications become more and more complex, as they inevitably do, so it's best to start out close to the way you wish to end up.

Before I move on, let me say a few more words about further associations between the three document-based application classes we have. The three classes NSDocumentController, NSDocument, and NSWindowController are related to one another in a one-to-many relationship.

That is, document-based applications have one NSDocumentController object that manages one or more NSDocument objects. NSDocument objects in turn have but one NSDocumentController object. Similarly, instances of NSDocument may own many instances of NSWindowController, while any NSWindowController object has but one NSDocument master. Instances of NSDocument keep a list of their window controllers which, when we write the code for using a custom window controller, we must add to.

If you're wondering what type of application would need multiple windows (every window controller has a window it owns), think of a CAD application, in which a single document may have three or four views of an object, each displayed in a separate window. This kind of document interface would be implemented by instantiating the required number of individual NSWindowControllers and adding them to the document's list of window controllers.

So that's where we're at with document-based applications. My description was pretty bare on the details, so for a more complete analysis of what I talked about, check out the document at Apple's main Cocoa documentation page called Application Design for Scripting, Documents, and Undo; it is found under the legacy section.

Let's move on now and start building the application.

Building ImageApp

To start out, open Project Builder and create a new "Cocoa Document-Based Application" project. Name the project ImageApp. Contrary to experience with previous applications, we will be doing relatively little work in Interface Builder (IB). The user interface for ImageApp is pretty straightforward -- nothing more than a window with a scroll view to hold the image view, and a text field in which the user can change the zoom of the document.

Before building the interface though, let's talk some more about the structure of our application and do a little coding. Our application will consist of three classes named MyDocument, IAWindowController, and IAImageView. These classes inherit from NSDocument, NSWindowController, and NSImageView, respectively. IAWindowController will serve the dual purpose of MyDocument's window controller, and as owner and controller of the user interface. NSImageView is a class whose primary role is to display a single NSImage in a view. We are subclassing it to add a small amount of functionality we will later find useful.

Let's get the chore of creating the class files for IAWindowController and IAImageView out of the way. Create IAImageView by creating an Objective-C NSView subclass from File --> New File.... This creates a subclass of NSView, but we need IAImageView to be a subclass of NSImageView. This is rectified by changing the @interface line in IAImageView.h from @interface IAImageView : NSView to @interface IAImageView : NSImageView. Now create the window controller subclass by choosing the Objective-C NSWindowController subclass from the new file assistant and name it IAWindowController.

Implementing the NSDocument Subclass

Because we're using a custom subclass of NSWindowController as our document window controller, we have to change parts of the default MyDocument implementation so the document loads IAWindowController as its window controller and interface. We're going to remove the method -windowNibName and put in its place the method -makeWindowControllers.

The purpose of -windowNibName is to return the name of the nib containing the document's interface. MyDocument uses this when creating the default NSWindowController used to manage the document's window. In its place we're going to implement a method of NSDocument called -makeWindowControllers:. This method is called when new documents are created or existing documents are loaded -- basically whenever a new instance of the document class is created. All we have to do here is instantiate and initialize IAWindowController and then add it to the document's list of window controllers.

To load a nib file containing the document's interface with an NSWindowController, we use the initialization method -initWithWindowNibName:. This method takes the name of the nib file (minus the extension) as its sole argument, and then IAWindowController takes care of loading the nib, performing any necessary initialization, and displaying the window controller's primary window.

Before we implement -makeWindowControllers, let's change the name of the nib file from MyDocument.nib to IAWindow.nib, to further reflect the idea that an instance of IAWindowController will be handling the nib rather than MyDocument. We will see in a moment how this concept of ownership is implemented in Interface Builder.

So we know how to initialize an instance of IAWindowController; now we need to know how to add that window controller to the document's list of window controllers. To do this we use NSDocument's addWindowController:. Let's create and add a window controller to the document with -makeWindowControllers in MyDocument.m:


- (void)makeWindowControllers
{
     windowController = [[IAWindowController alloc] initWithWindowNibName:@"IAWindow"];
    [self addWindowController:windowController];
}

As you can see, windowController is an instance variable that needs to be declared in MyDocument.h as:

IAWindowController *windowController;

Additionally, you need to import IAWindowController.h into MyDocument.h so the compiler knows what's going on with IAWindowController and doesn't feed us a bunch of warnings.

Back to our method. All we did was alloc and init the window controller using the aforementioned -initWithWindowNibName:, passing the name of our nib as the argument-@"IAWindow". After that we sent an -addWindowController: message to self with windowController as the argument. So that's all we have to do in MyDocument to support our NSWindowController subclass.

If you need to do some special initialization within MyDocument directly before or after the window controller loads the nib then NSDocument gives us the option of overriding -windowControllerWillLoadNib: and -windowControllerDidLoadNib:. As the names imply, these are called before and after the nib loads, respectively.

Since our data model will be an instance of NSImage, and in a moment we'll be writing code to create that object when a file is loaded, it would do us well to say a bit about NSImage.

NSImage

NSImage is the workhorse of image handling in Cocoa. NSImage provides developers with a convenient and easy-to-use front end to a powerful and flexible back end made up of many more classes in the AppKit. One of the key ideas behind NSImage is that of image representations. Think of NSImage as providing a higher-level, more abstract concept of an image, while image representations via NSImageRep and its subclasses provide a more specialized interface to the different ways image data can exist.

This is better understood if you look at the names of the several NSImageRep subclasses: NSBitmapImageRep, NSPDFImageRep, NSEPSImageRep, NSPictImageRep, NSCustomImageRep, and NSCachedImageRep. Each of these knows how to render images with different data formats, and the diversity of these classes is hidden behind the simplicity of NSImage.

Another key idea behind NSImage is that of NSImage keeping multiple image representations for an image. By doing this NSImage is able to provide whatever graphics-rendering device (screen at thousands of colors, screen at millions of colors, printer, plotter, whatever) with the image representation best suited to displaying the image on that particular device. We'll see in the next two columns how we can further exploit this structure for various tasks.

For our purposes today all we need to do is create an NSImage from an image file's data. This is done using the initializer -initWithData:, which is convenient since with -loadDataRepresentation:ofType: we are given an NSData object in the first argument. The data passed in this argument is the data initialized from the file selected in the open panel by the user.

Finishing Up MyDocument

The document's image object will be assigned to an instance variable named activeImage; so let's add this to MyDocument.h:


NSImage *activeImage;

Implementing -loadDataRepresentation:ofType: is done in the following way:

- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType
{
    activeImage = [[NSImage alloc] initWithData:data];
    return (activeImage != nil);
}

Notice that the return value is the result of a comparison between activeImage and nil. If -initWithData: is unable to initialize the NSImage with the provided data, then the NSImage object is freed and nil is returned. Therefore, by comparing activeImage to nil we have an indicator of the success of the file-open operation that we can use as the method's return value.

Finally, let's add a method -activeImage to give other objects access to the document's image object. This method will simply return the image instance variable:


- (NSImage *)activeImage
{
    return activeImage;
}

You should also declare this method in MyDocument.h so the compiler knows it's there.

Since ImageApp is a viewer application, we will not implement -dataRepresentationOfType:; that will be done in the next column to everyone's satisfaction. With that, we have put in place a skeleton implementation needed to open documents, and to set up the document's window controller. Let's press on with building our interface, and coding IAWindowController.

IAWindowController and the Interface

NSWindowControllers are responsible for owning and controlling a window. Subclasses can extend this functionality by overriding methods to give the window a custom title, providing a smart, window-zooming mechanism, and other things. As we now know, subclasses of NSWindowController in document-based applications have the additional job of managing the user interface for the document.

With regard to managing the user interface, IAWindowController has two responsibilities. The first is to control an image view in which the document's data is displayed. The second job is to take a user-input zoom setting from an interface control and tell the image view to resize the image accordingly, which is a standard feature in almost every graphics application I know of. So if the user wants to view the image full size he enters 100%, half-size 50%, and so on.

The actual working machinery of the scaling will be implemented by IAImageView -- IAWindowController acts as an intermediary between the UI control and the view. The second responsibility is to get a document's image and set it to be displayed in the image view as part of its initialization.

Before we can do our work in Interface Builder, we need to declare some actions and outlets in IAWindowController from Project Builder. Specifically, we have one action, -changeScale:, and two outlets -- one for the image view and the other for the zoom control. After it's all said and done IAWindowController.h should look like the following:


#import <AppKit/AppKit.h>
@class IAImageView;

@interface IAWindowController : NSWindowController {
    IBOutlet IAImageView *view;
    IBOutlet NSTextField *zoomControl;
}
- (IBAction)changeScale:(id)sender;

@end

Notice that rather than import the interface to IAImageView, we used Objective-C's @class compiler directive to indicate that IAWindowController is a class. Doing this saves time compiling in larger projects since it's one less file that needs to be imported.

Also, we get rid of any compiler warnings about not knowing what IAImageView is (note that we still need to import IAImageView.h into IAWindowController.m). We could also have done this in MyDocument.h -- adding @class IAWindowController rather than importing the header.

With the interface for IAWindowController set up in Project Builder we can now import it into our nib and build an interface around it. We also need to import IAImageView's interface. Open IAWindow.nib and drag the header files IAWindowController.h and IAImageView.h from Project Builder onto the nib window. We're now set to start working on the interface.

Building the Interface

The way we make connections to the outlets and actions of IAWindowController is to change the class of File's Owner in the nib. Right now its class is set to MyDocument; we'll change this to IAWindowController. This means we're transferring ownership of the nib from MyDocument to IAWindowController. Do this by selecting the File's Owner icon and opening the attributes info panel. Here you can choose IAWindowController from the list, and with that, our outlets and actions from IAWindowController are available for connecting to, by way of File's Owner. Slick.

As part of making IAWindowController File's Owner, we have to make a key connection between the window and File's Owner. Drag a connection from File's Owner to the window icon, and if it's not connected already, make a connection to the outlet window. This effectively makes IAWindowController the owner of the window.

Now delete any text in the window and drag an NSImageView object onto the window. This is found in the upper-left corner of the Cocoa-Others palette, with a picture of a mountain as part of the icon. With the image view in the window, we want to change the border of the view from its current style to the border-less style. We also need to change the scaling behavior from "Proportionally" to "To Fit." This is all done under the attributes info panel. Finally, we change the class of the image view from NSImageView to IAImageView from the custom class panel (Command-5).

The next step is to place the image view within a scroll view. A scroll view is a subclass of NSView that displays another view, but takes care of large contents by providing scroll bars (instances of NSScroller, called scrollers) to display different parts of the interior view, which in this case is our image view. To make the image view a subview of a scroll view we simply select the image view object and then choose Layout --> Make subviews of --> Scroll View.

Notice how the view container is stuck to the bottom left corner of the scroll view. That is because it is assumed the view size will change programmatically, and it is unimportant how it is laid out in IB. We also want to make sure the struts and springs of the scroll view are set up so that the size of the scroll view is flexible when the window is resized, as shown in the image below.

Set the Struts
Set the struts and springs of the scroll view as I have done here.

Finally, add a Zoom: label and text field below the scroll view, and arrange things so they look something like what I have below:

Interface
This is how my interface is arranged; make yours similar. The border of the image view is shown just so you know it's there; it should be a border-less image view.

One last thing we want to do before making the connections is to attach a number formatter to the text field. A number formatter is an instance of NSNumberFormatter that defines how numbers will be displayed in a text field. For our zoom control we want a scale percentage to be shown.

Number Formatter

The NSNumberFormatter object in the Cocoa-Others palette.

To attach an NSNumberFormatter to a text field, drag from the Cocoa-Views Palette the object with the dollar sign and an arrow and drop it on top of the text field. This action attaches the formatter to the text field.

To view and modify the options for the formatter select the text field to which it is attached (the only one in the window in our case) and press command-6. This will bring up in the Info panel for the NSNumberFormatter options. In this panel you will find a list of the various ways positive and negative numbers can be formatted. Choose the one which is 100% and -100%.

Formatters are a pretty handy class of objects. All the user needs to do now is type 85 and "85%" is displayed. Formatters are also capable of rounding; if the user input 12.3, then "12%" would show up. Because we configured the formatter wholly in IB, it is something we can experiment with by doing a test run of the interface (command-R).

To finish things up let's make some connections between File's Owner and the interface. Drag a wire from File's Owner to the IAImageView object within the scroll view and connect it to view. Likewise, make a connection from File's Owner to the text field (via the zoomControl outlet), and from the text field to the changeScale: action in File's Owner.

So that's our interface; not much else to it.

Implementing the Class

Let's start out by implementing IAWindowController with our action -changeScale:. This action method will work alongside another method of IAWindowController called -scaleImageTo:. The purpose of -scaleImageTo: is to handle higher-level interface actions related to scaling, while letting IAImageView execute the actual scaling machinery. -scaleImageTo: also performs error checking to make sure that the scale factor is positive and non-zero, neither of which make much sense in terms of zooming.

As part of this implementation we need to declare another instance variable in IAWindowController.h that will store the current scale factor:

float scale;

This variable will serve to keep a consistent zoom between various image operations (which will be added in later columns). Let's take a look at the implementation of -scaleImageTo: before getting to -changeScale:.


- (void)scaleImageTo:(float)_scale
{
    if ( _scale > 0 )
         scale = _scale;

    [view scaleFrameBy:scale];
    [zoomControl setFloatValue:(scale * 100)];
}

The first thing we do is check to see if the scale factor _scale is greater than 0; if so then the scale factor is valid and we set the value of the instance variable equal to the argument. If it is not greater than zero then the scale remains unchanged and the remainder of the method is carried out using the previous value of scale. Next we send a -scaleFrameBy: message to view, with scale as the argument. Note that -scaleFrameBy: is a method of our design, not one provided by NSImageView.

Finally, we set the float value of the zoomControl text field to scale * 100. We multiply by 100 because scale is a factor, not a percentage, that the zoom field displays. The reason we set the value of the zoom text field -- despite the fact that the scale we're setting it to was most likely taken from that very text field, and nothing will change -- is that there may be situations when -scaleImageTo: is invoked by something other than the user changing the zoom value in the interface. In these situations we want to keep the information displayed in the interface (the zoom percentage) consistent with the state of the application (the value of the scale instance variable).

Now, getting back to the -changeScale: action, we have the following implementation:


- (IBAction)changeScale:(id)sender
{
    [self scaleImageTo:([sender floatValue] / 100.0)];
}

Again, we divide by 100 for the same reason we multiplied by 100 above -- the control provides a percentage, and -scaleImageBy: wants a factor.

The last thing we want to do in IAWindowController is implement some form of initialization code. In NSWindowController subclasses, we can use the method -windowDidLoad for initialization in much the same way we use -awakeFromNib for initialization purposes.

Window controllers are created when documents are opened and a document's data has been loaded. So one thing we want to do in this method is get the document's image and display it. We do this using MyDocument's -activeImage method, and pass the returned image along to view for display. The other thing we want to do is set the initial zoom factor for images when they are first displayed. This is what -windowDidLoad looks like:


- (void)windowDidLoad
{
    NSImage *image = [[self document] activeImage];

    [view setImage:image];
    [self scaleImageTo:1.0];
}

Note in the first line how we can access the document object that owns the window controller object by sending a -document message to self. Next we send the newly obtained image object along to the image view using the method -setImage:. Finally, we do an initial scale operation to set the document zoom at 100%.

For the curious reader, just like there are -windowControllerDidLoadNib: and -windowControllerWillLoadNib: in NSDocument, NSWindowController has a -windowWillLoad to complement the -windowDidLoad method.

NSImageView -- Quick Tangent

With our discussion of IAWindowController finished, let's move on to talk about IAImageView, with a quick stop to look over its parent class, NSImageView. NSImageView is a direct subclass of NSControl, which is a subclass of NSView. Therefore, an image view is both a view, which we know something about, and a control, which we also know something about. With that we're already on familiar grounds regarding the capabilities of NSImageView since we know a bit about NSControl and NSView.

NSImageView only adds 10 methods beyond NSControl. The job of NSImageView is to display NSImage inside of an NSView frame (NSImage + NSView = NSImageView). As part of this job, NSImageView lets the developer define how the image should scale to fill the view's frame, as well as how the image should align within the view's frame. The default behavior is for images to scale proportionally to fill the view, and to align in the center of the view.

Another thing we gain with NSImageView is the ability to drag images into the view and have them display. This behavior won't be too useful for our purposes, but I have no doubt that it is to many people for many different applications. NSImageView allows us to define the style of decorative border around the image view. This is referred to as the frame style, but I'm reluctant to call it a frame since we will be talking quite a bit about frame rectangles for views. Finally, there are methods that allow us to set the image programmatically to be displayed in the view, as well as to retrieve that image object. We will use both of these methods later on.

It's not often that we get a complete synopsis of a class, but NSImageView is pretty straightforward as to what functionality it adds to its parent classes. So there you have NSImageView.

Implementing IAImageView

As we move along in our coding we come to the point where we implement IAImageView -- ever closer to the end! We already know from our work in IAWindowController that IAImageView must define a method called -scaleFrameBy:. As has been stated elsewhere, this method contains the machinery of the scaling operation (it is, however, fairly simple machinery).

To implement scaling we exploit the scaling behavior of the image view that we set in Interface Builder to automatically resize the image to fit the frame of the view. So if the image view's frame were larger than the native image size, then the image view would resize the image to make it fit in the larger frame. The same goes for a frame that is smaller than the image. Thus, by controlling the size of the image view's frame, we can control the size of the image displayed. This leads to an easy implementation of some scaling behavior. All we need to do in the scaling method is resize the view's frame relative to the image's size, according to the supplied scale factor.

This will be done by first obtaining the NSSize of the image view's image, and then applying a scaling affine transformation to that size. The scaled size is then used to set the size of the image view's frame, and we wrap things up by telling the view to redraw itself. In other words:


- (void)scaleFrameBy:(float)scale
{
    NSSize imageSize = [[self image] size];
    
    NSAffineTransform *at = [NSAffineTransform transform];
    [at scaleBy:scale];
    
    [self setFrameSize:[at transformSize:imageSize]];
    [self setNeedsDisplay:YES];
}

In the first line we create an NSSize variable and set its value to the size returned by a -size message to the view's image, which is accessed by sending an -image method to self.

Moving on we create an instance of NSAffineTransform and send it a -scaleBy: message with scale as the argument to this method. The -scaleFrameBy: method ends with a nested message in which we transform the NSSize imageSize according to our affine transform, using -transformSize:, and then use the return value of that method -- the scaled size -- as the argument of a -setFrameSize: message to self. Now the next time this view is redrawn the image will be drawn to fit the resized frame of the view. To finish things off we tell the view to redraw its contents so the image is drawn to fill the new frame.

This method too should be declared in IAImageView's header file.

Setting Document File Types in Project Builder

Before we have a functioning image viewer application we have to tell Project Builder what file types are supported by ImageApp. The place for making these changes is under the Targets tab, and within Targets under Application Settings. In this view, scroll down to the section labeled Document Types. Here you will see a table and several text fields for inputting information about document types. The table shows the document types currently supported by the application.

Changing Doc Types
Here is where we change the document types for the application.

Reading the documentation related to NSImage reveals that this class supports a number of image file formats out of the box: JPEG, GIF, PNG, TIFF, PICT, PDF, BMP, EPS, and raw, untagged image data.

Highlight the default entry in the Document Types table and let's make some changes. In the fields below we want to change the name to JPEG -- the name can be anything you like, but it's best to keep it relevant. Under Extensions list the possible file extensions that can be expected for a JPEG. Type in "jpg jpeg JPG JPEG" (with or without the quotes, it doesn't matter). Spaces separate the various extensions.

Now, under OS Types we want to put the four-letter type code for JPEG, which is JPEG. Finally make sure the document class is MyDocument, and the role is Viewer. To commit the changes click on the Change button.

We now want to add entries for the other supported file types, so make the changes shown in the table below for each file type and then click the Add button, rather than the Change button. The role for these types will be Viewer, and the document class will, of course, be MyDocument.

Name Extensions OS Type
JPEG jpg jpeg JPG JPEG JPEG
GIF GIF gif GIFf
PNG png PNG PNGf
TIFF tif tiff TIF TIFF TIFF
PICT pct pict PCT PICT PICT
PDF pdf PDF"PDF "
BMP bmp BMP"BMP "
EPSeps EPS"EPSF"

The two OS types in quotes are those that have a space at the end. OS types are four-character codes, and the ones for PDF and BMP have a space as their fourth character. We're not going to add support for raw image data, as that would require the user to provide information about the image before it can be displayed, and we don't have an interface set up to do such a thing.

By the way, if you're curious about how you can determine the OS Type for a particular file, Apple provides as part of the developer tools installation a command line utility found in /Developer/Tools called GetFileInfo. When you run this tool with a file name supplied as an argument it will print out information about that file, including the type code -- that's how I determined the type codes in the list above.

When Project Builder compiles an application the information from the Document Types table is put in the file Info.plist, which is found in the Contents directory of your application's bundle. If, after compiling and running your application, you find that you can't open up these file types, try doing a clean build of your project by clicking on the broom icon in the toolbar, and then building your project again. This will delete the old Info.plist and make a new one.

Finally we have an image viewer application. After a quick compile and run you should be able to open up the indicated image file formats and zoom and scroll around.

Before we close shop for the day, I have one last tidbit I would like to add. What we're going to do now is add another feature to our application that will make the user experience a bit more enjoyable. We're going to add some code that will do a type of validation on the scroll bars in the scroll view. Let's go on with this now.

The Scroll Bars

The thing about scroll views is that by default the scroll bars are always present, even if the contents of the scroll view are significantly smaller than the scroll view itself.

Also in Programming with Cocoa

Understanding the NSTableView Class

Inside StYNCies, Part 2

Inside StYNCies

Build an eDoc Reader for Your iPod, Part 3

What we're going to do now is implement some code that will compare the size of the view to the size of the scroll view, and based on that comparison hide or show the scroll bars. This validation will be done in IAImageView, and the name of this method is -validateScrollers. We will invoke this method before doing anything else in -drawRect:. So going back to -drawRect: in IAImageView, we add:


- (void)drawRect:(NSRect)rect {
    [self validateScrollers];
    [super drawRect:rect];
}

When overriding -drawRect: in a subclass of NSImageView we must be certain to send a similar -drawRect: message to super so the NSImagePart of IAImageView has a chance to render the image.

So the premise of -validateScrollers is easy enough. If the width of the image view's frame is greater than the width of the scroll view, then we show the horizontal scroller. If not, then we hide that scroller. The same goes for the vertical scroller: if the height of the image view's frame is greater than the height of the scroll view's frame, we show the scroller -- if not, we hide the scroller. We can set whether the scroll view has a horizontal or vertical scroller by sending -setHasHorizontalScroller: or -setHasVerticalScroller: messages, respectively, which take BOOL arguments. Let's now take a look at the code:


- (void)validateScrollers
{
    NSScrollView *scrollView = [[self superview] superview];

    NSSize selfSize = [self frame].size;
    NSSize sViewSize = [scrollView frame].size;
    
    BOOL hFlag = selfSize.width > sViewSize.width;
    BOOL vFlag = selfSize.height > sViewSize.height;
    
    [scrollView setHasHorizontalScroller:hFlag];
    [scrollView setHasVerticalScroller:vFlag];
}

In the first line of this method we access the scroll view by sending a superview message to self (IAImageView), and then another superview message to the view returned by the first superview message to self. What is a superview, you ask? In Cocoa, views within windows are arranged within a spatial hierarchy. That is, a view has one superview, and one or more subviews.

In our application, IAImageView is at the bottom of the hierarchy, and the content view of the window is at the top of the hierarchy. IAImageView's superview is an instance of NSClippingView, whose superview is in turn the scroll view. The superview of the scroll view is the content view of the window.

But that's not the whole story. NSScrollViews, in addition to having an NSClipView as a subview, have NSScrollers, which descend from NSView. So the scroll view has three subviews -- the two scrollers and the clip view. Likewise, the window's content view has two more subviews in addition to the scroll view; they are the two text fields at the bottom of the window that make up our zoom control.

The image below shows a schematic representation of this hierarchy in our document window.

Hierarchy
The view hierarchy for our application's window.

So by invoking superview twice, we step up two views from IAImageView in the view hierarchy to retrieve the scroll view.

Moving on with the code, we retrieve the sizes of the frames for the view and the scroll view. In the following two lines we have two BOOL variables, hFlag and vFlag, whose values are the results of the comparisons shown on the right side of the assignment operators.

Essentially, if the width of the view's frame is greater than the width of the scroll view's frame, then hFlag is YES. Likewise for vFlag in the y-direction. These two BOOL variables are then passed as arguments to -setHasHorizontalScroller: and -setHasVerticalScroller:. And that's all there is to it.

By calling [self validateScrollers] in drawRect:, we will have the scrollers appear and hide in real-time whenever the view's contents are redrawn.

Conclusion

Today's column was a long one. Here is the project folder for ImageApp to help you digest this mess of information. I had intended to throw a lot more stuff at you, but as I wrote I realized I had created a monster. In fact, the concept that impelled me to write this app, and that I wanted to write about, now won't see the light of day for another two columns! So while this column had -- in my opinion -- some pretty interesting pearls of wisdom, it was ultimately a foundation for even more interesting things to come.

In the next column you can look forward to implementing a fairly nifty save method, filled with lots of good info about images and image reps. We will also implement some smart, window-zooming behavior, and even printing. So see you next time.

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.


Return to the Mac DevCenter.

Copyright © 2009 O'Reilly Media, Inc.