Get a site

VC++ 6.0 ebook chapter index
Free counters!

Printing Fundamentals

When you use a printer in Windows, you're initiating a complex interaction involving the GDI32 library module, the printer device driver library module (which has a .DRV extension), and the Windows print spooler, as well as some other modules that get into the act. Before we start programming for the printer, let's examine how this process works.

Printing and Spooling

When an application program wants to begin using a printer, it first obtains a handle to the printer device context using CreateDC or PrintDlg. This causes the printer device driver library module to be loaded into memory (if it's not present already) and to initialize itself. The program then calls the StartDoc function, which signals the beginning of a new document. The StartDoc function is handled by the GDI module. The GDI module calls the Control function in the printer device driver, telling the device driver to prepare for printing.

The call to StartDoc begins the process of printing a document; the process ends when the program calls EndDoc. These two calls act as bookends for the normal GDI functions that display text or graphics to the document pages. Each page is itself delimited by a call to StartPage to begin a page and EndPage to end the page.

For example, if a program wants to draw an ellipse on the page, it first calls StartDoc to begin the print job, then StartPage to signal a new page. It then calls Ellipse, just as it does when drawing an ellipse on the screen. The GDI module generally stores any GDI call the program makes to the printer device context in a disk-based metafile, which has a filename that begins with the characters ~EMF ("enhanced metafile") and has a .TMP extension. However, as I'll discuss shortly, it's possible for the printer driver to skip this step.

When the application program is finished with the GDI calls that define the first page, the program calls EndPage. Now the real work begins. The printer driver must translate the various drawing commands stored in the metafile into output for the printer. The printer output required to define a page of graphics can be very large, particularly if the printer has no high-level page-composition language. For example, a 600-dots-per-inch laser printer using 8½-by-11-inch paper might require more than 4 megabytes of data to define just one page of graphics.

For this reason, printer drivers often implement a technique called "banding," which divides the page into rectangular bands. The GDI module obtains the dimensions of each band from the printer driver. It then sets a clipping region equal to this band and calls the printer device driver Output function for each of the drawing functions contained in the metafile. This process is called "playing the metafile into the device driver." The GDI module must play the entire metafile into the device driver for each band that the device driver defines on the page. After the process is completed, the metafile can be deleted.

For each band, the device driver translates these drawing functions into the output necessary to realize them on the printer. The format of this output will be specific to the printer. For dot-matrix printers, it will be a collection of control sequences, including graphics sequences. (For some assistance with constructing this output, the printer driver can call various "helper" routines also located in the GDI module.) For laser printers with a high-level page-composition language (such as PostScript), the printer output will be in that language.

The printer driver passes the printer output for each band to the GDI module, which then stores this printer output in another temporary file. This file begins with the characters ~SPL and has a .TMP extension. When the entire page is finished, the GDI module makes an interprocess call to the print spooler indicating that a new print job is ready. The application program then goes on to the next page. When the application is finished with all the pages it must print, it calls EndDoc to signal that the print job is completed. Figure 13-1 shows the interaction of the program, the GDI module, and the printer driver.

click here to view full size

Figure 13-1. The interaction of the application program, the GDI module, the printer driver, and the spooler.

The Windows print spooler is actually a collection of several components:

Spooler Component Description
Print Request Spooler Routes a data stream to the print provider
Local Print Provider Creates spool files destined for a local printer
Network Print Provider Creates spool files destined for a network printer
Print Processor Performs despooling, which is the conversion of spooled device-independent data into a form specific to the target printer
Port Monitor Controls the port to which the printer is connected
Language Monitor Controls printers capable of two-way communication to set device configuration and to monitor printer status

The spooler relieves application programs of some of the work involved with printing. Windows loads the print spooler at startup, so it is already active when a program begins printing. When the program prints a document, the GDI module creates the files that contain printer output. The print spooler's job is to send these files to the printer. It is notified of a new print job by the GDI module. It then begins reading the file and transferring it directly to the printer. To transfer the files, the spooler uses various communications functions for the parallel or serial port to which the printer is connected. When the spooler is done sending a file to a printer, it deletes the temporary file holding the output. This process is shown in Figure 13-2.

Click to view at full size.

Figure 13-2. The operation of the print spooler.

Most of this process is transparent to the application program. From the perspective of the application, "printing" occurs only during the time required for the GDI module to save all the printer output in disk files. After that—or even before, if printing is handled by a second thread—the program is freed up to do other things. The actual printing of the document becomes the print spooler's responsibility rather than the program's. The user is responsible for pausing print jobs, changing their priority, or canceling them if necessary. This arrangement allows programs to "print" faster than would be possible if they were printing in real time and had to wait for the printer to finish one page before proceeding to the next.

Although I've described how printing works in general, there are some variations on this theme. One variation is that the print spooler doesn't have to be present for Windows programs to use the printer. The user can usually turn off spooling for a printer from the printer's property sheet.

Why would a user want to bypass the Windows spooler? Well, perhaps the user has a hardware or software print spooler that works faster than the Windows spooler. Or perhaps the printer is on a network that has its own spooler. The general rule is that one spooler is faster than two. Removing the Windows spooler would speed up printing in these cases, because the printer output doesn't have to be stored on disk. It can go right out to the printer and be intercepted by the external hardware or software print spooler.

If the Windows spooler isn't active, the GDI module doesn't store the printer output from the device driver in a file. Instead, GDI itself sends the output directly to the parallel or serial printer port. Unlike the printing done by the spooler, the printing done by GDI has the potential of holding up the operation of application programs (particularly the program doing the printing) until the printing is completed.

Here's another variation: Normally, the GDI module stores all the functions necessary to define a page in a metafile and then plays this metafile into the printer driver once for each band defined by the driver. If the printer driver doesn't require banding, however, the metafile isn't created; GDI simply passes the drawing functions directly to the driver. In a further variation, it is also possible for an application to assume responsibility for dividing printer output into bands. This makes the printing code in the application program more complex, but it relieves the GDI module of creating the metafile. Once again, GDI simply passes the functions for each band to the printer driver.

Now perhaps you're starting to see how printing from a Windows program might involve a bit more overhead than that required for using the video display. Several problems can occur—particularly if the GDI module runs out of disk space while creating the metafile or the printer output files. Either you can get very involved in reporting these problems to the user and attempting to do something about them or you can remain relatively aloof.

For an application, the first step in printing is obtaining a printer device context.

The Printer Device Context

Just as you must obtain a handle to a device context before you paint on the video display, you must obtain a printer device context handle before printing. Once you have this handle (and have called StartDoc to announce your intention of creating a new document and StartPage to begin a page), you can use this printer device context handle the same way you use the video display device context handle—as the first parameter to the various GDI drawing functions.

Many applications use a standard print dialog box invoked by calling the PrintDlg function. (I'll show how to use this function later in this chapter.) PrintDlg gives the user the opportunity to change printers or specify other job characteristics before printing. It then gives the application a printer device context handle. This function can save an application some work. However, some applications (such as Notepad) prefer instead to just obtain a printer device context without displaying a dialog box. This task requires a job to CreateDC.

In Chapter 5, we discovered that we can get a handle to a device context for the entire video display by calling

hdc = CreateDC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;

You obtain a printer device context handle using this same function. However, for a printer device context, the general syntax of CreateDC is

hdc = CreateDC (NULL, szDeviceName, NULL, pInitializationData) ;

The pInitializationData argument is generally also set to NULL. The szDeviceName argument points to a character string that tells Windows the name of the printer device. Before you can set the device name, you must find out what printers are available.

A system can have more than one printer attached to it. It may even have other programs, such as fax software, masquerading as printers. Regardless of the number of attached printers, only one can be considered the "current" or "default" printer. This is the most recent printer that the user has chosen. Some small Windows programs use only this printer for printing.

Methods for obtaining the default printer device context have changed over the years. Currently, the standard method involves using the EnumPrinters function. This function fills an array of structures that contain information about each attached printer. You even have a choice of several structures to use with this function, depending on the level of detail you want. These structures have names of PRINTER_INFO_x, where x is a number.

Unfortunately, which structure you use also depends on whether your program is running under Windows 98 or Microsoft Windows NT. Figure 13-3 shows a GetPrinterDC function that will work under either operating system.

Figure 13-3. The GETPRNDC.C file.


   GETPRNDC.C -- GetPrinterDC function

#include <windows.h>

HDC GetPrinterDC (void)

     DWORD            dwNeeded, dwReturned ;
     HDC              hdc ;
     PRINTER_INFO_4 * pinfo4 ;
     PRINTER_INFO_5 * pinfo5 ; 

     if (GetVersion () & 0x80000000)         // Windows 98
          EnumPrinters (PRINTER_ENUM_DEFAULT, NULL, 5, NULL,
                        0, &dwNeeded, &dwReturned) ;

          pinfo5 = malloc (dwNeeded) ;

          EnumPrinters (PRINTER_ENUM_DEFAULT, NULL, 5, (PBYTE) pinfo5,
                        dwNeeded, &dwNeeded, &dwReturned) ;

          hdc = CreateDC (NULL, pinfo5->pPrinterName, NULL, NULL) ;

          free (pinfo5) ;
     else                                    // Windows NT
          EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 4, NULL,
                        0, &dwNeeded, &dwReturned) ;

          pinfo4 = malloc (dwNeeded) ;

          EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 4, (PBYTE) pinfo4,
                        dwNeeded, &dwNeeded, &dwReturned) ;

          hdc = CreateDC (NULL, pinfo4->pPrinterName, NULL, NULL) ;

          free (pinfo4) ;
     return hdc ;   

