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 his peripherals working properly, the programmer must take up some of the slack, by programming all devices in a general way. In the original Windows system, printing 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 to complete.
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:
Step 1: Single document
Step 2: Default settings
Step 3: Default settings
Step 4: Printing and Print preview
Step 5: Default settings
Step 6: Default settings
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 selecting the Build, Build command or by pressing f7 on your keyboard. Then, select the Build, Execute command to run the program. When you do, you see the window shown in Figure 19.3. This window contains the application's output data, which is simply a rectangle. If you select the application's File, Print Preview command, you see the print preview window shown in Figure 19.4. This window displays the document as it will appear if you print it. Go ahead and print the document (select the File, Print command).
Figure 19.3 : This is the Print1 application when you first run it.
Figure 19.4 : This is the Print1 application's print preview window.
One thing you may notice about the printed document and the one displayed on the 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 19.1 lists the mapping modes from which you can choose.
Mode | Unit | X | Y |
MM_HIENGLISH | 0.001 inch | Increases right | Increases up |
MM_HIMETRIC | 0.01 millimeter | Increases right | Increases up |
MM_ISOTROPIC | arbitrary | 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 |
A good 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 can accommodate 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 19.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 19.5. Figure 19.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.
Figure 19.6 : The print preview window now shows a larger rectangle.
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. 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, modify the Print1 application as shown in the following steps:
Listing 19.1 LST19_01.TXT-Lines for the OnRButtonDown() Function
if (m_numRects > 0) { -m_numRects; Invalidate(); }
Listing 19.2 LST19_02.TXT-New Lines for OnDraw()
pDC->SetMapMode(MM_LOENGLISH); char s[10]; wsprintf(s, "%d", m_numRects); pDC->TextOut(300, -100, s); for (int x=0; x<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 19.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 19.10. The five rectangles are displayed properly on the first page, with the second page blank.
Figure 19.9 : The Print1 application now enables you to select the number of rectangles to display.
Figure 19.10 : Five rectangles are displayed properly on a single page.
Now, go back to the application's main window and click inside the window twice to add two more rectangles. (The rectangle count displayed in the window should be seven.) After you add the additional rectangles, go back to the two-page print preview window. Figure 19.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.
Figure 19.11 : Seven rectangles do not yet appear correctly on multiple pages.
The first problem with the program is that you have to tell MFC how many pages to print (or preview). You do this by calling the SetMaxPage() function in the view class's OnBeginPrinting() function. In the unmodified AppWizard application, OnBeginPrinting() looks like Listing 19.3.
Listing 19.3 LST19_03.TXT-The Default OnBeginPrinting()
void CPrint1View::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/) { // TODO: add extra initialization before printing }
The OnBeginPrinting() function receives two parameters, which are pointers to the printer device context and 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. 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 like this:
int pageHeight = pDC->GetDeviceCaps(VERTRES);
The GetDeviceCaps() member function of the CDC class returns specific information about the device context. What information it returns depends upon the constant you use as the function call's single argument. In this case, VERTRES requests the vertical resolution of the device, which, for a printer, is the number of printable dots from the top of the page to the bottom. If you want to get the horizontal resolution, just call GetDeviceCaps(HORZRES).
You may recall that you're currently using the MM_LOENGLISH mapping mode for the device context, which means that the printer output uses units of 1/1000 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. Currently, though, you only know that each rectangle is two inches high with 20/100 of an inch of space between each rectangle. The total distance between one rectangle and the next, then, is 2.2 inches. To figure out how many dots of resolution a rectangle consumes, you have to know the number of dots per inch. You can get this information with another call to GetDeviceCaps():
int logPixelsY = pDC->GetDeviceCaps(LOGPIXELSY);
The LOGPIXELSY gets the number of vertical dots per inch. If you need to, you can get the number of horizontal dots per inch by calling GetDeviceCaps(LOGPIXELSX).
Now that you know the number of dots per inch, you can calculate the height of a rectangle by multiplying 2.2 times the dots per inch:
int rectHeight = (int)(2.2 * logPixelsY);
You now have all the information to calculate the number of pages needed to fit the requested number of rectangles. That calculation looks like this:
int numPages = m_numRects * rectHeight / pageHeight + 1;
Finally, you can tell MFC how many pages you want to print, by calling the SetMaxPage() function, which is a member of the CPrintInfo class:
pInfo->SetMaxPage(numPages);
Listing 19.4 shows the completed OnBeginPrinting() function. Replace your current OnBeginPrinting() with the new version to enable multiple-page printing and print preview.
Listing 19.4 LST19_04.TXT-The New OnBeginPrinting()
void CPrint1View::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo) { // TODO: add extra initialization before printing int pageHeight = pDC->GetDeviceCaps(VERTRES); int logPixelsY = pDC->GetDeviceCaps(LOGPIXELSY); int rectHeight = (int)(2.2 * logPixelsY); int numPages = m_numRects * rectHeight / pageHeight + 1; pInfo->SetMaxPage(numPages); }
When you run the new version of the program, add two rectangles by clicking twice in the main window. The displayed rectangle count should then be seven. Now, take a look at the two-page print preview window (see fig. 19.12). Whoops! You've obviously still got a serious 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. Looks like you still have some work to do.
Figure 19.12 : The Print1 application still doesn't display multiple pages correctly.
To get 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 told to do in the OnDraw() function, 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. You can do this easily using ClassWizard, as shown in Figure 19.13. After overriding the OnPrepareDC() function, add to the function the lines shown in Listing 19.5. Add the lines right after the line CView::OnPrepareDC(pDC, pInfo), which calls the base class's version of OnPrepareDC()to perform default processing.
Figure 19.13 : You can use ClassWizard to override the OnPrepareDC() function.
Listing 19.5 LST19_05.TXT-Lines for the OnPrepareDC() Function
if (pDC->IsPrinting()) { int pageHeight = pDC->GetDeviceCaps(VERTRES); int originY = pageHeight * (pInfo->m_nCurPage - 1); pDC->SetViewportOrg(0, -originY); }
The MFC framework calls OnPrepareDC() right before it displays data on the screen or before it prints the data to the printer. If the application is about to display data on the screen, you (probably) don't want to change the default processing performed by OnPrepareDC(). So, you must check whether or not the application is printing data. If it is, the device-context object's IsPrinting() function returns TRUE. Calling this function as part of an if statement prevents your custom code from executing when the application is updating the screen:
if (pDC->IsPrinting())
If the application is printing, you want to determine which part of the entire data to display on the page that's currently being printed. To do this, you first need to get the height in dots of a printed page:
int pageHeight = pDC->GetDeviceCaps(VERTRES);
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. You already know the size of one page, so the new origin should be the size of a page times the current page minus one. You can determine the page that's about to be printed by accessing the CPrintInfo object's m_nCurPage data member. The entire calculation, then, looks like this:
int originY = pageHeight * (pInfo->m_nCurPage - 1);
For example, my printer has a page height of 3175. When m_nCurPage is 1, the following calculation gets the page origin:
3175 * (1 - 1)
This evaluates to 0, which is the first line of the display. Obviously, because the program is printing the first page, it will want to begin on the first line. When the program is ready to print the second page, this calculation gets the new origin:
3175 * (2 - 1)
This evaluates to 3175, which is the starting line of the second page of data. Succeeding page origins are calculated similarly.
After you calculate the new origin, you need only give it to the program, which you do by calling the display-context object's SetViewportOrg() function, like this:
pDC->SetViewportOrg(0, -originY);
To see all this in action, compile 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.) Then, take a look at the two-page print preview window (see fig. 19.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.
Figure 19.14 : The new version of Print1 finally previews and prints properly.
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 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 19.2 along with their descriptions.
Function | Description |
OnBeginPrinting() | Override this function to create resources, such as fonts, that you need for printing the document. You can 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 complete the printing task, MFC calls the functions listed in Table 19.2 in a specific order. The first function called is OnPreparePrinting(), which in turn calls a function called DoPreparePrinting(). DoPreparePrinting() is responsible for displaying the Print dialog box and creating the printer DC. When you create your application with AppWizard, your view class automatically overrides OnPreparePrinting(), embedding the call to DoPreparePrinting() in the body of the function, as shown in Listing 19.6.
Listing 19.6 LST19_06.TXT-The Default Version of OnPreparePrinting()
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 19.3 lists the most useful data and function members of the CPrintInfo class, along with their descriptions.
Member | Description |
GetFromPage() | Gets the number of the first page that the user selected for printing. |
GetMaxPage() | Gets the document's maximum page number. |
GetMinPage() | Gets the document's minimum page number. |
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. |
SetMaxPage() | Sets the document's maximum page number. |
SetMinPage() | Sets the document's minimum page number. |
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 can, in turn, extract or set any of these values, as required for the specific print job. Usually, you'll at least want to call SetMaxPage(), which sets the document's maximum page number, which is then displayed in the Print dialog box. If you know ahead of time how many pages the document will be, you should call SetMaxPage() from OnPreparePrinting(). If you need to calculate a page length based on the selected printer, however, you have to wait until OnBeginPrinting(), when 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 the print loop that is executed once for each page in the document. OnPrepareDC() is the place to control what part of the whole document will be printed on the current page. As you saw previously, you can 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 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 19.7. 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 19.8).
Listing 19.7 LST19_07.TXT-Printing Headers and Footers in OnPrint()
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); }
Listing 19.8 LST19_08.TXT-Controlling the Entire Printing Process in OnPrint()
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 19.15.
Figure 19.15 : MFC calls various member functions during the printing process.
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: