Download demo project - 62 Kb
Download source files - 48 Kb
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 a CView, eliminating the unnecessary steps while keeping the
required ones performed by CView::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 a CView, 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 and GPrintUnit. You only need one instance of GPrintJob to manage the global resources required for printing. On the other hand, you may have multiple GPrintUnits, 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 highlite some important notes.
Features
- JRECT, JCUR, and JDC
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 variable m_pJob which gets passed to the unit in its constructor, or in a call to SetJob(). Instead of the cumbersomesyntax m_pJob->m_pDC to access the device context, orm_pJob->m_rectClient to access the page size, the GPrintUnit header file has several macros to make this easy, including:
#defineJDC (*(m_pJob->m_pDC))
#define JRECTm_pJob->m_rectClient
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
used GPrintJob 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 the GPrintJob::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 function InsertPrintColumn(). 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 function PrintColHeadings(). 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 function SetActiveHeading().
- 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 and GPrintUnit 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 the string, which allows you to control the format of sections of the string. 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 macro GSELECT_PJINDEXTREE(). An index tree is an object of type GPrintIndexTree � a type-safe array of INDEXITEMs. Each INDEXITEM has a string for the title, a bit wise flag field, integer page number, and pointer to a GPrintIndexTree. 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 an INDEXITEM 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's m_pum members with their required size:
pumHeaderHeight
pumHeaderLineHeight
pumFooterHeight
pumFooterLineHeight
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, override PrintHeader() and PrintFooter() to print the header and
footer. They will get called each time StartPage() and EndPage() 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 and GPrintUnit
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():
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()
{
InsertPrintCol(0, "Part Number", 0.45);
InsertPrintCol(1, "Description", 0.30);
InsertPrintCol(2, "Qty.", 0.10);
InsertPrintCol(3, "Cost", 0.15);
GPrintUnit::DefineColHeadings();
}
void MyPrintUnit::CreatePrintFonts()
{
m_fontHeading;
m_fontBody;
m_fontHeader.CreatePointFont(110, _T("Garamond"), &JDC);
m_fontFooter.CreatePointFont(90, _T("Garamond"), &JDC);
}
BOOL MyPrintUnit::Print()
{
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();
}
Figure 8. Sample usage.
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 all GPrintUnit print
functions already use JRECT when printing there is nothing else left to do.
- Mapping mode
The classes GPrintUnit and GPrintJob assume MM_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 and
GPrintJob. � 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 and GPrintUnit. If you have any questions, feel
free to drop me an email.