oreilly.comSafari Books Online.Conferences.


AddThis Social Bookmark Button Programming With Cocoa

All About the Little Green Glob

by Michael Beam

Whenever I see people talk about Mac OS X pet peeves and annoyances, I consistently find that one of the list-toppers is a dysfunctional window zoom button. You all know what I'm talking about: you click on that little emerald glob expecting the window to resize itself to efficiently display the window contents without taking over your screen (we can go to Windows for that). But instead it does something erratic like totally disrespecting the dock, or filling the entire screen to display a small picture. This is distracting and irritating at best.

Fortunately, as Cocoa developers we're not without recourse. We can't fix what's already wrong (that will, however, work itself out as applications mature on OS X), but we can take the time to implement this feature smartly. Doing this will add polish to your application, which will be received by users with gratitude and appreciation.

Today I will show you the built-in support for window zooming behavior by implementing it in our application from last column, ImageApp, which you can download here.

Window Zooming

Before we can implement effective window zooming, it's necessary to go over a bit of prerequisite facts about how this behavior is handled by Cocoa. The key thing to understand about window zooming is that Cocoa defines two states that a window exists in.

Related Reading

Learning Cocoa
By Apple Computer, Inc.

The standard state of a window is the size and location of the window as defined by your application. Our job today will be to write code to figure out the best size and location of the window based on the size of the displayed image. When the user resizes the window, or changes its location by seven pixels or more, then the window is in the user state. The green glob toggles between these two states -- the standard state, defined by the application, and the user state, defined by the user.

NSWindow is one of the many Cocoa classes whose instances may have a delegate object working for them. Remember, a delegate is just an object acting on behalf of another object. Recall from the previous column that we set up the nib, IAWindow, which contains the NSWindow object, to be owned by IAWindowController.

When we created this arrangement, we connected IAWindowController's window outlet to the window. Today we will make a connection in the opposite direction. That is, we want to connect the File's Owner object (an instance of IAWindowController) to the delegate outlet of the window. Do this now, and we've done all that needs to be done to assign a delegate to the window.

So why all the hubbub about delegates? One of the methods that NSWindow declares as implemented by the delegate is a method called --windowWillUseStandardFrame:defaultFrame:. This is the magic method invoked in the delegate by the window when the user clicks the zoom button.

Quite simply, this method returns the window's standard frame to the sender, which will be used to put the window in the standard state. The code we write in this method will tailor the returned NSRect to the contents of the window. If we write this method write, we'll have happy users.

The method supplies two arguments for us to use. The first argument is the window that sent the message. The second argument, the default frame, is the rect that effectively tells us the maximum useable space -- the largest possible size of the window. A crucial part of proper zooming is to limit the size of the window to the size of the default frame in situations when the contents warrant a window size larger than the screen.

One thing to note about the default frame is that its not the size of the screen. It is smaller than the screen size to accommodate the menu bar, the dock, and other interface elements. This includes a thin space at the bottom of the screen to permit clicking through to windows behind the current window. The image below shows the border of the default frame on my iBook's screen.

Default Frame
The default frame for my iBook's screen.

We'll write this method in two parts. In the first pass our primary concern is resizing the window to fit the scaled image. The code used to implement this is almost identical to the window resizing code we wrote when we learned how to animate a window resize. The second time around well take into consideration the restrictions of the default frame. In between these two passes, we'll observe another quality of the way zooming works.

Now, the first pass:

- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender 
defaultFrame:(NSRect)defaultFrame { // get the y-origin of the scroll view for use in computing newHeight int svOffset = [[[view superview] superview] frame].origin.y; NSSize viewSize = [view frame].size; float newHeight = viewSize.height + svOffset; float newWidth = viewSize.width; NSRect stdFrame = [NSWindow contentRectForFrameRect:[sender frame]
styleMask:[sender styleMask]]; stdFrame.origin.y += stdFrame.size.height; stdFrame.origin.y -= newHeight; stdFrame.size.height = newHeight; stdFrame.size.width = newWidth; stdFrame = [NSWindow frameRectForContentRect:stdFrame
styleMask:[sender styleMask]]; return stdFrame; }

As I said above, the bulk of this code is identical to the animated-window-resizing code discussed a couple columns ago. If you didn't read this column, or you have a bad memory like me, you might want to take a quick skim.

The first thing to notice about this code is the svOffset variable, and how we add its value to the value of newHeight. This offset is, as you can see in the code, nothing more than the origin of the scroll view's frame in the window's content view.

Why do we bother with this? The main idea here is to resize the content view of the window to the size of the image view's frame, which is, in fact, the size of the scaled image. However, there is more to our window than the image view: we have that small set of zoom (image, not window) controls at the bottom of the window. The space occupied by these controls must be taken into consideration when resizing the window.

The easiest thing to do is to add to the height of the new content view -- newHeight -- the vertical size of the space occupied by the controls. Since these are at the bottom of the window, the y-origin of the scroll view bounding this space gives us an excellent measure of the size of the space we need to maintain. So that's the story behind svOffset and its effect on newHeight.

One thing to note about this is how we obtained the scroll view object, with a -superview message to the object returned by a -superview message to view. This was discussed at great length in the last column.

The second thing to notice about this method is that rather then making stdFrame the argument to -setFrame:display:animate:, as was done previously to animate a resize, we return stdFrame to the sender where it will define the standard state of the window.

So that's our first shot at setting the standard frame for the window. Recompile ImageApp and give it a run to test the changes. When running ImageApp, you should notice some things about how our application behaves in general. The first thing to notice is how the window is always the same size when a document is first opened. This is the size of the window that was set in Interface Builder. Ideally, we would want to fit the window size to the initial size of the image.

Pages: 1, 2

Next Pagearrow