MFC Print Tutorial

Introduction to MFC Printing:

It’s common knowledge that printing is one of the hardest things for properly implementing in your Win32 program. You have probably heard or even experienced how hard printing is with Win32 API. The good news is that we are using MFC which greatly simplifies this task. You get print preview, standartized dialogs(print setup, page setup), print job interruption dialog and OS management for free. If this doesn’t look much to you just have a look at the MFC source files involved in printing and print preview.

Basically with MFC you only have to add drawing code and logic for paginating the document if it consists of multiple pages. Maybe after this you wonder what can this tutorial offer to you. The problem is that MFC hides most of the functionality in members of various classes. This tutorial will start ith basic control over the process and will continue with more advanced modifications of the internal structures involved in printing. I will use a technique i call “copy&modify for your needs”. For this you will need the MFC source code. You will find further instructions in the examples. Now proceed to Step 1.

Creating the MFC Visual C++ Printing program:

  • For this tutorial we will create a simple program. Start Visual C++ and use MFC AppWizard(exe).
  • In step 1 select “Single document”. Make sure the check box is selected.
  • Skip step 2 and in step 3 you can disable “ActiveX Controls” since we won’t use them.
  • In step 4 set the number of files to 0 and click on Advanced and delete the two bottom lines (File new name(short) and (long)). Otherwise the program will add its document type in the right click menu in “New->” and you wouldn’t want this. Press “Finish” and the project is ready.

Printing Functions provided by MFC in Visual C++:

The program has a class CTutorialView with some member functions. The ones involved in printing are OnPreparePrinting, OnBeginPrinting, OnEndPrinting, OnPrepareDC, OnDraw and OnPrint.

We have two options for printing:

1. To use OnDraw paint the window and OnPrint to print or paint in print preview mode.
Right click the class CTutorialView and select “Add Virtual Function…”. Find the function OnPrint and press “Add and Edit”. You are then taken to the function body and you should see this code

CView::OnPrint(pDC, pInfo);

which you may safely delete(or better add // for commenting it).

It is possible to make output both for the display and printer in OnDraw. If you want this you should use pDC->IsPrinting() which returns TRUE if printing and FALSE if displaying. You don’t need OnPrint function.

Now start the program and press the Print button in the toolbar or press Ctrl+P. You should see the Print dialog. Note that Page range is set to all and the text box next to “pages” shows 1-65535. This is the default for MFC which causes it to print only one page. The page range can be easily changed in OnPreparePrinting by using pInfo. Default selection and the other settings are harder and explained in later pInfo contains all the data related to printing. It’s best to look in the help for “CPrintInfo” where you will find all the information about member variables. For now just note that you can use pInfo->SetMinPage and pInfo->SetMaxPage to set the range.
Example: use  pInfo->SetMaxPage(1); to limit your printing job to exactly 1 page.

It is possible to know how much pages your application needs. If it prints out something really small you can safely assume it needs 1 page. If you print fixed-height objects like lines of text you can get the size of the page and the text and calculate how much pages you need. If your program is printing a highly specific thing like a 640X480 bitmap or four 20X15 inches charts you will be able to easily determine what you need.

Example 1: You know exactly how much pages you need


BOOL CTutorialView::OnPreparePrinting(CPrintInfo* pInfo)
{
pInfo->SetMaxPage(6); // or the number you need
return DoPreparePrinting(pInfo);
}

Example 2: You don’t know the how much pages you need before you get the user seletions form the “Print” or “Print Setup” dialogs.


void CTutorialView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo)
{
int nPageHeight=pDC->GetDeviceCaps(VERTRES);
int nDocLength=GetDocument()->DocLength();
int MaxPage=max(1, (nDocLength+nPageHeight-1)/nPageHeight);
pInfo->SetMaxPage(nMaxPage);
}

Note: In OnBeginPrinting you have a pointer to the initialized device context and you can use it to get some important information about the environment like the size of the selected font. This can be used to determine how much can be printed on one page. GetDeviceCaps is explained later. For now think that nPageHeight is the page height in pixels and nDocLength is the document size in pixels. You can easily modify the code so that nPageHeight is the amount of text lines printed on a page and nDocLength is the total amount of lines in the document.

