运用.NET Framework中的类来创建看上去很专业的报表。
by Michael Eaton
技术工具箱:C#
你可以用几种方法在.NET中编程来生成打印输出结果(如报表)。对 Windows程序员来说,Visual Studio提供的Crystal Reports实际上是人们常用的打印报表的工具,但对不太复杂的报表来说,这个工具就有些大材小用了。当然,你可以用很好的Win32 API调用,但是尽管API可以让你有完全的控制权,它同时也把你锁定到一个单一的平台上了。如果Microsoft或另外的公司(如拥有Mono项目的Ximian公司)把Framework转移到另外的平台上,那么运用API的程序仍会锁定在 Windows上,除非你为新的平台重新编写它们。
.NET Framework可以让你以一种新的方式来使用这些打印方法,System.Drawing.Printing名字空间的类将Win32 API的细粒度控制(fine-grain control)与相对简单的Visual Basic传统的Printer对象结合了起来(见 表1)。在此,我将重点讲述该名字空间的主要的类,它们可以让你创建复杂程度适度的报表。PrintDocument类是这些类中最重要的。它可以让你定义一个对象,该对象发送输出结果到一个目的地,可以是一台打印机,或者是显示一个打印预览(运用PrintPreviewDialog类)。我还会讲述PrinterSettings类,它可以让你控制文件是如何打印的。
你可以很容易地在C#中创建一个PrintDocument对象,并把它附到一个事件处理程序上(实际处理打印的代码):
PrintPage事件处理程序有一个PrintPageEventArgs类型的参数。这个参数包含与该事件相关的数据,包括当前页面的页面设置、页边距信息以及是否有更多的页需要打印。每个页面触发该事件一次,直到ev.HasMorePages等于false:
将Graphics用于Fonts和Fills
ev参数引用了你用来输出数据的Graphics制图区。Graphics有用来打印图象、文本、形状和线条的方法。你可以用DrawString方法来定义你要用的font(字体)和brush(画刷)。一个brush定义了一个图形的内部是如何填充的。你也需要告诉DrawString方法你想在哪里打印文本。PrintPageEventArgs类有一个读写属性——HasMorePages——它指明是否需要打印更多的页面:
现在,你已经创建了一个PrintDocument对象并把它和一个事件处理程序联系起来了。你已经给事件处理程序中添加了代码,来打印一个单独的字符串,它的位置是距页面左边一英寸、距页面顶部一英寸,用的是12磅、 Arial的字体。现在我们来打印文件:
在缺省设置下,PrintDocument类的Print方法在缺省打印机上打印结果,除非你另外指定。你可以用PrintDialog类来另外指定,它是System. Windows.Forms.CommonDialog名字空间的一部分。PrintDialog可以让你选择打印机,选择打印文件的哪部分,选择打印份数,来随意地打印一个文件。
PrintDialog类的PrinterSettings属性可以让你将诸如 copies、from page和 to page的属性放置到一个特定文件的PrinterSettings对象中。换句话说,单独的PrintDocument对象有一个PrinterSettings对象,它指定打印的份数、打印的范围、要运用的打印机的名字以及关于打印机本身的信息。
IPrinterSettings类中的一个bug会导致Copies属性总是返回一个为1的值,而不管你在PrintDialog中输入了多少份数。然而,PrintDocument.Print()方法仍然打印正确的份数,所以只有当你想将这个值用于其它地方时,才显示这个bug。当你执行Print方法时,系统运用这些设置。你可以提供一个实际的PrinterSettings对象或提供一个Document对象:
然后,运用CommonDialog名字空间的PageSetupDialog,你可以指定纸张大小、纸张来源、打印方向和页边距。PageSetupDialog和PrintDialog都需要你提供一个Document对象或一个PageSettings对象。PageSettings对象适用于一个单独的打印页面,可以处理页边距、纸张大小以及是否用颜色打印页面:
打印一个文件
当然,许多应用程序打印存储在一个文件或某种数据库中的信息,所以现在我将讲述一个更复杂的例子:打印一个以逗号分隔的联系清单文本文件。你可以将一个数据集、甚至一个XML文件中数据所运用的方法用在这里。本例的数据来自Northwind数据库的Customers表,格式如下:
通过在类的级别声明一个StreamReader对象,然后将一个文件分配给它,你就可以从一个文本文件打印该例子了。打开文件,修改PrintPage事件处理程序来打印文件的每一行,按需要格式化数据。
PrintDocument类有好几个事件。你已经用了PrintPage事件了;现在我们来添加更多的事件。当开始一个打印任务时(在实际打印任何东西前),触发BeginPrint事件。相应的事件是EndPrint,该事件是在所有页面结束打印时触发的。创建PrintPage事件需要的任何对象,或在BeginPrint中打开任何数据源,然后在EndPrint中关闭或释放(deallocate)它们。不要忘记将这些事件绑定到PrintDocument对象(见 列表1)。
作为选择,你也可以把所有的打印功能封装到PrintDocument派生的一个单独的类中,然后重载触发相应事件的方法。你可以用你自定义的类,而不用实例化一个PrintDocument对象(见
列表2)。这是一个很好的方法,但是为了保持一致,使代码清晰,我将继续讲述事件绑定方法。
PrintDocument类没有制图工具,所以创建报表需要花些时间——但这是值得的。创建一个新的报表的最好的方法是用一张空白的纸。在了解了报表的目的后,你就可以在纸上通过画方框来代表不同的区域进行设计了,如页眉、正文和页脚(见 图1)。然后花些时间详细编写每个区域的数据将来自哪里,以及各个数据将放置在什么位置。可以说这个时候是你确保最终用户所看到的页面与你设计的页面是否是一致的最佳时刻。用户对报表比对应用程序的任何部分的挑剔都要多,所以你应该尽可能多花些时间来设计它。一旦你花了很多时间来设计报表并对它的打印结果了如指掌后,那么写代码就不会花很多时间了。
写代码
Rectangle对象提供了最快、最简单的方法来将你的页面设计转换成代码。这些对象可以让你几乎很准确地在屏幕上模拟你的设计。在页面上定义一个区域,指定坐标(有一定的高度和宽度)。Rectangle对象可以给你提供指定的点,在这个点上,你可以用Graphics类的Draw方法放置文本和图形。特别的是,你可以用DrawRectangle方法来查看该区域在页面的什么位置。该方法需要一个Pen对象(用来画直线和曲线)。将所有的版面设计代码放在PrintPage事件过程中:
.NET Framework 1.0不能得到一台打印机的“hard”margins(实际可以打印到的页面最外面的区域)。但是,PrintPageEventArgs可以让你通过MarginBounds属性得到类似的功能。不幸的是,MarginBounds不考虑hard margins,所以你的输出可能不能在你期望的位置上结束。你必须用P/Invoke并调用Win32 GetDeviceCaps函数来得到打印机的hard margins(见 列表3)。在得到hard margins后,你就可以开始创建Rectangle对象并在你的报表上打印信息了(见 列表4)。
现在你已经创建了长方形边框(rectangles),你就可以在这个位置上打印你的数据了。一次打印一页数据,所以你需要定义一个页面有多大。用一个标准行的高度来划分可打印的区域(本例中你的正文区),从而计算每个页面的行数。通过用你运用的Font对象的GetHeight方法来确定一个行的高度。GetHeight是个属于Font类的重载的方法。你运用的方法需要一个Graphics对象参数。PrintPage事件的PrintPageEventArgs参数提供了这个Graphics对象:
一旦你确定了构成一个页面的行数,你就只需要进行简单的循环就行了,直到一个页面结束。然后设置ev.HasMorePages为true,或者一直等到打印的数据结束。在循环内部运用Graphics对象的Draw方法来打印你的数据。
你也需要确保DrawString方法将文本放置在了正确的位置上。通过用你选择的字体的高度乘以你用来跟踪已经打印了多少行的计数器,你就可以在每打印一行时计算下一行的位置了。然后为顶部的页边距添加值(见 列表5)。
你运用的DrawString方法也接受一个StringFormat对象。StringFormat类可以让你控制文本的布局——包括对齐和行间距——以及省略符号的插入(如果一个给定的字符串对于你的长方形边框来说太长了时)。通过创建StringFormat对象,并设置其Alignment属性为StringAlignment.Center,你就可以使你的文本居中;然后将对象用于你调用的DrawString方法中。为你的联系清单页眉写以下代码:
正如你所看到的,一旦你确定了报表的布局并创建了长方形边框来包含数据,实际的打印并不是很难。
PrintPreviewControl和PrintPreviewDialog类在Printing名字空间中提供了很好的功能。PrintPreviewDialog封装了PrintPreviewControl类,并提供了一个很好的用户界面,用来在页面间导航、改变缩放比例、选择一次可以预览的页数(见
图2)。你可以用PrintPreviewControl来创建一个与你的应用程序其它部分相一致的打印预览窗口。一旦你定义了你的PrintDocument,并为必需的打印事件编写了代码后,添加打印预览就很简单了
by Michael Eaton
技术工具箱:C#
你可以用几种方法在.NET中编程来生成打印输出结果(如报表)。对 Windows程序员来说,Visual Studio提供的Crystal Reports实际上是人们常用的打印报表的工具,但对不太复杂的报表来说,这个工具就有些大材小用了。当然,你可以用很好的Win32 API调用,但是尽管API可以让你有完全的控制权,它同时也把你锁定到一个单一的平台上了。如果Microsoft或另外的公司(如拥有Mono项目的Ximian公司)把Framework转移到另外的平台上,那么运用API的程序仍会锁定在 Windows上,除非你为新的平台重新编写它们。
.NET Framework可以让你以一种新的方式来使用这些打印方法,System.Drawing.Printing名字空间的类将Win32 API的细粒度控制(fine-grain control)与相对简单的Visual Basic传统的Printer对象结合了起来(见 表1)。在此,我将重点讲述该名字空间的主要的类,它们可以让你创建复杂程度适度的报表。PrintDocument类是这些类中最重要的。它可以让你定义一个对象,该对象发送输出结果到一个目的地,可以是一台打印机,或者是显示一个打印预览(运用PrintPreviewDialog类)。我还会讲述PrinterSettings类,它可以让你控制文件是如何打印的。
你可以很容易地在C#中创建一个PrintDocument对象,并把它附到一个事件处理程序上(实际处理打印的代码):
PrintDocument doc = new PrintDocument(); doc.PrintPage += new PrintPageEventHandler( doc_PrintPage); |
PrintPage事件处理程序有一个PrintPageEventArgs类型的参数。这个参数包含与该事件相关的数据,包括当前页面的页面设置、页边距信息以及是否有更多的页需要打印。每个页面触发该事件一次,直到ev.HasMorePages等于false:
private void doc_PrintPage(object sender, PrintPageEventArgs ev) |
将Graphics用于Fonts和Fills
ev参数引用了你用来输出数据的Graphics制图区。Graphics有用来打印图象、文本、形状和线条的方法。你可以用DrawString方法来定义你要用的font(字体)和brush(画刷)。一个brush定义了一个图形的内部是如何填充的。你也需要告诉DrawString方法你想在哪里打印文本。PrintPageEventArgs类有一个读写属性——HasMorePages——它指明是否需要打印更多的页面:
Font f = new Font("Arial", 12);ev.Graphics.Drawstring("Hello, World.", f, Brushes.Black, 100, 100);ev.HasMorePages = false; |
现在,你已经创建了一个PrintDocument对象并把它和一个事件处理程序联系起来了。你已经给事件处理程序中添加了代码,来打印一个单独的字符串,它的位置是距页面左边一英寸、距页面顶部一英寸,用的是12磅、 Arial的字体。现在我们来打印文件:
doc.Print(); |
在缺省设置下,PrintDocument类的Print方法在缺省打印机上打印结果,除非你另外指定。你可以用PrintDialog类来另外指定,它是System. Windows.Forms.CommonDialog名字空间的一部分。PrintDialog可以让你选择打印机,选择打印文件的哪部分,选择打印份数,来随意地打印一个文件。
PrintDialog类的PrinterSettings属性可以让你将诸如 copies、from page和 to page的属性放置到一个特定文件的PrinterSettings对象中。换句话说,单独的PrintDocument对象有一个PrinterSettings对象,它指定打印的份数、打印的范围、要运用的打印机的名字以及关于打印机本身的信息。
IPrinterSettings类中的一个bug会导致Copies属性总是返回一个为1的值,而不管你在PrintDialog中输入了多少份数。然而,PrintDocument.Print()方法仍然打印正确的份数,所以只有当你想将这个值用于其它地方时,才显示这个bug。当你执行Print方法时,系统运用这些设置。你可以提供一个实际的PrinterSettings对象或提供一个Document对象:
PrintDialog pd = new PrintDialog();pd.Document = doc;pd.ShowDialog(); |
然后,运用CommonDialog名字空间的PageSetupDialog,你可以指定纸张大小、纸张来源、打印方向和页边距。PageSetupDialog和PrintDialog都需要你提供一个Document对象或一个PageSettings对象。PageSettings对象适用于一个单独的打印页面,可以处理页边距、纸张大小以及是否用颜色打印页面:
PageSetupDialog ps = new PageSetupDialog();ps.Document = doc;ps.ShowDialog(); |
打印一个文件
当然,许多应用程序打印存储在一个文件或某种数据库中的信息,所以现在我将讲述一个更复杂的例子:打印一个以逗号分隔的联系清单文本文件。你可以将一个数据集、甚至一个XML文件中数据所运用的方法用在这里。本例的数据来自Northwind数据库的Customers表,格式如下:
Name, Title, Phone, Fax |
通过在类的级别声明一个StreamReader对象,然后将一个文件分配给它,你就可以从一个文本文件打印该例子了。打开文件,修改PrintPage事件处理程序来打印文件的每一行,按需要格式化数据。
PrintDocument类有好几个事件。你已经用了PrintPage事件了;现在我们来添加更多的事件。当开始一个打印任务时(在实际打印任何东西前),触发BeginPrint事件。相应的事件是EndPrint,该事件是在所有页面结束打印时触发的。创建PrintPage事件需要的任何对象,或在BeginPrint中打开任何数据源,然后在EndPrint中关闭或释放(deallocate)它们。不要忘记将这些事件绑定到PrintDocument对象(见 列表1)。
图1. 显示你的报表 |
PrintDocument类没有制图工具,所以创建报表需要花些时间——但这是值得的。创建一个新的报表的最好的方法是用一张空白的纸。在了解了报表的目的后,你就可以在纸上通过画方框来代表不同的区域进行设计了,如页眉、正文和页脚(见 图1)。然后花些时间详细编写每个区域的数据将来自哪里,以及各个数据将放置在什么位置。可以说这个时候是你确保最终用户所看到的页面与你设计的页面是否是一致的最佳时刻。用户对报表比对应用程序的任何部分的挑剔都要多,所以你应该尽可能多花些时间来设计它。一旦你花了很多时间来设计报表并对它的打印结果了如指掌后,那么写代码就不会花很多时间了。
写代码
Rectangle对象提供了最快、最简单的方法来将你的页面设计转换成代码。这些对象可以让你几乎很准确地在屏幕上模拟你的设计。在页面上定义一个区域,指定坐标(有一定的高度和宽度)。Rectangle对象可以给你提供指定的点,在这个点上,你可以用Graphics类的Draw方法放置文本和图形。特别的是,你可以用DrawRectangle方法来查看该区域在页面的什么位置。该方法需要一个Pen对象(用来画直线和曲线)。将所有的版面设计代码放在PrintPage事件过程中:
// print a red line around the border // of the pageev.Graphics.DrawRectangle( Pens.Red, leftMargin, topMargin, pageWidth, pageHeight); |
.NET Framework 1.0不能得到一台打印机的“hard”margins(实际可以打印到的页面最外面的区域)。但是,PrintPageEventArgs可以让你通过MarginBounds属性得到类似的功能。不幸的是,MarginBounds不考虑hard margins,所以你的输出可能不能在你期望的位置上结束。你必须用P/Invoke并调用Win32 GetDeviceCaps函数来得到打印机的hard margins(见 列表3)。在得到hard margins后,你就可以开始创建Rectangle对象并在你的报表上打印信息了(见 列表4)。
现在你已经创建了长方形边框(rectangles),你就可以在这个位置上打印你的数据了。一次打印一页数据,所以你需要定义一个页面有多大。用一个标准行的高度来划分可打印的区域(本例中你的正文区),从而计算每个页面的行数。通过用你运用的Font对象的GetHeight方法来确定一个行的高度。GetHeight是个属于Font类的重载的方法。你运用的方法需要一个Graphics对象参数。PrintPage事件的PrintPageEventArgs参数提供了这个Graphics对象:
int linesPerPage = Convert.ToInt32(body.Height / bodyFont.GetHeight(ev.Graphics)); |
一旦你确定了构成一个页面的行数,你就只需要进行简单的循环就行了,直到一个页面结束。然后设置ev.HasMorePages为true,或者一直等到打印的数据结束。在循环内部运用Graphics对象的Draw方法来打印你的数据。
你也需要确保DrawString方法将文本放置在了正确的位置上。通过用你选择的字体的高度乘以你用来跟踪已经打印了多少行的计数器,你就可以在每打印一行时计算下一行的位置了。然后为顶部的页边距添加值(见 列表5)。
你运用的DrawString方法也接受一个StringFormat对象。StringFormat类可以让你控制文本的布局——包括对齐和行间距——以及省略符号的插入(如果一个给定的字符串对于你的长方形边框来说太长了时)。通过创建StringFormat对象,并设置其Alignment属性为StringAlignment.Center,你就可以使你的文本居中;然后将对象用于你调用的DrawString方法中。为你的联系清单页眉写以下代码:
StringFormat sf = new StringFormat();sf.Alignment = StringAlignment.Center;ev.Graphics.DrawString("Northwinds Customer Contacts", headerFont, defaultBrush, body, sf); |
正如你所看到的,一旦你确定了报表的布局并创建了长方形边框来包含数据,实际的打印并不是很难。
图2. 预览打印 |