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 tucancode.net. 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.