Printing with MFC Made Easy






4.71/5 (22 votes)
Dec 2, 1999
10 min read

481698

10386
Two classes to add advanced print functionality to your MFC application
Three of the biggest frustrations you're apt to encounter when adding print functionality to your MFC application are:
- Printing simple row/column based reports.
There hasn't been and probably never will be support for this - you're out of luck if you want to print a simple text based row and column report. You have to write it from scratch and worry about device contexts, page numbers, and headers and footers – all things you'd expect to be built into MFC but are not.
- Figuring out how to print something without a
CView
.The MFC class
CView
has a lot of print functionality built into it. The problem is knowing how to apply it when printing outside of aCView
, eliminating the unnecessary steps while keeping the required ones performed byCView::OnPrint()
. - Smoothly combining the printing of individual documents into one job.
Users want the ability to generate custom reports – to be able to include different types of information in any combination they desire. There is no direct support for this in MFC, no class structure which makes coding this easy, and no standard way to communicate from one document to the next what page number or position on the page it left off.
Knowing this, I decided it was time to create my own classes for dealing with these issues: GPrintJob
and GPrintUnit
. The benefits of using them are:
- You can print any type of report.
These classes were designed with printing text based row and column reports in mind. You work in terms of rows, columns, and text – the burden of managing the device context, coordinates, word-wrapping, and other grunt work is done for you. However, because these classes come with tons of overrides which allow access to all stages of the printing process, you can use it to print anything you want.
- They don't require a
CView
.Many types of reports are printed with "hidden" documents. That is, just because a view of the document isn't open doesn't mean the user won't want to print it. However, because most print functionality is built into
CView
, this is a difficult task in MFC. These two classes get around this because they have no dependence upon aCView
, or any window for that matter. - Allows you to combine individual documents into a single report.
There is a one-to-many relationship between
GPrintJob
andGPrintUnit
. You only need one instance ofGPrintJob
to manage the global resources required for printing. On the other hand, you may have multipleGPrintUnit
s, where the actual "meat" of the report is printed.
For any given report, you'll need one and only one instance of a GPrintJob
object. It displays the print dialog, creates the device context, and manages a lot of the "global" details (i.e., current page number) of the printing process. Your derived GPrintJob
class' job is to coordinate the printing of one or more derived GPrintUnit
objects. How many, in what order, and what they print is up to the user and you. Each derived GPrintUnit
of yours will typically know how to print only one particular type of document. GPrintUnit
itself prints nothing. Instead, it contains the functionality which allows your derived unit to print headers, footers, column headings, and the meat of the document. Each GPrintUnit
must always have a parent GPrintJob
.
The remainder of this article will point out the major features of these two classes, show some sample code demonstrating how to use them, and highlight some important notes.
Features
JRECT
,JCUR
, andJDC
As mentioned above, each unit must have a parent job. Without a parent job, the unit wouldn't be able to print anything, as the job is what creates and manages the device context. The parent job is accessed via a
GPrintUnit
member variablem_pJob
which gets passed to the unit in its constructor, or in a call toSetJob()
. Instead of the cumbersome syntaxm_pJob->m_pDC
to access the device context,orm_pJob->m_rectClient
to access the page size, theGPrintUnit
header file has several macros to make this easy, including:#defineJDC (*(m_pJob->m_pDC)) // the device context #define JRECTm_pJob->m_rectClient // the printed page size
Figure 2. Helper macros.These macros can be used only from inside a
GPrintUnit
class. There are several other macros just like these which give you access to other highly usedGPrintJob
members.- Don't need a print dialog
Printing some reports requires either your own custom dialog, or no dialog at all. This can be a problem, because the standard
CPrintDialog
typically creates the device context for you. In these non-standard situations, you can use theGPrintJob::UseDefaults()
function to create a default device context for you. - Definable columns
In typical row and column reports, the page is divided into columns, one column for each field in the records being printed.
GPrintJob
has all-around support for this type of report. You define all of the columns up front with the functionInsertPrintColumn()
. With this function, you define the column's heading, attributes, and size. The size is defined as a percentage of the printed page width. The column headings can be printed all at once with the functionPrintColHeadings()
. You can even define multiple sets of column headings in a given unit (each with its own number of columns, titles, and column size) and switch between them on-the-fly using the functionSetActiveHeading()
. - Word wrapping
To print text on the current line in a column of a report, use the function
GPrintUnit::PrintCol()
. This function will automatically detect if the text will overflow the boundaries of the given cell, and, using a hidden rich edit control, will word wrap the overflow onto the next line(s) (or page even!). Every column of all subsequent rows will be automatically shifted down by the amount of the overflow. For example:PART NUMBER DESCRIPTION QTY. COST 123-4567 Binary flip flop 1 $34.45 module, with 4t5 rating aq4-9909 Overhead flimflam 12 $0.99 b59-123 Left-handed gangly 6 $99.99 wrench
Figure 3. Word wrap example. - Print to file
After the print dialog has been displayed,
GPrintJob
checks if the user selected the print to file option. If so, it displays the open file dialog, allowing the user to pick a file name to print to. - Overridables
Both
GPrintJob
andGPrintUnit
have tons of virtual functions, allowing you to tweak each and every stage of the print process to your suit application's particular requirements. - Text alignment chars
Most row and column reports are interspersed with plain lines of text. The function
PrintTextLine()
was created for just this reason. It prints a line of text at the current cursor location on the page. More importantly, you can embed special control characters in thestring
, which allows you to control the format of sections of thestring
. For example, the line of code:PrintTextLine("one two\x1c\x1fthree four\x1efive six");
Will print:
"one two…………………………three four five six"
where "
one two
" is left justified (by default), "three four
" is centered and separated from "one two
" with dots, and "five six
" is right justified. This extremely powerful feature lets you justify parts of a single line of text differently, without ever dealing with coordinates. There are macros (HFC_*
) for these special formatting characters in gfx_printunit.h. This functionality lends itself wonderfully to printing headers and footers which often are a single line of text, with parts centered, and left and right justified. - Index
An index is a tree like structure which not only shows the break down of a printed report, but also the page number on which each sub-section begins. Building an index couldn't be easier using these classes. In order to print an index, you must first create a
GPrintIndexTree
, usually declared by value in your job, and at the beginning of the print process, select it as your active tree using the macroGSELECT_PJINDEXTREE()
. An index tree is an object of typeGPrintIndexTree
– a type-safe array ofINDEXITEM
s. EachINDEXITEM
has astring
for the title, a bit wise flag field, integer page number, and pointer to aGPrintIndexTree
. In this way, the tree can have multiple recursive levels.Next, as each unit is printed, it is responsible for adding itself to the active tree using the function
GPrintUnit::AddIndexItem()
. It passes to this function anINDEXITEM
structure which defines its title and starting page number (if applicable). If you never select another tree (other than the one declared in your job) as your active tree, all entries would appear under the root item:Introduction..............................................1 Languages C++.......................................................2 MFC.......................................................3 Pascal....................................................4 Basic.....................................................4 Fortran...................................................5 Computers Dell......................................................6 Compaq...................................................10 Gateway..................................................11 Conclusion...............................................15
Figure 4. Single level index.However, if you want a multilevel index, you must change the active item. In order to do this, use the macro
GSELECT_PJINDEXTREE()
passing it a pointer to the index item you want to make active/select. The item will then become active for the current "scope". For example, in the below table, the items "Languages
", "C++
", and "Computers
" were all selected when they were added and subsequent items added afterward were automatically nested:Introduction..............................................1 Languages C++....................................................2 MFC.................................................3 Pascal.................................................4 Basic..................................................4 Fortran................................................5 Computers Dell...................................................6 Compaq................................................10 Gateway...............................................11 Conclusion...............................................15
Figure 5. Multi-level index.When the "C++" unit was printed, its index item had to be first created, initialized, and selected:
INDEXITEM itemCPP; itemCPP.strName = "C++"; itemCPP.nPage = JINFO.m_nCurPage; GSELECT_PJINDEXTREE(&itemCPP.pChildren); AddIndexItem(&itemCPP);
Figure 6. Adding a unit's index entry.With these lines of code, any items added during this scope (i.e., "
MFC
") will get added as a branch off "C++
".In order to print the index, use the function
GPrintUnit::PrintTree()
, passing it the index tree member variable you earlier declared as part of your derived job. It will know how to decipher the contents of the tree, will automatically indent nested levels of entries, and, if desired, put dots in between the title and page number. This is typically done as the last step in printing a report. - Headers and footers
As mentioned previously, the function
PrintTextLine()
can be used to easily print the header and footer text. However, you must tell the unit you want them. You do this by first initializing the unit'sm_pum
members with their required size:pumHeaderHeight // the height of the entire header pumHeaderLineHeight // the height of an individual line in the header pumFooterHeight // the height of the entire footer pumFooterLineHeight // the height of an individual line in the footer
Figure 7. Print unit metrics.When you call
GPrintUnit::RealizeMetrics()
, the unit will reserve space for them by subtracting their dimensions out of the printed page area,JRECT
. Next, overridePrintHeader()
andPrintFooter()
to print the header and footer. They will get called each timeStartPage()
andEndPage()
are called, whether this happens directly or indirectly. - Print bitmaps: There is no special functionality built in to the classes for printing bitmaps, I just used this to show that there is no limitation to what can be printed with the
GPrintJob
andGPrintUnit
classes. Because you have access to the printer device context, you can print anything you want, including bitmaps. The same bitmap printing code you use for painting on the screen is just as valid when printing.
Sample Usage
This sample shows how to print the small report listed in the above Figure 2. A derived unit and job are created and several virtual functions are overridden. We start the process by calling the job's function Print()
from within handler OnFilePrint()
:
//////////////////////////////////////////////////////////////////////////
// WARNING: uncompiled code ahead
//////////////////////////////////////////////////////////////////////////
class MyPrintUnit : public GPrintUnit
{
public:
MyPrintUnit() {;}
virtual ~MyPrintUnit() {;}
virtual void DefineColHeadings();
virtual void CreatePrintFonts();
void InitPrintMetrics()
virtual BOOL Print();
CFont m_fontHeading;
CFont m_fontBody;
};
void MyPrintUnit::DefineColHeadings()
{
// define my four columns...percentages should all add up to 1.00
InsertPrintCol(0, "Part Number", 0.45);
InsertPrintCol(1, "Description", 0.30);
InsertPrintCol(2, "Qty.", 0.10);
InsertPrintCol(3, "Cost", 0.15);
// must call base class
GPrintUnit::DefineColHeadings();
}
void MyPrintUnit::CreatePrintFonts()
{
m_fontHeading;
m_fontBody;
m_fontHeader.CreatePointFont(110, _T("Garamond"), &JDC);//I18nOK
m_fontFooter.CreatePointFont(90, _T("Garamond"), &JDC);//I18nOK
}
BOOL MyPrintUnit::Print()
{
// must call base class
GPrintUnit::Print();
StartPage();
PrintColHeadings(DT_LEFT);
struct part
{
LPCTSTR lpszPart;
LPCTSTR lpszDesc;
LPCTSTR lpszQty;
LPCTSTR lpszCost;
};
struct part parts[] =
{"123-4567", "Binary flip flop module, with 4t5
rating", "1","$34.45, "aq4-9909", "Overhead flimflam",
"12", "$0.99", "b59-123","Left-handed gangly wrench",
"6", "$99.99"}; for(int i = 0; i
<
sizeof(parts)/sizeof(parts[0]);
i++) { StartRow(); struct
part *pPart=
&parts[i]; PrintCol(0,pPart->lpszPart);
PrintCol(1, pPart->lpszDesc);PrintCol(2,
pPart->lpszQty); PrintCol(3,pPart->lpszCost);
EndRow();
}
EndPage();
}
//////////////////////////////////////////////////////////////////////////
class MyPrintJob : public GPrintJob
{
public:
MyPrintJob() {;}
virtual ~MyPrintJob() {;}
void OnPrint();
};
void MyPrintJob::OnPrint()
{
My PrintUnit unit(this);
unit.Print();
}
//////////////////////////////////////////////////////////////////////////
void OnFilePrint()
{
MyPrintJob job;
job.Print();
}
The meat of the work is done in the unit's overridden Print()
function. Several other functions like the unit's DefineColHeadings()
, CreatePrintFonts()
, and InitPrintMetrics()
are all required overrides used to define the fields to be printed, create the fonts we want to use for our headings and body, and let the unit know the dimensions of each, respectively. Notice how StartPage()
/StartRow()
are used to begin every page/row, and EndPage()
/EndRow()
are used to complete and advance to the next page/row. You're exposure to fonts and DCs is kept to a minimum, as all you have to do is create the fonts and tell the dc to use them like you would when drawing on-screen.
Additional Notes
- Margins
The
GPrintJob
class has no direct support for printing margins. However, all you need to do is deflate the drawing rectangle (JRECT
) to include your margins. Since allGPrintUnit
print functions already useJRECT
when printing, there is nothing else left to do. - Mapping mode
The classes
GPrintUnit
andGPrintJob
assumeMM_TEXT
mapping mode. You might have to do some extra work to use other mapping modes. It's fairly easy to do it with these classes, but this article won't demonstrate how to do this. - Print preview
Print preview is not supported by
GPrintUnit
andGPrintJob
. - Helpers
There are several utility type classes/functions/macros in the file gfx_printunit.cpp and gfx_printunit.h which are not specifically related to the classes
GPrintJob
andGPrintUnit
. If you have any questions, feel free to drop me an email.
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.