This function uses the GetVersion function to determine whether the program is running under Windows 98 or Windows NT. Regardless of which is running, the function calls EnumPrinters twice—once to obtain the size of a structure it needs, and again to actually fill the structure. Under Windows 98, the function uses the PRINTER_INFO_5 structure; under Windows NT, it uses the PRINTER_INFO_4 structure. These structures are specifically indicated in the EnumPrinters documentation (/Platform SDK/Graphics and Multimedia Services/GDI/Printing and Print Spooler/Printing and Print Spooler Reference/Printing and Print Spooler Functions/EnumPrinters, right before the Examples section) as being "easy and extremely fast."

The Revised DEVCAPS Program

The original DEVCAPS1 program in Chapter 5 displayed basic information available from the GetDeviceCaps function for the video display. The new version, shown in Figure 13-4, shows more information for both the video display and all printers attached to the system.

Figure 13-4. The DEVCAPS2 program.


   DEVCAPS2.C -- Displays Device Capability Information (Version 2)
                 (c) Charles Petzold, 1998

#include <windows.h>
#include "resource.h"

void DoBasicInfo    (HDC, HDC, int, int) ;
void DoOtherInfo    (HDC, HDC, int, int) ;
void DoBitCodedCaps (HDC, HDC, int, int, int) ;

