利用 .NET Printing 命名空间从 Windows 窗体应用程序中进行预览和打印

来源:互联网 发布:编程器使用教程 编辑:程序博客网 时间:2024/04/30 20:55
 
利用 .NET Printing 命名空间从 Windows 窗体应用程序中进行预览和打印
发布日期 : 12/13/2004 | 更新日期 : 12/13/2004

Alex Calvo

本文假设您熟悉 C# 和 Windows 窗体

下载本文的代码: PrintinginNET.exe (134KB)

摘要

打印是构成每个完整的、基于 Windows 的应用程序所需要的一部分。在这些应用程序中提供健壮的打印功能通常已被认定是一件乏味的苦差事。现在,利用 .NET Framework 从 Windows 窗体中进行打印意味着必须采用以文档为中心的方法,从而产生更整洁、更易于管理的代码。尽管 System.Windows.Forms 命名空间提供了与所有标准打印对话框(例如,“Print Preview”、“Page Setup”和“Print”)的无缝集成,但 System.Drawing.Printing 命名空间提供了大量的类,以便进行扩展和自定义。本文将讨论这些类以及它们如何提供对打印功能的访问。本文还对其他有用的技术(例如,在后台打印以使用户可以继续完成其他任务)进行阐释。

本页内容

使用 PrintDocument 类 使用 PrintDocument 类
实现派生的 PrintDocument 类 实现派生的 PrintDocument 类
利用 GDI+ 进行打印 利用 GDI+ 进行打印
PrintController 类 PrintController 类
使用打印对话框 使用打印对话框
进行后台打印 进行后台打印
小结 小结

从开发角度来看,Microsoft.NET 已经改变了几乎所有的方方面面。其中的一些变化(例如,Web 窗体和 ADO.NET)已经要求在完成任务的方式上进行重大改变,而其他一些变化在本质上则更为缓和一些,仅仅是在现有技术(如 System.Xml)的基础上进行了一些改进。对于传统的使用 Visual Basic 和 Visual C++ 的开发人员而言,从 Windows窗体进行打印预示着一项重大改变。但是,对于大量使用 .NET Framework 的情况,该更改无疑会更好。

使用 Visual Basic Print 对象及其 Printers 集合的日子一去不返了。在 .NET Framework 中,没有任何整体式的 Print 对象,并且您再也不会设置 CurrentX 和 CurrentY 属性或者发出象 EndDoc 和 NewPage 这样的命令了。如果您从 Visual C++ 转而使用 .NET,那么您或许意识到打印可能是一项乏味的任务。例如,它要求您使用 Win32 API 仔细跟踪打印过程,以确保页被正确地打印。这并不是说您再也不必做诸如此类的事情了。只是说通过 .NET,您最终会得到更为整洁且更容易维护的打印逻辑。通过 .NET Framework 类可以完全封装打印代码。因为可以从一组基类派生代码,所以您将免费获得所有种类的附加功能。挂钩到 Print Preview 对话框,对于 .NET 来说是小菜一碟,这只不过是个示例而已。

本文中的大多数代码示例,均摘自可从本文页顶部链接处下载的示例打印应用程序。图 1 中显示的示例 Windows 窗体应用程序演示了在 .NET 中打印时可以使用的很多新特性和功能。它使您可以选择任何文本文档并将其发送到 Print Preview 对话框或特定的打印机。出于演示目的,我将为您提供一个选项,便于您选择是否应当在每个页上显示水印。

图 1 Windows 窗体应用程序

在使用 Print Preview 对话框进行打印时,可以启用或禁用消除锯齿功能 — 这是一项内置功能,它使在屏幕上呈现的文本和图形具有更为平滑的外观。但是,请记住,这是以输出速度的降低为代价的。此外,Print Preview 对话框会自动利用 Windows 提供的任何字体平滑显示 (ClearType),因而减少了使用消除锯齿功能的需要。在将输出发送到打印机时,示例打印应用程序也使您可以选择其他多种选项。然后,您可以决定是显示状态对话框、在状态栏中显示动画打印机,或者是在后台线程中进行打印。

通过首先分析将输出发送到打印机的快速而杂乱的方式,让我们来体验一番 Windows 窗体打印。然后,我将更严密地考察通过 Windows 窗体进行打印的正确方式 — 使用派生的 PrintDocument 类。

使用 PrintDocument 类


从 Windows 窗体进行打印是一种以文档为中心的事件驱动过程。您的大部分精力将花在使用通用的 PrintDocument 对象或实现派生的 PrintDocument 类上。从 PrintDocument 基类继承是一种更好的方式 — 其原因我很快会予以说明。不过,有时候使用 PrintDocument 基类的实例可能会更快、更简单。

用 PrintDocument 基类进行打印需要将该类的 PrintPage 事件关联到其签名与 PrintPageEventHandler 委托匹配的某个处理程序方法(静态或实例)。当代码调用 PrintDocument 对象实例上的 Print 方法时,将激发该事件。要实际绘制页,可以使用 PrintPageEventArgs 对象的 Graphics 属性。PrintPageEventArgs 类的实例作为参数传递给 PrintPage 事件处理程序。PrintPageEventArgs 对象的 Graphics 属性公开了一个 GDI+ 对象,该对象封装您用来绘制页的绘图表面。(本文稍后将对某些基本 GDI+ 命令进行讨论。)要打印一张以上的页,需要通知基础打印控制器您还有更多要打印的页。可以使用 PrintPageEventArgs 对象的 HasMorePages 属性完成该任务。将 HasMorePages 属性设置为真可以确保再次调用 PrintPage 事件处理程序。

此外,可以为其他常见打印事件(例如,BeginPrint、EndPrint 和 QueryPageSettings)设置事件处理程序。BeginPrint 是初始化 PrintPage 例程可能依赖的任何对象(例如,Fonts)的不错的选择。QueryPageSettings 事件恰好在每个 PrintPage 事件之前激发。它使您可以使用不同的页设置来打印每一页(您可以通过修改 QueryPageSettingsEventArgs.PageSettings 属性得到不同的页设置)。为了修改整个文档的页设置,可以使用 PrintDocument 类的 DefaultPageSettings 属性。

下面的示例说明了如何使用 PrintDocument 基类启动打印作业:

PrintDocument printDoc = new PrintDocument();printDoc.PrintPage += new PrintPageEventHandler(this.printDoc_PrintPage);printDoc.Print();// The PrintPage event is raised for each page to be printed.private void printDoc_PrintPage(object sender, PrintPageEventArgs e) {    // TODO: Print your page using the e.Graphics GDI+ object    // Notify the PrintController whether there are any more pages    e.HasMorePages = false;}

正如您可以看到的那样,该方法存在许多缺点。最大的缺点在于:您必须在对 PrintPage 事件处理程序进行的连续调用之间维护具有状态意识的对象。例如,如果您要打印文本文档,则将需要维持一个打开的 StreamReader 对象。您可以在 BeginPrint 事件期间初始化 StreamReader,然后在 EndPrint 事件期间关闭它。但是,无论您如何分割 StreamReader 变量,都需要将它的作用域与其他变量一起限制在 PrintPage 事件处理程序的外部。当发生这种情况时,您的打印代码将被公开并且易受攻击,而且可能使其余代码变得混乱。

返回页首

实现派生的 PrintDocument 类


当从 Windows 窗体进行打印时,更好的方法是实现一个从通用的 PrintDocument 类继承的类。这样,您就可以迅速地获得封装的回报。无需为 BeginPrint、EndPrint 和 PrintPage 事件实现事件处理程序,而是重写基础的 PrintDocument 基类的 OnBeginPrint、OnEndPrint 和 OnPrintPage 方法。现在,OnPrintPage 方法所使用的任何具有状态意识的对象都可以保持在私有类字段中。这完全消除了我刚刚提到的潜在的代码问题。此外,您现在可以随意地向派生的 PrintDocument 类中添加自定义属性、方法、事件和构造函数。

示例打印应用程序使用 TextPrintDocument 类型的派生 PrintDocument 类,如图 2 所示。TextPrintDocument 类公开了一个采用一个文件名作为参数的重载构造函数。另外,还可以使用自定义 FileToPrint 属性设置和读取该文件名。当被设置为不存在的文件时,该属性会引发异常。该类还公开了一个名为 Watermark 的公用布尔型字段,该字段用于启用或禁用页背景图形。(页背景图形作为名为 Watermark.gif 的嵌入式资源存储在程序集中。)最后,派生的 TextPrintDocument 类公开了 Font 属性,以指定哪个字体是要在呈现页时使用的正确字体。

可以在 OnPrintPage 方法中找到 TextPrintDocument 类的内部机制。在这里,可以使用 PrintPageEventArgs Graphics 属性提供的 GDI+ 绘图表面绘制页。此外,PrintPageEventArgs 对象包含下列属性:Cancel、HasMorePages、MarginBounds、PageBounds 和 PageSettings。通过 Cancel 可以取消打印作业。MarginBounds 属性返回一个 Rectangle 对象,该对象代表位于边距内页的部分。可以使用该矩形来确定每页上开始和停止打印的位置。另一方面,PageBounds 代表页的整个区域(包括边距)。

返回页首

