MFC 打印制作简单






4.71/5 (22投票s)
1999 年 12 月 2 日
10分钟阅读

485432

10429
两个类,
在为 MFC 应用程序添加打印功能时,您可能会遇到的三个最大的挫败感是:
- 打印简单的基于行/列的报表。
对此还没有,而且可能永远也不会有支持——如果您想打印一个简单的基于文本的行和列报表,您就倒霉了。您必须从头开始编写,并担心设备上下文、页码、页眉和页脚——所有您期望在 MFC 中内置但却不存在的东西。
- 弄清楚如何在没有
CView
的情况下打印内容。MFC 类
CView
内置了许多打印功能。问题在于,当在CView
外部打印时,如何应用这些功能,从而在省略不必要步骤的同时,又能执行CView::OnPrint()
所必需的步骤。 - 将单个文档的打印无缝地合并到一个作业中。
用户希望能够生成自定义报表——能够以他们想要的任何组合包含不同类型的信息。MFC 中对此没有直接支持,没有使编码变得容易的类结构,也没有标准的方法来通信一个文档在什么页码或页面位置上停止了。
了解了这些,我决定是时候创建自己的类来处理这些问题了:GPrintJob
和 GPrintUnit
。使用它们的优点是:
- 您可以打印任何类型的报表。
这些类在设计时就考虑了打印基于文本的行和列报表。您按照行、列和文本进行操作——设备上下文、坐标、单词换行和其他繁琐工作的管理负担由它们承担。然而,由于这些类带有大量的重载,允许访问打印过程的所有阶段,因此您可以使用它们来打印任何您想要的东西。
- 它们不需要
CView
。许多类型的报表都是使用“隐藏”文档打印的。也就是说,仅仅因为文档没有打开视图,并不意味着用户不想打印它。然而,由于大多数打印功能都内置在
CView
中,这在 MFC 中是一项艰巨的任务。这两个类绕过了这个问题,因为它们不依赖于CView
,也不依赖于任何窗口。 - 允许您将单个文档合并到一个报表中。
GPrintJob
和GPrintUnit
之间存在一对多的关系。您只需要一个GPrintJob
实例来管理打印所需的全局资源。另一方面,您可能拥有多个GPrintUnit
,其中打印报表的实际“主体”内容。
对于任何给定的报表,您只需要一个 GPrintJob
对象实例。它会显示打印对话框,创建设备上下文,并管理打印过程的许多“全局”细节(例如,当前页码)。您派生的 GPrintJob
类的任务是协调一个或多个派生的 GPrintUnit
对象的打印。打印多少、以什么顺序以及打印什么内容,都取决于用户和您。您的每个派生的 GPrintUnit
通常只知道如何打印一种特定类型的文档。GPrintUnit
本身不打印任何内容。相反,它包含了允许您的派生单元打印页眉、页脚、列标题和文档正文的功能。每个 GPrintUnit
必须始终有一个父 GPrintJob
。
本文的其余部分将介绍这两个类的主要功能,展示一些演示如何使用它们的示例代码,并重点介绍一些重要注意事项。
特点
JRECT
、JCUR
和JDC
如上所述,每个单元都必须有一个父作业。没有父作业,单元就无法打印任何内容,因为作业负责创建和管理设备上下文。父作业通过
GPrintUnit
成员变量m_pJob
访问,该变量在构造函数中传递给单元,或在调用SetJob()
时传递。为了避免使用冗长的语法m_pJob->m_pDC
来访问设备上下文,orm_pJob->m_rectClient
来访问页面大小,GPrintUnit
头文件提供了一些宏,使此操作更加简便,包括:#defineJDC (*(m_pJob->m_pDC)) // the device context #define JRECTm_pJob->m_rectClient // the printed page size
图 2. 辅助宏。这些宏只能在
GPrintUnit
类内部使用。还有许多类似的宏可以访问其他常用的GPrintJob
成员。- 无需打印对话框
打印某些报表需要您自己的自定义对话框,或者根本不需要对话框。这可能是一个问题,因为标准的
CPrintDialog
通常会为您创建设备上下文。在这些非标准情况下,您可以使用GPrintJob::UseDefaults()
函数来为您创建一个默认的设备上下文。 - 可定义的列
在典型的行和列报表中,页面被划分为列,每列对应于打印记录中的一个字段。
GPrintJob
对这种类型的报表提供了全面的支持。您可以使用InsertPrintColumn()
函数预先定义所有列。通过此函数,您可以定义列的标题、属性和大小。大小定义为打印页面宽度的百分比。可以使用PrintColHeadings()
函数一次性打印列标题。您甚至可以在给定单元中定义多个列标题集(每个集都有自己的列数、标题和列大小),并使用SetActiveHeading()
函数在它们之间实时切换。 - 自动换行
要在报表的列中打印当前行的文本,请使用
GPrintUnit::PrintCol()
函数。此函数将自动检测文本是否会溢出给定单元格的边界,并使用一个隐藏的富文本控件,将溢出部分自动换行到下一行(甚至下一页!)。所有后续行的每一列都将根据溢出量自动向下移动。例如: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
图 3. 自动换行示例。 - 打印到文件
显示打印对话框后,
GPrintJob
会检查用户是否选择了“打印到文件”选项。如果选择了,它会显示“打开文件”对话框,允许用户选择一个文件名进行打印。 - 可重写函数
GPrintJob
和GPrintUnit
都拥有大量的虚函数,允许您根据应用程序的特定需求调整打印过程的每一个阶段。 - 文本对齐字符
大多数行和列报表都穿插着普通文本行。
PrintTextLine()
函数就是为此目的而创建的。它会在页面上当前光标位置打印一行文本。更重要的是,您可以在string
中嵌入特殊控制字符,这允许您控制string
中各个部分的格式。例如,下面的代码行:PrintTextLine("one two\x1c\x1fthree four\x1efive six");
将打印:
"one two…………………………three four five six"
其中“
one two
”左对齐(默认),“three four
”居中显示,并用点与“one two
”分隔,而“five six
”右对齐。这个功能极其强大,可以让您对单行文本的不同部分进行不同的对齐,而无需处理坐标。在 gfx_printunit.h 中有用于这些特殊格式字符的宏(HFC_*
)。此功能非常适合打印页眉和页脚,它们通常是单行文本,其中一部分居中,一部分左对齐,一部分右对齐。 - 目录
索引是一个类似树的结构,它不仅显示打印报表的细分,还显示每个子节开始的页码。使用这些类构建索引非常容易。要打印索引,您必须首先创建一个
GPrintIndexTree
,通常在作业中按值声明,并在打印过程开始时,使用宏GSELECT_PJINDEXTREE()
将其选为活动树。索引树是GPrintIndexTree
类型的对象——这是一个类型安全的INDEXITEM
数组。每个INDEXITEM
都有一个用于标题的string
、一个位标志字段、一个整数页码和一个指向GPrintIndexTree
的指针。这样,树就可以有多个递归级别。接下来,在打印每个单元时,它负责使用
GPrintUnit::AddIndexItem()
函数将自己添加到活动树中。它向此函数传递一个定义其标题和起始页码(如果适用)的INDEXITEM
结构。如果您从未选择除作业中声明的树以外的任何其他树作为活动树,所有条目都会出现在根项下。Introduction..............................................1 Languages C++.......................................................2 MFC.......................................................3 Pascal....................................................4 Basic.....................................................4 Fortran...................................................5 Computers Dell......................................................6 Compaq...................................................10 Gateway..................................................11 Conclusion...............................................15
图 4. 单级索引。然而,如果您想要一个多级索引,您必须更改活动项。要做到这一点,请使用宏
GSELECT_PJINDEXTREE()
,并将指向您想要使其成为活动/选定项的索引项的指针传递给它。然后该项将成为当前“作用域”的活动项。例如,在下表中,“Languages
”、“C++
”和“Computers
”都在添加时被选中,并且之后添加的项自动嵌套。Introduction..............................................1 Languages C++....................................................2 MFC.................................................3 Pascal.................................................4 Basic..................................................4 Fortran................................................5 Computers Dell...................................................6 Compaq................................................10 Gateway...............................................11 Conclusion...............................................15
图 5. 多级索引。当打印“C++”单元时,其索引项必须首先创建、初始化并选择。
INDEXITEM itemCPP; itemCPP.strName = "C++"; itemCPP.nPage = JINFO.m_nCurPage; GSELECT_PJINDEXTREE(&itemCPP.pChildren); AddIndexItem(&itemCPP);
图 6. 添加单元的索引条目。通过这些代码行,在此作用域内添加的任何项(例如,“
MFC
”)都将被添加为“C++
”的分支。要打印索引,请使用
GPrintUnit::PrintTree()
函数,并将您之前在派生作业中声明的索引树成员变量传递给它。它将能够解析树的内容,自动缩进条目的嵌套级别,并在需要时,在标题和页码之间插入点。这通常作为打印报表的最后一步。 - 页眉和页脚
如前所述,可以使用
PrintTextLine()
函数轻松打印页眉和页脚文本。但是,您必须告诉单元您想要它们。您可以通过首先初始化单元的m_pum
成员及其所需大小来做到这一点。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
图 7. 打印单元度量。当您调用
GPrintUnit::RealizeMetrics()
时,单元将通过从打印页面区域JRECT
中减去它们的尺寸来为它们保留空间。接下来,重载PrintHeader()
和PrintFooter()
来打印页眉和页脚。它们将在每次调用StartPage()
和EndPage()
时被调用,无论这是直接还是间接发生的。 - 打印位图:类中没有为打印位图提供特殊的内置功能,我只是用它来表明使用
GPrintJob
和GPrintUnit
类打印内容没有限制。因为您可以访问打印机设备上下文,所以您可以打印任何您想要的东西,包括位图。您在屏幕绘制时使用的位图打印代码,在打印时同样有效。
示例用法
此示例显示了如何打印上面图 2 中列出的较小报表。创建了派生单元和作业,并重载了几个虚函数。我们通过在处理程序 OnFilePrint()
中调用作业的 Print()
函数来启动该过程。
//////////////////////////////////////////////////////////////////////////
// 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();
}
大部分工作在单元重载的 Print()
函数中完成。几个其他函数,如单元的 DefineColHeadings()
、CreatePrintFonts()
和 InitPrintMetrics()
,都是必需的重载,用于定义要打印的字段,创建我们希望用于标题和正文的字体,并分别告知单元每个字体的尺寸。请注意,StartPage()
/StartRow()
用于开始每一页/行,而 EndPage()
/EndRow()
用于完成并移至下一页/行。您接触字体和 DCs 的机会被最小化了,因为您所要做的就是创建字体并像在屏幕上绘图时一样让 DC 使用它们。
其他说明
- 边距
GPrintJob
类不直接支持打印边距。但是,您只需要缩小绘图矩形(JRECT
)以包含您的边距。由于所有GPrintUnit
打印函数都已经使用JRECT
进行打印,因此无需再做其他工作。 - 映射模式
类
GPrintUnit
和GPrintJob
假定使用MM_TEXT
映射模式。您可能需要做一些额外的工作才能使用其他映射模式。对于这些类来说,这很容易做到,但本文档不会演示如何做到这一点。 - 打印预览
GPrintUnit
和GPrintJob
不支持打印预览。 - 辅助函数
在文件 gfx_printunit.cpp 和 gfx_printunit.h 中有几个工具类/函数/宏,它们与
GPrintJob
和GPrintUnit
类没有直接关系。如果您有任何问题,请随时给我发送电子邮件。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。