typedef struct
     int     iMask ;
     TCHAR * szDesc ;

#define IDM_DEVMODE      1000

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
     static TCHAR szAppName[] = TEXT ("DevCaps2") ;
     HWND         hwnd ;
     MSG          msg ;
     WNDCLASS     wndclass ;
          = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = szAppName ;
     wndclass.lpszClassName = szAppName ;
     if (!RegisterClass (&wndclass))
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     hwnd = CreateWindow (szAppName, NULL,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;
     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;
     while (GetMessage (&msg, NULL, 0, 0))
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     return msg.wParam ;

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
     static TCHAR            szDevice[32], szWindowText[64] ;
     static int              cxChar, cyChar, nCurrentDevice = IDM_SCREEN,
                                             nCurrentInfo   = IDM_BASIC ;
     static DWORD            dwNeeded, dwReturned ;
     static PRINTER_INFO_4 * pinfo4 ;
     static PRINTER_INFO_5 * pinfo5 ;
     DWORD                   i ;
     HDC                     hdc, hdcInfo ;
     HMENU                   hMenu ;
     HANDLE                  hPrint ;
     PAINTSTRUCT             ps ;
     TEXTMETRIC              tm ;
     switch (message)
     case WM_CREATE :
          hdc = GetDC (hwnd) ;
          SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
          GetTextMetrics (hdc, &tm) ;
          cxChar = tm.tmAveCharWidth ;
          cyChar = tm.tmHeight + tm.tmExternalLeading ;
          ReleaseDC (hwnd, hdc) ;
                                           // fall through
          hMenu = GetSubMenu (GetMenu (hwnd), 0) ;
          while (GetMenuItemCount (hMenu) > 1)
               DeleteMenu (hMenu, 1, MF_BYPOSITION) ;

               // Get a list of all local and remote printers
               // First, find out how large an array we need; this
               //   call will fail, leaving the required size in dwNeeded
               // Next, allocate space for the info array and fill it
               // Put the printer names on the menu

          if (GetVersion () & 0x80000000)         // Windows 98
               EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 5, NULL, 
                             0, &dwNeeded, &dwReturned) ;

               pinfo5 = malloc (dwNeeded) ;

               EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 5, (PBYTE) pinfo5,
                             dwNeeded, &dwNeeded, &dwReturned) ;

               for (i = 0 ; i < dwReturned ; i++)
                    AppendMenu (hMenu, (i+1) % 16 ? 0 : MF_MENUBARBREAK, i + 1, 
                                pinfo5[i].pPrinterName) ;
               free (pinfo5) ;
          else                                    // Windows NT
               EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 4, NULL, 
                             0, &dwNeeded, &dwReturned) ;

               pinfo4 = malloc (dwNeeded) ;

			   EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 4, (PBYTE) pinfo4,
                             dwNeeded, &dwNeeded, &dwReturned) ;

               for (i = 0 ; i < dwReturned ; i++)
                    AppendMenu (hMenu, (i+1) % 16 ? 0 : MF_MENUBARBREAK, i + 1, 
                                pinfo4[i].pPrinterName) ;
               free (pinfo4) ;
          AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ;
          AppendMenu (hMenu, 0, IDM_DEVMODE, TEXT ("Properties")) ;
          wParam = IDM_SCREEN ;
                                             // fall through
     case WM_COMMAND :
          hMenu = GetMenu (hwnd) ;
          if (LOWORD (wParam) == IDM_SCREEN ||         // IDM_SCREEN & Printers
              LOWORD (wParam) < IDM_DEVMODE)       
               CheckMenuItem (hMenu, nCurrentDevice, MF_UNCHECKED) ;
               nCurrentDevice = LOWORD (wParam) ;
               CheckMenuItem (hMenu, nCurrentDevice, MF_CHECKED) ;
          else if (LOWORD (wParam) == IDM_DEVMODE)     // Properties selection
               GetMenuString (hMenu, nCurrentDevice, szDevice,
                              sizeof (szDevice) / sizeof (TCHAR), MF_BYCOMMAND);
               if (OpenPrinter (szDevice, &hPrint, NULL))
                    PrinterProperties (hwnd, hPrint) ;
                    ClosePrinter (hPrint) ;
          else                               // info menu items
               CheckMenuItem (hMenu, nCurrentInfo, MF_UNCHECKED) ;
               nCurrentInfo = LOWORD (wParam) ;
               CheckMenuItem (hMenu, nCurrentInfo, MF_CHECKED) ;
          InvalidateRect (hwnd, NULL, TRUE) ;
          return 0 ;
          if (lParam == 0)
               EnableMenuItem (GetMenu (hwnd), IDM_DEVMODE,
                    nCurrentDevice == IDM_SCREEN ? MF_GRAYED : MF_ENABLED) ;
          return 0 ;
     case WM_PAINT :
          lstrcpy (szWindowText, TEXT ("Device Capabilities: ")) ;
          if (nCurrentDevice == IDM_SCREEN)
               lstrcpy (szDevice, TEXT ("DISPLAY")) ;
               hdcInfo = CreateIC (szDevice, NULL, NULL, NULL) ;
               hMenu = GetMenu (hwnd) ;
               GetMenuString (hMenu, nCurrentDevice, szDevice,
                              sizeof (szDevice), MF_BYCOMMAND) ;
               hdcInfo = CreateIC (NULL, szDevice, NULL, NULL) ;
          lstrcat (szWindowText, szDevice) ;
          SetWindowText (hwnd, szWindowText) ;
          hdc = BeginPaint (hwnd, &ps) ;
          SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
          if (hdcInfo)
               switch (nCurrentInfo)
               case IDM_BASIC :
                    DoBasicInfo (hdc, hdcInfo, cxChar, cyChar) ;
                    break ;
               case IDM_OTHER :
                    DoOtherInfo (hdc, hdcInfo, cxChar, cyChar) ;
                    break ;
               case IDM_CURVE :
               case IDM_LINE :
               case IDM_POLY :
               case IDM_TEXT :
                    DoBitCodedCaps (hdc, hdcInfo, cxChar, cyChar,
                                    nCurrentInfo - IDM_CURVE) ;
                    break ;
               DeleteDC (hdcInfo) ;
          EndPaint (hwnd, &ps) ;
          return 0 ;
     case WM_DESTROY :
          PostQuitMessage (0) ;
          return 0 ;
     return DefWindowProc (hwnd, message, wParam, lParam) ;