利用 GDI+ 进行打印


GDI+ 的细节本身需要一篇文章才能完整介绍,因此,出于本文的目的,我将只从 TextPrintDocument 类如何呈现每个页的角度来讨论 GDI+。然后,我将讨论一些您很可能在典型的打印过程中进行的 GDI+ 调用。

首先,OnPrintPage 方法使用 Font 类的 GetHeight 方法确定当前字体的高度。GetHeight 方法可以通过采用一个 Graphics 对象作为参数来确定当前页的每英寸点数 (dpi)。一旦确定了当前字体的高度,就使用当前的 MarginBounds.Height 计算每页的行数。接下来,读取文本文件(一次一行),并且使用 DrawString 方法打印。如果在到达文件结尾之前到达了页尾,则将 HasMorePages 属性设置为真。

正如您可以看到的那样,只是使用 DrawString 就可以完成基本打印。但是,GDI+ 为您提供了 15 种以上的绘制方法,并且每种方法都可以进行大量的重载。可以使用向量图形(例如,DrawBezier、DrawEllipse)和栅格图形(例如,DrawImage、DrawIcon)进行打印。请注意 OnPrintPage 方法如何使用 DrawImage 显示水印。您还可以利用 GDI+ 的很多高级功能,例如剪辑和转换。请尝试用 Visual Basic Print 对象完成该任务!

返回页首

PrintController 类


早些时候,我提到示例打印应用程序(如图 1 所示)允许您显示可选的状态对话框和/或动画状态栏图标(在打印的同时吐出页的那种)。这两种功能都是使用派生的打印控制器类实现的。PrintController 是一个抽象类,它由 .NET Framework 中的三个不同的具体类实现:StandardPrintController、PrintControllerWithStatusDialog 和 PreviewPrintController。

打印控制器负责如何对打印文档进行打印。PrintDocument 类将它的基础打印控制器公开为属性。调用打印文档的 Print 方法会触发基础打印控制器的 OnStartPrint、OnEndPrint、OnStartPage 和 OnEndPage 方法。图 3 显示了在打印文档和打印控制器之间发生的事件序列。OnStartPage 是唯一一个返回值的方法。返回值的类型是 Graphics,而且,正像您可能已经猜到的那样,它是通过 PrintPageEventArgs 参数传递给打印文档的 GDI+ 绘图表面。

misprintinginnetfig03

图 3 打印流程

默认打印控制器的类型是 PrintControllerWithStatusDialog。因此,如果要关闭打印状态对话框,则将需要使用 StandardPrintController。PreviewPrintController 由 PrintPreviewDialog 和 PrintPreviewControl 类使用。PrintControllerWithStatusDialog 位于 System.Windows.Forms 命名空间中,而 StandardPrintController 和 PreviewPrintController 位于 System.Drawing.Printing 命名空间下面。PrintControllerWithStatusDialog 提供了一个重载构造函数,该函数采用另外一个打印控制器作为参数。这使您可以将 PrintControllerWithStatusDialog 与您可能添加到自己的打印控制器中的其他任何功能组合起来。当运行示例打印应用程序时,请尝试选中 PrintControllerWithStatusDialog 和 PrintControllerWithStatusBar 复选框以查看这是如何工作的。以下片段说明了代码是如何进行工作的:

   CustomPrintDocument printDoc = new       CustomPrintDocument();   CustomPrintController printCtl = new       CustomPrintController();   printDoc.PrintController = new       PrintControllerWithStatusDialog(         printCtl, "Dialog Caption");   printDoc.Print();

示例打印应用程序使用一个 PrintControllerWithStatusBar 类型的自定义打印控制器(参见图 4)。PrintControllerWithStatusBar 公开了一个 StatusBarPanel 属性,该属性确定哪个状态栏面板应当显示动画打印机图标。我使用一个 System.Timers.Timer 类型的计时器以便完成实际的动画。System.Timers.Timer 类在多线程应用程序(执行后台打印时就属于这种情况)中运行良好。

返回页首

使用打印对话框


在 .NET 中打印的优点是打印文档与打印对话框配合完美。在 System.Windows.Forms 命名空间中有三种不同的打印对话框:PrintDialog、PrintPreviewDialog 和 PageSetupDialog。此外,还有一个不包含环绕对话框的 PrintPreviewControl 类,从而提供了更大的 UI 设计灵活性。出于简单的目的,示例打印应用程序使用了 PrintPreviewDialog 类(参见图 5)。

misprintinginnetfig05

图 5 打印预览

PrintDialog 类可以用来在 Windows 中显示标准的 Print 对话框,并且使用户能够选择打印机、指定要打印的页以及确定副本的数量。使用它就像下面显示的那样简单:

