Published on MacDevCenter (
 See this if you're having trouble printing code examples

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 it’s 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 we’ll 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.

Another thing to notice is that when the image is zoomed, the window dumbly does nothing to accommodate the changes in the information being displayed. Both of these things can be fixed with one simple line of code that takes advantage of window zooming.

Quit the application and open IAWindowController.m in Project Builder. At the very end of the method -scaleImageTo: append the single line of code:

[[self window] zoom:nil];

NSWindow's -zoom: method is the same method invoked when the user clicks on the window zoom button. We're invoking it directly in response to any zoom activity, and thus ensuring that the image is displayed as best as it can be. Effectively, the application pushes the green button for you whenever the scale of the image changes. The reason this takes care of the problem of the incorrectly sized initial window is the line of code executed in –windowDidLoad:

[self scaleImageTo:1.0];

By sending this message whenever the application opens a new document, we always zoom the window to fit the current image.

Now, compile and run the application again, and let's look at one other bit of behavior. Open an image and enter some large-scale value, such as 1000 percent, that will make the image much larger than the screen. Did you notice what happened? Cocoa's window-zooming mechanism knows when the window will be larger than the screen. If we return a standard frame that is larger than the useable area of the screen, which excludes the menu bar and the dock, the window will know and simply make itself as large as the useable area.

So, given this built-in behavior, why should we bother testing the size of stdFrame against the default frame ourselves? Compare the two images below. The image on the left uses the built-in size restriction mechanism; in the image on the right I compare stdFrame against defaultFrame. Do you see the difference?

Size restriction using the built-in behavior is shown on the top. Size restriction implemented by ourselves using the default frame is shown on the bottom.

Also in Programming with Cocoa

Understanding the NSTableView Class

Inside StYNCies, Part 2

Inside StYNCies

Build an eDoc Reader for Your iPod, Part 3

The difference is subtle, but one that is important to me as a user. The difference is the presence of a thin (approximately 10 pixels) empty strip below the window. The default frame treats this area as off-limits, just like the dock and the menu bar. The utility of this is that we have a convenient spot to click through the windows or the desktop in the background. If you think this is important like I do, then continue reading as we implement our own version of size constraint using the default frame.

When taking the default frame into account there are two things to consider. First is the obvious case of stdFrame being larger than the default frame. We handle this case in the x and y directions independently. So if stdFrame is wider than the default frame, we set the width of stdFrame to the width of defaultFrame, and set the x origin of stdFrame to the origin of defaultFrame. The same goes for the vertical direction. If stdFrame is higher than defaultFrame, then we set the height of stdFrame and the y origin of stdFrame to those values of defaultFrame.

The second thing we must consider are situations when the size of stdFrame isn't larger than the default frame, but part of the window may be off the screen. We handle this by moving stdFrame back onto the window. Let's take a look at the modifications to our method to see how this all fits together:

- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender 
defaultFrame:(NSRect)defaultFrame { float stdX, stdY, stdW, stdH, defX, defY, defW, defH; // 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]]; stdX = stdFrame.origin.x; stdY = stdFrame.origin.y; stdW = stdFrame.size.width; stdH = stdFrame.size.height; defX = defaultFrame.origin.x; defY = defaultFrame.origin.y; defW = defaultFrame.size.width; defH = defaultFrame.size.height; if ( stdH > defH ) { stdFrame.size.height = defH; stdFrame.origin.y = defY; } else if ( stdY < defY ) { stdFrame.origin.y = defY; } if ( stdW > defW ) { stdFrame.size.width = defW; stdFrame.origin.x = defX; } else if ( stdX < defX ) { stdFrame.origin.x = defX; } return stdFrame; }

The first thing added in this implementation over the previous one was the string of float variable declarations. These are used just after the previous method ended as a convenience in the following if statements. As you can see, we assign to each of these eight variables the values of the different parts of the two frames we wish to compare.

Following that are two if statements, conceptually alike, but operating on the two dimensions of the window. First we look at the vertical dimensions of the two frames. In the first if statement we check to see if the height of the standard frame is greater than the height of the default frame. If this is true, then we set the height of stdFrame to defH (which is the height of the default frame), and the vertical origin of stdFrame to that of the default frame (defY).

If this first conditional is false we move on to check whether the bottom of the standard frame is below the bottom of the screen. If it is, we reset the standard frame's origin to that of the default origin (again, only in the y direction), and move on. We don't have to worry about translating the origin of stdFrame up to defFrame's origin (making the top of the window higher than the screen) since in the first conditional we checked to make sure the height of stdFrame was less than defaultFrame.

So that's the logic behind using the default frame. What we did for the y direction is exactly how we do it for the x direction, except Ys are replaced by Xs and Hs by Ws. Go ahead and compile ImageApp and give it a run to try out this newest change.

Now that we know how window zooming works in Cocoa, we'll all have happier users of the software we create. If you're interested you can download the project containing the changes we made to ImageApp today. In the next column we're going to learn how we can further polish ImageApp.

Copyright © 2009 O'Reilly Media, Inc.