void DoBasicInfo (HDC hdc, HDC hdcInfo, int cxChar, int cyChar)
     static struct
          int     nIndex ;
          TCHAR * szDesc ;
     info[] =
          HORZSIZE,        TEXT ("HORZSIZE        Width in millimeters:"),
          VERTSIZE,        TEXT ("VERTSIZE        Height in millimeters:"),
          HORZRES,         TEXT ("HORZRES         Width in pixels:"),
          VERTRES,         TEXT ("VERTRES         Height in raster lines:"),
          BITSPIXEL,       TEXT ("BITSPIXEL       Color bits per pixel:"),
          PLANES,          TEXT ("PLANES          Number of color planes:"),
          NUMBRUSHES,      TEXT ("NUMBRUSHES      Number of device brushes:"),
          NUMPENS,         TEXT ("NUMPENS         Number of device pens:"),
          NUMMARKERS,      TEXT ("NUMMARKERS      Number of device markers:"),
          NUMFONTS,        TEXT ("NUMFONTS        Number of device fonts:"),
          NUMCOLORS,       TEXT ("NUMCOLORS       Number of device colors:"),
          PDEVICESIZE,     TEXT ("PDEVICESIZE     Size of device structure:"),
          ASPECTX,         TEXT ("ASPECTX         Relative width of pixel:"),
          ASPECTY,         TEXT ("ASPECTY         Relative height of pixel:"),
          ASPECTXY,        TEXT ("ASPECTXY        Relative diagonal of pixel:"),
          LOGPIXELSX,      TEXT ("LOGPIXELSX      Horizontal dots per inch:"),
          LOGPIXELSY,      TEXT ("LOGPIXELSY      Vertical dots per inch:"),
          SIZEPALETTE,     TEXT ("SIZEPALETTE     Number of palette entries:"),
          NUMRESERVED,     TEXT ("NUMRESERVED     Reserved palette entries:"),
          COLORRES,        TEXT ("COLORRES        Actual color resolution:"),
          PHYSICALWIDTH,   TEXT ("PHYSICALWIDTH   Printer page pixel width:"),
          PHYSICALHEIGHT,  TEXT ("PHYSICALHEIGHT  Printer page pixel height:"),
          PHYSICALOFFSETX, TEXT ("PHYSICALOFFSETX Printer page x offset:"),
          PHYSICALOFFSETY, TEXT ("PHYSICALOFFSETY Printer page y offset:") 
     } ;
     int   i ;
     TCHAR szBuffer[80] ;
     for (i = 0 ; i < sizeof (info) / sizeof (info[0]) ; i++)
          TextOut (hdc, cxChar, (i + 1) * cyChar, szBuffer,
               wsprintf (szBuffer, TEXT ("%-45s%8d"), info[i].szDesc,
                    GetDeviceCaps (hdcInfo, info[i].nIndex))) ;
