Special Edition Using Visual C++ 6

Previous chapterNext chapterContents


- 6 -

Printing and Print Preview


Understanding Basic Printing and Print Preview with MFC

If you brought together 10 Windows programmers and asked them what part of creating Windows applications they thought was the hardest, probably at least half of them would choose printing documents. Although the device-independent nature of Windows makes it easier for users to get peripherals working properly, programmers must take up some of the slack by programming all devices in a general way. At one time, printing from a Windows application was a nightmare that only the most experienced programmers could handle. Now, however, thanks to application frameworks such as MFC, the job of printing documents from a Windows application is much simpler.

MFC handles so much of the printing task for you that, when it comes to simple one-page documents, you have little to do on your own. To see what I mean, follow these steps to create a basic MFC application that supports printing and print preview:

1. Choose File, New; select the Projects tab and start a new AppWizard project workspace called Print1 (see Figure 6.1).

FIG. 6.1 Start an AppWizard project workspace called Print1.

2. Give the new project the following settings in the AppWizard dialog boxes. The New Project Information dialog box should then look like Figure 6.2.

Step 1: Choose Single Document.

Step 2: Don't change the defaults presented by AppWizard.

Step 3: Don't change the defaults presented by AppWizard.

Step 4: Turn off all features except Printing and Print Preview.

Step 5: Don't change the defaults presented by AppWizard.

Step 6: Don't change the defaults presented by AppWizard.

FIG. 6.2 The New Project Information dialog box.

3. Expand the classes in ClassView, expand CPrint1View, double-click the OnDraw() function, and add the following line of code to it, right after the comment TODO: add draw code for native data here:
pDC->Rectangle(20, 20, 220, 220);

You've seen the Rectangle() function twice already: in the Recs app of Chapter 4, "Documents and Views," and the Paint1 app of Chapter 5, "Drawing on the Screen." Adding this function to the OnDraw() function of an MFC program's view class causes the program to draw a rectangle. This one is 200 pixels by 200 pixels, located 20 pixels down from the top of the view and 20 pixels from the left edge.


TIP: If you haven't read Chapter 5 and aren't comfortable with device contexts, go back and read it now. Also, if you didn't read Chapter 4 and aren't comfortable with the document/view paradigm, you should read it, too. In this chapter, you override a number of virtual functions in your view class and work extensively with device contexts.

Believe it or not, you've just created a fully print-capable application that can display its data (a rectangle) not only in its main window but also in a print preview window and on the printer. To run the Print1 application, first compile and link the source code by choosing Build, Build or by pressing F7. Then, choose Build, Execute to run the program. You will see the window shown in Figure 6.3. This window contains the application's output data, which is simply a rectangle. Next, choose File, Print Preview. You see the print preview window, which displays the document as it will appear if you print it (see Figure 6.4). Go ahead and print the document (choose File, Print). These commands have been implemented for you because you chose support for printing and print preview when you created this application with AppWizard.

FIG. 6.3 Print1 displays a rectangle when you first run it.

FIG. 6.4 The Print1 application automatically handles print previewing, thanks to the MFC AppWizard.

Scaling

One thing you may notice about the printed document and the one displayed onscreen is that, although the screen version of the rectangle takes up a fairly large portion of the application's window, the printed version is tiny. That's because the pixels onscreen and the dots on your printer are different sizes. Although the rectangle is 200 dots square in both cases, the smaller printer dots yield a rectangle that appears smaller. This is how the default Windows MM_TEXT graphics mapping mode works. If you want to scale the printed image to a specific size, you might want to choose a different mapping mode. Table 6.1 lists the mapping modes from which you can choose.

Table 6.1  Mapping Modes

Mode Unit X Y
MM_HIENGLISH 0.001 inch Increases right Increases up
MM_HIMETRIC 0.01 millimeter Increases right Increases up
MM_ISOTROPIC User-defined User-defined User-defined
MM_LOENGLISH 0.01 inch Increases right Increases up
MM_LOMETRIC 0.1 millimeter Increases right Increases up
MM_TEXT Device pixel Increases right Increases down
MM_TWIPS 1/1440 inch Increases right Increases up

