Chapter 15  Using the Printer
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

The concept of device independence may have seemed all well and good in the
past four chapters, when we were using the video display for text and
graphics, but how well does the concept hold up for printers and plotters?
In general, the news is good. Under Windows, printers and plotters have a
device-independent graphics interface. You can forget about printer control
sequences and communications protocols when programming for the printer.
Retail Windows programs conspicuously lack the disks of specialized printer
drivers that have characterized recent word-processing software and graphics
programs for MS-DOS. When a retail Windows program includes printer drivers,
they are usually enhanced versions of existing printer drivers.

From a Windows program, you can print text and graphics on paper using the
same GDI functions that we've been using for the video display. Many of the
issues of device independence that we've explored in the past four
chapters--mostly related to the size and resolution of the display surface
and its color capabilities--can be approached and resolved in the same way.
Yet a printer or plotter is not simply a display that uses paper rather than
a cathode-ray tube. There are some very significant differences. For
example, we have never had to consider the problem of a video display not
being connected to the display adapter or of the display "running out of
screen," but it is common for a printer to be off line or to run out of
paper.

Nor have we worried about the video display adapter being incapable of
performing certain graphics operations. Either the display adapter can
handle graphics or it can't. And if it can't, then it can't be used with
Windows at all. But some printers can't print graphics  (although they can
still be used with Windows), and plotters can do vector graphics but have a
real problem with bit-block transfers.

Here are some other issues to consider:

  þ   Printers are slower than video displays. Although we have on occasion
      tried to tune our programs for best performance, we haven't worried
      about the time required for the video display to be refreshed. But
      nobody wants to wait for a printer to finish printing before getting
      back to work.

  þ   Programs reuse the surface of the video display as they overwrite
      previous display output with new output. This can't be done on a
      printer. Instead, a printer must eject a completed page and go on to
      the next page.

  þ   On the video display, different applications are windowed. On a
      printer, output from different applications must be separated into
      distinct documents or print jobs.

To add printer support to the rest of GDI, Windows includes only one new
function, called Escape. Well, it's actually more than one. Escape has many
subfunctions that are indicated by one of the Escape parameters. For
example, the three most common Escape subfunctions are STARTDOC and ENDDOC
(which begin and end a printing job) and NEWFRAME (which ends one page and
goes on to the next).

PRINTING, SPOOLING, AND ESCAPE

When you use a printer in Windows, you're actually initiating a complex
interaction involving the GDI library module, the printer device driver
library module (which has a .DRV extension), and the Windows Print Manager
program (PRINTMAN.EXE), 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.

When an application program wants to begin using a printer, it first obtains
a handle to the printer device context using CreateDC. 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 Escape
subfunction named STARTDOC, which signals the beginning of a new document.
The Escape function is handled by the GDI module. The GDI module calls the
Control function (which is equivalent to Escape) in the printer device
driver. The device driver performs some initialization and calls OpenJob,
which is in the GDI module. The GDI module then loads the Windows Print
Manager program into memory.

Following the STARTDOC Escape call, the program can make the appropriate GDI
calls for the first page of the document. For example, if the program wants
to draw an ellipse on the page, it calls Ellipse, just as it does when
drawing an ellipse on the screen.  The GDI module generally stores all these
GDI calls in a disk-based metafile, which is located in the subdirectory
indicated by the TEMP variable in the MS-DOS environment. (If no TEMP
variable exists, Windows uses the root directory of the first fixed disk on
the system.) The file begins with the characters ~MF and has a .TMP
extension.

When the application program is finished with the GDI calls that define the
first page, the program calls the NEWFRAME subfunction of Escape. 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
300-dots-per-inch laser printer using 8-1/2-by-11-inch paper might require
more than a megabyte of data to define one page of graphics.

For this reason, printer drivers often implement a technique called
"banding," which divides the page into rectangles called bands. (We'll
examine banding later in this chapter.) 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 this language.

The printer driver uses the WriteSpool function to pass the printer output
for each band to the GDI module, which then stores this printer output in a
temporary file also located in the TEMP subdirectory. This file begins with
the characters ~SPL and has a .TMP extension. When the entire page is
finished, the GDI module uses the SendMessage function to send a message to
the Print Manager 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 makes the ENDDOC Escape call to signal that
the print job is completed. Figure 15-1 on the following page shows the
interaction of the program, the GDI module, and the printer driver.

The Windows Print Manager program is a print spooler that relieves
application programs of some of the work involved with printing. The GDI
module loads the Print Manager (if it is not already loaded) automatically
when a program begins printing. The GDI module then creates the files that
contain printer output. The Print Manager's job is to send these files out
to the printer. It is notified of a new print job by a message from the GDI


  (Figure 15-1. may be found in the printed book.)

module. It then begins reading the file and transferring it directly to the
printer. To transfer the files, the Print Manager uses various
communications functions (OpenComm, WriteComm, and so forth included in the
USER module) for the parallel or serial port that the printer is connected
to. During the time that the Print Manager is writing the printer output to
the output port, other Windows programs can function normally. When the
Print Manager is done sending a file to a printer, it can delete the
temporary file holding the output. This process is shown in Figure 15-2.

Most of this process is transparent to the application program. From the
perspective of the program, "printing" occurs only during the time required
for the GDI module to save all the printer output in disk files. After that,
the program is freed up to do other things. The actual printing of the
document becomes the Print Manager's responsibility rather than the
program's. A user can direct the Print Manager to pause print jobs, to
change their priority, or to cancel print jobs. 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.

  (Figure 15-2. may be found in the printed book.)

Although I've described how printing works in general, there are some
variations on this theme. One is that the Print Manager doesn't have to be
present in order for Windows programs to use the printer. Normally, the
[windows] section of WIN.INI contains this line:

Spooler=yes

But a user can change that to:

Spooler=no

If this line is present, the Print Manager doesn't allow itself to be
executed.

Why would a user not want the Print Manager to be loaded? Well, perhaps the
user has a hardware or software print spooler that works faster than the
Print Manager. 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 Print Manager would speed up printing, 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 Print Manager can't be loaded, 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 Print Manager, 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 on the general theme. 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. You can
either get very involved in reporting these problems to the user and
attempting to do something about them, or you can remain relatively aloof.

We'll examine several different approaches to printing in the pages that
follow. But first things first--let's begin by 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 made the Escape calls
necessary to announce your intention of creating a new document), 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
calls we've covered in the last four chapters.

In Chapter 11, you learned that you can get a handle to a device context for
the entire video display by calling:

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

You obtain a printer device context handle using this same function.
However, for a printer device context, the first three parameters are not
fixed. The general syntax of CreateDC is:

hdc = CreateDC (lpszDriverName, lpszDeviceName, lpszOutputPort,
                                lpInitializationData) ;

Although lpInitializationData is generally set to NULL, the first three
parameters must be far pointers to character strings that tell Windows the
name of the printer driver, the name of the printer device, and the output
port to which the device is connected. Before you can set these three
parameters, you must do a little fishing in the WIN.INI file.

Getting the CreateDC Parameters

Printers are listed in two different sections of the WIN.INI file,
reflecting the possibility that a system can have more than one printer
attached to it. A single printer is listed in the [windows] section with the
keyword device. The string that follows contains the device name, driver
name, and output port required in the CreateDC call:

[windows]
[other lines]
device=IBM Graphics,IBMGRX,LPT1:

In this case, the device name is IBM Graphics, the driver name is IBMGRX,
and the output port is LPT1. The printer listed in this section of WIN.INI
is the most recent printer that the user has selected using the Windows
Control Panel. This printer is chosen from the dialog box invoked by the
Printer option. It can be considered the "current printer" or the "default
printer." Most small Windows programs use this printer for printing.

Here is one way to write a function to obtain this string from WIN.INI,
parse it into the three components, and call CreateDC to obtain a printer
device context handle:

HDC GetPrinterDC ()
     {
     char szPrinter [80] ;
     char *szDevice, *szDriver, *szOutput ;

     GetProfileString ("windows", "device", ",,,", szPrinter, 80) ;

     if ((szDevice = strtok (szPrinter, "," )) &&
         (szDriver = strtok (NULL,      ", ")) &&
         (szOutput = strtok (NULL,      ", ")))
               return CreateDC (szDriver, szDevice, szOutput, NULL) ;

     return 0 ;
     }

GetProfileString looks in the [windows] section for the keyword device and
copies up to 80 characters following the equal sign into szPrinter. (The
third parameter to GetProfileString is a default string if Windows can't
find the [windows] section or the device keyword in WIN.INI.) The string is
parsed using the normal C strtok function, which breaks a string into
tokens. Note that I use only a comma to find the end of the szDevice string,
because the device name can include embedded blanks. Both a comma and space
are used to separate the driver name and output port so that leading or
trailing blanks are stripped from these strings. (Using strtok is not an
entirely satisfactory method of parsing this string, because strtok doesn't
take into account the multibyte character codes that can be used in versions
of Windows for countries in the Far East. If this is of concern, you can
write your own version of strtok that uses AnsiNext to advance through the
string.)

The CreateDC function returns 0 if the printer device context could not be
created. This can occur if the string in WIN.INI is malformed, if Windows
can't find the printer driver, or if the output port name is "none" (which
means the printer is not connected to an output port). You can, however,
obtain an information context for a printer connected to "none" by using
CreateIC rather than CreateDC.

The [windows] section of WIN.INI lists only one printer. But multiple
printers can be listed in the [devices] section of WIN.INI. A printer is
listed in this section when a user  chooses the Add New Printer option from
the Installation menu of CONTROL.EXE. The [devices] section looks something
like this:

[devices]
IBM Graphics=IBMGRX,LPT1:
Generic / Text Only=TTY,output.prn
HP Plotter=HPPLOT,COM1:
Postscript Printer=PSCRIPT,COM2:

To the left of each equal sign is the device name; to the right is first the
driver name and then the output port. Getting a device context handle using
the printer specified in the [windows] section of WIN.INI is essentially the
same as getting a device context handle using one of the printers from the
[devices] section, except that the latter is more difficult because you have
more than one choice.

Some larger Windows programs include a File menu option called Change
Printer, which invokes a dialog box that lists all the printers from the
[devices] section with the port each printer is connected to. This option
allows the user to select a printer other than the one listed in the
[windows] section of WIN.INI. A program that includes this option must first
call GetProfileString with a NULL second parameter:

static char szAllDevices [4096] ;
[other program lines]
GetProfileString ("devices", NULL, "", szAllDevices,
                                   sizeof szAllDevices) ;

On return, szAllDevices contains a list of the keywords (the device names)
in the [devices] section. Each keyword is terminated by a NULL except for
the final keyword, which is terminated by two NULLs. For the example list
shown above, szAllDevices would contain (using C notation):

IBM Graphics\0Generic / Text Only\0HP Plotter\0Postscript Printer\0\0

You can then present these names to the user. (We'll do this shortly in the
DEVCAPS2 program.)

Let's assume that a user selects one of these devices and that you've set
the pointer szDevice to the beginning of that device name in szAllDevices.
You can then obtain the rest of the string (the driver name and output port)
by calling GetProfileString again:

GetProfileString ("devices", szDevice, "", szPrinter, 64) ;

You need to parse the szPrinter string to extract the driver name and the
output port name:

szDriver = strtok (szPrinter, ", ") ;
szOutput = strtok (NULL     , ", ") ;

Now you have the szDevice, szDriver, and szOutput pointers necessary to call
CreateDC or CreateIC.