void DoOtherInfo (HDC hdc, HDC hdcInfo, int cxChar, int cyChar)
     static BITS clip[] =
          CP_RECTANGLE,    TEXT ("CP_RECTANGLE    Can Clip To Rectangle:")
     } ; 
     static BITS raster[] =
          RC_BITBLT,       TEXT ("RC_BITBLT       Capable of simple BitBlt:"),
          RC_BANDING,      TEXT ("RC_BANDING      Requires banding support:"),
          RC_SCALING,      TEXT ("RC_SCALING      Requires scaling support:"),
          RC_BITMAP64,     TEXT ("RC_BITMAP64     Supports bitmaps >64K:"),
          RC_GDI20_OUTPUT, TEXT ("RC_GDI20_OUTPUT Has 2.0 output calls:"),
          RC_DI_BITMAP,    TEXT ("RC_DI_BITMAP    Supports DIB to memory:"),
          RC_PALETTE,      TEXT ("RC_PALETTE      Supports a palette:"),
          RC_DIBTODEV,     TEXT ("RC_DIBTODEV     Supports bitmap conversion:"),
          RC_BIGFONT,      TEXT ("RC_BIGFONT      Supports fonts >64K:"),
          RC_STRETCHBLT,   TEXT ("RC_STRETCHBLT   Supports StretchBlt:"),
          RC_FLOODFILL,    TEXT ("RC_FLOODFILL    Supports FloodFill:"),
          RC_STRETCHDIB,   TEXT ("RC_STRETCHDIB   Supports StretchDIBits:")
     } ;
     static TCHAR * szTech[] = { TEXT ("DT_PLOTTER (Vector plotter)"),
                                 TEXT ("DT_RASDISPLAY (Raster display)"),
                                 TEXT ("DT_RASPRINTER (Raster printer)"),
                                 TEXT ("DT_RASCAMERA (Raster camera)"),
                                 TEXT ("DT_CHARSTREAM (Character stream)"),
                                 TEXT ("DT_METAFILE (Metafile)"),
                                 TEXT ("DT_DISPFILE (Display file)") } ;
     int            i ;
     TCHAR          szBuffer[80] ;
     TextOut (hdc, cxChar, cyChar, szBuffer,
          wsprintf (szBuffer, TEXT ("%-24s%04XH"), TEXT ("DRIVERVERSION:"),
               GetDeviceCaps (hdcInfo, DRIVERVERSION))) ;
     TextOut (hdc, cxChar, 2 * cyChar, szBuffer,
          wsprintf (szBuffer, TEXT ("%-24s%-40s"), TEXT ("TECHNOLOGY:"), 
               szTech[GetDeviceCaps (hdcInfo, TECHNOLOGY)])) ;
     TextOut (hdc, cxChar, 4 * cyChar, szBuffer,
          wsprintf (szBuffer, TEXT ("CLIPCAPS (Clipping capabilities)"))) ;
     for (i = 0 ; i < sizeof (clip) / sizeof (clip[0]) ; i++)
          TextOut (hdc, 9 * cxChar, (i + 6) * cyChar, szBuffer,
               wsprintf (szBuffer, TEXT ("%-45s %3s"), clip[i].szDesc,
                    GetDeviceCaps (hdcInfo, CLIPCAPS) & clip[i].iMask ?
                         TEXT ("Yes") : TEXT ("No"))) ;
     TextOut (hdc, cxChar, 8 * cyChar, szBuffer,
          wsprintf (szBuffer, TEXT ("RASTERCAPS (Raster capabilities)"))) ;
     for (i = 0 ; i < sizeof (raster) / sizeof (raster[0]) ; i++)
          TextOut (hdc, 9 * cxChar, (i + 10) * cyChar, szBuffer,
               wsprintf (szBuffer, TEXT ("%-45s %3s"), raster[i].szDesc,
                    GetDeviceCaps (hdcInfo, RASTERCAPS) & raster[i].iMask ?
                         TEXT ("Yes") : TEXT ("No"))) ;