Working with graphics in MM_TEXT mode causes problems when printers and screens can accommodate a different number of pixels per page. A better mapping mode for working with graphics is MM_LOENGLISH, which uses a hundredth of an inch, instead of a dot or pixel, as a unit of measure. To change the Print1 application so that it uses the MM_LOENGLISH mapping mode, replace the line you added to the OnDraw() function with the following two lines:

pDC->SetMapMode(MM_LOENGLISH);
pDC->Rectangle(20, -20, 220, -220);

The first line sets the mapping mode for the device context. The second line draws the rectangle by using the new coordinate system. Why the negative values? If you look at MM_LOENGLISH in Table 6.1, you see that although X coordinates increase to the right as you expect, Y coordinates increase upward rather than downward. Moreover, the default coordinates for the window are located in the lower-right quadrant of the Cartesian coordinate system, as shown in Figure 6.5. Figure 6.6 shows the print preview window when the application uses the MM_LOENGLISH mapping mode. When you print the document, the rectangle is exactly 2 inches square because a unit is now 1/100 of an inch and the rectangle is 200 units square.

FIG. 6.5 The MM_LOENGLISH mapping mode's default coordinates derive from the Cartesian coordinate system.

FIG. 6.6 The rectangle to be printed matches the rectangle onscreen when you use MM_LOENGLISH as your mapping mode.

Printing Multiple Pages

When your application's document is as simple as Print1's, adding printing and print previewing capabilities to the application is virtually automatic. This is because the document is only a single page and requires no pagination. No matter what you draw in the document window (except bitmaps), MFC handles all the printing tasks for you. Your view's OnDraw() function is used for drawing onscreen, printing to the printer, and drawing the print preview screen. Things become more complex, however, when you have larger documents that require pagination or some other special handling, such as the printing of headers and footers.

To get an idea of the problems with which you're faced with a more complex document, modify Print1 so that it prints lots of rectangles--so many that they can't fit on a single page. This will give you an opportunity to deal with pagination. Just to make things more interesting, add a member variable to the document class to hold the number of rectangles to be drawn, and allow the users to increase or decrease the number of rectangles by left- or right-clicking. Follow these steps:

1. Expand CPrint1Doc in ClassView, right-click it, and choose Add Member Variable from the shortcut menu. The variable type is int, the declaration is m_numRects, and the access should be public. This variable will hold the number of rectangles to display.

2. Double-click the CPrint1Doc constructor and add this line to it:
m_numRects = 5;


This line arranges to display five rectangles in a brand new document.

3. Use ClassWizard to catch mouse clicks (WM_LBUTTONDOWN messages) by adding an OnLButtonDown() function to the view class (see Figure 6.7).

4. Click the Edit Code button to edit the new OnLButtonDown() function. It should resemble Listing 6.1. Now the number of rectangles to be displayed increases each time users click the left mouse button.

FIG. 6.7 Use ClassWizard to add the OnLButtonDown() function.

Listing 6.1  print1View.cpp --CPrint1View::OnLButtonDown()

void CPrint1View::OnLButtonDown(UINT nFlags, CPoint point)
{
     CPrint1Doc* pDoc = GetDocument();
     ASSERT_VALID(pDoc);
     pDoc->m_numRects++;
     Invalidate();
     CView::OnLButtonDown(nFlags, point);
}
5. Use ClassWizard to add the OnRButtonDown() function to the view class, as shown in Figure 6.8.

FIG. 6.8 Use ClassWizard to add the OnRButtonDown() function.

6. Click the Edit Code button to edit the new OnRButtonDown() function. It should resemble Listing 6.2. Now the number of rectangles to be displayed decreases each time users right-click.

Listing 6.2  print1View.cpp --CPrint1View::OnRButtonDown()

void CPrint1View::OnRButtonDown(UINT nFlags, CPoint point)
{
     CPrint1Doc* pDoc = GetDocument();
     ASSERT_VALID(pDoc);
     if (pDoc->m_numRects > 0)
     {
          pDoc->m_numRects--;
          Invalidate();
     }
     CView::OnRButtonDown(nFlags, point);
}
7. Rewrite the view's OnDraw() to draw many rectangles (refer to Listing 6.3). Print1 now draws the selected number of rectangles one below the other, which may cause the document to span multiple pages. It also displays the number of rectangles that have been added to the document.

Listing 6.3  print1View.cpp --CPrint1View::OnDraw()