The valid output ports on a particular system are listed in the [ports]
section of WIN.INI. You don't need to access this section of WIN.INI to use
the printer. You can assume that the user has identified a particular port
for the printer using the Printer option in the Control Panel, and you can
further assume that the user has properly defined the communications
parameters for serial (COM) ports using the Ports option.

The [ports] section often looks something like this:

[ports]
LPT1:=
LPT2:=
LPT3:=
COM1:=9600,n,8,1
COM2:=1200,n,8,1
output.prn=

The OUTPUT.PRN file (or any file with a .PRN extension) can be listed here
to direct printer output to a file. This filename can appear as the output
port for a printer in the [windows] section or [devices] section of WIN.INI.


The Revised DEVCAPS Program

The original DEVCAPS1 program in Chapter 11 displays all the information
available from the GetDeviceCaps function for the video display and the
current printer. The new version, shown in Figure 15-3, displays a menu of
all the printers from the [devices] section of WIN.INI and lets you choose
one. In addition to the DEVCAPS2 files shown here, you'll also need the
DEVCAPS.C file from Chapter 11 (Figure 11-1).

 DEVCAPS2.MAK

#-----------------------
# DEVCAPS.MAK make file
#-----------------------

devcaps2.exe : devcaps2.obj devcaps.obj devcaps2.res devcaps2.def
     link devcaps2 devcaps, /align:16, NUL, /nod slibcew libw, devcaps2
     rc devcaps2.res

devcaps2.obj : devcaps2.c devcaps2.h
     cl -c -Gsw -Ow -W2 -Zp devcaps2.c

devcaps.obj : devcaps.c
     cl -c -Gsw -Ow -W2 -Zp devcaps.c

devcaps2.res : devcaps2.rc devcaps2.h
     rc -r devcaps2.rc


 DEVCAPS2.C

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

#include 
#include 
#include "devcaps2.h"

void DoBasicInfo (HDC, HDC, short, short) ;            // in DEVCAPS.C
void DoOtherInfo (HDC, HDC, short, short) ;
void DoBitCodedCaps (HDC, HDC, short, short, short) ;

typedef VOID (FAR PASCAL *DEVMODEPROC) (HWND, HANDLE, LPSTR, LPSTR) ;

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "DevCaps2" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = 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 = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = szAppName ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, NULL,
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;
     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

void DoEscSupport (HDC hdc, HDC hdcInfo, short cxChar, short cyChar)
     {
     static struct
          {
          char  *szEscCode ;
          short nEscCode ;
          }
          esc [] =
          {
          "NEWFRAME",          NEWFRAME,
          "ABORTDOC",          ABORTDOC,
          "NEXTBAND",          NEXTBAND,
          "SETCOLORTABLE",     SETCOLORTABLE,
          "GETCOLORTABLE",     GETCOLORTABLE,
          "FLUSHOUTPUT",       FLUSHOUTPUT,
          "DRAFTMODE",         DRAFTMODE,
          "QUERYESCSUPPORT",   QUERYESCSUPPORT,
          "SETABORTPROC",      SETABORTPROC,
          "STARTDOC",          STARTDOC,
          "ENDDOC",            ENDDOC,
          "GETPHYSPAGESIZE",   GETPHYSPAGESIZE,
          "GETPRINTINGOFFSET", GETPRINTINGOFFSET,
          "GETSCALINGFACTOR",  GETSCALINGFACTOR } ;

     static char *szYesNo [] = { "Yes", "No" } ;
     char        szBuffer [32] ;
     POINT       pt ;
     short       n, nReturn ;

     TextOut (hdc, cxChar, cyChar, "Escape Support", 14) ;

     for (n = 0 ; n < sizeof esc / sizeof esc [0] ; n++)
          {
          nReturn = Escape (hdcInfo, QUERYESCSUPPORT, 1,
                                   (LPSTR) & esc[n].nEscCode, NULL) ;
          TextOut (hdc, 6 * cxChar, (n + 3) * cyChar, szBuffer,
               wsprintf (szBuffer, "%-24s %3s", (LPSTR) esc[n].szEscCode,
                         (LPSTR) szYesNo [nReturn > 0 ? 0 : 1])) ;
          if (nReturn > 0 && esc[n].nEscCode >= GETPHYSPAGESIZE
                          && esc[n].nEscCode <= GETSCALINGFACTOR)
               {
               Escape (hdcInfo, esc[n].nEscCode, 0, NULL, (LPSTR) &pt) ;
               TextOut (hdc, 36 * cxChar, (n + 3) * cyChar, szBuffer,
                        wsprintf (szBuffer, "(%u,%u)", pt.x, pt.y)) ;
               }
          }
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static char   szAllDevices [4096], szDevice [32], szDriver [16],
                   szDriverFile [16], szWindowText [64] ;
     static HANDLE hLibrary ;
     static short  n, cxChar, cyChar, nCurrentDevice = IDM_SCREEN,
                                      nCurrentInfo   = IDM_BASIC ;
     char          *szOutput, *szPtr ;
     DEVMODEPROC   lpfnDM ;
     HDC           hdc, hdcInfo ;
     HMENU         hMenu ;
     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) ;

               lParam = NULL ;
                                                  // fall through
          case WM_WININICHANGE :
               if (lParam != NULL && lstrcmp ((LPSTR) lParam, "devices") !=
0)
                    return 0 ;

               hMenu = GetSubMenu (GetMenu (hwnd), 0) ;

               while (GetMenuItemCount (hMenu) > 1)
                    DeleteMenu (hMenu, 1, MF_BYPOSITION) ;

               GetProfileString ("devices", NULL, "", szAllDevices,
                         sizeof szAllDevices) ;

               n = IDM_SCREEN + 1 ;
               szPtr = szAllDevices ;
               while (*szPtr)
                    {
                    AppendMenu (hMenu, n % 16 ? 0 : MF_MENUBARBREAK, n,
szPtr) ;
                    n++ ;
                    szPtr += strlen (szPtr) + 1 ;
                    }
               AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ;
               AppendMenu (hMenu, 0, IDM_DEVMODE, "Device Mode") ;

               wParam = IDM_SCREEN ;
                                                  // fall through
          case WM_COMMAND :
               hMenu = GetMenu (hwnd) ;

               if (wParam < IDM_DEVMODE)          // IDM_SCREEN & Printers
                    {
                    CheckMenuItem (hMenu, nCurrentDevice, MF_UNCHECKED) ;
                    nCurrentDevice = wParam ;
                    CheckMenuItem (hMenu, nCurrentDevice, MF_CHECKED) ;
                    }
               else if (wParam == IDM_DEVMODE)
                    {
                    GetMenuString (hMenu, nCurrentDevice, szDevice,
                                   sizeof szDevice, MF_BYCOMMAND) ;

                    GetProfileString ("devices", szDevice, "",
                                      szDriver, sizeof szDriver) ;

                    szOutput = strtok (szDriver, ", ") ;
                    strcat (strcpy (szDriverFile, szDriver), ".DRV") ;

                    if (hLibrary >= 32)
                         FreeLibrary (hLibrary) ;

                    hLibrary = LoadLibrary (szDriverFile) ;
                    if (hLibrary >= 32)
                         {
                         lpfnDM = GetProcAddress (hLibrary, "DEVICEMODE") ;
                         (*lpfnDM) (hwnd, hLibrary, (LPSTR) szDevice,
                                                    (LPSTR) szOutput) ;
                         }
                    }
               else                               // info menu items
                    {
                    CheckMenuItem (hMenu, nCurrentInfo, MF_UNCHECKED) ;
                    nCurrentInfo = wParam ;
                    CheckMenuItem (hMenu, nCurrentInfo, MF_CHECKED) ;
                    }
               InvalidateRect (hwnd, NULL, TRUE) ;
               return 0 ;
          case WM_INITMENUPOPUP :
               if (lParam == 0)
                    EnableMenuItem (GetMenu (hwnd), IDM_DEVMODE,
                         nCurrentDevice == IDM_SCREEN ?
                              MF_GRAYED : MF_ENABLED) ;
               return 0 ;

          case WM_PAINT :
               strcpy (szWindowText, "Device Capabilities: ") ;

               if (nCurrentDevice == IDM_SCREEN)
                    {
                    strcpy (szDriver, "DISPLAY") ;
                    strcat (szWindowText, szDriver) ;
                    hdcInfo = CreateIC (szDriver, NULL, NULL, NULL) ;
                    }
               else
                    {
                    hMenu = GetMenu (hwnd) ;

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

                    GetProfileString ("devices", szDevice, "", szDriver, 10)
;
                    szOutput = strtok (szDriver, ", ") ;
                    strcat (szWindowText, szDevice) ;

                    hdcInfo = CreateIC (szDriver, szDevice, szOutput, NULL)
;
                    }
               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 ;

                         case IDM_ESC :
                              DoEscSupport (hdc, hdcInfo, cxChar, cyChar) ;
                              break ;
                         }
                    DeleteDC (hdcInfo) ;
                    }

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               if (hLibrary >= 32)
                    FreeLibrary (hLibrary) ;

               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 DEVCAPS2.RC

/*-----------------------------
   DEVCAPS2.RC resource script
  -----------------------------*/

#include "devcaps2.h"

DevCaps2 MENU
     {
     POPUP "&Device"
          {
          MENUITEM "&Screen",                IDM_SCREEN, CHECKED
          }
     POPUP "&Capabilities"
          {
          MENUITEM "&Basic Information",     IDM_BASIC, CHECKED
          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
          MENUITEM SEPARATOR
          MENUITEM "&Escape Support",        IDM_ESC
          }
     }

 DEVCAPS2.H

/*------------------------
   DEVCAPS2.H header file
  ------------------------*/

#define IDM_SCREEN  1

#define IDM_DEVMODE 0x100

#define IDM_BASIC   0x101
#define IDM_OTHER   0x102
#define IDM_CURVE   0x103
#define IDM_LINE    0x104
#define IDM_POLY    0x105
#define IDM_TEXT    0x106
#define IDM_ESC     0x107

 DEVCAPS2.DEF

;-------------------------------------
; DEVCAPS2.DEF module definition file
;-------------------------------------

NAME           DEVCAPS2

DESCRIPTION    'Displays Device Capability Info (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

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 Control Panel to add various printer drivers
to WIN.INI.

DEVCAPS2 has an additional option on the Capabilities menu called Escape
Support, which lets you see which of the more common Escape subfunctions are
supported by the device driver. This information will become more meaningful
a little later in this chapter, when we discuss the Escape function and its
subfunctions.


The DeviceMode Call

The Device menu of the DEVCAPS2 program includes an option called Device
Mode. To use it, first select a printer from the Device menu, and then
select Device Mode: Up pops a dialog box. Where did 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. Device Mode 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 DeviceMode that
invokes this dialog box and saves the information that the user enters. Some
printer drivers store this in- formation in their own section of the WIN.INI
file, 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 call the
DeviceMode function of the printer driver so that the user can make changes
in preparation for printing. Calling this function from a program requires a
technique that we'll learn more about in Chapter 19. 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, MF_BYCOMMAND) ;

Then it obtains the driver name and output port of this device using
GetProfileString. This information is stored in szDriver:

GetProfileString ("devices", szDevice, "",
                    szDriver, sizeof szDriver) ;

The output port is separated from the szDriver string using strtok, and the
pointer is saved in szOutput:

szOutput = strtok (szDriver, ", ") ;

The szDriver string contains the name of the driver, which is the driver's
filename without the .DRV extension. This statement creates the full name of
the driver file and saves it in szDriverFile:

strcat (strcpy (szDriverFile, szDriver), ".DRV") ;