If you print the entire document contents rather than printing only the current page you will have to implement the virtual function OnPrepareDC which is called before printing every page and can be used to set the viewport. Otherwise your program will print the first page every time OnPrint(or OnDraw) is called.


void CTutorialView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo)
{
CView::OnPrepareDC(pDC, pInfo);
if(pDC->IsPrinting())
{
int y=(pInfo->m_nCurPage-1)*m_nPageHeight;
pDC->SetViewportOrg(0, -y);
// remove the minus sign if you are printing in MM_TEXT
}
}

Now the printing code will print only the appropriate lines of text. The printing code will look like this:


for(int a=0;a<numStrings;++a)
{
Cpoint point(0,0); // start point for drawing
pDC->TextOut(str[a], point.x, point.y);
point.y-=nHeight; // if map mode is MM_TEXT change this to +=
}

Note: For big documents scrolling in print preview will work slow because the entire document is printed. If this is a problem you should use code like the one below.

If you don’t know how much pages are needed you can start printing and determine when to stop while printing.


Example:
You have an array of CString objects. OnPrint could look like this (this is only part of code and won’t work by itself):


// x and y are some starting positions on x and y axis
// for MM_TEXT the point (0,0) is the top left corner of the screen and coordinates increase to the right and down
// for MM_HIMETRIC, MM_LOMETRIC, MM_HIENGLISH and MM_LOENGLISH
// the start is the bottom left point and coordinates decrease(become negative) upwards and increase to the right.
// for MM_ISOTROPIC and MM_ANISOTROPIC they are user-defined
CPoint(x,y);
// get the current page number (for first page returns 1, for second - 2 and so on)
nCurPage=pInfo->m_nCurPage;
// get the index of the first CString in the array that has to be printed in this page
nStartPos=(nCurPage-1)*linesPerPage;
// calculate the index of the last CString
nEndPos=nStartPos+linesPerPage;
// fill a TEXTMETRIC struct with various information about the selected font
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
int nHeight=tm.tmHeight+tm.tmExternalLeading;
for(int a=nStartPos;a<nEndPos && a<numStrings;++a)
{
pDC->TextOut(str[a], point.x, point.y);
point.y-=nHeight; // if map mode is MM_TEXT change this to +=
}
if(a>=numStrings)
// will stop printing if all strings are printed
pInfo->m_bContinuePrinting=FALSE;

Printing:

There are several ways to print your document but first you need to know something about mapping modes. Printers have fixed physical measures. Most printers support at least 600X600 dpi. This means a printer can print 600 “pixels in one inch” while a monitor has something like 10. So if you print in MM_TEXT mode where one logical unit means one pixel the display might work for the screen but the printed images will be very small or invisible at all. So you should either use some of the MM_[HI/LO][ENGLISH/METRIC] map modes:

MM_HIMETRIC: Each logical unit is converted to 0.01 millimeter
MM_LOMETRIC: Each logical unit is converted to 0.1 millimeter
MM_HIENGLISH: Each logical unit is converted to 0.001 inches
MM_LOENGLISH: Each logical unit is converted to 0.01 inches

These are usefull when printing charts and tables:
MM_TWIPS is very usefull when printing with text. Each logical unit is converted to 1/20 of a point. A piont is the unit used in font measuring. The size which you select in Word for example is in points. Also the standard “Select font” dialog uses this measure.

-or-

Get the sizes of the sheet of paper and size your output according to it. Here’s how to do this:

// GetDeviceCaps can give much important information about the display device
int horzsize=pDC->GetDeviceCaps(HORZSIZE); // gives the width of the physical display in millimeters
int vertsize=pDC->GetDeviceCaps(VERTSIZE); // gives the height of the physical display in millimeters
int horzres=pDC->GetDeviceCaps(HORZRES); // gives the height of the physical display in pixels
int vertres=pDC->GetDeviceCaps(VERTRES); // gives the width of the physical display in pixels
int hdps=horzres/horzsize; // calculate the horizontal pixels per millimeter
int vdps=vertres/vertsize; // calculate the verticalpixels per millimeter
// note 1: if the resolution of the printer is 600X600, 1200X1200 or anything ***X*** hdps will be equal to vdps
// note 2: multiply hdps and vdps by 2.54 to receive the dpi
// since you didn't set the map mode it is still MM_TEXT
// now when calculating sizes in millimeters multiply them by hdps or vdps and the sizes will be correct
CRect rectDraw=pInfo->m_rectDraw;
// this assumes the page is A$, the printer can print without margins
// (this is not very good to assume but will work for now)
// and the page is in landscape mode (297mmx210mm)
CRect rectOut(rectDraw.left,rectDraw.top,rectDraw.left+297*hdps,rectDraw.top+210*vdps);
// now print only inside this rectangle
...

Advanced settings – MFC Printing:

Maybe your printer supports many types of paper(A3, A4, B3 ,B4, Envelope, Letter, etc.) but you want your program to print on a certain type. You can set the printer defaults but sometimes the user may need the defaults or you can’t set al the user’s settings. In this case it is best to set the type of paper and orientation (portrait or landscape) in your program. The users won’t have to worry about setting anything. But this is not as simple as it sounds. The pInfo has a member m_pPD of type CPrintDialog* which is a pointer to the printer settings dialog. You can use it to make changes before the user opens the dialog or when starting the print job. The key is m_pPD->m_pd which is PRINTDLG struct(actually it is a pseudanim but it the same for our purposes). It contains the hDevMode member which is a handle to a DEVMODE data structure containing information about the device initialization and environment of a printer.

Through this  pInfo->m_pPD->m_pd.hDevMode (The path to hDevMode) handle you gain total control over the printing process but since it is a handle you can’t just set it to what you want. You have to lock the memory to it and access it instead. But before the DoPreparePrinting function the handle is not set so you can’t access the data. If you access it after calling DoPreparePrinting the changes will take effect at the next print job. So you have to do something else. My best solution was to take the code of DoPreparePrinting and modify it according to my needs.

If you want to control the defaults when the user selects Print Setup from File menu you have to associate the message with your function. Press Ctrl+W to open ClassWizard. Select CtutorialApp for Class name and ID_FILE_PRINT_SETUP for Object ID. Associate the COMMAND message with a function(the default is OnFilePrintSetup). Now add this code instead whatever is there:

CPrintDialog pd(TRUE);
if (GetPrinterDeviceDefaults(&pd.m_pd))
{
LPDEVMODE dev = pd.GetDevMode();
GlobalUnlock(dev);
dev->dmOrientation=DMORIENT_LANDSCAPE;
dev->dmPaperSize=DMPAPER_A4;
}
DoPrintDialog(&pd);

The default function does only this:

CPrintDialog pd(TRUE);
DoPrintDialog(&pd);

The function has to be a CTutorialApp member because DoPrintDialog is only accessible from the application class (try to put this code in a member function of the View or MainFrame and see the error messages).

Sometimes you will want to know if the user has right clicked on a document and selected print from there. There is a variable cmdInfo in InitInstance. It contains the information about how is the program started. Unfortunately it is processed by ProcessShellCommand which you have to copy and paste in InitInstance. Have in mind that ProcessShellCommand returns a boolean value in several cases and you don?t want this to happen in InitInstance. Just replace all the ?return ?? lines with this bResult=? where bResult is a boolean variable. Then when the copied function ends just decide what to do with the result you have in bResult (it will be the same as if you have called the function). Now back to ProcessShellCommand. It has a case block and one of the cases is this:

case CCommandLineInfo::FilePrintTo:
case CCommandLineInfo::FilePrint:

Just add your code before or after the original depending on your needs

It’s always best to run the code step by step in the debugger. Thus you can see which part of code is executed currently and the order of called functions ending with the current(call stack). Example: You want to see what and when happens in OnPreparePrinting. The way to check is to set a breakpoint at the beginning of the function body after the opening curly bracket ( ?{? ), start the program in the debugger by pressing F5(you will have to build in Win32 Debug mode. Then when the program stops in the function use ?Step Into? to go to the desired function and copy its source.

Note: If you add a breakpoint in some functions like the ones involved in printing the program will break only if you start printing/open the print setup dialog.

Please download the sample project here.