void CPrint1View::OnDraw(CDC* pDC)
{
    CPrint1Doc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
    // TODO: add draw code for native data here
    pDC->SetMapMode(MM_LOENGLISH);
    char s[10];
    wsprintf(s, "%d", pDoc->m_numRects);
    pDC->TextOut(300, -100, s);
    for (int x=0; x<pDoc->m_numRects; ++x)
    {
        pDC->Rectangle(20, -(20+x*200),
            200, -(200+x*200));
    }
}

When you run the application now, you see the window shown in Figure 6.9. The window not only displays the rectangles but also displays the rectangle count so that you can see how many rectangles you've requested. When you choose File, Print Preview, you see the print preview window. Click the Two Page button to see the window shown in Figure 6.10. The five rectangles display properly on the first page, with the second page blank.

FIG. 6.9 Print1 now displays multiple rectangles.

FIG. 6.10 Five rectangles are previewed properly; they will print on a single page.

Now, go back to the application's main window and click inside it three times to add three more rectangles. Right-click to remove one. (The rectangle count displayed in the window should be seven.) After you add the rectangles, choose File, Print Preview again to see the two-page print preview window. Figure 6.11 shows what you see. The program hasn't a clue how to print or preview the additional page. The sixth rectangle runs off the bottom of the first page, but nothing appears on the second page.

The first step is to tell MFC how many pages to print (or preview) by calling the SetMaxPage() function in the view class's OnBeginPrinting() function. AppWizard gives you a skeleton OnBeginPrinting() that does nothing. Modify it so that it resembles Listing 6.4.

FIG. 6.11 Seven rectangles do not yet appear correctly on multiple pages.

Listing 6.4  print1View.cpp --CPrint1View::OnBeginPrinting()

void CPrint1View::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo)
{
    CPrint1Doc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
     int pageHeight = pDC->GetDeviceCaps(VERTRES);
     int logPixelsY = pDC->GetDeviceCaps(LOGPIXELSY);
     int rectHeight = (int)(2.2 * logPixelsY);
     int numPages = pDoc->m_numRects * rectHeight / pageHeight + 1;
     pInfo->SetMaxPage(numPages);
}

OnBeginPrinting() takes two parameters: a pointer to the printer device context and a pointer to a CPrintInfo object. Because the default version of OnBeginPrinting() doesn't refer to these two pointers, the parameter names are commented out to avoid compilation warnings, like this:

void CPrint1View::OnBeginPrinting(CDC* /*pDC*/ , CPrintInfo* /*pInfo*/)

However, to set the page count, you need to access both the CDC and CPrintInfo objects, so your first task is to uncomment the function's parameters.

Now you need to get some information about the device context (which, in this case, is a printer device context). Specifically, you need to know the page height (in single dots) and the number of dots per inch. You obtain the page height with a call to GetDeviceCaps(), which gives you information about the capabilities of the device context. You ask for the vertical resolution (the number of printable dots from the top of the page to the bottom) by passing the constant VERTRES as the argument. Passing HORZRES gives you the horizontal resolution. There are 29 constants you can pass to GetDeviceCaps(), such as NUMFONTS for the number of fonts that are supported and DRIVERVERSION for the driver version number. For a complete list, consult the online Visual C++ documentation.

Print1 uses the MM_LOENGLISH mapping mode for the device context, which means that the printer output uses units of 1/100 of an inch. To know how many rectangles will fit on a page, you have to know the height of a rectangle in dots so that you can divide dots per page by dots per rectangle to get rectangles per page. (You can see now why your application must know all about your document to calculate the page count.) You know that each rectangle is 2 inches high with 20/100 of an inch of space between each rectangle. The total distance from the start of one rectangle to the start of the next, then, is 2.2 inches. The call to GetDeviceCaps() with an argument of LOGPIXELSY gives the dots per inch of this printer; multiplying by 2.2 gives the dots per rectangle.

You now have all the information to calculate the number of pages needed to fit the requested number of rectangles. You pass that number to SetMaxPage(), and the new OnBeginPrinting() function is complete.

Again, build and run the program. Increase the number of rectangles to seven by clicking twice in the main window. Now choose File, Print Preview and look at the two-page print preview window (see Figure 6.12). Whoops! You obviously still have a problem somewhere. Although the application is previewing two pages, as it should with seven rectangles, it's printing exactly the same thing on both pages. Obviously, page two should take up where page one left off, rather than redisplay the same data from the beginning. There's still some work to do.