This driver file is a dynamic link library module. (Library modules are the
subject of Chapter 19.) We can obtain a handle to this module (which is
actually the instance handle of the module) by calling LoadLibrary. If
LoadLibrary returns a value greater than or equal to 32, the function was
successful. Otherwise, the return value indicates an MS-DOS error code.

The library can be freed by a call to FreeLibrary. If no other program is
using this library, then it can be deleted from memory. DEVCAPS2 holds the
library handle (or whatever was returned from LoadLibrary) in a static
variable, so before trying to load a new library, it first frees the old one
if the handle was valid:

if (hLibrary >= 32)
     FreeLibrary (hLibrary) ;

hLibrary = LoadLibrary (szDriverFile) ;

Before proceeding, the program checks to see if this new handle is valid:

if (hLibrary >= 32)

It then calls GetProcAddress to obtain the address of the DeviceMode
function:

lpfnDM = GetProcAddress (hLibrary, "DEVICEMODE") ;

The DeviceMode function can be called indirectly by prefacing it with an
asterisk. The function is passed the window handle, library module handle,
device name, and output port:

(*lpfnDM) (hwnd, hLibrary, (LPSTR) szDevice,
                           (LPSTR) szOutput) ;

This invokes the dialog box. Note that you must explicitly cast the strings
into far pointers, because this function has no template in WINDOWS.H or
anywhere else.

The currently loaded driver file is freed when the program terminates:

case WM_DESTROY :
     if (hLibrary >= 32)
          FreeLibrary (hLibrary) ;

The LoadLibrary call increments the library module's "reference count" (a
number Windows maintains to indicate the number of programs using a module),
and the FreeLibrary call decrements it. The library can be freed from memory
when the reference count is 0. Calls to CreateDC and CreateIC for a printer
driver also increment the reference count,  and DeleteDC decrements it.


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 entire size of the paper.) You can also obtain the relative pixel
width and height, if you want to do your own scaling.

You can obtain another important printer characteristic 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 and laser printers are capable of
bit-block transfers, but most 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.



PRINTING FUNDAMENTALS

We're now ready to print, and we're going to start as simply as possible. In
fact, our first two printing programs will be so simple that they won't work
unless the Print Manager program gets loaded when printing begins. (The
Print Manager doesn't get loaded if a user specifies Spooler=no in the
WIN.INI file or if Windows can't find the PRINTMAN.EXE file.)

The Escape Function

The Windows GDI module includes only one function--Escape--to support the
additional requirements of printers. The name of this function implies that
it is ignored by the GDI module and that it goes straight to the printer
driver. In some cases, this is true, but often GDI also does some work
during Escape calls.

The general syntax of Escape is:

nResult = Escape (hdcPrinter, nEscapeCode, nCount,
                    lpsDataIn, lpsDataOut) ;

The nEscapeCode parameter is a subfunction code that is specified using an
identifier defined in WINDOWS.H. The last three parameters depend on the
subfunction. Although the last two parameters are declared as far pointers
to character strings, they are sometimes

far pointers to structures. To cast the pointers into far pointers to
strings, use (LPSTR).

Not all Escape subfunctions are implemented in all device drivers. In fact,
Escape has been designed to be open-ended so that manufacturers of display
devices can define their own Escape subfunctions to access certain unique
facilities of the devices. The following Escape subfunctions are the ones I
discuss in this chapter. They are implemented in all printer drivers:


nEscapeCode                      Description
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
STARTDOC                         Starts a document
ENDDOC                           Ends a document
SETABORTPROC                     Sets a pointer to the
"abort procedure"
NEWFRAME                         Ends the current page
NEXTBAND                         Gets rectangle coordi-
nates for the next band




nEscapeCode                      Description
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
ABORTDOC                         Aborts printing of
a document
GETPHYSPAGESIZE                  Gets the physical size
of the paper
QUERYESCSUPPORT                  Finds out if the driver
supports an Escape code




DEVCAPS2 used the QUERYESCSUPPORT subfunction to list supported Escape
functions. The GETPHYSPAGESIZE subfunction returns the size of the paper,
which will generally be larger than the printable area obtained from
GetDeviceCaps. We'll use other subfunctions in programs later in this
chapter. Escape always returns 0 if the subfunction is not implemented and a
negative value if an error occurs. A positive value indicates success.


The FORMFEED Program

Our first printing program does nothing but cause a printer formfeed to
eject the page. The FORMFEED program, shown in Figure 15-4, demonstrates the
absolute minimum requirements for printing.

 FORMFEED.MAK

#------------------------
# FORMFEED.MAK make file
#------------------------

formfeed.exe: formfeed.obj formfeed.def
     link formfeed, /align:16, NUL, /nod slibcew libw, formfeed
     rc formfeed.exe

formfeed.obj: formfeed.c
     cl -c -Gsw -Ow -W2 -Zp formfeed.c

 FORMFEED.C

/*---------------------------------------------
   FORMFEED.C -- Advances printer to next page
                 (c) Charles Petzold, 1990
  ---------------------------------------------*/



#include 
#include 

HDC  GetPrinterDC (void) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szMsg [] = "FormFeed" ;
     HDC         hdcPrint ;

     if (hdcPrint = GetPrinterDC ())
          {
          if (Escape (hdcPrint, STARTDOC, sizeof szMsg - 1, szMsg, NULL) >
0)
               if (Escape (hdcPrint, NEWFRAME, 0, NULL, NULL) > 0)
                    Escape (hdcPrint, ENDDOC, 0, NULL, NULL) ;

          DeleteDC (hdcPrint) ;
          }
     return FALSE ;
     }

HDC GetPrinterDC (void)
     {
     static char szPrinter [80] ;
     char        *szDevice, *szDriver, *szOutput ;

     GetProfileString ("windows", "device", ",,,", szPrinter, 80) ;

     if ((szDevice = strtok (szPrinter, "," )) &&
         (szDriver = strtok (NULL,      ", ")) &&
         (szOutput = strtok (NULL,      ", ")))

               return CreateDC (szDriver, szDevice, szOutput, NULL) ;

     return 0 ;
     }

 FORMFEED.DEF

;-------------------------------------
; FORMFEED.DEF module definition file
;-------------------------------------

NAME           FORMFEED

DESCRIPTION    'Printer Form Feed Program (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192

FORMFEED includes the GetPrinterDC function shown earlier. Other than
obtaining the printer device context (and later deleting it), the program
makes only three Escape calls. The first uses the STARTDOC subfunction to
start a new document. It tests the return value from Escape and proceeds
only if the value is positive:

if (Escape (hdcPrint, STARTDOC, sizeof szMsg - 1, szMsg, NULL) > 0)

The fourth parameter is a far pointer to the string that the Print Manager
will display in its client area to identify the document being printed.
Generally, this string includes the name of the application doing the
printing and the file being printed. In this case, it's simply the name
"FormFeed." The third parameter is the length of this string.

If STARTDOC is successful (indicated by a positive return value), then
FORMFEED calls the NEWFRAME Escape subfunction, which advances the printer
to a new page. Once again, the return value is tested:

if (Escape (hdcPrint, NEWFRAME, 0, NULL, NULL) > 0)

The third, fourth, and fifth parameters are not used in this Escape call.

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

Escape (hdcPrint, ENDDOC, 0, NULL, NULL) ;

Again, the last three parameters are not used. Note that the ENDDOC Escape
function is called only if no printing errors have been reported. If one of
the other Escape functions returns an error code, then 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 Escape calls is the easiest way to
check for errors. However, WINDOWS.H includes identifiers for the error
codes, which you can use if you want to report the particular error to the
user. For example, the NEWFRAME Escape call could return the SP_OUTOFDISK
error (-4), indicating insufficient disk space for GDI to store the printer
output necessary to trigger the printer to do a formfeed. For most printers,
this occurrence is extremely unlikely. For your own amusement, however, you
might try specifying the PostScript printer driver as your current printer,
with the output port OUTPUT.PRN. Run FORMFEED and check the size of the
file. (It will be nearly 8 KB!)

If you've ever written a simple formfeed program for MS-DOS, you know that
ASCII number 12 activates a formfeed for most printers. Why not simply open
the printer port  using the C library function open and then output an ASCII
number 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--that's available from WIN.INI. You then have to determine if
another program (the Print Manager, for instance) is currently using the
printer. You don't want the formfeed to be output in the middle of a
document, do you? Finally, you have to determine if ASCII number 12 is a
formfeed character for the connected printer. It's not universal, you know.
In fact, the formfeed 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.



PRINTING GRAPHICS AND TEXT

Printing from a Windows program usually involves more overhead than shown in
the FORMFEED program, as well as some GDI calls to actually print something.
Let's write a program that prints one page of text and graphics. We'll start
with the method shown in the FORMFEED program and then add some
enhancements. We'll be looking at four versions of this program called
PRINT1, PRINT2, PRINT3, and PRINT4. To avoid a lot of duplicated source
code, each of these programs will use functions contained in the PRINT.C
file, which is shown in Figure 15-5.

 PRINT.C

/*-------------------------------------------------------------------
   PRINT.C -- Common Routines for Print1, Print2, Print3, and Print4
              (c) Charles Petzold, 1990
  -------------------------------------------------------------------*/

#include 
#include 

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;
BOOL PrintMyPage (HWND) ;

