Write a Windows Program for Printing

 


1. How do I write a Windows program that prints using the API?

You can easily write a Windows Program that carries out printing commands using the Windows API. This is done as follows:

(a) Get a device context (DC) for the printer

This can be done in several different ways:

(i) The default printer is stored under "device=" in the [windows] section of the WIN.INI file and can be obtained with the GetProfileString() function. Even though Windows NT / 2000 / XP stores the information in the registry, this function can still be used as it maps to the correct place in the registry. After getting the default printer name, get the port and driver names from the [devices] section of the WIN.INI file. Then supply the printer name, driver name and port to CreateDC().

(ii) To use a non-default (named) printer, just get the driver and port details from the [devices] section of the WIN.INI file. You already have the name which is the printer to print to. Then supply the printer name, driver name and port to CreateDC().

(iii) Use the common dialog PrintDlg() to allow the user to choose and configure the printer. Although the dialog configures the default printer, the user can press the Setup (or Properties) button to select another printer and to configure it differently. The returned PRINTDLG structure has a DC field which is set up to reflect the choices the user made in the dialog. Note however the changes are not permanent, after finishing with the DC the default printer and its settings are not changed. To permanently change the default printer and/or its settings is done through the control panel. It can be done programatically using the DEVMODE structure and ExtDeviceMode() with the DM_UPDATE flag, however Microsoft do not approve of this and it does not work under Windows NT / 2000 / XP. Under these versions of Windows the default DEVMODE structure appears to be stored in the registry.

After getting the DC from the returned PRINTDLG structure, note the hDevMode and hDevNames fields may store global memory handles. If non-null these should be GlobalFree'd to avoid leaking memory blocks.

(b) Set the AbortProc callback

The AbortProc is a call-back function called by GDI every few seconds while printing, and is called by some printer drivers during StartDoc(). The purpose of the AbortProc() is to allow processing of messages by the application and any other apps that happen to be running (eg Print Manager) - in effect it allows non-preemptive task switching to work properly. The AbortProc() is set using the Windows API SetAbortProc() function as follows:

  SetAbortProc(hdcPrinter, AbortProc());

The AbortProc() should contain a "no-wait" PeekMessage() message loop which drops out when there are no more messages. It returns TRUE to allow printing to continue, or FALSE to cancel the print job. A simple AbortProc() looks like this:

  BOOL EXPORT CALLBACK AbortProc(HDC hdcPrinter, int iError) 
  { 
    MSG stMsg;
    while (PeekMessage(&stMsg, NULL, 0, 0, PM_REMOVE))
    {
      TranslateMessage(&stMsg) ;
      DispatchMessage(&stMsg) ;
    }
    return(TRUE);
  }

If the print job needs to be cancelled, it is not recommended that AbortProc() calls AbortDoc(), see "Printing" by Ron Gery, MSDN article created March 20 1992. The probable cause is AbortProc() is called from inside the print driver, and the print driver does not expect to find itself terminated while in the midst of a call to an AbortProc(). Instead the best way to cancel the print job is to have a Cancel dialog which sets a flag if the user cancels, the flag can be tested in AbortProc() and used to return FALSE.

(c) Display the Cancel Dialog

The Cancel Dialog is a programmer-defined dialog box with a push button labelled "Cancel" and a few static text fields describing the document being printed. Its purpose is to allow the user to cancel a lengthy print. It needs to be created as a modeless dialog using CreateDialog(), as the modal DialogBox() function does not return until the dialog is closed, which is no use here.

There remains a problem with the modeless dialog as printing is generally not re-entrant, eg in the middle of printing you don't want the user to return to the application, delete the document being printed or start printing something else. To guard against this the Cancel dialog is made pseudo-modal, by disabling the application's top level window. If the application has several top level windows with no common owner these should all be disabled. The code to disable a single top level window is as follows:

  hwndCancel = CreateDialog(hInst, "CANCELDLG", hwndMain, Cancel_proc);
  ShowWindow(hwndCancel, SW_SHOW);
  EnableWindow(hwndMain, FALSE);

CreateDialog() loads and creates the Cancel dialog box, ShowWindow is needed because modeless dialog are not created visible, and EnableWindow makes the dialog modal. The dialog procedure can be as follows:

  BOOL EXPORT CALLBACK Cancel_proc(HWND hDlg, UINT msg,
                                   WPARAM wp, LPARAM lp)
  {
    switch (msg)
    {
    case WM_INITDIALOG:
      /* set the focus to the Cancel button */
      SetFocus(GetDlgItem(hDlg, IDCANCEL));
      /* return FALSE since we set the focus */
      return(FALSE);
      break;
    case WM_COMMAND:
      if (GET_WM_COMMAND_ID(wp,lp) == IDCANCEL)
      {
        /* set Cancel flag */
        gfCancel = TRUE;
        /* return TRUE since we processed the message */
        return(TRUE);
      }
      break;
    }
    /* return FALSE for messages we do not process */
    return(FALSE);
  }

The flag gfCancel is a global which is set to TRUE to cancel the print. It can be tested in AbortProc() and if set AbortProc() should return FALSE for cancel. If the printing code is placed in a DLL then globals should not be used (DLLs are supposed to be fully re-entrant) so the Cancel flag can be stored in a window control instead. For example there could be a checkbox which is set, or there could be a hidden control, or the button label could be changed to reflect the cancel request.

(d) Call StartDoc

For this step a DOCINFO structure must be initialized, which contains the print job details that show up in Print manager. The DOCINFO structure is then passed to StartDoc().

(e) Do the printing

Print each actual page using normal GDI calls (TextOut, LineTo etc). Each printed page must be bracketed with a call to StartPage() and EndPage(), which cause the printer to move to the next page. Check the return codes from EndPage() - a value of <= 0 means something went wrong. This could be an off-line printer, no disk space left for temporary files, or if AbortProc() returns FALSE to cancel the print. There should be no need to report the error as this should have been done. Windows printing is normally spooled, which means it is sent first to a temporary file (a metafile) before being printed. This reduces the time the application spends printing (the real time that passes between StartDoc and EndDoc) as Windows prints the temporary file in the background while the user is doing other things.

Some versions of Windows (Windows 95) reset the DC whenever StartPage() is called, other versions do not. To be safe it is best to reselect all custom drawing objects back into the DC after calling StartPage().

(f) Call EndDoc

EndDoc() marks the end of the print job. Windows stops storing the GDI calls in the metafile and prints the metafile as a background task while the user is doing other things.

(g) Remove the Cancel dialog

When the print is finished the Cancel dialog should be destroyed. Dialogs created with CreateDialog() are destroyed with DestroyWindow(). The application's main window should be re-enabled before calling DestroyWindow(), the reason being the application should always have at least one window that is enabled so Windows does not activate a window in another application. If EnableWindow() is not called first there will be a short period of time when the application has no enabled windows, and Windows will temporarily activate a window in another application during this time. Probably this would not matter much as the application would continue running and re-enable its main window, however it produces flickering effects on the screen. The correct sequence is:

  EnableWindow(hwndMain, TRUE);
  DestroyWindow(hwndCancel);

(h) Delete the printer DC

Call DeleteDC() to delete the ptinter device context. All device contexts created with CreateDC() should be deleted when finished with.