Adding Spit and Polish to Your Cocoa App
Pages: 1, 2
Changing the Document Window Title
Ordinarily document windows display as their title the last component of the path under which the document is saved -- that is, the document name. Thus, a document saved at /Users/mike/Pictures/pigs.jpg displays the window title "pigs.jpg."
It's possible, however, to customize your document window's title to display information in addition to (or in place of) the document name. We see this sort of thing in Photoshop where the current zoom factor and the color space of the image are appended to the document name in the window title.
By overriding a method of NSWindowController, it's not difficult to put whatever we want in the window title. The method to override is -windowTitleForDocumentDisplayName:, which returns a string to display as the window title, and whose sole argument is the document name.
As an example of what we can do, let's implement this method to append the current scale to the document name in the following way:
- (NSString *)windowTitleForDocumentDisplayName:(NSString *)displayName
{
return [displayName stringByAppendingFormat:@" @ %2.2f%%", scale * 100];
}
If you don't understand the formatting used in the string above, you might want to read up on the printf function (using your favorite C book, or by typing man printf in the Terminal). Essentially, we're saying the number should be displayed as a float with two digits to the right of the decimal place, followed by a percent sign. (Since "%" is used to escape formatting we have to precede a "%" we want to appear as a character in the string with that same formatting escape character).
The window title produced by this method looks like the following:
|
|
Now there's an issue with this method. Whenever we change the scale of the image, we'd like the window title to reflect those changes. We need to tell the window controller the state of the window contents has changed, and that change in state needs to be reflected in the information displayed as part of the window title. NSWindowController provides a method to force an update to the window title, -synchronizeWindowTitleWithDocumentName. Since the zoom factor is displayed in the title, we want to update it whenever the zoom changes. The -scaleImageTo: is the lowest common denominator for all zoom operations, so we put as the last line in that method:
[self synchronizeWindowTitleWithDocumentName]
With that we've added another bit of polish to our application. To finish things up today we're going to add a custom About panel to our application.
A Custom About Panel
When making our About panel, we'll use a nib dedicated to this sole purpose.
The main reason you might want to put the About window in a separate nib is to improve the launch time of your application, as well as to decrease the amount of memory it requires. When the application starts, it has to load the entire contents of MainMenu.nib into memory, so by putting the About window in a separate nib, the application has one less object to load with MainMenu.nib at launch time. The nib with the About panel isn't actually loaded until the user needs to see it.
Granted, for our relatively small application and small About panel, a separate nib won't yield significant improvement. However, the technique is worth learning and practicing for when your applications get larger and more complex.
In Interface Builder create a new nib from File-->New...; in the Starting Point dialog choose Empty from the list of templates. As the name suggests, this will create an empty nib file, to which you will now add a window. Go ahead and add any text or images to the About panel to reflect the information you would like displayed. There shouldn't be any controls like buttons or sliders for our implementation.
Save the nib as AboutPanel.nib, and put it in the English.lproj folder of your ImageApp project folder. You will then be prompted to choose what target to add the nib to. Choose ImageApp. I kept mine simple and uninformative, as shown in the image below:
|
|
To open the window in response to the user clicking on the "About ImageApp" menu item, we need a controller class for the About panel nib that has an outlet through which we can communicate with the About window. That is, so we can send the window a message telling it to display itself onscreen.
As you can see, AboutPanel.nib presently has no object capable of serving this purpose. To fulfill this role we employ the services of our class AppController. With a bit of modification it can be set up to serve the dual purpose of application delegate, and controller for the About panel. To prepare AppController for its new job, add to the interface file the outlet declaration:
IBOutlet NSWindow *aboutWindow;
Save the changes to this file, and drag it into AboutPanel.nib to add AppController to the nib's repertoire of classes.
Now we're not going to create an instance of AppController in the About panel nib like we have in MainMenu.nib -- our application only needs one instance of this class. Rather, we will make the class of AboutPanel.nib's File's Owner AppController. This brings me to an aside about File's Owner.
An important thing to remember about File's Owner is that it is not an instance of the class we assigned to File's Owner. The nature of File's Owner is quite different from that of the window object, or the instance of AppController in MainMenu.nib. The latter are actual instances of classes saved to disk, and then reloaded to memory when the nib is loaded.
Such is the wonder of Interface Builder, Cocoa, and nib files. You'll see this commonly referred to as an example of Cocoa's ability to "freeze-dry" objects (which is accomplished using the classes NSArchiver and NSUnarchiver. We'll cover these down the road.).
File's Owner, on the other hand, is a proxy object: a stand-in object that represents a link to an instance of its designated class that exists elsewhere at runtime. The reason we assign a class to File's Owner is so we can make meaningful connections that will become true connections between objects when the nib is loaded at runtime. These connections don't exist yet in the nib, as connections between real instances do. We will see in a moment how we tell the nib at runtime what object is really the File's Owner.
Before we get to that, make a connection the window and the About panel outlet of File's Owner. Save your work in AboutPanel.nib, and let's return to Project Builder to make a method to load the nib and open the window.
The next step is to make a point of access to it. That is, we need to write a method that will load the nib containing the window, and open the window in response to the user selecting the "About ImageApp" menu item. This method will go in AppController, which is convenient since the instance of AppController is part of MainMenu.nib where we can easily connect this action to the menu item in question.
To set this up add to AppController.h this method declaration:
- (IBAction)showAboutPanel:(id)sender;
Open MainMenu.nib in Interface Builder and read in AppController.h to update Interface Builder's information about the available methods of AppController. Next, select "About NewApplication" from the NewApplication menu, and change its name to "About ImageApp" (change any other NewApplication to ImageApp as well). Next open the Connections Info Panel and disconnect its current connection to File's Owner. The default connection is to an action of NSApplication that opens the default Cocoa About Panel.
Now drag a new connection from the menu item to AppController, and make the connection to the showAboutPanel: action. Save the changes to MainMenu.nib, and head on back to Project Builder.
Now we will see the code manifestation of our lengthy discussion of File's Owner. Remember, I said that File's Owner isn't a real object until we load the nib and assign an object as the owner. To do this we use a method of NSBundle called -loadNibNamed:owner:. This should set off alarms in your head; the second argument is owner:.
The object we pass as this argument is the object to which all of our fake connections to File's Owner will become real connections. The only restriction to the object passed here is that it must be of the same class that we assigned to File's Owner in the nib. For our purposes, we will pass self, which is the instance of AppController from MainMenu.nib. The first argument is the name of the nib that we wish to load, in our case AboutPanel.
Let's take a look at the implementation of -showAboutPanel:
- (IBAction)showAboutPanel:(id)sender
{
if ( !aboutWindow )
[NSBundle loadNibNamed:@"AboutPanel" owner:self];
[aboutWindow makeKeyAndOrderFront:nil];
}
You see here that the nib-loading method is encased in an if statement. All this says is that we only want to load the nib if aboutWindow doesn't point to an object. Before we load the nib, aboutWindow is nil. When we load the nib with self as the owner, the connection between the window and the aboutWindow outlet is actualized, and aboutWindow points to a real object.
Subsequent calls to -showAboutPanel: will pass over the nib-loading code since it is already loaded in memory. Finally, we display the window by sending a makeKeyandOrderFront: message to aboutWindow. And that's all there is to it!
When we started this section I said not to put any controls in the About panel. That was for simplicity's sake. Adding controls is easy. All you have to do is declare the actions you want the controls to invoke in AppController, and make the connections through File's Owner.
If you have a lot of controls, then you might be better off creating a dedicated AboutPanelController class. If you choose to do so, follow the example of using AppController as the About panel controller. That is, create and instantiate the controller class in MainMenu.nib so you can connect the About ... menu item to it, and then make it the File's Owner of the AboutPanel.nib.
The End
So there you have several things you can do to add polish to your application before releasing it into the wild. I hope you got plenty of ideas to take off on your own with whatever application you're building. In the next column we'll get into the class NSBitmapImageRep, and we'll learn how to work with images on a pixel-by-pixel basis by building a class that will convert color images into grayscale images.
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.
Read more Programming With Cocoa columns.
Return to the Mac DevCenter.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 7 of 7.
-
full screen
2002-07-08 06:36:57 juliengenestoux [Reply | View]
How can i manage to make a window full screen?? please, help me...
julien@jobetudiant.net
-
why have a delegate rather than merely a
2002-06-27 15:46:45 psheldon [Reply | View]
Maybe this doesn't mean much to a guy who developes alone, but something to someone in a work group coding? Maybe I am already in a work group, since I am "subscribing" to cocoa framework.
I think using delegates and proxies has a theme getting value from having a choice when something needs to be known.
The proxy stuff allows you to set connections before runtime when the actual object is known. Not having to know at compile time means a certain kind of hiding information or abstraction that allows me or my user to be creative in putting things together live and with choice at runtime.
I'm imagining that delegates mean someone else can work on that code and someone can write in interface builder the outline of connections to his work as a plan. This can mean some division of knowledge between people in working in parallel rather than force things to be known in time sequence.
Knowledge might be cost at time Apple built frameworks :
Main.m with NSApplication seems to be hanging around for command line I/O and unix pipelines. I've fooled with a unix program for all permutations with output to the terminal in project builder. I imagine I could do command line input as well in either the same window of project builder or terminal.
To override NSApplication with a subclass, I would have to inherit that function, NSApplication, and import the subclass into main.m. I never heard of inheriting functions, only methods.
Is NSApplication an object from a class? Inspector shows file's owner in mainmenu.nib has NSApplication as it's attribute. Does NSApplication do anything for itself? Yes, it has 13 actions. None of these are things we did in the column. For some reason, Apple decided to specify (where?) some methods as delegate methods of NSApplication that you'd wire up just as in this column. Maybe that keeps NSApplication from having unneeded bulk covering all possibilities of all programmers choices.
I am glad Mike gave all those examples and maybe they must suffice before I am even ready to hear a formal description of distinction of overrides from delegate actions. Maybe my struggles at writing questions will further burn in the knowledge of those examples, even if no answer is worthwhile now.
Sorry for this confused post. Hope someone can write better than me on it. Got to get to a meeting.
-
OT: help with dealloc
2002-06-25 14:23:11 blukens [Reply | View]
I'm still working my way through the older articles here, but I didn't figure anyone would bother to check them for new messages. I'm playing with the AddressBook app, but it doesn't seem to be executing my dealloc instructions when the app quits. I tried downloading mike's version, and dropped an NSLog in dealloc, but I get nothing from that either. So, how does one specify commands to be executed when an app quits?
-
posted last column thread on zahadum's request
2002-06-22 10:24:39 psheldon [Reply | View]
focus object selection methods thoughts
at
http://www.oreillynet.com/cs/user/create/cs_msg?x-lr=cs_msg/7417&x-lr2=a/2398
Thought it belonged there as it was asked there, but thought you wouldn't notice here, so I referenced it here.
Again, atta boy zahadum! Having said that, I feel dwarfed no more!
;-)
-
my zooming broke somewhere in devp't
2002-06-22 09:50:52 psheldon [Reply | View]
I may have extreme confusion isolating where the mistake is. I've gotten to page 5 of spit and polish and noticed this error in my zooming.
I don't know whether revision control notions I heard 20 years ago have ever been refined, but I sure know my revisions were out of control, as you will see.
Mike Beam's image app from both working with bitmaps 1 and 2 did zoom right. In particular, Mike's image doesn't distort on my drag of lower right corner window reshaper, mine does.
Michele suggested more code for dragging in working with bitmaps article 1. For some reason, I put this in a folder for working with bitmaps article 2. Since I chose to put it in #2, I am not sure whether I added Michele's code to article 2's code or article 1's. I did working wth bitmaps article 3 on top of that revision wherein I first observed that zooming broke.
Here's what happens :
(at first glance impressions)
At 100% my image is "right". I zoom down to 50% and I see (at least) two jumps of 50% but things settle down to about three jumps netting about an 8 times rescaling. I had the impression there might have been both horizontal and vertical rescalings in time sequence.
I don't know how to slow down the graphics of os x so my brain can follow how many jumps there are in this "first glance set of impressions". Steve Jobs did genie with slow motion; I can't.
Isolation choice : Since Michele's drag code is older than the stuff I build on top of it, it will have less to search amongst than code build on top of it. I can use debugger and step through slow graphics changes.
scaleimageto's [[self window] zoom:nil] line called windowwillusestandardframe though, somehow, I couldn't step into windowwillusestandardframe. Passing through gave no overkill zoom on 50% from 100%. It, however, returned standard frame to something else that resized the window. But, recall, I observed that dragging the lower right corner to resize the "containing" window also resized the image. That could have compounded the zooming, you see.
Candidate methodology : How do I find that something else called?
I don't know how, other than flagging all methods in debugger to see "finer resolution" (of calls) on that scaleimageto's [[self window] zoom:nil] line. This methodology is bad on two counts. It is both highly inefficient, forcing slow stepping through of everything irrelevant, and might miss things called other than in my code, eg. in the frameworks.
-
Didn't Compile
2002-06-14 16:47:13 johnts [Reply | View]
After the first changes, when the article said to compile and try it, I got syntax errors, even after copying the code from the page. I had to change it to:
- (BOOL)application:(NSApplication *)theApplication
openFile:(NSString *)filename
{
NSDocumentController *dc;
id doc;
dc = [NSDocumentController sharedDocumentController];
doc = [dc openDocumentWithContentsOfFile:filename display:YES];
return ( doc != nil);
}
(it complained about the declaration of doc as it was.)







the [aboutWindow makeKeyAndOrderFront:nil]; in the showAboutPanel method (which I put in the AppController.m) statment seems to have no effect: no panel displays. I get into the method as I verified with a simple printf() to stdout.
I set "visible at launch time" in the window attributes of the about panel; this makes a window appear, but without focus. I presume so that the nib loads correctly, there is another glitch.
Is it me missing something?