Chapter 7

Printing and Print Preview


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 Windows' device-independent nature makes it easier for the user to get peripherals working properly, the programmer must take up some of the slack, by programming all devices in a general way. There was a time when printing from a Windows application was a nightmare that only the most experienced programmers could handle. Now, however, thanks to application frameworks like MFC, the job of printing documents from a Windows application is much simpler.

Understanding Basic Printing and Print Preview with MFC

MFC handles so much of the printing task for you that, when it comes to simple one-page documents, there's little you have 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; then select the Projects tab and start a new AppWizard project workspace called Print1, as shown in Figure 7.1.

Fig. 7.1 Start an AppWizard project workspace called Print1.

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

    Step 1: 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. 7.2 The New Project Information dialog box.

  1. 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 that says 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 5, "Documents and Views" and the Paint1 app of Chapter 6, "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 over from the left edge.

If you haven't read Chapter 6, "Drawing on the Screen," and are not comfortable with device contexts, go back and read it now. Also, if you didn't read Chapter 5, "Documents and Views," 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 on your keyboard. Then, choose Build, Execute to run the program. When you do, you should see the window shown in Figure 7.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 shown in Figure 7.4. This window displays the document as it will appear if you print it. 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. 7.3 Print1 displays a rectangle when you first run it.

Fig. 7.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 on-screen is that, although the screen version of the rectangle takes up a fairly large portion of the application's window, the printed version is pretty tiny. That's because the pixels on your screen 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 Windows' MM_TEXT graphics mapping mode, which is the default, works. If you want to scale the printed image to a specific size, you might want to choose a different mapping mode. Table 7.1 lists the mapping modes from which you can choose.

Table 7.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, rather than 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 using the new coordinate system. Why the negative values? If you look at MM_LOENGLISH in Table 7.1, you see that although X coordinates increase to the right as you expect, Y coordinates increase upwards rather than downwards. Moreover, the default coordinates for the window are located in the lower-right quadrant of the Cartesian coordinate system, as shown in Figure 7.5. Figure 7.6 shows the print preview window when the application uses the MM_LOENGLISH mapping mode. When you print the document, the rectangle is exactly two inches square. This is because a unit is now 1/100 of an inch and the rectangle is 200 units square.

FIG. 7.5 The MM_LOENGLISH mapping mode's default coordinates are derived from the Cartesian coordinate system.

Fig. 7.6 The rectangle to be printed matches the rectangle on the screen 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's window (except bitmaps), MFC handles all the printing tasks for you. Your view's OnDraw() function is used for drawing on-screen, for printing to the printer, and for drawing the print preview screen. Things get more complex, however, when you have larger documents that require pagination or some other special handling, like the printing of headers and footers.

To get an idea of the problems with which you're faced with a more complex document, you are going to 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, you will add a member variable to the document class to hold the number of rectangles to be drawn, then allow the user 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) with the OnLButtonDown() function of the view class, as shown in Figure 7.7.

Fig. 7.7 Use ClassWizard to add the OnLButtonDown() function.

  1. Click the Edit Code button to edit the new OnLButtonDown() function. It should resemble Listing 7.1. Now the number of rectangles to be displayed increases each time the user clicks the left mouse button.

    Listing 7.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);
    }
  2. Use ClassWizard to add the OnRButtonDown() function to the view class, as shown in Figure 7.8.

Fig. 7.8 Use ClassWizard to add the OnRButtonDown() function.

  1. Click the Edit Code button to edit the new OnRButtonDown() function. It should resemble Listing 7.2. Now the number of rectangles to be displayed decreases each time the user clicks the right mouse button.

    Listing 7.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);
    }
  2. Rewrite the view's OnDraw() to draw many rectangles. The code is in Listing 7.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 7.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 7.9. The window not only displays the rectangles, but also displays the rectangle count so you can see how many rectangles you've requested. When you choose the File, Print Preview command, you see the print preview window. Click the Two Page button, and you see the window shown in Figure 7.10. The five rectangles display properly on the first page, with the second page blank.

Fig. 7.9 Print1 now displays multiple rectangles.

Fig. 7.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 the window 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 additional rectangles, choose File, Print Preview again to see the two-page print preview window. Figure 7.11 shows what you see. The program hasn't a clue as to 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.

Fig. 7.11 Seven rectangles do not yet appear correctly on multiple pages.

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 gave you a skeleton OnBeginPrinting() that does nothing. You are going to modify it so that it resembles Listing 7.4.

Listing 7.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 height of a page (in single dots) and the number of dots per inch. You obtain the height of a page with a call to GetDeviceCaps(). This function 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 gets 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 are going to 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 two inches high with 20/100 of an inch of space between each rectangle. The total distance from the start of one rectangle and 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() is complete.

Once again, build and run the program. Increase the number of rectangles to seven by clicking twice in the main window. The displayed rectangle count should then be seven. Now, choose File, Print Preview and take a look at the two-page, print preview window (see Figure 7.12). Whoops! You've obviously still got 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. 7.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 7.13. Click CPrintView in the Object IDs box and OnPrepareDC in the Messages box, then click Add Function. Click the Edit Code button to edit the newly added function. Add the code shown in Listing 7.5.

Fig. 7.13 Use ClassWizard to override the OnPrepareDC() function.

Listing 7.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 on-screen or before it prints the data to the printer. (One of the strengths of the device context approach to screen display is that the same code can often be used for on-screen display and printing.) If the application is about to display data on-screen, 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 will determine which part of the data belongs on the current page. You will 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 is moved down by the number of dots on a page. In general, the vertical component is the size of a page times the current page minus one. The page number is a member variable of the CPrintInfo class.

After you calculate the new origin, you need only 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 additional rectangles to the display. (The displayed rectangle count should be seven.) Once again, choose File, Print Preview and look at the two-page print preview window (see Figure 7.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. 7.14 Print1 finally previews and prints properly.

MFC and Printing

Now you've had a chance to see 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 process. However, there are also other functions that enable you to add even more printing power to your applications. The functions important to the printing process are listed in Table 7.2 along with their descriptions.

Table 7.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 that is 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 7.2 in a specific order. First it calls OnPreparePrinting(), which simply calls DoPreparePrinting(), as shown in Listing 7.6. DoPreparePrinting() is responsible for displaying the Print dialog box and creating the printer DC.

Listing 7.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. Using this object, you can obtain information about the print job, as well as initialize attributes such as the maximum page number. Table 7.3 lists the most useful data and function members of the CPrintInfo class, along with their descriptions.

Table 7.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 the user 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 the user 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) that are 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, the user can set the value of many of the 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 cannot 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 is 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 to the screen. 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 7.7. 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 does not overlap the header or footer.

Listing 7.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);
}

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

Listing 7.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(). The entire printing process is summarized in Figure 7.15.

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

From Here...

Under MFC, printing and print preview can be as simple or complex as you want or need it to be. For example, MFC can print simple one-page documents almost automatically. All you have to do is supply code for the OnDraw() function, which is responsible for displaying data both in a window and on the printer. If you need to, however, you can override other member functions of the view class to gain more control over the printing process. You have to do this, for example, when printing multiple-page documents or when you want to separate the display duties of OnDraw() from the printing and print preview process.

Please refer to the following for more information on the topics covered in this chapter:


© 1997, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company.