CustomPrintDocument printDoc = new CustomPrintDocument();PrintDialog dlgPrint = new PrintDialog();dlgPrint.Document = printDoc;if (dlgPrint.ShowDialog() == DialogResult.OK){    printDoc.Print();}

使用 PrintPreviewDialog 类甚至更加容易。我发现 Print Preview 对话框在开发过程中极为有用。在调试打印文档时,它可以节约大量的纸张。下面是它的工作原理:

CustomPrintDocument printDoc = new CustomPrintDocument();PrintPreviewDialog dlgPrintPreview = new PrintPreviewDialog();// Set any optional properties of dlgPrintPreview here...dlgPrintPreview.Document = printDoc;dlgPrintPreview.ShowDialog();

在创建 PrintPreviewDialog 类的实例之后,需要将它的 Document 属性设置为任何从 PrintDocument 派生的类的实例。PrintPreviewClass 的 ShowDialog 方法将自动处理文档的打印预览的呈现(参见图 5)。正如您所知道的,没有比这更简单的了。

PageSetupDialog 类以类似的方式工作。但是,除了支持 Document 属性以外,您还可以选择将 PageSettings 或 PrinterSettings 属性设置为 PageSettings 或 PrinterSettings 类的实例。PageSettings 类定义了适用于实际打印页的设置,例如 Margins 和 PaperSize,而 PrinterSettings 类则指定有关如何打印文档的信息,例如,FromPage、ToPage 和 PrinterName。通过 PrinterSettings 类,还可以使用 InstalledPrinters 静态方法(它返回 PrinterSettings.StringCollection 对象)获得可用打印机的列表。。根据从 Page Setup 对话框的返回结果,Document、PageSettings 或 PrinterSettings 对象将会进行相应地修改。

返回页首

进行后台打印


线程处理较难对付,而我绝不打算否认这一情况,即在编写多线程应用程序时会有问题出现的趋势。尽管如此,事实是使用后台线程确实可以改善用户的打印体验。它使用户可以在打印作业在后台处理的同时继续使用应用程序的其余部分。要对后台打印进行试验,最好使用大型文档并确保打印机暂停。为什么要浪费纸张呢?您还可以使用 Thread 类的静态 Sleep 方法降低打印速度。通过示例打印应用程序,可以在将输出发送到打印机时对此进行尝试。将后台打印与 Print Preview 对话框一起使用是没有意义的。简而言之,下面是它的工作方式:

private void cmdBackgroundPrint_Click(object sender, System.EventArgs e){    Thread t = new Thread(new ThreadStart(PrintInBackground));    t.IsBackground = true;    t.Start();}private void PrintInBackground(){    printDoc.Print();}

您还可以使用委托完成相同的工作。最初,当我编写该示例打印应用程序时,我使用了 Thread 类。但是,因为我希望在打印完成时得到通知,所以我决定改而使用委托。通过使用委托的 BeginInvoke 方法,可以指定一个回调函数,以便代码可以在异步操作已经完成时得到通知。我需要这样做,以便在打印完成之后以线程安全方式重新启用 Print 按钮(参见图 6)。

图 6包含用来驱动该示例打印应用程序的大多数代码。特别地,它包含一个名为 PrintInBackgroundDelegate 的委托。如果选中了 Background 线程复选框,则会在 Print 按钮的 Click 事件处理程序内部调用该委托的 BeginInvoke 方法。否则,直接从 UI 主线程中调用打印文档的 Print 方法。

您可能遇到的任何缺陷多半都将涉及到打印文档类可能依赖的具有状态意识的对象。例如,以前面的示例为例,如果用户单击 cmdBackgroundPrint 一次以上,会发生什么情况?结果在最佳情况下也是不可预知的。因为 PrintDocument.PrintPage 事件通常依赖于具有状态意识的类字段,所以在类似情况下,事情确实会变得很麻烦。处理这些种类问题的一种简易方式是简单地禁用某些 UI 控件,以防止用户重新发出同一命令。同时,他们可以继续使用应用程序的其余部分。这就是示例打印应用程序所使用的方法。在处理后台打印之前,我建议您阅读本期第 68 页上 Ian Griffiths 的文章。

返回页首

小结


尽管本文只讨论了 .NET 中的本机 Windows 窗体打印,但 .NET 还提供有其他的打印机输出方法,例如,Crystal Report。在某些特定情况下,报表工具可能比本机打印更为适当。但是,如果您不希望支付现成产品的开销,并且您的应用程序需要自定义或专用打印功能(就像许多基于桌面 Windows 的应用程序中所具有的那些功能),那么一定要采用本机打印的方法。

原创粉丝点击