Code to handle printing is always relatively hard to write. There are many factors at play: pagination, margins, and page orientation (landscape versus portrait). This chapter is designed to get you started on your journey toward the perfect printout.
Compared to most operating systems, Mac OS X makes writing print routines considerably easier. After all, your views already know how to generate PDF, and Mac OS X knows how to print PDF. If you have a document-based application and a view that knows how to draw itself, you just implement printShowingPrintPanel:
. In this method, you create an NSPrintOperation
object and run it. The code would look like this:
- (void)printShowingPrintPanel:(BOOL)flag { NSPrintInfo *printInfo = [self printInfo]; NSPrintOperation *printOp; printOp = [NSPrintOperation printOperationWithView:aView printInfo:printInfo]; [printOp setShowPanels:flag]; [self runModalPrintOperation:printOp delegate:nil didRunSelector:NULL contextInfo:NULL]; }
If your application is not document based, you will implement a print:
method in the target of the menu item. For example, add the following method to the AppController
class in your TypingTutor project:
- (IBAction)print:(id)sender { NSPrintInfo *printInfo = [NSPrintInfo sharedPrintInfo]; NSPrintOperation *printOp; printOp = [NSPrintOperation printOperationWithView:inLetterView printInfo:printInfo]; [printOp setShowPanels:YES]; [printOp runOperation]; }
Declare the method in AppController.h
. Drag AppController.h
into the nib file. Select the menu item called Print…. In the connections inspector, disconnect its target outlet. Make the AppController
be the target of the menu item, and set the action to print:
(Figure 24.1).
Build the project and confirm that printing works. Unless you have a printer set up, you will only be able to preview what would have been printed (Figure 24.2).
To print all the views on the window, simply change the print:
method to use the window's content view:
- (IBAction)print:(id)sender { NSPrintInfo *printInfo = [NSPrintInfo sharedPrintInfo]; NSPrintOperation *printOp; NSView *v = [[inLetterView window] contentView]; printOp = [NSPrintOperation printOperationWithView:v printInfo:printInfo]; [printOp setShowPanels:YES]; [printOp runOperation]; }
What about multiple pages? A view, after all, has only a single page. How will you get a view to print multiple-page documents? Off-screen, you will make a huge view that can display all the pages of the document simultaneously (Figure 24.3). The print system will ask the view how many pages it is displaying. Then it will ask the view where each page can be found in the view.
Your view, then, must override two methods:
// How many pages? - (BOOL)knowsPageRange:(NSRange *)rptr; // Where is each page? - (NSRect)rectForPage:(int)pageNum;
As an example, you will add printing to the RaiseMan application. You will print the name and expected raise for as many people as will fit on the paper size that the user selected from the print panel (Figure 24.4).
To do so, you will create a view that does the printing. The view will be big enough to display all of the people simultaneously, but for each page there will be a rectangle that contains the information for the people that will be printed on that page. The view will be called PeopleView
(Figure 24.5).
The code in MyDocument.m
is actually pretty simple:
- (void)printShowingPrintPanel:(BOOL)flag { NSPrintInfo *printInfo = [self printInfo]; NSPrintOperation *printOp; PeopleView *view; // End editing [personController commitEditing]; view = [[PeopleView alloc] initWithPeople:employees printInfo:printInfo]; printOp = [NSPrintOperation printOperationWithView:view printInfo:printInfo]; [printOp setShowPanels:flag]; [self runModalPrintOperation:printOp delegate:nil didRunSelector:NULL contextInfo:NULL]; [view release]; }
You will have to import PeopleView.h
. This is not so different from the TypingTutor example, but there are some minor differences:
NSDocument
implements printDocument:
to call [self printShowingPrint Panel:YES]
, so your subclass of NSDocument
is implementing printShowing
PrintPanel:
instead of print:
.
The view that does the printing needs to be created. Usually you will create a view by calling initWithFrame:
. In PeopleView
, you will create another constructor that takes the array of people and the print info object. The print info object knows the paper size. Using the array of people and the print info object, you can figure out how big the frame should be.
NSDocument
has a method printInfo
that returns the instance of NSPrintInfo
for that document.
In the MainMenu.nib
file, make sure the Print… menu item is nil-targeted and has set its action to printDocument
(Figure 24.6).
Create a class called PeopleView
that is a subclass of NSView
. PeopleView.h
would look like this:
#import <Cocoa/Cocoa.h> @interface PeopleView : NSView { NSArray *people; NSMutableDictionary *attributes; NSSize paperSize; float leftMargin; float topMargin; } - (id)initWithPeople:(NSArray *)array printInfo:(NSPrintInfo *)pi; - (NSRect)rectForPerson:(int)index; - (int)peoplePerPage; @end
In PeopleView.m
, you will implement the initWithPeople:printInfo:
method. This initializer will call NSView
's initWithFrame:
method.
#import "PeopleView.h" #import "Person.h" #define VSPACE 30.0 @implementation PeopleView - (id)initWithPeople:(NSArray *)array printInfo:(NSPrintInfo *)pi { NSRange pageRange; NSRect frame; // Get the useful data out of the print info paperSize = [pi paperSize]; leftMargin = [pi leftMargin]; topMargin = [pi topMargin]; people = [array retain]; // Get the number of pages [self knowsPageRange:&pageRange]; // The view must be big enough to hold the first and last pages frame = NSUnionRect([self rectForPage:pageRange.location], [self rectForPage:NSMaxRange(pageRange)-1]); // Call the superclass's designated initializer [super initWithFrame:frame]; // The attributes of the text to be printed attributes = [[NSMutableDictionary alloc] init]; [attributes setObject:[NSFont fontWithName:@"Helvetica" size:15.0] forKey:NSFontAttributeName]; return self; } // The origin of the view is at the upper-left corner - (BOOL)isFlipped { return YES; } - (NSRect)rectForPage:(int)page { NSRect result; result.size = paperSize; // Page numbers start at 1 result.origin.y = (page - 1) * paperSize.height; result.origin.x = 0.0; return result; } - (int)peoplePerPage { float ppp = (paperSize.height - (2.0 * topMargin)) / VSPACE; return (int)ppp; } - (BOOL)knowsPageRange:(NSRange *)r { int peoplePerPage = [self peoplePerPage]; // Page counts start at 1 r->location = 1; r->length = ([people count] / peoplePerPage); if ([people count] % peoplePerPage > 0) { r->length = r->length + 1; } return YES; } - (NSRect)rectForPerson:(int)i { NSRect result; int peoplePerPage = [self peoplePerPage]; result.size.height = VSPACE; result.size.width = paperSize.width - (2 * leftMargin); result.origin.x = leftMargin; int page = i / peoplePerPage; int indexOnPage = i % peoplePerPage; result.origin.y = (page * paperSize.height) + topMargin + (indexOnPage * VSPACE); return result; } - (void)drawRect:(NSRect)r { int count, i; count = [people count]; for (i=0; i<count; i++) { NSRect personRect = [self rectForPerson:i]; if (NSIntersectsRect(r, personRect)) { Person *p = [people objectAtIndex:i]; NSString *dataString; dataString = [NSString stringWithFormat:@"%d. %@ %f", i, [p personName], [p expectedRaise]]; [dataString drawInRect:personRect withAttributes:attributes]; } } } - (void)dealloc { [attributes release]; [people release]; [super dealloc]; } @end
Build and run the application. Notice that multiple-pages-per-sheet setup (4-up, for example) works. Notice that you can change the paper size and more or less people subsequently appear on each page.
Often in an application, you will want to draw things differently on screen than on the printer. For example, in a drawing program, the view might show a grid on-screen, but not when printed on paper.
In your drawRect:
method, you can ask the current graphics context if it is currently drawing to the screen:
if ([[NSGraphicsContext currentContext] isDrawingToScreen]) { ...draw grid... }
Apple supplies developers with a very handy tool for hunting down memory leaks—an application called ObjectAlloc. To see how it works, create a memory leak by commenting out the line that releases PeopleView
in MyDocument.m
:
[self runModalPrintOperation:printOp
delegate:nil
didRunSelector:NULL
contextInfo:NULL];
//[view release];
Recompile your app. In the Debug menu, you will find the Launch Using Performance Tool submenu. Choose ObjectAlloc.
When ObjectAlloc starts, you will be presented with a window that displays the number of instances of each class that is created in your application. Click the run button as shown in Figure 24.7
When the sheet appears, check Also record CF & ObjC reference counting as shown in Figure 24.8.
As your application runs, you will see three numbers for each class:
Current: how many objects of this class are in memory right now
Peak: the maximum number of objects of this class that have been in memory at the same time
Total: the total number of instances that have been created
Check the box labeled Show since mark.
In RaiseMan, create a new document. In ObjectAlloc, click the mark button. Create some Person
objects and note their existence in ObjectAlloc. In RaiseMan, close the document. In ObjectAlloc, notice that the Person
objects are deallocated.
Create another new document. Print/preview it three times. Notice that the PeopleView
objects are not deallocated correctly.
Change to the Instance Browser tab of ObjectAlloc. Find an instance of PeopleView
, and look at its allocation event. (A panel will appear asking whether it is okay to pause the app; it is.) You should see the state of the stack as it was when the PeopleView
object was allocated (Figure 24.9).
Using ObjectAlloc, you can hunt down many types of memory leaks.