extern HANDLE hInst ;
extern char szAppName [] ;
extern char szCaption [] ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;



     if (!hPrevInstance)
          {
          wndclass.style         = 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 = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hInst = hInstance ;

     hwnd = CreateWindow (szAppName, szCaption,
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     HMENU hMenu ;

     switch (message)
          {
          case WM_CREATE :
               hMenu = GetSystemMenu (hwnd, FALSE) ;
               AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ;
               AppendMenu (hMenu, 0, 1, "&Print") ;
               return 0 ;

          case WM_SYSCOMMAND :
               if (wParam == 1)
                    {
                    if (PrintMyPage (hwnd))
                         MessageBox (hwnd, "Could not print page",
                              szAppName, MB_OK | MB_ICONEXCLAMATION) ;
                    return 0 ;
                    }
               break ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

HDC GetPrinterDC (void)
     {
     static char szPrinter [80] ;
     char        *szDevice, *szDriver, *szOutput ;

     GetProfileString ("windows", "device", ",,,", szPrinter, 80) ;

     if ((szDevice = strtok (szPrinter, "," )) &&
         (szDriver = strtok (NULL,      ", ")) &&
         (szOutput = strtok (NULL,      ", ")))

               return CreateDC (szDriver, szDevice, szOutput, NULL) ;

     return 0 ;
     }

void PageGDICalls (HDC hdcPrn, short cxPage, short cyPage)
     {
     static char szTextStr [] = "Hello, Printer!" ;
     DWORD       dwExtent ;

     Rectangle (hdcPrn, 0, 0, cxPage, cyPage) ;

     MoveTo (hdcPrn, 0, 0) ;
     LineTo (hdcPrn, cxPage, cyPage) ;
     MoveTo (hdcPrn, cxPage, 0) ;
     LineTo (hdcPrn, 0, cyPage) ;

     SaveDC (hdcPrn) ;

     SetMapMode (hdcPrn, MM_ISOTROPIC) ;
     SetWindowExt   (hdcPrn, 1000, 1000) ;
     SetViewportExt (hdcPrn, cxPage / 2, -cyPage / 2) ;
     SetViewportOrg (hdcPrn, cxPage / 2,  cyPage / 2) ;
     Ellipse (hdcPrn, -500, 500, 500, -500) ;

     dwExtent = GetTextExtent (hdcPrn, szTextStr, sizeof szTextStr - 1) ;
     TextOut (hdcPrn, LOWORD (dwExtent) / 2, HIWORD (dwExtent) / 2,
              szTextStr, sizeof szTextStr - 1) ;

     RestoreDC (hdcPrn, -1) ;
     }

PRINT.C contains the functions WinMain, WndProc, and GetPrinterDC and a
function called PageGDICalls, which expects to receive a handle to the
printer device context and two variables containing the width and height of
the printer page. PageGDICalls draws a rectangle that encompasses the entire
page, two lines between opposite corners of the page, a circle in the middle
of the page (its diameter half the lesser of the printer height and width),
and the text "Hello, Printer!" in the center of this ellipse.

During processing of the WM_CREATE message, WndProc adds a Print option to
the system menu. Selecting this option causes a call to PrintMyPage, a
function that we'll enhance over the course of the four versions of the
program. PrintMyPage returns TRUE (nonzero) if it encounters an error during
printing and returns FALSE otherwise. If PrintMyPage returns TRUE, WndProc
displays a message box to inform you of the error.

Bare-Bones Printing

PRINT1, the first version of the printing program, is shown in Figure 15-6.
After compiling PRINT1, you can execute it and then select Print from the
system menu. If your WIN.INI file has the line Spooler=yes and if Windows
can find PRINTMAN.EXE, you should see the Print Manager icon appear at the
bottom of the screen. If the TEMP variable in your MS-DOS environment
indicates a fixed disk (or if you have no TEMP variable), then you should
see some disk activity as the GDI module saves the printer output to a
temporary file. You won't be able to do anything in Windows during this
time. After PRINT1 has finished, the Print Manager should display the text
"Print1: Printing" in its client area and begin sending the disk file out to
the printer. You'll be able to work normally in Windows again.

Let's look at the code in PRINT1.C. If PrintMyPage can't obtain a device
context handle for the printer, it returns TRUE, and WndProc displays the
message box indicating an error. If the function succeeds in obtaining the
device context handle, it then determines the horizontal and vertical size
of the page in pixels by calling GetDeviceCaps:

xPage = GetDeviceCaps (hdcPrn, HORZRES) ;
yPage = GetDeviceCaps (hdcPrn, VERTRES) ;

This is not the full size of the paper but rather its printable area. After
that call, the code in PRINT1's PrintMyPage function is structurally the
same as the code in FORMFEED, except  that PRINT1 calls PageGDICalls between
the STARTDOC and NEWFRAME Escape calls. Only if both the STARTDOC and
NEWFRAME calls are successful does PRINT1 call ENDDOC Escape.

 PRINT1.MAK

#----------------------
# PRINT1.MAK make file
#----------------------

print1.exe : print.obj print1.obj print1.def
     link print1 print, /align:16, NUL, /nod slibcew libw, print1
     rc print1.exe

print.obj : print.c
     cl -c -Gsw -Ow -W2 -Zp print.c

print1.obj : print1.c
     cl -c -Gsw -Ow -W2 -Zp print1.c

 PRINT1.C

/*---------------------------------------
   PRINT1.C -- Bare-Bones Printing
               (c) Charles Petzold, 1990
  ---------------------------------------*/

#include 

HDC  GetPrinterDC (void) ;              // in PRINT.C
void PageGDICalls (HDC, short, short) ;

HANDLE hInst ;
char   szAppName [] = "Print1" ;
char   szCaption [] = "Print Program 1" ;

BOOL PrintMyPage (HWND hwnd)
     {
     static char szMessage [] = "Print1: Printing" ;
     BOOL        bError = FALSE ;
     HDC         hdcPrn ;
     short       xPage, yPage ;

     if (NULL == (hdcPrn = GetPrinterDC ()))
          return TRUE ;



     xPage = GetDeviceCaps (hdcPrn, HORZRES) ;
     yPage = GetDeviceCaps (hdcPrn, VERTRES) ;

     if (Escape (hdcPrn, STARTDOC, sizeof szMessage - 1, szMessage, NULL) >
0)
          {
          PageGDICalls (hdcPrn, xPage, yPage) ;

          if (Escape (hdcPrn, NEWFRAME, 0, NULL, NULL) > 0)
               Escape (hdcPrn, ENDDOC, 0, NULL, NULL) ;
          else
               bError = TRUE ;
          }
     else
          bError = TRUE ;

     DeleteDC (hdcPrn) ;
     return bError ;
     }

 PRINT1.DEF

;-----------------------------------
; PRINT1.DEF module definition file
;-----------------------------------

NAME           PRINT1

DESCRIPTION    'Printing Program No. 1 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc


Setting an Abort Procedure

The enhancement that we'll add in the PRINT2 version of our program prevents
problems related to disk space. If you try to run PRINT1 when the drive
containing the TEMP subdirectory lacks sufficient space to store the full
page of graphics output, the NEWFRAME Escape call will return an
SP_OUTOFDISK error. This error could result from the presence in the TEMP
subdirectory of other temporary print files created by GDI for printing. If
the Print Manager were given enough time to send these files to the printer,
then the program currently printing could continue. It wouldn't be necessary
for Windows to return an SP_OUTOFDISK error to the program. However, the
Print Manager cannot transfer these files to the printer because--like all
other Windows programs--it isn't receiving messages during the time your
program is printing.

This problem is solved with something called an "abort procedure". The abort
procedure is a small exported function in your program. You give Windows the
address of this function using the Escape SETABORTPROC subfunction. If GDI
runs out of disk space while creating temporary print files, and if enough
space could eventually become available by having the Print Manager send
existing print files to the printer, then the GDI module calls the program's
abort procedure. The abort procedure then effectively yields control to
allow the Print Manager to print.

Let's look first at what's required to add an abort procedure to the
printing logic and then examine some of the ramifications. The abort
procedure is commonly called AbortProc, and it takes the following form:

BOOL FAR PASCAL AbortProc (HDC hdcPrn, short nCode)
     {
[other program lines]
     }

The function must be listed in the EXPORTS section of your module definition
file. Before printing, you must obtain a pointer to this function from
MakeProcInstance:

FARPROC   lpfnAbortProc ;
[other program lines]
lpfnAbortProc = MakeProcInstance (AbortProc, hInstance) ;

You then set the abort procedure using the Escape SETABORTPROC subfunction.
The lpsDataIn parameter is the pointer returned from MakeProcInstance:

Escape (hdcPrn, SETABORTPROC, 0, (LPSTR) lpfnAbortProc, NULL) ;

You make this call before the STARTDOC Escape call. You don't need to
"unset" the abort procedure after you finish printing.

While processing the NEWFRAME Escape call (that is, while playing the
metafile into the device driver and creating the temporary printer output
files), GDI frequently calls the abort procedure. The hdcPrn parameter is
the printer device context handle. The nCode parameter is 0 if all is going
well or is SP_OUTOFDISK if the GDI module has run out of disk space because
of the temporary printer output files.

AbortProc must return TRUE (nonzero) if the print job is to be continued and
returns FALSE (0) if the print job is to be aborted. If AbortProc receives
an nCode parameter of  SP_OUTOFDISK and returns FALSE, then the NEWFRAME
Escape call currently in progress returns an SP_APPABORT error code (equal
to -2), and the print job is aborted.

The abort procedure can be as simple as this:

BOOL FAR PASCAL AbortProc (HDC hdcPrn, short nCode)
     {
     MSG   msg ;

     while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return TRUE ;
     }

This function may seem a little peculiar. In fact, it looks suspiciously
like a message loop. What's a message loop doing here of all places? Well,
it is a message loop. You'll note, however, that this message loop calls
PeekMessage rather than GetMessage. I discussed PeekMessage in connection
with the RANDRECT program at the end of Chapter 12. You'll recall that
PeekMessage returns control to a program with a message from the program's
message queue (just like GetMessage) but also returns control if there are
no messages waiting in any program's message queue.

The message loop in the AbortProc function repeatedly calls PeekMessage
while PeekMessage returns TRUE. This TRUE value means that PeekMessage has
retrieved a message that can be sent to one of the program's window
procedures using TranslateMessage and DispatchMessage. When there are no
more messages in the program's message queue, Windows allows other programs
to process messages from their queues. When there are no more messages in
any program's message queue, Windows returns control to the program calling
PeekMessage (that is, to the AbortProc function). The return value of
PeekMessage is then FALSE, so AbortProc returns control to Windows.

The PRINT1 version of our program doesn't yield control during the entire
time it is printing. Windows is essentially frozen during that time because
no other program can process messages. As you've probably discovered by now,
the PrintMyPage function in PRINT1 can take a while. Only when the function
is finished can the Print Manager actually start to print. An abort
procedure gives the Print Manager--and other programs running under
Windows--a chance to run while a program is printing.


How Windows Uses AbortProc

When a program is printing, the bulk of the work takes place during the
NEWFRAME Escape call. Before that call, the GDI module simply adds another
record to the disk-based metafile every time the program calls a GDI drawing
function. When GDI gets the NEWFRAME Escape call, it plays this metafile
into the device driver once for each band the device driver defines on a
page. GDI then stores the printer output created by the printer driver in a
file. If the Print Manager isn't loaded, the GDI module itself must write
this printer output to the printer.

During the NEWFRAME Escape call, the GDI module calls the abort procedure
you've set. Normally, the nCode parameter is 0, but if GDI has run out of
disk space because of the presence of other temporary files that haven't
been printed yet, then the nCode parameter is SP_OUTOFDISK. (You wouldn't
normally check this value, but you can if you want.) The abort procedure
then goes into its PeekMessage loop. The loop first retrieves messages from
the program's own message queue and then yields control so that other
programs can retrieve and process their own messages. When no messages
remain in any program's queue, control passes to another program currently
waiting for its own PeekMessage call to return.

One of those programs is PRINTMAN.EXE, which also uses a PeekMessage call to
retrieve messages. When the Print Manager returns from the PeekMessage call
in its own message loop, it can transfer part of a disk file to the printer.
The Print Manager then calls PeekMessage again in its own message loop. If
there are still no messages in any program's message queue, control returns
to AbortProc, and PeekMessage returns FALSE. The abort procedure then drops
out of its message loop and returns a TRUE value to the GDI module to
indicate that printing should continue. The GDI module then continues to
process the NEWFRAME Escape call.

While the main purpose of the abort procedure is to allow the Print Manager
the opportunity to transfer existing files to the printer to free up disk
space, it also allows all other programs to run during the time a program is
printing. This effect of the abort procedure is particularly important if
the Print Manager isn't installed.


Implementing an Abort Procedure

Let's quickly review the mechanics of the abort procedure. You define an
abort procedure that looks like this:

BOOL FAR PASCAL AbortProc (HDC hdcPrn, short nCode)
     {
     MSG  msg ;

     while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return TRUE ;
     }

You list AbortProc in the EXPORTS section of your module definition file.
You obtain a pointer to the function using MakeProcInstance:

lpfnAbortProc = MakeProcInstance (AbortProc, hInstance) ;

When you want to print something, you give Windows this pointer with an
Escape call:

Escape (hdcPrn, SETABORTPROC, 0, (LPSTR) lpfnAbortProc, NULL) ;

You make this Escape call before the Escape call for STARTDOC. And that's
it.

Well, not quite. We've overlooked a problem with that PeekMessage loop in
AbortProc--a big problem. AbortProc is called only while your program is in
the midst of printing. Some very ugly things can happen if you retrieve a
message in AbortProc and dispatch it to your own window procedure. A user
could select Print from the menu again. But the program is already in the
middle of the printing routine. A user could load a new file into the
program while the program is trying to print the previous file. A user could
even quit your program! If that happens, all your program's windows will be
destroyed. You'll eventually return from the printing routine, but you'll
have nowhere to go except to a window procedure that's no longer valid.

This stuff boggles the mind. Your program isn't prepared for it. For this
reason, when you set an abort procedure, you should first disable your
program's window so that it can't receive keyboard and mouse input. You do
this with:

EnableWindow (hwnd, FALSE) ;

This prevents keyboard and mouse input from getting into the message queue.
The user therefore can't do anything with your program during the time it's
printing. When printing is finished, you reenable the window for input:

EnableWindow (hwnd, TRUE) ;

So why, you ask, do we even bother with the TranslateMessage and
DispatchMessage calls in AbortProc when no keyboard or mouse messages will
get into the message queue in the first place? It's true that the
TranslateMessage call isn't strictly needed (although it's almost always
included). But we must use DispatchMessage in case a WM_PAINT message gets
in the message queue. If WM_PAINT isn't processed properly with a BeginPaint
and EndPaint pair in the window procedure, the message will remain in the
queue and clog up the works, because PeekMessage will never return a FALSE.

When you disable your window during the time you're printing, your program
remains inert on the display. But a user can switch to another program and
do some work there, and Print Manager can continue sending output files to
the printer.

The PRINT2 program, shown in Figure 15-7, adds an abort procedure (and the
necessary support) to the logic in PRINT1. More specifically, PRINT2 adds
the abort procedure (including a listing in the EXPORTS section of
PRINT2.DEF), a call to MakeProcInstance and Escape using the SETABORTPROC
subfunction, a FreeProcInstance call at the end, and two calls to
EnableWindow, the first to disable the window and the second to reenable it.

 PRINT2.MAK

#----------------------
# PRINT2.MAK make file
#----------------------

print2.exe : print.obj print2.obj print2.def
     link print2 print, /align:16, NUL, /nod slibcew libw, print2
     rc print2.exe

print.obj : print.c
     cl -c -Gsw -Ow -W2 -Zp print.c

print2.obj : print2.c
     cl -c -Gsw -Ow -W2 -Zp print2.c

 PRINT2.C

/*------------------------------------------
   PRINT2.C -- Printing with Abort Function
               (c) Charles Petzold, 1990
  ------------------------------------------*/

#include 

HDC  GetPrinterDC (void) ;              // in PRINT.C
void PageGDICalls (HDC, short, short) ;

HANDLE hInst ;
char   szAppName [] = "Print2" ;
char   szCaption [] = "Print Program 2 (Abort Function)" ;

BOOL FAR PASCAL AbortProc (HDC hdcPrn, short nCode)
     {
     MSG   msg ;

     while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return TRUE ;
     }



BOOL PrintMyPage (HWND hwnd)
     {
     static char szMessage [] = "Print2: Printing" ;
     BOOL        bError = FALSE ;
     FARPROC     lpfnAbortProc ;
     HDC         hdcPrn ;
     RECT        rect ;
     short       xPage, yPage ;

     if (NULL == (hdcPrn = GetPrinterDC ()))
          return TRUE ;

     xPage = GetDeviceCaps (hdcPrn, HORZRES) ;
     yPage = GetDeviceCaps (hdcPrn, VERTRES) ;

     EnableWindow (hwnd, FALSE) ;

     lpfnAbortProc = MakeProcInstance (AbortProc, hInst) ;
     Escape (hdcPrn, SETABORTPROC, 0, (LPSTR) lpfnAbortProc, NULL) ;

     if (Escape (hdcPrn, STARTDOC, sizeof szMessage - 1, szMessage, NULL) >
0)
          {
          PageGDICalls (hdcPrn, xPage, yPage) ;

          if (Escape (hdcPrn, NEWFRAME, 0, NULL, NULL) > 0)
               Escape (hdcPrn, ENDDOC, 0, NULL, NULL) ;
          else
               bError = TRUE ;
          }
     else
          bError = TRUE ;

     if (!bError)
          Escape (hdcPrn, ENDDOC, 0, NULL, NULL) ;

     FreeProcInstance (lpfnAbortProc) ;
     EnableWindow (hwnd, TRUE) ;
     DeleteDC (hdcPrn) ;
     return bError ;
     }

 PRINT2.DEF

;-----------------------------------
; PRINT2.DEF module definition file
;-----------------------------------

NAME           PRINT2
DESCRIPTION    'Printing Program No. 2 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               AbortProc


Adding a Printing Dialog Box

PRINT2 is not entirely satisfactory. First, the program doesn't directly
indicate when it is printing and when it is finished with printing. Only
when you poke at the program with the mouse and find that it doesn't respond
can you determine that it must still be processing the PrintMyPage routine.
Nor does PRINT2 give the user the opportunity to cancel the print job before
it shows up in the Print Manager's client area.

You're probably aware that most Windows programs give users a chance to
cancel a printing operation currently in progress. A small dialog box comes
up on the screen; it contains some text and a push button labeled Cancel.
The program displays this dialog box during the entire time that GDI is
saving the printer output in a disk file or (if the Print Manager isn't
loaded) while the printer is printing. This is a modeless dialog box, and
you must supply the dialog procedure. As for all dialog boxes, you include
the name of the dialog procedure in the EXPORTS section of the module
definition file and use MakeProcInstance to obtain a pointer to the
function.

This dialog box is often called the "abort dialog box," and the dialog
procedure is often called the "abort dialog procedure." To distinguish it
more clearly from the "abort procedure," I'll call this dialog procedure the
"printing dialog procedure." The abort procedure (with the name AbortProc)
and the printing dialog procedure (which I'll name PrintDlgProc) are two
separate exported functions. If you want to print in a professional
Windows-like manner, you must have both of these.

These two functions interact as follows. The PeekMessage loop in AbortProc
must be modified to send messages for the modeless dialog box to the dialog
box window procedure. PrintDlgProc must process WM_COMMAND messages to check
the status of the Cancel button. If the Cancel button is pressed, it sets a
variable called bUserAbort to TRUE. The value returned from AbortProc is the
inverse of bUserAbort. You'll recall that AbortProc returns TRUE to continue
printing and FALSE to abort printing. In PRINT2 we always returned TRUE. Now
we'll return FALSE if the user clicks the Cancel button in the printing
dialog box. This logic is implemented in the PRINT3 program, shown in Figure
15-8, beginning on the following page.

 PRINT3.MAK

#----------------------
# PRINT3.MAK make file
#----------------------

print3.exe : print.obj print3.obj print3.def print.res
     link print3 print, /align:16, NUL, /nod slibcew libw, print3
     rc print.res print3.exe

print.obj : print.c
     cl -c -Gsw -Ow -W2 -Zp print.c

print3.obj : print3.c
     cl -c -Gsw -Ow -W2 -Zp print3.c

print.res : print.rc
     rc -r print.rc

 PRINT3.C

/*---------------------------------------
   PRINT3.C -- Printing with Dialog Box
               (c) Charles Petzold, 1990
  ---------------------------------------*/

#include 

HDC  GetPrinterDC (void) ;              // in PRINT.C
void PageGDICalls (HDC, short, short) ;

HANDLE hInst ;
char   szAppName [] = "Print3" ;
char   szCaption [] = "Print Program 3 (Dialog Box)" ;

BOOL   bUserAbort ;
HWND   hDlgPrint ;

BOOL FAR PASCAL PrintDlgProc (HWND hDlg, WORD message, WORD wParam, LONG
lParam)
     {
     switch (message)
          {
          case WM_INITDIALOG :
               SetWindowText (hDlg, szAppName) ;
               EnableMenuItem (GetSystemMenu (hDlg, FALSE), SC_CLOSE,
                                                            MF_GRAYED) ;
               return TRUE ;



          case WM_COMMAND :
               bUserAbort = TRUE ;
               EnableWindow (GetParent (hDlg), TRUE) ;
               DestroyWindow (hDlg) ;
               hDlgPrint = 0 ;
               return TRUE ;
          }
     return FALSE ;
     }

BOOL FAR PASCAL AbortProc (HDC hdcPrn, short nCode)
     {
     MSG   msg ;

     while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
          {
          if (!hDlgPrint || !IsDialogMessage (hDlgPrint, &msg))
               {
               TranslateMessage (&msg) ;
               DispatchMessage (&msg) ;
               }
          }
     return !bUserAbort ;
     }

BOOL PrintMyPage (HWND hwnd)
     {
     static char szMessage [] = "Print3: Printing" ;
     BOOL        bError = FALSE ;
     FARPROC     lpfnAbortProc, lpfnPrintDlgProc ;
     HDC         hdcPrn ;
     RECT        rect ;
     short       xPage, yPage ;

     if (NULL == (hdcPrn = GetPrinterDC ()))
          return TRUE ;

     xPage = GetDeviceCaps (hdcPrn, HORZRES) ;
     yPage = GetDeviceCaps (hdcPrn, VERTRES) ;

     EnableWindow (hwnd, FALSE) ;

     bUserAbort = FALSE ;
     lpfnPrintDlgProc = MakeProcInstance (PrintDlgProc, hInst) ;
     hDlgPrint = CreateDialog (hInst, "PrintDlgBox", hwnd, lpfnPrintDlgProc)
;

     lpfnAbortProc = MakeProcInstance (AbortProc, hInst) ;
     Escape (hdcPrn, SETABORTPROC, 0, (LPSTR) lpfnAbortProc, NULL) ;
     if (Escape (hdcPrn, STARTDOC, sizeof szMessage - 1, szMessage, NULL) >
0)
          {
          PageGDICalls (hdcPrn, xPage, yPage) ;

          if (Escape (hdcPrn, NEWFRAME, 0, NULL, NULL) > 0)
               Escape (hdcPrn, ENDDOC, 0, NULL, NULL) ;
          else
               bError = TRUE ;
          }
     else
          bError = TRUE ;

     if (!bUserAbort)
          {
          EnableWindow (hwnd, TRUE) ;
          DestroyWindow (hDlgPrint) ;
          }

     FreeProcInstance (lpfnPrintDlgProc) ;
     FreeProcInstance (lpfnAbortProc) ;
     DeleteDC (hdcPrn) ;

     return bError || bUserAbort ;
     }

 PRINT.RC

/*--------------------------
   PRINT.RC resource script
  --------------------------*/

#include 

PrintDlgBox DIALOG 40, 40, 120, 40
     STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_VISIBLE
     {
     CTEXT          "Cancel Printing", -1,  4,  6, 120, 12
     DEFPUSHBUTTON  "Cancel",    IDCANCEL, 44, 22,  32, 14, WS_GROUP
     }

 PRINT3.DEF

;-----------------------------------
; PRINT3.DEF module definition file
;-----------------------------------

NAME           PRINT3

DESCRIPTION    'Printing Program No. 3 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               AbortProc
               PrintDlgProc

Two global variables are added to PRINT3: a BOOL called bUserAbort and a
handle to the dialog box window called hDlgPrint. The PrintMyPage function
initializes bUserAbort to FALSE, and as in PRINT2, the program's main window
is disabled. PrintMyPage then calls MakeProcInstance for both AbortProc and
PrintDlgProc. The pointer to AbortProc is used in the SETABORTPROC Escape
call, and the pointer to PrintDlgProc is used in a CreateDialog call. The
window handle returned from CreateDialog is saved in hDlgPrint.

The message loop in AbortProc now looks like this:

while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
     {
     if (!hDlgPrint || !IsDialogMessage (hDlgPrint, &msg))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     }
return !bUserAbort ;

It calls PeekMessage only if bUserAbort is FALSE, that is, if the user
hasn't yet aborted the printing operation. The IsDialogMessage function is
required to send the message to the modeless dialog box. As is normal with
modeless dialog boxes, the handle to the dialog box window is checked before
this call is made. AbortProc returns the inverse of bUserAbort. Initially,
bUserAbort is FALSE, so AbortProc returns TRUE, indicating that printing is
to continue. But bUserAbort could be set to TRUE in the printing dialog
procedure.

The PrintDlgProc function is fairly simple. While processing WM_INITDIALOG,
the function sets the window caption to the name of the program and disables
the Close option on the system menu. If the user clicks the Cancel button,
PrintDlgProc receives a WM_COMMAND message:

case WM_COMMAND :
     bUserAbort = TRUE ;
     EnableWindow (GetParent (hDlg), TRUE) ;
     DestroyWindow (hDlg) ;
     hDlgPrint = 0 ;
     return TRUE ;

Setting bUserAbort to TRUE indicates that the user has decided to cancel the
printing operation. The main window is enabled, and the dialog box is
destroyed. (It is important that you perform these two actions in this
order. Otherwise, some other program running under Windows will become the
active program, and your program might disappear into the background.) As is
normal, hDlgPrint is set to 0 to prevent IsDialogMessage from being called
in the message loop.

The only time this dialog box receives messages is when AbortProc retrieves
messages with PeekMessage and sends them to the dialog box window procedure
with IsDialogMessage. The only time AbortProc is called is when the GDI
module is processing the NEWFRAME Escape function. If GDI sees that the
return value from AbortProc is FALSE, it returns control from the Escape
call back to PrintMyPage. It doesn't return an error code. At that point,
PrintMyPage thinks that the page is complete and calls the ENDDOC Escape
function. Nothing is printed, however, because the GDI module didn't finish
processing the NEWFRAME Escape call.

Some cleanup remains. If the user didn't cancel the print job from the
dialog box, then the dialog box is still displayed. PrintMyPage reenables
its main window and destroys the dialog box:

if (!bUserAbort)
     {
     EnableWindow (hwnd, TRUE) ;
     DestroyWindow (hDlgPrint) ;
     }

Two variables tell you what happened: bUserAbort tells you if the user
aborted the print job, and bError tells you if an error occurred. You can do
what you want with these variables. PrintMyPage simply performs a logical OR
operation to return to WndProc:

return bError || bUserAbort ;


Adding Printing to POPPAD

Now we're ready to add a printing facility to the POPPAD series of programs
and declare POPPAD finished. You'll need the various POPPAD files from
Chapter 10, plus the three new files in Figure 15-9.

 POPPAD.MAK

#----------------------
# POPPAD.MAK make file
#----------------------

poppad.exe : poppad.obj  poppadf.obj poppadp.obj \
             filedlg.obj poppad.def  poppad.res
     link poppad poppadf poppadp filedlg, poppad.exe /align:16, \
          NUL, /nod slibcew libw, poppad
     rc poppad.res  poppad.exe

poppad.obj : poppad.c poppad.h
     cl -c -Gsw -Ow -W2 -Zp poppad.c

poppadf.obj : poppadf.c
     cl -c -Gsw -Ow -W2 -Zp poppadf.c

poppadp.obj : poppadp.c
     cl -c -Gsw -Ow -W2 -Zp poppadp.c

filedlg.obj : filedlg.c filedlg.h
     cl -c -Gsw -Ow -W2 -Zp filedlg.c

poppad.res : poppad.rc poppad.h poppad.ico filedlg.dlg filedlg.h
     rc -r poppad.rc

 POPPADP.C

/*----------------------------------------------
   POPPADP.C -- Popup Editor Printing Functions
                (c) Charles Petzold, 1990
  ----------------------------------------------*/

#include 
#include 
#include "filedlg.h"                    // for IDD_FNAME definition

extern char szAppName [] ;              // in POPPAD.C



BOOL bUserAbort ;
HWND hDlgPrint ;

BOOL FAR PASCAL PrintDlgProc (HWND hDlg, WORD message, WORD wParam, LONG
lParam)
     {
     switch (message)
          {
          case WM_INITDIALOG :
               EnableMenuItem (GetSystemMenu (hDlg, FALSE), SC_CLOSE,
                                                            MF_GRAYED) ;
               return TRUE ;

          case WM_COMMAND :
               bUserAbort = TRUE ;
               EnableWindow (GetParent (hDlg), TRUE) ;
               DestroyWindow (hDlg) ;
               hDlgPrint = 0 ;
               return TRUE ;
          }
     return FALSE ;
     }

BOOL FAR PASCAL AbortProc (HDC hPrinterDC, short nCode)
     {
     MSG   msg ;

     while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
          {
          if (!hDlgPrint || !IsDialogMessage (hDlgPrint, &msg))
               {
               TranslateMessage (&msg) ;
               DispatchMessage (&msg) ;
               }
          }
     return !bUserAbort ;
     }

HDC GetPrinterDC (void)
     {
     static char szPrinter [80] ;
     char        *szDevice, *szDriver, *szOutput ;

     GetProfileString ("windows", "device", ",,,", szPrinter, 80) ;

     if ((szDevice = strtok (szPrinter, "," )) &&
         (szDriver = strtok (NULL,      ", ")) &&
         (szOutput = strtok (NULL,      ", ")))

               return CreateDC (szDriver, szDevice, szOutput, NULL) ;

     return 0 ;
     }
BOOL PrintFile (HANDLE hInst, HWND hwnd, HWND hwndEdit, char *szFileName)
     {
     BOOL       bError = FALSE ;
     char       szMsg [40] ;
     FARPROC    lpfnAbortProc, lpfnPrintDlgProc ;
     HDC        hdcPrn ;
     NPSTR      psBuffer ;
     RECT       rect ;
     short      yChar, nCharsPerLine, nLinesPerPage,
                nTotalLines, nTotalPages, nPage, nLine, nLineNum = 0 ;
     TEXTMETRIC tm ;

     if (0 == (nTotalLines = (short) SendMessage (hwndEdit,
                                             EM_GETLINECOUNT, 0, 0L)))
          return FALSE ;

     if (NULL == (hdcPrn = GetPrinterDC ()))
          return TRUE ;

     GetTextMetrics (hdcPrn, &tm) ;
     yChar = tm.tmHeight + tm.tmExternalLeading ;

     nCharsPerLine = GetDeviceCaps (hdcPrn, HORZRES) / tm.tmAveCharWidth ;
     nLinesPerPage = GetDeviceCaps (hdcPrn, VERTRES) / yChar ;
     nTotalPages   = (nTotalLines + nLinesPerPage - 1) / nLinesPerPage ;

     psBuffer = (NPSTR) LocalAlloc (LPTR, nCharsPerLine) ;

     EnableWindow (hwnd, FALSE) ;

     bUserAbort = FALSE ;
     lpfnPrintDlgProc = MakeProcInstance (PrintDlgProc, hInst) ;
     hDlgPrint = CreateDialog (hInst, "PrintDlgBox", hwnd, lpfnPrintDlgProc)
;
     SetDlgItemText (hDlgPrint, IDD_FNAME, szFileName) ;

     lpfnAbortProc = MakeProcInstance (AbortProc, hInst) ;
     Escape (hdcPrn, SETABORTPROC, 0, (LPSTR) lpfnAbortProc, NULL) ;

     strcat (strcat (strcpy (szMsg, szAppName), " - "), szFileName) ;

     if (Escape (hdcPrn, STARTDOC, strlen (szMsg), szMsg, NULL) > 0)
          {
          for (nPage = 0 ; nPage < nTotalPages ; nPage++)
               {
               for (nLine = 0 ; nLine < nLinesPerPage &&
                                nLineNum < nTotalLines ; nLine++,
nLineNum++)
                    {
                    *(short *) psBuffer = nCharsPerLine ;
                    TextOut (hdcPrn, 0, yChar * nLine, psBuffer,
                         (short) SendMessage (hwndEdit, EM_GETLINE,
                                        nLineNum, (LONG) (LPSTR) psBuffer))
;
                    }

               if (Escape (hdcPrn, NEWFRAME, 0, NULL, (LPSTR) &rect) < 0)
                    {
                    bError = TRUE ;
                    break ;
                    }

               if (bUserAbort)
                    break ;
               }
          }
     else
          bError = TRUE ;

     if (!bError)
          Escape (hdcPrn, ENDDOC, 0, NULL, NULL) ;

     if (!bUserAbort)
          {
          EnableWindow (hwnd, TRUE) ;
          DestroyWindow (hDlgPrint) ;
          }

     if (bError || bUserAbort)
          {
          strcat (strcpy (szMsg, "Could not print: "), szFileName) ;
          MessageBox (hwnd, szMsg, szAppName, MB_OK | MB_ICONEXCLAMATION) ;
          }

     LocalFree ((LOCALHANDLE) psBuffer) ;
     FreeProcInstance (lpfnPrintDlgProc) ;
     FreeProcInstance (lpfnAbortProc) ;
     DeleteDC (hdcPrn) ;

     return bError || bUserAbort ;
     }

 POPPAD.DEF

;-----------------------------------
; POPPAD.DEF module definition file
;-----------------------------------

NAME           POPPAD
DESCRIPTION    'Popup Editor (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               AboutDlgProc
               FileOpenDlgProc
               FileSaveDlgProc
               PrintDlgProc
               AbortProc

POPPADP.C is structurally similar to PRINT3.C except that it is able to
print multiple pages. The PrintFile routine performs some calculations to
determine the number of characters it can fit on a line and the number of
lines it can fit on a page. This process involves calls to GetDeviceCaps to
determine the resolution of the page and to GetTextMetrics for the
dimensions of a character.

The program obtains the total number of lines in the document (the variable
nTotalLines) by sending an EM_GETLINECOUNT message to the edit control. A
buffer for holding the contents of each line is allocated from local memory.
For each line, the first word of this buffer is set to the number of
characters in the line. Sending the edit control an EM_GETLINE message
copies a line into the buffer; the line is then sent to the printer device
context using TextOut.

The program breaks from the for loop incrementing the page number if the
NEWFRAME Escape call returns an error or if bUserAbort is TRUE. Although the
NEWFRAME call will return before GDI finishes the call if the return value
of the abort procedure is FALSE, it doesn't return an error. For this
reason, bUserAbort is tested explicitly before the next page is started. If
no error is reported, the ENDDOC Escape call is made:

if (!bError)
     Escape (hdcPrn, ENDDOC, 0, NULL, NULL) ;

You might want to experiment with POPPAD by printing a multipage file. The
file being printed first shows up in the Print Manager's client area after
GDI has finished processing the first NEWFRAME Escape call. At that time,
the Print Manager starts sending the file to the printer. If you then cancel
the print job from POPPAD, the Print Manager aborts the printing
also--that's a result of returning FALSE from the abort procedure. Once the
file appears in the Print Manager's client area, you can also cancel the
printing by selecting Terminate from the Queue menu. In that case, the
NEWFRAME Escape call in progress in POPPAD returns an SP_USERABORT error
(equal to -3).

Programmers new to Windows often become inordinately obsessed with the
ABORTDOC Escape function. This function is rarely used in printing that also
uses the NEWFRAME Escape function. As you can see in POPPAD, a user can
cancel a print job at almost any time, either through POPPAD's printing
dialog box or through the Print Manager. Neither requires that the program
use the ABORTDOC Escape function. The only time that ABORTDOC would be
allowed in POPPAD is between the STARTDOC Escape call and the first NEWFRAME
Escape call, but that code goes so quickly that ABORTDOC isn't necessary.

Figure 15-10 shows the correct sequence of Escape calls for printing a
multipage document. The best place to check for a bUserAbort value of TRUE
is after each NEWFRAME Escape call. The ENDDOC Escape function is used only
when the previous Escape calls have proceeded without error. In fact, once
you get an error from any Escape call, the show is over, and you can go
home.


Handling Error Codes

We have been handling the return value from the Escape function in a
relatively simple manner: If Escape returns a negative value, then an error
has occurred, and the printing operation is aborted. You can report more
precise errors to the user by checking the Escape return value against five
identifiers defined in WINDOWS.H. WINDOWS.H also includes an identifier
called SP_NOTREPORTED, which is equal to 0x4000. If a bitwise AND of the
return value from Escape with SP_NOTREPORTED is 0, then the error has
already been reported to the user. A bitwise OR of the return value of
Escape with  SP_NOTREPORTED can be used to compare with the five error-code
identifiers whether the error has been reported or not.

The following function shows one method of obtaining a text string
identifying the error. The function returns NULL if no error has occurred or
if the error has already been reported to the user:

char *GetErrorText (short nEscapeReturn)
     {
     static char *szErrorText [] = { "General Error",
                                     "Canceled from Program",
                                     "Canceled from Print Manager",
                                     "Out of disk space",
                                     "Out of memory space" } ;

     if (nEscapeReturn >= 0)
          return NULL ;

     if ((nEscapeReturn & SP_NOTREPORTED) == 0)
          return NULL ;

     return szErrorText [~nEscapeReturn] ;
     }

  (Figure 15-10. may be found in the printed book.)

The five error codes (with some likely causes) are as follows:

  þ   SP_ERROR (0xFFFF, or -1)--Defined as indicating a "general error,"
      this is the only error code that can be returned from STARTDOC. It can
      occur if the GDI module or the printer device driver can't begin a
      document. If the Print Manager isn't loaded, you can also get this
      error from STARTDOC if another program is currently printing or if the
      printer is off line or has no paper.

  þ   SP_APPABORT (0xFFFE, or -2)--This code is documented as indicating
      that the program's abort procedure has returned a FALSE value.
      However, this is the case only if the Print Manager isn't loaded. If
      the Print Manager is loaded and if the abort procedure is passed an
      nCode parameter of 0 and then returns a FALSE, the NEWFRAME Escape
      call will return a positive value, not an SP_APPABORT error.

  þ   SP_USERABORT (0xFFFD, or -3)--This code indicates that the user
      canceled the printing job from the Print Manager.

  þ   SP_OUTOFDISK (0xFFFC, or -4)--This code indicates that no more disk
      space is available. You'll encounter this error code if the disk drive
      containing the TEMP subdirectory can't accommodate any temporary
      metafiles or spooler files. If the TEMP subdirectory has some existing
      temporary spooler files, then the abort procedure is called during a
      NEWFRAME or NEXTBAND Escape call with an nCode parameter of
      SP_OUTOFDISK. If the abort procedure then returns FALSE, the Escape
      call returns SP_OUTOFDISK.

  þ   SP_OUTOFMEMORY (0xFFFB, or -5)--This code indicates that insufficient
      memory is available for printing.



THE TECHNIQUE OF BANDING

Banding is the technique of defining a page of graphics as a series of
separately constructed rectangles called bands. This approach relieves a
printer driver of the necessity of constructing an entire bitmapped page
image in memory. Banding is most important for raster printers that have no
high-level page-composition control, such as dot-matrix printers and some
laser printers.

Banding is one of the most misunderstood aspects of programming for the
printer in Windows. Part of the problem lies in the documentation for the
GetDeviceCaps function. The RC_BANDING bit of the value returned from
GetDeviceCaps with the RASTERCAPS index is documented as "requires banding
support." Programmers looking at this documentation assume that their
applications must use banding with such printers. But this isn't  so. Most
of the information available from GetDeviceCaps is intended solely for the
GDI module. This information allows GDI to determine what the device can do
by itself and what it needs help with. The banding requirement falls into
this category.

In general, an application program doesn't need to include its own banding
logic. As you've seen, when you make GDI calls that define a page of
graphics, the GDI module stores these calls in a metafile and then uses
banding to set a clipping region before playing this metafile into the
printer device driver. This is transparent to the application program. Under
certain conditions, however, an application might want to take over the
responsibility for doing banding. When an application uses banding, the GDI
module doesn't create the intermediary metafile. Instead, the drawing
commands for each band are passed to the printer device driver. There are
two advantages to this approach:

  þ   It can increase printing speed. The application needs to call only
      those GDI functions that draw something in each particular band, which
      is faster than having the GDI module play the entire metafile into the
      device driver for each band. Even if the program simply draws the
      entire page for each band, the process can still be faster than having
      the GDI module create and read the disk-based metafile, because the
      program doesn't have to access a disk.

  þ   It can reduce the disk space normally required for printing. If the
      application is printing bitmaps but is not doing its own banding, then
      these bitmaps must be stored in the metafile that GDI creates. This
      situation can result in a metafile as large as the printer output file
      that the GDI module eventually creates.

  þ   Banding is particularly important for printing bitmaps, because they
      occupy a large amount of space in the metafile. (Printing a bitmap
      requires selecting the bitmap into a memory device context and using
      BitBlt or StretchBlt to write it to the printer device context.)

But banding also further complicates the printing process, as you'll see
when we create PRINT4, the final version of our printing program.

Strike Up the Bands

To have your program do its own banding, you first define a variable of type
RECT:

RECT rect ;

You'll recall that the RECT structure has four fields named left, top,
right, and bottom. For each page, you start by making an Escape call for the
subfunction NEXTBAND, passing to it a pointer to rect. On return, rect
contains the coordinates of the first band. The coordinates are always
device coordinates (pixels) regardless of the current mapping mode of the
printer device context. You make GDI calls to print in that band. You then
call the NEXTBAND Escape function again to obtain the coordinates of the
next band, and you print in that band. When the RECT structure passed to
Escape is returned empty (all fields set to 0), the page is done.

Here's what the code looks like to print a single page. For simplicity's
sake, this code doesn't take into account errors that can be returned from
the Escape functions or checks of the bUserAbort value:

Escape (hdcPrn, NEXTBAND, 0, NULL, (LPSTR) &rect) ;

while (!IsRectEmpty (&rect))
     {
[call GDI functions to print in band]
     Escape (hdcPrn, NEXTBAND, 0, NULL, (LPSTR) &rect) ;
     }

Each NEXTBAND Escape call except the first performs a function similar to
the NEWFRAME Escape call: It signals to the GDI module and to the printer
device driver that the entire band has been defined and that it can now be
saved in a disk file (or written to the printer if the Print Manager is not
loaded). You don't want to call the NEWFRAME Escape function after this loop
has run its course. If you do so, you'll get a blank page between each
printed page. Nor can you terminate the loop before receiving an empty
rectangle and then make a NEWFRAME Escape call to skip the rest of the page.
In short, you use either NEWFRAME to print a page without banding or
multiple NEXTBAND calls to print a page with banding. Don't mix NEWFRAME and
NEXTBAND Escape functions for the same page.

It's easiest to visualize banding for a dot-matrix printer. Before
illustrating the process, we need to make a distinction between the "top of
the paper" (which is always the section of the paper printed first) and the
"top of the page" (which depends on whether the printer driver is in
portrait or landscape mode).

In portrait mode, the top of the page is the same as the top of the paper.
The bands go down the page. The rect.left value in the RECT structure set by
the NEXTBAND Escape call is always 0, and rect.right is always equal to the
width of the printing area in pixels (the value obtained from GetDeviceCaps
with a HORZRES parameter). For the first band, rect.top equals 0. For each
successive band, rect.top equals the rect.bottom value of the previous band.
For the last band, rect.bottom equals the height of the printing area in
pixels. (See Figure 15-11.)

Thus in each band, you can print from the rect.left and rect.top coordinates
up to (but not including) the rect.right and rect.bottom coordinates. If you
call the function:

Rectangle (hdcPrn, rect.left, rect.top, rect.right, rect.bottom) ;

  (Figure 15-11. may be found in the printed book.)

the rectangle will be printed on the outermost edges of the band. (Recall
that the right and bottom sides of the rectangle drawn by Rectangle are
actually one pixel short of the points indicated by the last two
parameters.)

In landscape mode, the dot-matrix printer must print the document sideways,
starting from the left side of the page. The bands are in exactly the same
area on the paper, but the rectangle coordinates are different, because the
left side of the page is now the top  of the paper. In landscape mode,
rect.top is always 0, and rect.bottom is a constant equal  to the height of
the printing area in pixels (the value obtained from GetDeviceCaps using
the VERTRES parameter). For the first band, rect.left equals 0. For the last
band, rect.right is the width of the printing area in pixels. (See Figure
15-12 on the following page.)

A laser printer or a plotter might handle banding differently than a
dot-matrix printer, because the printer output might not need to be sent to
the printer sequentially from the top of the page to the bottom. Although
Figures 15-11 and 15-12 represent the normal case, your program shouldn't
assume that the banding rectangles will follow these patterns.

Separating your printer output into bands might seem like a major headache.
But even if you use banding, you don't need to include a lot of banding
logic. The band is a clipping region. You can make GDI calls that print
outside the band, and Windows will ignore everything except what falls
inside the band. This means that for each band, you can make all the GDI
calls for the entire page.

  (Figure 15-12. may be found in the printed book.)

You can determine whether a particular driver requires banding support by
checking the RC_BANDING bit of the value returned from GetDeviceCaps using
the RASTERCAPS parameter. As I mentioned before, this information is of
concern only to GDI. Whether a driver requires banding support or not, the
GDI module always supports the NEXTBAND Escape call. If the driver doesn't
require banding support, the first NEXTBAND Escape call for a page returns a
rectangle equal to the size of the printing area. The second NEXTBAND call
for a page returns an empty rectangle.


A Different Use of the Abort Procedure

When a program assumes responsibility for banding, the GDI module uses the
abort procedure somewhat differently than it does otherwise. If the Print
Manager isn't loaded, the GDI module frequently calls the abort procedure
with an nCode parameter of 0 while processing the NEXTBAND Escape call, just
as it does when processing NEWFRAME. However, if the Print Manager is loaded
(the more normal case), the GDI module calls the abort procedure only if it
runs out of disk space. The nCode parameter is SP_OUTOFDISK.

This arrangement presents a problem. Unless the GDI module runs out of disk
space, the user can't switch to another program until the application
currently printing is finished. Moreover, although the printing dialog box
is displayed, the user can't cancel the print job, because the dialog box
can't get messages until the abort procedure is called. The solution to this
problem is fairly simple. Your printing routine can call the abort procedure
itself between the GDI drawing functions that make up the page. Although the
operation of Windows isn't as smooth as when the GDI module calls the abort
procedure, this approach at least allows the user to cancel the print job or
move on to another task.

Don't call the abort procedure directly. Instead, use the pointer returned
from MakeProcInstance. For instance, if your abort procedure is called
AbortProc and the pointer returned from MakeProcInstance is called
lpfnAbortProc, you can call AbortProc using:

(*lpfnAbortProc) (hdcPrn, 0) ;

The PRINT4 program, shown in Figure 15-13, adds banding to the printing
logic in PRINT3. PRINT4 also requires the PRINT.RC file in Figure 15-8
and--like all our PRINT programs--the PRINT.C file in Figure 15-5.

 PRINT4.MAK

#----------------------
# PRINT4.MAK make file
#----------------------

print4.exe : print.obj print4.obj print4.def print.res
     link print4 print, /align:16, NUL, /nod slibcew libw, print4
     rc print.res print4.exe

print.obj : print.c
     cl -c -Gsw -Ow -W2 -Zp print.c

print4.obj : print4.c
     cl -c -Gsw -Ow -W2 -Zp print4.c

print.res : print.rc
     rc -r print.rc

 PRINT4.C

/*---------------------------------------
   PRINT4.C -- Printing with Banding
               (c) Charles Petzold, 1990
  ---------------------------------------*/

#include 

HDC  GetPrinterDC (void) ;              // in PRINT.C

typedef BOOL (FAR PASCAL * ABORTPROC) (HDC, short) ;



HANDLE hInst ;
char   szAppName [] = "Print4" ;
char   szCaption [] = "Print Program 4 (Banding)" ;

BOOL   bUserAbort ;
HWND   hDlgPrint ;

BOOL FAR PASCAL PrintDlgProc (HWND hDlg, WORD message, WORD wParam, LONG
lParam)
     {
     switch (message)
          {
          case WM_INITDIALOG :
               SetWindowText (hDlg, szAppName) ;
               EnableMenuItem (GetSystemMenu (hDlg, FALSE), SC_CLOSE,
                                                            MF_GRAYED) ;
               return TRUE ;

          case WM_COMMAND :
               bUserAbort = TRUE ;
               EnableWindow (GetParent (hDlg), TRUE) ;
               DestroyWindow (hDlg) ;
               hDlgPrint = 0 ;
               return TRUE ;
          }
     return FALSE ;
     }

BOOL FAR PASCAL AbortProc (HDC hdcPrn, short nCode)
     {
     MSG   msg ;

     while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
          {
          if (!hDlgPrint || !IsDialogMessage (hDlgPrint, &msg))
               {
               TranslateMessage (&msg) ;
               DispatchMessage (&msg) ;
               }
          }
     return !bUserAbort ;
     }

BOOL PrintMyPage (HWND hwnd)
     {
     static char szSpMsg [] = "Print4: Printing" ;
     static char szText  [] = "Hello, Printer!" ;
     ABORTPROC   lpfnAbortProc ;
     BOOL        bError = FALSE ;
     DWORD       dwExtent ;
     FARPROC     lpfnPrintDlgProc ;
     HDC         hdcPrn ;
     POINT       ptExtent ;
     RECT        rect ;
     short       xPage, yPage ;

     if (NULL == (hdcPrn = GetPrinterDC ()))
          return TRUE ;

     xPage = GetDeviceCaps (hdcPrn, HORZRES) ;
     yPage = GetDeviceCaps (hdcPrn, VERTRES) ;

     EnableWindow (hwnd, FALSE) ;

     bUserAbort = FALSE ;
     lpfnPrintDlgProc = MakeProcInstance (PrintDlgProc, hInst) ;
     hDlgPrint = CreateDialog (hInst, "PrintDlgBox", hwnd, lpfnPrintDlgProc)
;

     lpfnAbortProc = MakeProcInstance (AbortProc, hInst) ;
     Escape (hdcPrn, SETABORTPROC, 0, (LPSTR) lpfnAbortProc, NULL) ;

     if (Escape (hdcPrn, STARTDOC, sizeof szSpMsg - 1, szSpMsg, NULL) > 0 &&
         Escape (hdcPrn, NEXTBAND, 0, NULL, (LPSTR) &rect) > 0)
          {
          while (!IsRectEmpty (&rect) && !bUserAbort)
               {
               (*lpfnAbortProc) (hdcPrn, 0) ;

               Rectangle (hdcPrn, rect.left, rect.top, rect.right,
                                                       rect.bottom) ;
               (*lpfnAbortProc) (hdcPrn, 0) ;

               MoveTo (hdcPrn, 0, 0) ;
               LineTo (hdcPrn, xPage, yPage) ;

               (*lpfnAbortProc) (hdcPrn, 0) ;

               MoveTo (hdcPrn, xPage, 0) ;
               LineTo (hdcPrn, 0, yPage) ;

               SaveDC (hdcPrn) ;

               SetMapMode (hdcPrn, MM_ISOTROPIC) ;
               SetWindowExt   (hdcPrn, 1000, 1000) ;
               SetViewportExt (hdcPrn, xPage / 2, -yPage / 2) ;
               SetViewportOrg (hdcPrn, xPage / 2,  yPage / 2) ;

               (*lpfnAbortProc) (hdcPrn, 0) ;
               Ellipse (hdcPrn, -500, 500, 500, -500) ;

               (*lpfnAbortProc) (hdcPrn, 0) ;

               dwExtent = GetTextExtent (hdcPrn, szText, sizeof szText - 1)
;
               ptExtent = MAKEPOINT (dwExtent) ;
               TextOut (hdcPrn, -ptExtent.x / 2, ptExtent.y / 2, szText,
                                                  sizeof szText - 1) ;

               RestoreDC (hdcPrn, -1) ;

               (*lpfnAbortProc) (hdcPrn, 0) ;

               if (Escape (hdcPrn, NEXTBAND, 0, NULL, (LPSTR) &rect) < 0)
                    {
                    bError = TRUE ;
                    break ;
                    }
               }
          }
     else
          bError = TRUE ;

     if (!bError)
          {
          if (bUserAbort)
               Escape (hdcPrn, ABORTDOC, 0, NULL, NULL) ;
          else
               Escape (hdcPrn, ENDDOC, 0, NULL, NULL) ;
          }

     if (!bUserAbort)
          {
          EnableWindow (hwnd, TRUE) ;
          DestroyWindow (hDlgPrint) ;
          }

     FreeProcInstance (lpfnPrintDlgProc) ;
     FreeProcInstance (lpfnAbortProc) ;
     DeleteDC (hdcPrn) ;

     return bError || bUserAbort ;
     }

 PRINT4.DEF

;-----------------------------------
; PRINT4.DEF module definition file
;-----------------------------------

NAME           PRINT4

DESCRIPTION    'Printing Program No. 4 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               AbortProc
               PrintDlgProc

PRINT4 differs from PRINT3 in only a few particulars. In order for AbortProc
to be called while the program is printing, the GDI drawing routines have
been moved into PrintMyPage. You'll notice that the Rectangle function
prints the rectangle for each band rather than a rectangle on the border of
the entire page. This allows you to see where the bands are for a particular
printer. The structure of the printing operation looks like this:

if (Escape (hdcPrn, STARTDOC, sizeof szSpMsg - 1, szSpMsg, NULL) > 0 &&
    Escape (hdcPrn, NEXTBAND, 0, NULL, (LPSTR) &rect) > 0)
     {
     while (!IsRectEmpty (&rect) && !bUserAbort)
          {
[make GDI calls and call abort procedure]
          if (Escape (hdcPrn, NEXTBAND, 0, NULL, (LPSTR) &rect) < 0)
               {
               bError = TRUE ;
               break ;
               }
          }
     }
else
     bError = TRUE ;

The while loop for the band proceeds only if the rectangle isn't empty and
if the user hasn't canceled the print job from the dialog box. PRINT4 has to
check the return value from each NEXTBAND Escape call and set bError if
Escape returns a negative value. If no Escape call returns an error, then
the print job must either be ended with the ENDDOC Escape call or be aborted
with the ABORTDOC Escape call. If the user cancels printing  during the
NEXTBAND loop, then the print job must be aborted using the ABORTDOC call.
The code to do this is as follows:

if (!bError)
     {
     if (bUserAbort)
          Escape (hdcPrn, ABORTDOC, 0, NULL, NULL) ;
     else
          Escape (hdcPrn, ENDDOC, 0, NULL, NULL) ;
     }



THE PRINTER AND FONTS

Chapter 14 culminated in a program called JUSTIFY that uses GDI-based raster
fonts to display formatted text. Programs that work with formatted text on
the screen usually also need to print this text. In fact, word-processing
and desktop publishing programs generally use the display solely to provide
a preview of the printed output.

This is a difficult task: What you show on the display can only approximate
what the printer will print. Even if the printer uses only GDI-based fonts
(those fonts stored in .FON font resource files), it generally has a
different resolution than the screen, meaning that the printed characters
will be a slightly different size than on the display. And if the printer
uses device-based fonts (fonts that are internal to the printer), the
problem becomes even more complex. For example, in the case of a printer
that offers a 15-point Zapf Chancery font, you'll be approximating that font
on the display with a 14-point font of another typeface. Even if the user
wants to print text in 14-point Times Roman, the various character widths of
the printer's device-based font can differ from those of the display's
GDI-based font. In short, if you're writing a program that must display
formatted text destined for a printer, you can count on some work ahead.
Here are some guidelines to get you started.

You'll want to let the user choose from a list of typeface names and sizes
that are supported by the currently selected printer. That is, you need to
enumerate the printer fonts (as we did in the FONTLIST program in Chapter
14), which requires using EnumFonts with a call-back function. This
call-back function receives a pointer to a logical font (LOGFONT) structure
and a text metrics (TEXTMETRICS) structure describing each font. The
call-back function also receives a short integer that indicates whether the
font is a GDI-based or a device-based font. Another bit indicates whether
the font is a raster font, in which case it is scalable (within limits) by
integer multiples, or a vector font, in which case it is continuously
scalable.

For device-based vector fonts, the call-back function will receive only one
typeface size. You can check the value returned from GetDeviceCaps using the
TEXTCAPS parameter to determine how the device can scale these vector fonts.
If they can be scaled by any multiple, you might want to allow the user to
specify a point size.

When you display formatted text on the screen, you want to space the text
based on how it will be eventually printed. You can use GetDeviceCaps and
the GETPHYSPAGESIZE Escape call to determine the size of the paper and the
size of the printable area. For instance, if the paper is 8-1/2 inches wide
and the user selects left and right margins of 1 inch, then you want to
display text on the screen using a width of 6-1/2 inches. The "logical
twips" mapping mode discussed in Chapter 14 is appropriate for this display.
There's a catch, however. If the user selects a 15-point font that the
printer supports, you'll have to approximate that font on the display with a
14-point font--but you can't use this 14-point display font to determine the
amount of 15-point text that can fit in one printed line. You must determine
this instead based on the printer font. Likewise, you must use the printer
font to determine how many lines fit on a page.

To format the display text, you'll need both a handle to the screen device
context (to display the text on the screen) and a handle to a printer
information context. You don't need a printer device context handle until
you actually want to print. Follow these steps:

  1.  Put together a logical font structure with the typeface name, the type
      size, and the attributes selected by the user, and select that logical
      font into the printer information context.

  2.  Call GetTextMetrics for the printer information context to determine
      the real size and characteristics of the selected printer font. Call
      GetTextFace to obtain the typeface name.

  3.  Use the information obtained in Step 2 to create another logical font
      structure based on the size and characteristics of the printer font,
      and select this logical font into the screen device context. The font
      now selected into the screen device context closely approximates the
      font selected into the printer information context.

  4.  When you write the text to the display, follow the general procedure
      used in the Justify function of the JUSTIFY program. However, go
      through the GetTextExtent and SetTextJustification logic using the
      printer information context, but stop short of TextOut. This approach
      allows you to determine the amount of text that fits on each line and
      the number of lines that fit on a page.

  5.  When you have established each line of text as appropriate for the
      printer, you can call GetTextExtent and (possibly)
      SetTextJustification using the screen display context. You then call
      TextOut to display the line.

To print the text, you'll probably use code structured like that in the
POPPADP.C file combined with the logic in the Justify function of JUSTIFY.C.
You obtain a printer device context and go through the GetTextExtent and
SetTextJustification logic again, this time using TextOut to print each
line.