FIG. 6.12 The Print1 application still doesn't display multiple pages correctly.

Setting the Origin

To get the second and subsequent pages to print properly, you have to change where MFC believes the top of the page to be. Currently, MFC just draws the pages exactly as you told it to do in CPrint1View::OnDraw(), which displays all seven rectangles from the top of the page to the bottom. To tell MFC where the new top of the page should be, you first need to override the view class's OnPrepareDC() function.

Bring up ClassWizard and choose the Message Maps tab. Ensure that CPrintView is selected in the Class Name box, as shown in Figure 6.13. Click CPrintView in the Object IDs box and OnPrepareDC in the Messages box, and then click Add Function. Click the Edit Code button to edit the newly added function. Add the code shown in Listing 6.5.

FIG. 6.13 Use ClassWizard to override the OnPrepareDC() function.

Listing 6.5  print1View.cpp --CPrint1View::OnPrepareDC()

void CPrint1View::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo)
{    if (pDC->IsPrinting())
    {
        int pageHeight = pDC->GetDeviceCaps(VERTRES);
        int originY = pageHeight * (pInfo->m_nCurPage - 1);
        pDC->SetViewportOrg(0, -originY);
    }
    CView::OnPrepareDC(pDC, pInfo);
}

The MFC framework calls OnPrepareDC() right before it displays data onscreen or before it prints the data to the printer. (One strength of the device context approach to screen display is that the same code can often be used for display and printing.) If the application is about to display data, you (probably) don't want to change the default processing performed by OnPrepareDC(). So, you must check whether the application is printing data by calling IsPrinting(), a member function of the device context class.

If the application is printing, you must determine which part of the data belongs on the current page. You need the height in dots of a printed page, so you call GetDeviceCaps() again.

Next, you must determine a new viewport origin (the position of the coordinates 0,0) for the display. Changing the origin tells MFC where to begin displaying data. For page one, the origin is zero; for page two, it's moved down by the number of dots on a page. In general, the vertical component is the page size times the current page minus one. The page number is a member variable of the CPrintInfo class.

After you calculate the new origin, you only need to give it to the device context by calling SetViewportOrg(). Your changes to OnPrepareDC() are complete.

To see your changes in action, build and run your new version of Print1. When the program's main window appears, click twice in the window to add two rectangles to the display. (The displayed rectangle count should be seven.) Again, choose File, Print Preview and look at the two-page print preview window (see Figure 6.14). Now the program previews the document correctly. If you print the document, it will look the same in hard copy as it does in the preview.

FIG. 6.14 Print1 finally previews and prints properly.

MFC and Printing

Now you've seen MFC's printing and print preview support in action. As you added more functionality to the Print1 application, you modified several member functions that were overridden in the view class, including OnDraw(), OnBeginPrinting(), and OnPrepareDC(). These functions are important to the printing and print preview processes. However, other functions also enable you to add even more printing power to your applications. Table 6.2 describes the functions important to the printing process.

Table 6.2  Printing Functions of a View Class

Function Description
OnBeginPrinting() Override this function to create resources, such as fonts, that you need for printing the document. You also set the maximum page count here.
OnDraw() This function serves triple duty, displaying data in a frame window, a print preview window, or on the printer, depending on the device context sent as the function's parameter.
OnEndPrinting() Override this function to release resources created in OnBeginPrinting().
OnPrepareDC() Override this function to modify the device context used to display or print the document. You can, for example, handle pagination here.
OnPreparePrinting() Override this function to provide a maximum page count for the document. If you don't set the page count here, you should set it in OnBeginPrinting().
OnPrint() Override this function to provide additional printing services, such as printing headers and footers, not provided in OnDraw().

To print a document, MFC calls the functions listed in Table 6.2 in a specific order. First it calls OnPreparePrinting(), which simply calls DoPreparePrinting(), as shown in Listing 6.6. DoPreparePrinting() is responsible for displaying the Print dialog box and creating the printer DC.

Listing 6.6  print1View.cpp --CPrint1View::OnPreparePrinting() as Generated by AppWizard