void DoBitCodedCaps (HDC hdc, HDC hdcInfo, int cxChar, int cyChar, int iType)
     static BITS curves[] =
          CC_CIRCLES,    TEXT ("CC_CIRCLES    Can do circles:"),
          CC_PIE,        TEXT ("CC_PIE        Can do pie wedges:"),
          CC_CHORD,      TEXT ("CC_CHORD      Can do chord arcs:"),
          CC_ELLIPSES,   TEXT ("CC_ELLIPSES   Can do ellipses:"),
          CC_WIDE,       TEXT ("CC_WIDE       Can do wide borders:"),
          CC_STYLED,     TEXT ("CC_STYLED     Can do styled borders:"),
          CC_WIDESTYLED, TEXT ("CC_WIDESTYLED Can do wide and styled borders:"),
          CC_INTERIORS,  TEXT ("CC_INTERIORS  Can do interiors:")
     } ; 
     static BITS lines[] =
          LC_POLYLINE,   TEXT ("LC_POLYLINE   Can do polyline:"),
          LC_MARKER,     TEXT ("LC_MARKER     Can do markers:"),
          LC_POLYMARKER, TEXT ("LC_POLYMARKER Can do polymarkers"),
          LC_WIDE,       TEXT ("LC_WIDE       Can do wide lines:"),
          LC_STYLED,     TEXT ("LC_STYLED     Can do styled lines:"),
          LC_WIDESTYLED, TEXT ("LC_WIDESTYLED Can do wide and styled lines:"),
          LC_INTERIORS,  TEXT ("LC_INTERIORS  Can do interiors:")
     } ;
     static BITS poly[] =
               TEXT ("PC_POLYGON     Can do alternate fill polygon:"),
          PC_RECTANGLE,   TEXT ("PC_RECTANGLE   Can do rectangle:"),
               TEXT ("PC_WINDPOLYGON Can do winding number fill polygon:"),
          PC_SCANLINE,    TEXT ("PC_SCANLINE    Can do scanlines:"),
          PC_WIDE,        TEXT ("PC_WIDE        Can do wide borders:"),
          PC_STYLED,      TEXT ("PC_STYLED      Can do styled borders:"),
               TEXT ("PC_WIDESTYLED  Can do wide and styled borders:"),
          PC_INTERIORS,   TEXT ("PC_INTERIORS   Can do interiors:")
     } ;
     static BITS text[] =
               TEXT ("TC_OP_CHARACTER Can do character output precision:"),
               TEXT ("TC_OP_STROKE    Can do stroke output precision:"),
               TEXT ("TC_CP_STROKE    Can do stroke clip precision:"),
               TEXT ("TC_CP_90        Can do 90 degree character rotation:"),
               TEXT ("TC_CR_ANY       Can do any character rotation:"),
               TEXT ("TC_SF_X_YINDEP  Can do scaling independent of X and Y:"),
               TEXT ("TC_SA_DOUBLE    Can do doubled character for scaling:"),
               TEXT ("TC_SA_INTEGER   Can do integer multiples for scaling:"),
               TEXT ("TC_SA_CONTIN    Can do any multiples for exact scaling:"),
               TEXT ("TC_EA_DOUBLE    Can do double weight characters:"),
          TC_IA_ABLE,      TEXT ("TC_IA_ABLE      Can do italicizing:"),
          TC_UA_ABLE,      TEXT ("TC_UA_ABLE      Can do underlining:"),
          TC_SO_ABLE,      TEXT ("TC_SO_ABLE      Can do strikeouts:"),
          TC_RA_ABLE,      TEXT ("TC_RA_ABLE      Can do raster fonts:"),
          TC_VA_ABLE,      TEXT ("TC_VA_ABLE      Can do vector fonts:")
     } ;
     static struct
          int     iIndex ;
          TCHAR * szTitle ;
          BITS    (*pbits)[] ;
          int     iSize ;
     bitinfo[] =
          CURVECAPS,  TEXT ("CURVCAPS (Curve Capabilities)"),
               (BITS (*)[]) curves, sizeof (curves) / sizeof (curves[0]),
          LINECAPS,   TEXT ("LINECAPS (Line Capabilities)"),
               (BITS (*)[]) lines, sizeof (lines) / sizeof (lines[0]),
          POLYGONALCAPS, TEXT ("POLYGONALCAPS (Polygonal Capabilities)"),
               (BITS (*)[]) poly, sizeof (poly) / sizeof (poly[0]),
          TEXTCAPS,   TEXT ("TEXTCAPS (Text Capabilities)"),
               (BITS (*)[]) text, sizeof (text) / sizeof (text[0])
     } ;
     static TCHAR szBuffer[80] ;
     BITS         (*pbits)[] = bitinfo[iType].pbits ;
     int          i, iDevCaps = GetDeviceCaps (hdcInfo, bitinfo[iType].iIndex) ;
     TextOut (hdc, cxChar, cyChar, bitinfo[iType].szTitle,
              lstrlen (bitinfo[iType].szTitle)) ;
     for (i = 0 ; i < bitinfo[iType].iSize ; i++)
          TextOut (hdc, cxChar, (i + 3) * cyChar, szBuffer,
               wsprintf (szBuffer, TEXT ("%-55s %3s"), (*pbits)[i].szDesc,
                    iDevCaps & (*pbits)[i].iMask ? TEXT ("Yes") : TEXT ("No")));

