Printing Tips and Tricks
Here are a few tips, tricks, and answers to frequently asked questions to help you write better printing code and resolve problems that aren't addressed in this chapter's sample programs.
Using the Print Dialog's Selection Button
The Print dialog that MFC displays before printing begins includes a Selection radio button that the user can click to print the current selection rather than the entire document or a range of pages. By default, the button is disabled. You can enable it by adding the following statement to OnPreparePrinting just before the call to DoPreparePrinting:
pInfo->m_pPD->m_pd.Flags &= ~PD_NOSELECTION;
To select the radio button after it's enabled, add this statement as well:
pInfo->m_pPD->m_pd.Flags ¦= PD_SELECTION;
The m_pPD data member of the CPrintInfo structure passed to OnPreparePrinting points to the CPrintDialog object that DoPreparePrinting uses to display the Print dialog box. CPrintDialog::m_pd holds a reference to the PRINTDLG structure the dialog is based on, and PRINTDLG's Flags field holds bit flags that define the dialog box's properties. Removing the PD_NOSELECTION flag added by CPrintInfo's constructor enables the Selection button, and adding a PD_SELECTION flag selects the button. If DoPreparePrinting returns a nonzero value, indicating that the dialog was dismissed with the OK button, you can find out whether the Selection button was selected by calling CPrintDialog::PrintSelection. A nonzero return value means the button was selected; 0 means it wasn't:
if (pInfo->m_pPD->PrintSelection ()) {
// Print the current selection.
}
You can call PrintSelection and other CPrintDialog functions that return information about settings entered in a Print or Print Setup dialog through the pInfo parameter passed to OnPreparePrinting after DoPreparePrinting returns. You can also call them through the pInfo parameter passed to OnBeginPrinting and other CView print overridables.
You can use CPrintInfo::m_pPD in other ways to modify the appearance and behavior of the Print dialog that DoPreparePrinting displays. Refer to the documentation that accompanies Visual C++ for more information about PRINTDLG and its data members.
Assume Nothing—And Test Thoroughly!
When you send output to the printed page, it's generally a mistake to assume anything about the printable area of the pages you'll be printing. Even if you know you're printing to, say, an 8½-by-11-inch page, the printable page area will differ for different printers. The printable page area can even differ for the same printer and the same paper size depending on which printer driver is being used, and the horizontal and vertical dimensions of the printable page area will be switched if the user opts to print in landscape rather than portrait mode. Rather than assume you have a given amount of space to work with, do as HexDump does and call GetDeviceCaps through the CDC pointer provided to CView print functions to determine the printable page area each time you print, or use CPrintInfo::m_rectDraw in your OnPrint function. This simple precaution will enable your printing code to work with any printer Windows can throw at it and will greatly reduce the number of problem reports you receive from users.
As you've already learned, calling GetDeviceCaps with HORZRES and VERTRES parameters returns the horizontal and vertical dimensions of the printable page area. You can pass the following values to GetDeviceCaps to get more information about a printer or other hardcopy device:
Value
Description
HORZRES
Returns the width of the printable page area in pixels.
VERTRES
Returns the height of the printable page area in pixels.
HORSIZE
Returns the width of the printable page area in millimeters.
VERTSIZE
Returns the height of the printable page area in millimeters.
LOGPIXELSX
Returns the number of pixels per inch in the horizontal direction (300 for a 300-dpi printer).
LOGPIXELSY
Returns the number of pixels per inch in the vertical direction (300 for a 300-dpi printer).
PHYSICALWIDTH
Returns the page width in pixels (2,550 for an 8½-by-11-inch page on a 300-dpi printer).
PHYSICALHEIGHT
Returns the page height in pixels (3,300 for an 8½-by-11-inch page on a 300-dpi printer).
PHYSICALOFFSETX
Returns the distance in pixels from the left side of the page to the beginning of the page's printable area.
PHYSICALOFFSETY
Returns the distance in pixels from the top of the page to the beginning of the page's printable area.
TECHNOLOGY
Returns a value that identifies the type of output device the DC pertains to. The most common return values are DT_RASDISPLAY for screens, DT_RASPRINTER for printers, and DT_PLOTTER for plotters.
RASTERCAPS
Returns a series of bit flags identifying the level of GDI support provided by the printer driver. For example, an RC_BITBLT flag indicates that the printer supports BitBlts, and RC_STRETCHBLT indicates that the printer supports StretchBlts.
NUMCOLORS
Returns the number of colors the printer supports. The return value is 2 for black-and-white printers.
You've already seen one use for the GetDeviceCaps NUMCOLORS parameter: to detect when a black-and-white printer is being used so that you draw print previews in shades of gray. The PHYSICALOFFSETX and PHYSICALOFFSETY parameters are useful for setting margin widths based on information the user enters in a Page Setup dialog. (MFC's CWinApp::OnFilePrintSetup function displays a Print Setup dialog instead of a Page Setup dialog, but you can display a Page Setup dialog yourself using MFC's CPageSetupDialog class.) If the user wants 1-inch margins on the left side of the page, for example, you can subtract the PHYSICALOFFSETX value returned by GetDeviceCaps from the number of pixels printed per inch (LOGPIXELSX) to compute the x offset from the left of the printable page area where printing should begin. If the printer driver returns accurate information, the resulting margin will fall within a few pixels of being exactly 1 inch. You can use the HORZRES, VERTRES, LOGPIXELSX, LOGPIXELSY, PHYSICALWIDTH, PHYSICALHEIGHT, PHYSICALOFFSETX, and PHYSICALOFFSETY values to characterize the printable area of a page and pinpoint exactly where on the page the printable area lies.
If you're concerned about the occasional hardcopy device that won't draw bitmaps, you can find out whether CDC::BitBlt and CDC::StretchBlt are supported by calling GetDeviceCaps with a RASTERCAPS parameter and checking the return flags. For the most part, only vector devices such as plotters don't support the GDI's Blt functions. If the driver for a raster device doesn't support blitting directly, the GDI will compensate by doing the blitting itself. You can determine outright whether printed output is destined for a plotter by calling GetDeviceCaps with a TECHNOLOGY parameter and checking to see if the return value equals DT_PLOTTER.
When you use a number of different printers to test an application that prints, you'll find that printer drivers are maddeningly inconsistent in the information they report and the output they produce. For example, some printer drivers return the same values for PHYSICALWIDTH and PHYSICALHEIGHT as they return for HORZRES and VERTRES. And sometimes an ordinary GDI function such as CDC::TextOut will work fine on hundreds of printers but will fail on one particular model because of a driver bug. Other times, a GDI function won't fail outright but will behave differently on different printers. I once ran across a printer driver that defaulted to the TRANSPARENT background mode even though other drivers for the same family of printers correctly set the device context's default background mode to OPAQUE. Printer drivers are notoriously flaky, so you need to anticipate problems and test as thoroughly as you can on as many printers as possible. The more ambitious your program's printing needs, the more likely that driver quirks will require you to write workarounds for problems that crop up only on certain printers.
Adding Default Pagination Support
HexDump calls CPrintInfo::SetMaxPage from OnBeginPrinting rather than from OnPreparePrinting because the pagination process relies on the printable page area and OnBeginPrinting is the first virtual CView function that's called with a pointer to a printer DC. Because the maximum page number isn't set until after OnPreparePrinting returns, the From box in the Print dialog is filled in (with a 1) but the To box isn't. Some users might think it incongruous that an application can correctly paginate a document for print preview but can't fill in the maximum page number in a dialog box. In addition to displaying the maximum page number correctly, many commercial applications display page breaks outside print preview and "Page mm of nn" strings in status bars. How do these applications know how the document will be paginated when they don't know what printer the document will be printed on or what the page orientation will be?
The answer is that they don't know for sure, so they make their best guess based on the properties of the default printer. The following code snippet initializes a CSize object with the pixel dimensions of the printable page area on the default printer or the last printer that the user selected in Print Setup. You can call it from OnPreparePrinting or elsewhere to compute a page count or to get the information you need to provide other forms of default pagination support:
CSize size;
CPrintInfo pi;
if (AfxGetApp ()->GetPrinterDeviceDefaults (&pi.m_pPD->m_pd)) {
HDC hDC = pi.m_pPD->m_pd.hDC;
if (hDC == NULL)
hDC = pi.m_pPD->CreatePrinterDC ();
if (hDC != NULL) {
CDC dc;
dc.Attach (hDC);
size.cx = dc.GetDeviceCaps (VERTRES);
size.cy = dc.GetDeviceCaps (HORZRES);
::DeleteDC (dc.Detach ());
}
}
CWinApp::GetPrinterDeviceDefaults initializes a PRINTDLG structure with values describing the default printing configuration. A 0 return means that the function failed, which usually indicates that no printers are installed or that a default printer hasn't been designated. CPrintInfo::CreatePrinterDC creates a device context handle from the information in the PRINTDLG structure encapsulated in a CPrintInfo object. With the device context in hand, it's a simple matter to wrap it in a CDC object and use CDC::GetDeviceCaps to measure the printable page area.
Enumerating Printers
Sometimes it's useful to be able to build a list of all the printers available so that the user can select a printer outside a Print or Print Setup dialog box. The following routine uses the Win32 ::EnumPrinters function to enumerate the printers currently installed and adds an entry for each to the combo box pointed to by pComboBox.
#include <winspool.h>
DWORD dwSize, dwPrinters;
::EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 5,
NULL, 0, &dwSize, &dwPrinters);
BYTE* pBuffer = new BYTE[dwSize];
::EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 5,
pBuffer, dwSize, &dwSize, &dwPrinters);
if (dwPrinters != 0) {
PRINTER_INFO_5* pPrnInfo = (PRINTER_INFO_5*) pBuffer;
for (UINT i=0; i<dwPrinters; i++) {
pComboBox->AddString (pPrnInfo->pPrinterName);
pPrnInfo++;
}
}
delete[] pBuffer;
The first call to ::EnumPrinters retrieves the amount of buffer space needed to hold an array of PRINTER_INFO_5 structures describing individual printers. The second call to ::EnumPrinters initializes the buffer pointed to by pBuffer with an array of PRINTER_INFO_5 structures. On return, dwPrinters holds a count of the printers enumerated (which equals the count of PRINTER_INFO_5 structures copied to the buffer), and each structure's pPrinterName field holds a pointer to a zero-delimited string containing the device name of the associated printer. Enumerating printers with PRINTER_INFO_5 structures is fast because no remote calls are required; all information needed to fill the buffer is obtained from the registry. For fast printer enumerations in Windows NT or Windows 2000, use PRINTER_INFO_4 structures instead.
If a printer is selected from the combo box and you want to create a device context for it, you can pass the device name copied from the PRINTER_INFO_5 structure to CDC::CreateDC as follows:
CString strPrinterName;
int nIndex = pComboBox->GetCurSel ();
pComboBox->GetLBText (nIndex, strPrinterName);
CDC dc;
dc.CreateDC (NULL, strPrinterName, NULL, NULL);
You can use the resulting CDC object just like the CDC objects whose addresses are passed to OnBeginPrinting and other CView print functions.