BOOL CPrint1View::OnPreparePrinting(CPrintInfo* pInfo)
{
     // default preparation
     return DoPreparePrinting(pInfo);
}

As you can see, OnPreparePrinting() receives as a parameter a pointer to a CPrintInfo object. By using this object, you can obtain information about the print job as well as initialize attributes such as the maximum page number. Table 6.3 describes the most useful data and function members of the CPrintInfo class.

Table 6.3  Members of the CPrintInfo Class

Member Description
SetMaxPage() Sets the document's maximum page number.
SetMinPage() Sets the document's minimum page number.
GetFromPage() Gets the number of the first page that users selected for printing.
GetMaxPage() Gets the document's maximum page number, which may be changed in OnBeginPrinting().
GetMinPage() Gets the document's minimum page number, which may be changed in OnBeginPrinting().
GetToPage() Gets the number of the last page users selected for printing.
m_bContinuePrinting Controls the printing process. Setting the flag to FALSE ends the print job.
m_bDirect Indicates whether the document is being directly printed.
m_bPreview Indicates whether the document is in print preview.
m_nCurPage Holds the current number of the page being printed.
m_nNumPreviewPages Holds the number of pages (1 or 2) being displayed in print preview.
m_pPD Holds a pointer to the print job's CPrintDialog object.
m_rectDraw Holds a rectangle that defines the usable area for the current page.
m_strPageDesc Holds a page-number format string.

When the DoPreparePrinting() function displays the Print dialog box, users can set the value of many data members of the CPrintInfo class. Your program then can use or set any of these values. Usually, you'll at least call SetMaxPage(), which sets the document's maximum page number, before DoPreparePrinting() so that the maximum page number displays in the Print dialog box. If you can't determine the number of pages until you calculate a page length based on the selected printer, you have to wait until you have a printer DC for the printer.

After OnPreparePrinting(), MFC calls OnBeginPrinting(), which is not only another place to set the maximum page count but also the place to create resources, such as fonts, that you need to complete the print job. OnPreparePrinting() receives as parameters a pointer to the printer DC and a pointer to the associated CPrintInfo object.

Next, MFC calls OnPrepareDC() for the first page in the document. This is the beginning of a print loop that's executed once for each page in the document. OnPrepareDC() is the place to control what part of the whole document prints on the current page. As you saw previously, you handle this task by setting the document's viewport origin.

After OnPrepareDC(), MFC calls OnPrint() to print the actual page. Normally, OnPrint() calls OnDraw() with the printer DC, which automatically directs OnDraw()'s output to the printer rather than onscreen. You can override OnPrint() to control how the document is printed. You can print headers and footers in OnPrint() and then call the base class's version (which in turn calls OnDraw()) to print the body of the document, as demonstrated in Listing 6.7. (The footer will appear below the body, even though PrintFooter() is called before OnPrint()--don't worry.) To prevent the base class version from overwriting your header and footer area, restrict the printable area by setting the m_rectDraw member of the CPrintInfo object to a rectangle that doesn't overlap the header or footer.

Listing 6.7  Possible OnPrint() with Headers and Footers

void CPrint1View::OnPrint(CDC* pDC, CPrintInfo* pInfo)
{
    // TODO: Add your specialized code here and/or call the base class
    // Call local functions to print a header and footer.
    PrintHeader();
    PrintFooter();
    CView::OnPrint(pDC, pInfo);
}

Alternatively, you can remove OnDraw() from the print loop entirely by doing your own printing in OnPrint() and not calling OnDraw() at all (see Listing 6.8).

Listing 6.8  Possible OnPrint() Without OnDraw()

void CPrint1View::OnPrint(CDC* pDC, CPrintInfo* pInfo)
{
    // TODO: Add your specialized code here and/or call the base class
    // Call local functions to print a header and footer.
    PrintHeader();
    PrintFooter();
    // Call a local function to print the body of the document.
    PrintDocument();
}

As long as there are more pages to print, MFC continues to call OnPrepareDC() and OnPrint() for each page in the document. After the last page is printed, MFC calls OnEndPrinting(), where you can destroy any resources you created in OnBeginPrinting(). Figure 6.15 summarizes the entire printing process.

FIG. 6.15 MFC calls various member functions during the printing process.


Previous chapterNext chapterContents

© Copyright, Macmillan Computer Publishing. All rights reserved.