Introduction to Printing

How to print from a Windows application.

This tutorial will show you how to print a correctly, including how to calculate the page margins. We are going to let the standard print and page setup dialogs do most of the hard work for us, but there is still a little work to do. In the example I will present won't do anything fancy - just work out the page margins, calculate how many lines per page and how many pages to print, and lastly draw a few lines of text.

The Printer Device Context

Printing is performed by drawing to a device context, in exactly the same way graphics are drawn to a window's device context. The only difference is that the printer's device context will be a different aspect ratio to the screen, and will be much higher resolution as well.

The absolute best way to get a device context is to use the PrintDlg function, which is found in the common controls library. This function displays the standard Print dialog box which allows the user to select which printer to print to, how many pages to print and so on. PrintDlg requires a single argument - a pointer to a PRINTDLG structure. This structure is contains quite a few members, but we only need to use a few of them in this example. Here's how to set up the PRINTDLG structure and call PrintDlg itself:

PRINTDLG pd;
...
ZeroMemory(&pd, sizeof(pd));

pd.lStructSize = sizeof(pd);    // always remember to set this!
pd.hInstance = hInst;           // application process handle
pd.hwndOwner = hwnd;
pd.hDevMode  = hDevMode;
pd.hDevNames = hDevNames;
pd.Flags     = PD_ALLPAGES|PD_HIDEPRINTTOFILE|PD_NOPAGENUMS|PD_RETURNDC;
pd.nCopies   = 1;

...
// now we can show the print dialog

// if the user chose cancel, then quit out.
if(!PrintDlg(&pd))
    return FALSE;

Notice that PD_RETURNDC is set in the Flags member. This is how we get a device context to print into. When PrintDlg returns, the hDC member will contain a handle to a valid device context to print into.

You will also notice the two variables highlighted in the code. PrintDlg and PageSetupDlg both require these two handles each time they are called. If these members are set to NULL in the PRINTDLG structure, then PrintDlg will automatically allocate space for them, and store the handles in the hDevMode and hDevNames members when it returns. I recommend allocating these memory objects once at the start of your program, and storing the returned handles in two global variables. Then each time you need to call PrintDlg or PageSetupDlg for real, you can use the two saved handles. You can use the function below to allocate these two memory blocks. Note that PSD_RETURNDC is not set in the Flags member.

static HANDLE hDevMode, hDevNames;
...

HANDLE GetDevMode()
{
    PAGESETUPDLG ps;
    ZeroMemory(&ps, sizeof ps);

    ps.lStructSize = sizeof ps;
    ps.Flags       = PSD_RETURNDEFAULT;

    PageSetupDlg(&ps);
    CopyRect(&rcMargin, &ps.rtMargin);

    hDevMode = ps.hDevMode;
    hDevNames = ps.hDevNames;

    return ps.hDevMode;
}

Calculating the margins

It is more than likely that you will want to set up the margins on the printed page to correspond with the user's margin settings.

The first thing to realise is that most (if not all) printers cannot print to the edge of a piece of paper. There will always be a small border around the outside of this printable area which cannot be printed to. The printable area of a printing device is represented by four numbers, which can be obtained using the GetDeviceCaps API call.

PHYSICALWIDTH The width of the physical page, in device units.
PHYSICALHEIGHT The height of the physical page, in device units.
PHYSICALOFFSETX The distance from the left edge of the physical page to the left edge of the printable area, in device units.
PHYSICALOFFSETY The distance from the top edge of the physical page to the top edge of the printable area, in device units.

Assuming that the user selects a 1 inch margin around the page, we must calculate the position of the page area. The real margin will be the user's margin minus the size of the unprintable area. The picture below shows the relationship between the page area, the unprintable area and the margin size.

Page-margins

Right, on with the coding. The first step is to convert the current user-selected page margins into device units. We do this because currently the margins are specified using either inches or millimetres. So, we need to convert these units into something that is meaningful to the printer device.

margins.left   = MulDiv(rcMargin.left,   GetDeviceCaps(pd.hDC, LOGPIXELSX), 1000);
margins.top    = MulDiv(rcMargin.top,    GetDeviceCaps(pd.hDC, LOGPIXELSY), 1000);
margins.right  = MulDiv(rcMargin.right,  GetDeviceCaps(pd.hDC, LOGPIXELSX), 1000);
margins.bottom = MulDiv(rcMargin.bottom, GetDeviceCaps(pd.hDC, LOGPIXELSY), 1000);

Once we know physically how big the margins should look like, we need to adjust these margins to take into account the border around the outside of the printable area. We do this by subtracting the physical borders from the user-defined borders. This leaves us with a set of offsets which can be added onto each edge of the printable area to finally yield the correct borders.

int iLeftAdjust   = margins.left  - iPhysOffsetX;
int iTopAdjust    = margins.top   - iPhysOffsetY;
int iRightAdjust  = margins.right - (iPhysWidth  - iPhysOffsetX - GetDeviceCaps(pd.hDC, HORZRES));
int iBottomAdjust = margins.right - (iPhysHeight - iPhysOffsetY - GetDeviceCaps(pd.hDC, VERTRES));

The final thing to do is to work out how big an area we have to print to, taking into account the borders we have just worked out. The code below tells us how big each page is. Remember that we can still print outside of this area - these are self-imposed printing limits rather than physical limits.