DEVCAPS2.RC (excerpts)

//Microsoft Developer Studio generated resource script.

#include "resource.h"
#include "afxres.h"

// Menu
    POPUP "&Device"
        MENUITEM "&Screen",                     IDM_SCREEN, CHECKED
    POPUP "&Capabilities"
        MENUITEM "&Basic Information",          IDM_BASIC
        MENUITEM "&Other Information",          IDM_OTHER
        MENUITEM "&Curve Capabilities",         IDM_CURVE
        MENUITEM "&Line Capabilities",          IDM_LINE
        MENUITEM "&Polygonal Capabilities",     IDM_POLY
        MENUITEM "&Text Capabilities",          IDM_TEXT

RESOURCE.H (excerpts)

// Microsoft Developer Studio generated include file.
// Used by DevCaps2.rc

#define IDM_SCREEN                      40001
#define IDM_BASIC                       40002
#define IDM_OTHER                       40003
#define IDM_CURVE                       40004
#define IDM_LINE                        40005
#define IDM_POLY                        40006
#define IDM_TEXT                        40007

Because DEVCAPS2 obtains only an information context for the printer, you can select printers from DEVCAPS2's menu, even though they may have an output port of "none." If you want to compare the capabilities of different printers, you can first use the Printers folder to add various printer drivers.

The PrinterProperties Call

The Device menu of the DEVCAPS2 program includes an option called Properties. To use it, first select a printer from the Device menu and then select Properties. Up pops a dialog box. Where does the dialog box come from? It is invoked by the printer driver, and—at the very least—it requests that you make a choice of paper size. Most printer drivers also give you a choice of "portrait" or "landscape" mode. In portrait mode (often the default), the short side of the paper is the top; in landscape mode, the long side is the top. If you change this mode, the change is reflected in the information the DEVCAPS2 program obtains from the GetDeviceCaps function: the horizontal size and resolution are switched with the vertical size and resolution. Properties dialog boxes for color plotters can be quite extensive, requesting the colors of the pens installed in the plotter and the type of paper (or transparencies) being used.

All printer drivers contain an exported function called ExtDeviceMode that invokes this dialog box and saves the information that the user enters. Some printer drivers store this information in their own section of the Registry, and some don't. Those that store the information have access to it during the next Windows session.

Windows programs that allow the user a choice of printers generally just call PrintDlg, which I'll show you how to use later in this chapter. This useful function takes care of all the work of communicating with the user and handles any changes the user requests in preparation for printing. PrintDlg also invokes the property sheet dialog when the user clicks the Properties button.

A program can also display a printer's properties dialog by directly calling the printer driver's ExtDeviceMode or ExtDeveModePropSheet functions. However, I don't recommend this. It's far better to invoke the dialog indirectly by calling PrinterProperties, as DEVCAPS2 does.

PrinterProperties requires a handle to a printer object, which you get by calling the OpenPrinter function. When the user cancels a property sheet dialog, PrinterProperties returns. You can then close the printer handle by calling ClosePrinter. Here's how DEVCAPS2 does it:

The program first obtains the name of the printer currently selected in the Device menu and saves it in a character array named szDevice:

