Win32 Default Printer Print Sample: Code showing how to print in a Windows program without PrintDlg

Printing in Win32 programming is often seen as a difficult proposition. In the days of DOS and command line programming, we quite often simply redirected to the line printer, or could use printer codes to control the output. Windows, due to the possibilities offered by the system to have any kind of printer attached, wraps up the interface and uses device abstraction to control it.

The upside is that we can print on any printer, using the same code, and Windows will take care of translating our commands into commands that will be understood by the printer. The downside is that it does present an additional layer of complexity, which can be hard to visualize for programmers just starting out, and experienced coders alike. The things to remember are:

  • It is treated just like a screen;
  • We can use most HDC related output mechanisms;
  • The printer is controlled transparently.

Moreover, we can also select the default printer with one command, which negates the necessity to display the printer dialog box to the user. This might seem restrictive at first, but it helps us to understand the process without over complicating it. There may also be times when we actively want to print without allowing the user to choose different printers interactively; command line programs, for instance.

Printing in Windows

We print in Windows as if it were a display surface. In essence, this means that before we do anything else we need to obtain a device context (HDC). The correct sequence is therefore:

  • Select a printer;
  • Obtain a suitable device context;
  • Start the document;
  • Start the page;
  • End the page;
  • End the document;
  • Delete the device context.

We can use any of the standard text output functions, for example TextOut and DrawText, to direct text to the printer. Of course, we can also determine the number of pages in advance by calculating the available space, and using DrawText with the DT_CALCRECT option set to determine the exact area that will be required. Then we can use StartPage and EndPage to properly arrange the printing.

Selecting the Default Printer

The default printer is selected by name. It is a two step process – get the name, select the printer by obtaining a device context to it:

char szPrinterName[255];
unsigned long lPrinterNameLentgth;
GetDefaultPrinter( szPrinterName, &lPrinterNameLength; );
HDC hPrinterDC;
hPrinterDC = CreateDC(“WINSPOOL\0”, szPrinterName, NULL, NULL);

In the above, the reader will note that we have used the WINSPOOL queue in order to obtain the actual handle to the default printer. We have one final task to do as part of the initialization process – call StartDoc to start a new document. To do this, we need to use the DOCINFO structure:

DOCINFO di;
memset( &di;, 0, sizeof(DOCINFO) );
di.cbSize = sizeof(DOCINFO);
di.lpszDocName = “PrintIt”;
di.lpszOutput = (LPTSTR) NULL;
di.lpszDatatype = (LPTSTR) NULL;
di.fwType = 0;

The DocName can be anything that the application programmer finds appropriate. It will appear on the printer as part of the job name. Having set some reasonable defaults, we can now call StartDoc, and check for errors:

int nError = StartDoc(hPrinterDC, &di;);
if (nError == SP_ERROR)
{
printf(“\nError – please check printer.”);
// Handle the error intelligently
}

We are now ready to start printing the text.

Printing the Text

To print text, we call StartPage, then any text printing function, and finally EndPage. If we are using DrawText (for multiline formatted text), we also need to obtain the device capabilities:

int cWidthPels, cHeightPels;
cWidthPels = GetDeviceCaps(hPrinterDC, HORZRES);
cHeightPels = GetDeviceCaps(hPrinterDC, VERTRES);

With the horizontal and vertical number of pixels, we can then define our rectangle for the call to DrawText as:

RECT rcPrinter;
rcPrinter.top = 5;
rcPrinter.left = 5;
rcPrinter.right = cWidthPels – 10;
rcPrinter.bottom = cHeightPels – 10;

We have allowed a 5 pixel margin, just in case. Now we can call StartPage:

nError = StartPage(hPrinterDC);

This will return a zero or negative result in case of error. We can then call DrawText:

DrawText(hPrinterDC, text, strlen(text), &rcPrinter;, DT_TOP|DT_LEFT|DT_WORDBREAK);

The options tell Windows to put the text in the top left corner, and break words as necessary over the right hand edge of the rectangle. If we want to just calculate the required rectangle, we would of course set the rcPrinter.bottom to 0, and call DrawText with the DT_CALCRECT option set. This would not draw, but it would tell us how deep the printing area should be.

Finally, we call EndPage to tell the printer we have finished with this page:

nError = EndPage(hPrinterDC);

Again, this will return a zero or negative result in case of error. We have now finished and can either start a new page, or stop printing.

Closing the Printer

Finally, we can tell the printer that we have stopped printing, and delete the device context. This is very simply achieved:

nError = EndDoc(hPrinterDC);
DeleteDC(hPrinterDC);

At this point, if there are any errors, we should tell the user to check the printer – we have no longer any use for it, so can delete the DC and carry on with the application tasks.

Next Steps

This shows how to print a single page. To print a multipage document, we need to use the following steps:

  • Use GetDeviceCaps to get the HORZRES and VERTRES;
  • Use DrawText with the DT_WORDBREAK and DT_CALCRECT options;
  • Determine the number of pages (clue: using VERTRES);
  • Call StartPage / EndPage in sequence with printer banding.

The above list should give the reader an adequate starting point, but if in any doubt; just start a discussion below.