iWidth  = GetDeviceCaps(pd.hDC, HORZRES) - (iLeftAdjust + iRightAdjust);
iHeight = GetDeviceCaps(pd.hDC, VERTRES) - (iTopAdjust + iBottomAdjust);

How many lines on a page?

Before we can print anything, we need to know how many lines of text we can fit on each page before we start the next one. To do this, we need to know how many lines will be printed in total. Only you know the answer to this one. The other piece of information we need is the height of a line of text on the printed page. We can get this by using the GetTextMetrics call.

TEXTMETRIC tm;
HFONT hfont = (HFONT)GetStockObject(ANSI_FIXED_FONT);
int yChar;

// Setup the current device context
SetMapMode(pd.hDC, MM_TEXT);
SelectObject(pd.hDC, hfont);

// work out the character dimensions for the current font
GetTextMetrics(pd.hDC, &tm);
yChar = tm.tmHeight;

Now that we know the height of a single line of text (the yChar variable), we can calculate how many lines fit onto a single page.

//work out how much data can be squeezed onto each page
int iHeaderHeight   = 0;
int iTotalLines     = ???;   
int iLinesPerPage   = (iHeight - iHeaderHeight) / yChar;
int iTotalPages     = (iTotalLines + iLinesPerPage - 1) / iLinesPerPage;

The iHeaderHeight variable could be used if you display some form of header at the top of each page (e.g. page numbering or a title). If you don't display anything here, then iHeaderHeight can be set to zero, or omitted completely.

Printing (at last!)

That all probably seems like alot of effort for nothing. However, we are now ready to print our page out. The method we use here is baed around the one Charles Petzold uses in his "Programming Windows" book. For more details I would advise you to refer to this.

DOCINFO di;
BOOL bSuccess = TRUE;
BOOL bUserAbort = FALSE;
...

ZeroMemory(&di, sizeof(di));

di.cbSize = sizeof(di);
di.lpszDocName = szFileName;    // name of the file you are printing.
                                // this will appear in the print manager
if(StartDoc(pd.hDC, &di) > 0)
{
    for(iColCopy = 0; iColCopy < ((pd.Flags & PD_COLLATE) ? pd.nCopies : 1); iColCopy++)
    {
        for(iPage = 0; iPage < iTotalPages; iPage++)
        {
            for(iNonColCopy = 0; iNonColCopy < ((pd.Flags & PD_COLLATE) ? 1 : pd.nCopies); iNonColCopy++)
            {
                HANDLE hold;
                if(StartPage(pd.hDC) < 0)
                {
                    bSuccess = FALSE;
                    break;
                }

                // Make all printing be offset by the amount specified for the margins
                SetViewportOrgEx(pd.hDC, iLeftAdjust, iTopAdjust, NULL);

                // select the fixed-width font into the printer DC
                hold = SelectObject(pd.hDC, hfont);

                //print the current file line by line
                for(iLine = 0; iLine < iLinesPerPage; iLine++)
                {
                    char szBuffer[200];
                    iLineNum = iLinesPerPage * iPage + iLine;
                    if(iLineNum > iTotalLines) break;

                    //get line (iLine) from the application, and store it
                    //into szBuffer

                    //output a line of text to the printer
                    TextOut(pd.hDC, 0, yChar * iLine + iHeaderHeight, szBuffer, 
                    
                    lstrlen(szBuffer) - 2);                    
                    bUserAbort = !QueryAbort();
                }

                SelectObject(pd.hDC, hold);

                if(EndPage(pd.hDC) < 0)
                {
                    bSuccess = FALSE;
                    break;
                }

                bUserAbort = !QueryAbort();
                if(bUserAbort) break;
            }

            if(!bSuccess || bUserAbort) break;
        }

        if(!bSuccess || bUserAbort) break;
    }
}
else
{
    bSuccess = FALSE;
}

The code above looks pretty complicated because of all the nested for loops. The printing is structured like this to take into account two important factors. The user may have selected to print out multiple copies of the same document, and might have also selected to collate (collect) pages together as well. The for-loops take care of these situations.

Cancelling Printing

What happens if the user want to cancel printing half-way through? This is what the QueryAbort function does. It runs its own message loop and detects if the user presses the Escape key.

UINT CALLBACK ab(size_w pos, size_w len)
{
    MSG msg;
    BOOL bAbort = FALSE;

    while(!bAbort && PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
    {
        TranslateMessage(&msg);
        switch(msg.message)
        {
        case WM_QUIT:

            PostQuitMessage(0);
            bAbort = TRUE;
            break;

        case WM_KEYDOWN:
            if(msg.wParam == VK_ESCAPE)
            {
                bAbort = QueryAbortDlg();
                continue;
            }
            break;

        case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MBUTTONDOWN:
            bAbort = QueryAbortDlg();
            continue;
        }

        DispatchMessage(&msg);
    }
    return !bAbort;
}

Cleaning Up

After the document has been printed (or printing cancelled), then we need to release the printer device context. We also need to commit the document to the printer spooler, or cancel the document if the user aborted for some reason.

if(bSuccess && !bUserAbort) 
    EndDoc(pd.hDC);
else
    AbortDoc(pd.hDC);

DeleteDC(pd.hDC);

Conclusion

Well, I never said it was going to be easy. I ran out of steam at the end so I havn't described the actual printing process very well. For now I'd refer to Charles Petzold's book "Programming Windows 5th Ed". Maybe someday I'll update this tutorial, but for now this is how it stands.