GetMenuString (hMenu, nCurrentDevice, szDevice,
               sizeof (szDevice) / sizeof (TCHAR), MF_BYCOMMAND) ;

Then it obtains the handle of this device by using OpenPrinter. If the call is successful, the program next calls PrinterProperties to invoke the dialog box and then ClosePrinter to delete the device handle:

if (OpenPrinter (szDevice, &hPrint, NULL))
     PrinterProperties (hwnd, hPrint) ;
     ClosePrinter (hPrint) ;

Checking for BitBlt Capability

You can use the GetDeviceCaps function to obtain the size and resolution of the printable area of the page. (In most cases, this area won't be the same as the full size of the paper.) You can also obtain the relative pixel width and height, if you want to do your own scaling.

Much of the information regarding various capabilities of the printer is for the purpose of GDI rather than applications. Often when a printer can't do something itself, GDI will simulate it. However, there is one capability that some applications should check.

This is the printer characteristic obtained from the RC_BITBLT bit of the value returned from GetDeviceCaps with a parameter of RASTERCAPS ("raster capabilities"). This bit indicates whether the device is capable of bit-block transfers. Most dot-matrix, laser, and ink-jet printers are capable of bit-block transfers, but plotters are not. Devices that can't handle bit-block transfers do not support the following GDI functions: CreateCompatibleDC, CreateCompatibleBitmap, PatBlt, BitBlt, StretchBlt, GrayString, DrawIcon, SetPixel, GetPixel, FloodFill, ExtFloodFill, FillRgn, FrameRgn, InvertRgn, PaintRgn, FillRect, FrameRect, and InvertRect. This is the single most important distinction between using GDI calls on a video display and using them on a printer.

The Simplest Printing Program

We're now ready to print, and we're going to start as simply as possible. In fact, our first printing program does nothing but cause a printer form feed to eject the page. The FORMFEED program, shown in Figure 13-5, demonstrates the absolute minimum requirements for printing.

Figure 13-5. The FORMFEED program.


   FORMFEED.C -- Advances printer to next page
                 (c) Charles Petzold, 1998

#include <windows.h>

HDC GetPrinterDC (void) ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPSTR lpszCmdLine, int iCmdShow)
     static DOCINFO di = { sizeof (DOCINFO), TEXT ("FormFeed") } ;
     HDC            hdcPrint = GetPrinterDC () ;
     if (hdcPrint != NULL)
          if (StartDoc (hdcPrint, &di) > 0)
               if (StartPage (hdcPrint) > 0 && EndPage (hdcPrint) > 0)
                    EndDoc (hdcPrint) ;
          DeleteDC (hdcPrint) ;
     return 0 ;

This program also requires the GETPRNDC.C file shown previously in Figure 13-3.

Other than obtaining the printer device context (and later deleting it), the program calls only the four print functions discussed earlier in this chapter. FORMFEED first calls StartDoc to start a new document. The program tests the return value from the function and proceeds only if the value is positive:

if (StartDoc (hdcPrint, &di) > 0)

The second argument to StartDoc is a pointer to a DOCINFO structure. This structure contains the size of the structure in the first field and the text string "FormFeed" in the second. As the document prints or while it is waiting to print, this string appears in the Document Name column of the printer's job queue. Generally the identification string includes the name of the application doing the printing and the file being printed.

If StartDoc is successful (indicated by a positive return value), FORMFEED calls StartPage, followed immediately by a call to EndPage. This sequence advances the printer to a new page. Once again, the return values are tested:

if (StartPage (hdcPrint) > 0 && EndPage (hdcPrint) > 0)

Finally, if everything has proceeded without error to this point, the document is ended:

EndDoc (hdcPrint) ;

Note that the EndDoc function is called only if no printing errors have been reported. If one of the other print functions returns an error code, GDI has already aborted the document. If the printer is not currently printing, such an error code often results in the printer being reset. Simply testing the return values from the print functions is the easiest way to check for errors. If you want to report a particular error to the user, you must call GetLastError to determine the error.

If you've ever written a simple form-feed program for MS-DOS, you know that ASCII code 12 (Ctrl-L) activates a form feed for most printers. Why not simply open the printer port using the C library function open and then output an ASCII code 12 using write? Well, nothing prevents you from doing this. You first have to determine the parallel port or the serial port the printer is attached to. You then have to determine whether another program (the print spooler, for instance) is currently using the printer. (You don't want the form feed to be output in the middle of some other program's document, do you?) Finally, you have to determine if ASCII code 12 is a form-feed character for the connected printer. It's not universal, you know. In fact, the form-feed command in PostScript isn't a 12; it's the word showpage.

In short, don't even think about going around Windows; stick with the Windows functions for printing.