65.9K
CodeProject 正在变化。 阅读更多。
Home

MFC 打印制作简单

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (22投票s)

1999 年 12 月 2 日

10分钟阅读

viewsIcon

485432

downloadIcon

10429

两个类, 为您的 MFC 应用程序添加高级打印功能

Sample Image - printingmadeeasy.jpg

在为 MFC 应用程序添加打印功能时,您可能会遇到的三个最大的挫败感是:

  • 打印简单的基于行/列的报表。
    对此还没有,而且可能永远也不会有支持——如果您想打印一个简单的基于文本的行和列报表,您就倒霉了。您必须从头开始编写,并担心设备上下文、页码、页眉和页脚——所有您期望在 MFC 中内置但却不存在的东西。
  • 弄清楚如何在没有 CView 的情况下打印内容。
    MFC 类 CView 内置了许多打印功能。问题在于,当在 CView 外部打印时,如何应用这些功能,从而在省略不必要步骤的同时,又能执行 CView::OnPrint() 所必需的步骤。
  • 将单个文档的打印无缝地合并到一个作业中。
    用户希望能够生成自定义报表——能够以他们想要的任何组合包含不同类型的信息。MFC 中对此没有直接支持,没有使编码变得容易的类结构,也没有标准的方法来通信一个文档在什么页码或页面位置上停止了。

了解了这些,我决定是时候创建自己的类来处理这些问题了:GPrintJobGPrintUnit。使用它们的优点是:

  • 您可以打印任何类型的报表。
    这些类在设计时就考虑了打印基于文本的行和列报表。您按照行、列和文本进行操作——设备上下文、坐标、单词换行和其他繁琐工作的管理负担由它们承担。然而,由于这些类带有大量的重载,允许访问打印过程的所有阶段,因此您可以使用它们来打印任何您想要的东西。
  • 它们不需要 CView
    许多类型的报表都是使用“隐藏”文档打印的。也就是说,仅仅因为文档没有打开视图,并不意味着用户不想打印它。然而,由于大多数打印功能都内置在 CView 中,这在 MFC 中是一项艰巨的任务。这两个类绕过了这个问题,因为它们不依赖于 CView,也不依赖于任何窗口。
  • 允许您将单个文档合并到一个报表中。
    GPrintJobGPrintUnit 之间存在一对多的关系。您只需要一个 GPrintJob 实例来管理打印所需的全局资源。另一方面,您可能拥有多个 GPrintUnit,其中打印报表的实际“主体”内容。

对于任何给定的报表,您只需要一个 GPrintJob 对象实例。它会显示打印对话框,创建设备上下文,并管理打印过程的许多“全局”细节(例如,当前页码)。您派生的 GPrintJob 类的任务是协调一个或多个派生的 GPrintUnit 对象的打印。打印多少、以什么顺序以及打印什么内容,都取决于用户和您。您的每个派生的 GPrintUnit 通常只知道如何打印一种特定类型的文档。GPrintUnit 本身不打印任何内容。相反,它包含了允许您的派生单元打印页眉、页脚、列标题和文档正文的功能。每个 GPrintUnit 必须始终有一个父 GPrintJob

本文的其余部分将介绍这两个类的主要功能,展示一些演示如何使用它们的示例代码,并重点介绍一些重要注意事项。

特点

  • JRECTJCURJDC

    如上所述,每个单元都必须有一个父作业。没有父作业,单元就无法打印任何内容,因为作业负责创建和管理设备上下文。父作业通过 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 会检查用户是否选择了“打印到文件”选项。如果选择了,它会显示“打开文件”对话框,允许用户选择一个文件名进行打印。
  • 可重写函数
    GPrintJobGPrintUnit 都拥有大量的虚函数,允许您根据应用程序的特定需求调整打印过程的每一个阶段。
  • 文本对齐字符
    大多数行和列报表都穿插着普通文本行。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() 时被调用,无论这是直接还是间接发生的。

  • 打印位图:类中没有为打印位图提供特殊的内置功能,我只是用它来表明使用 GPrintJobGPrintUnit 类打印内容没有限制。因为您可以访问打印机设备上下文,所以您可以打印任何您想要的东西,包括位图。您在屏幕绘制时使用的位图打印代码,在打印时同样有效。

示例用法

此示例显示了如何打印上面图 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();
}
图 8. 示例用法。

大部分工作在单元重载的 Print() 函数中完成。几个其他函数,如单元的 DefineColHeadings()CreatePrintFonts()InitPrintMetrics(),都是必需的重载,用于定义要打印的字段,创建我们希望用于标题和正文的字体,并分别告知单元每个字体的尺寸。请注意,StartPage()/StartRow() 用于开始每一页/行,而 EndPage()/EndRow() 用于完成并移至下一页/行。您接触字体和 DCs 的机会被最小化了,因为您所要做的就是创建字体并像在屏幕上绘图时一样让 DC 使用它们。

其他说明

  • 边距
    GPrintJob 类不直接支持打印边距。但是,您只需要缩小绘图矩形(JRECT)以包含您的边距。由于所有 GPrintUnit 打印函数都已经使用 JRECT 进行打印,因此无需再做其他工作。
  • 映射模式
    GPrintUnitGPrintJob 假定使用 MM_TEXT 映射模式。您可能需要做一些额外的工作才能使用其他映射模式。对于这些类来说,这很容易做到,但本文档不会演示如何做到这一点。
  • 打印预览
    GPrintUnitGPrintJob 不支持打印预览。
  • 辅助函数
    在文件 gfx_printunit.cppgfx_printunit.h 中有几个工具类/函数/宏,它们与 GPrintJobGPrintUnit 类没有直接关系。如果您有任何问题,请随时给我发送电子邮件。

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.