技术工具箱:C#
(百度N久,终于找到了,大多是残篇,费了大功夫,汇集起来,最后一个图仍然没找到)
你可以用几种方法在.NET中编程来生成打印输出结果(如报表)。对Windows程序员来说,Visual Studio提供的Crystal Reports实际上是人们常用的打印报表的工具,但对不太复杂的报表来说,这个工具就有些大材小用了。
你可以用几种方法在.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对象结合了起来(见表下)。
fine-grain control(精细控制)与相对简单的Visual Basic传统的Printer对象结合了起来(见表下)。
表1 研究.NET的众多的打印类.
类 |
说 明 |
PrintDocument |
可以让你定义一个对象,该对象把输出发送到一台打印机。 |
PrintEventArgs |
为BeginPrint和EndPrint事件提供所有的必需的信息。 |
PrintPageEventArgs |
为PrintPage事件提供所有必需的信息。 |
QueryPageSettingsEventArgs |
为QueryPageSettings事件提供信息。 |
PrintController |
控制一个PrintDocument是如何打印的。 |
StandardPrintController |
派生于PrintController。 |
PreviewPrintController |
派生于PrintController。 |
PreviewPageInfo |
PreviewPrintController 经常运用这个类。它为一个单独的页面指定了预览信息。 |
PageSettings |
指定一个单独的打印页面的设置。 |
Margins |
可以让你指定一个打印页面的页边距。页面的顶部、左边、右边和底部边距属性是以英寸(×100)的形式返回的。 |
MarginsConverter |
可以让你把一个Margins对象转换成另外的类型,把另外的类型转换成一个Margins对象。 |
PaperSize |
指定一页纸的大小。当同一个PageSettings对象一起使用时,它指的是一个页面的纸张大小。当用于PrinterSettings时,它可以让你得到打印机上可以用的纸张大小。 |
PaperSizeCollection |
PaperSize对象的一个集合。 |
PaperSource |
像PaperSize类一样,PaperSource 是由一个PageSettings对象和一个PrinterSettings对象使用的。当用于PageSettings时,它可以让你为一个特定页面得到纸张来源。当用于PrinterSettings时,它可以让你得到打印机运用的所有纸张来源。 |
PaperSourceCollection |
PaperSource 对象的一个集合。 |
PrinterSettings |
指定一个文件是如何打印的,包括文件在哪个打印机上打印。它是与特定的PrintDocument相应的。 |
PrinterResolution |
可以让你得到横向的和纵向的DPI,以及实际的打印机分辨率(draft, high, low,等等)。 |
PrinterResolutionCollection |
PrinterResolution 对象的一个集合。 |
InvalidPrinterException |
表示将抛出的异常(如果你用无效的设置来访问一台打印机)。 |
PrintingPermission |
控制对打印机的使用权限。 |
PrintingPermissionAttribute |
允许检测打印权限。 |
运用.NET Framework的System.Drawing.Printing名字空间,你就可以程序化地处理许多打印任务,该名字空间包含20多个类、7个枚举类型和3个委托类型(delegate)。让我们来看看对每个类的简要说明吧!
在此,我将重点讲述该名字空间的主要的类,它们可以让你创建复杂程度适度的报表。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;
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();
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)。
C#:创建和部署
列表 1.
PrintDocument对象提供了几个事件。BeginPrint是在实际打印页面前触发的,可以给你提供机会来配置对象或打开文件。EndPrint在打印完最后一页时触发。不要忘记将事件绑定到你的PrintDocument对象。
// At the class level private Font bodyFont; private Font headerFont; private StreamReader data; private PrintDocument doc; private void MainForm_Load(object sender, System.EventArgs e) { doc = new PrintDocument(); // shows up in Print Manager doc.DocumentName = "Contact List"; doc.BeginPrint += new PrintEventHandler(doc_BeginPrint); doc.EndPrint += new PrintEventHandler(doc_EndPrint); } private void doc_BeginPrint(object sender, PrintEventArgs pv) { data = new StreamReader("contacts.csv"); Font bodyFont = new Font("Arial", 12); Font headerFont = new Font("Arial", 24); } private void doc_EndPrint(object sender, PrintEventArgs pv) { data.Close(); bodyFont.Dispose(); headerFont.Dispose(); }
作为选择,你也可以把所有的打印功能封装到PrintDocument派生的一个单独的类中,然后重载触发相应事件的方法。你可以用你自定义的类,而不用实例化一个PrintDocument对象(见列表2)。这是一个很好的方法,但是为了保持一致,使代码清晰,我将继续讲述事件绑定方法。
C#:继承 PrintDocument
列表 2.
从PrintDocument派生一个新类,这样你就可以把所有打印功能封装在一个单独的地方,从而提高代码的重用性。
public class CustomPrintDocument : PrintDocument { private StreamReader dataToPrint; public CustomPrintDocument(StreamReader data) :base(){ dataToPrint = data; } protected override void OnBeginPrint(PrintEventArgs ev){ base.OnBeginPrint(ev) ; } protected override void OnEndPrint(PrintEventArgs ev){ base.OnEndPrint(ev); } protected override void OnQueryPageSettings(QueryPageSettingsEventArgs ev) { base.OnQueryPageSettings(ev); } protected override void OnPrintPage(PrintPageEventArgs ev) { base.OnPrintPage(ev); ev.Graphics.DrawString("this is a test", new Font("Arial", 24), Brushes.Black, 100, 100); ev.HasMorePages = false; } }
PrintDocument类没有制图工具,所以创建报表需要花些时间——但这是值得的。创建一个新的报表的最好的方法是用一张空白的纸。在了解了报表的目的后,你就可以在纸上通过画方框来代表不同的区域进行设计了,如页眉、正文和页脚(见图1)。然后花些时间详细编写每个区域的数据将来自哪里,以及各个数据将放置在什么位置。可以说这个时候是你确保最终用户所看到的页面与你设计的页面是否是一致的最佳时刻。用户对报表比对应用程序的任何部分的挑剔都要多,所以你应该尽可能多花些时间来设计它。一旦你花了很多时间来设计报表并对它的打印结果了如指掌后,那么写代码就不会花很多时间了。
图1 显示你的报表
写代码
Rectangle对象提供了最快、最简单的方法来将你的页面设计转换成代码。这些对象可以让你几乎很准确地在屏幕上模拟你的设计。在页面上定义一个区域,指定坐标(有一定的高度和宽度)。Rectangle对象可以给你提供指定的点,在这个点上,你可以用Graphics类的Draw方法放置文本和图形。特别的是,你可以用DrawRectangle方法来查看该区域在页面的什么位置。该方法需要一个Pen对象(用来画直线和曲线)。将所有的版面设计代码放在PrintPage事件过程中:
// print a red line around the border // of the page ev.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)。
C#:用P/Invoke得到页边距
列表 3.
.NET Framework不提供方法得到一台打印机的hard margins,所以你需要用P/Invoke并调用Win32的GetDeviceCaps函数。这个类的这个方法中包含一个设备驱动程序代码(hDc),然后用你需要的信息来填充类成员。
[DllImport("gdi32.dll")] private static extern Int16 GetDeviceCaps([In][MarshalAs (UnmanagedType.U4)] int hDc, [In] [MarshalAs(UnmanagedType.U2)] Int16 funct); private float _leftMargin = 0; private float _topMargin = 0; private float _rightMargin = 0; private float _bottomMargin = 0; const short HORZSIZE = 4; const short VERTSIZE = 6; const short HORZRES = 8; const short VERTRES = 10; const short PHYSICALOFFSETX = 112; const short PHYSICALOFFSETY = 113; public marginInfo(int deviceHandle) { float offx = Convert.ToSingle(GetDeviceCaps(deviceHandle, PHYSICALOFFSETX)); float offy = Convert.ToSingle(GetDeviceCaps(deviceHandle, PHYSICALOFFSETY)); float resx = Convert.ToSingle(GetDeviceCaps(deviceHandle, HORZRES)); GetDeviceCaps(deviceHandle, VERTRES)); float hsz = Convert.ToSingle(GetDeviceCaps(deviceHandle, HORZSIZE))/25.4f; float vsz = Convert.ToSingle(GetDeviceCaps(deviceHandle,VERTSIZE))/25.4f; float ppix = resx/hsz; float ppiy = resy/vsz; _leftMargin = (offx/ppix) * 100.0f; _topMargin = (offy/ppix) * 100.0f; _bottomMargin = _topMargin + (vsz * 100.0f); _rightMargin = _leftMargin + (hsz * 100.0f); }
在得到hard margins后,你就可以开始创建Rectangle对象并在你的报表上打印信息了(见列表4)。
C#:设计报表的版面
列表 4.
任何报表的基本格式至少必须包含一个页眉、正文和页脚区域。运用Rectangle对象可以很容易地设计你的报表版面。通过添加更多的Rectangle对象,你就可以提供这些报表的复杂程度。
// create the header int headerHeight = hf.GetHeight(ev.Graphics); RectangleF header = new RectangleF(leftMargin, topMargin,pageWidth, headerHeight); // create the footer int bodyFontHeight =bodyFont.GetHeight(ev.Graphics); RectangleF footer = new RectangleF(leftMargin, body.Bottom, pageWidth, bodyFontHeight); // create the body section RectangleF body = new RectangleF(leftMargin, header.Bottom, pageWidth, pageHeight - bodyFontHeight);
现在你已经创建了长方形边框(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)。
C#:PrintPage是打印的关键
列表 5.
当运用System.Drawing.Printing对象来打印时, PrintPage是你要用到的主要的事件。该事件为每个页面触发一次,直到你将ev.HasMorePages的值设置成false。用来得到hard margins的代码弥补了.NET Framework中的不足。
private void doc_PrintPage(object sender,System.Drawing.Printing.PrintPageEventArgs ev) { _currentPage++; String headerText = "Northwinds Customer Contacts"; IntPtr hDc = ev.Graphics.GetHdc(); ev.Graphics.ReleaseHdc(hDc); marginInfo mi = new marginInfo(hDc.ToInt32()); // take the hard margins into account? float leftMargin = ev.MarginBounds.Left - mi.Left; float rightMargin = ev.MarginBounds.Right; float topMargin = ev.MarginBounds.Top - mi.Left; float bottomMargin = ev.MarginBounds.Bottom; float pageHeight = bottomMargin - topMargin; float pageWidth = rightMargin - leftMargin; float headerHeight = headerFont.GetHeight(ev.Graphics); float footerHeight = bodyFont.GetHeight(ev.Graphics); // report header RectangleF ReportheaderR = new RectangleF(leftMargin,topMargin, pageWidth, headerHeight); // report body RectangleF bodyR = new RectangleF(leftMargin,ReportheaderR.Bottom, pageWidth, pageHeight - ReportheaderR.Height - footerHeight); // report footer RectangleF ReportfooterR = new RectangleF(leftMargin,bodyR.Bottom, pageWidth, footerHeight * 2); // results of using the Split function on the text String[] el; // a line of text from our file string text = ""; // print the header once per page centerText(ev.Graphics, headerText, headerFont,defaultBrush, ReportheaderR); // the header is equal to 2 normal lines int currentLine = 2; // how many lines can we fit on a page? int linesPerPage = Convert.ToInt32(bodyR.Height / bodyFont.GetHeight(ev.Graphics)) - 1; float bodyFontHeight = bodyFont.GetHeight(ev.Graphics); float currentY; // Print each line of the file. while(currentLine < linesPerPage && ((text=data.ReadLine()) != null)) { el = text.Split(','); currentY = getCurrentY(currentLine, topMargin,bodyFontHeight); ev.Graphics.DrawString(el[0], bodyFont,defaultBrush, bodyR.Left, currentY); currentLine++; currentY = getCurrentY(currentLine, topMargin, bodyFontHeight); ev.Graphics.DrawString(el[1], bodyFont, defaultBrush, bodyR.Left + 20, currentY); currentLine++; currentY = getCurrentY(currentLine, topMargin, bodyFontHeight); ev.Graphics.DrawString("Phone: " + el[2], bodyFont, defaultBrush, bodyR.Left + 20, currentY); currentLine++; currentY = getCurrentY(currentLine, topMargin, bodyFontHeight); ev.Graphics.DrawString("Fax: " + el[3], bodyFont,defaultBrush, bodyR.Left + 20,currentY); currentLine++; currentY = getCurrentY(currentLine, topMargin, bodyFontHeight); ev.Graphics.DrawLine(Pens.Black, leftMargin,currentY, ev.MarginBounds.Right, currentY); } // page number centerText(ev.Graphics, "Page " + currentPage.ToString(), bodyFont, defaultBrush, ReportfooterR); if (text != null) { ev.HasMorePages = true; } else { // no more pages to print ev.HasMorePages = false; } } private float getCurrentY(int currentLine, float topMargin, float fontHeight) { return topMargin + (currentLine * fontHeight); } private void centerText(Graphics g, string t, Font f, Brush b, RectangleF rect) { StringFormat sf = new StringFormat(); sf.Alignment = StringAlignment.Center; g.DrawString(t, f, b, rect, sf); }
你运用的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);
private void preview_Click(object sender, System.EventArgs e) { PrintPreviewDialog pd = new PrintPreviewDialog(); pd.Document = doc; pd.ShowDialog(); }
我希望我已经给你们提供了研究System.Drawing.Printing名字空间的动力。我发现这个名字空间是用来自动生成许多重复性的打印项目(尤其是报表)的最好的方法。尝试用这种方法来完成一个打印任务吧,我敢打赌在用过一次后,你就会立刻将这个方法用于所有的打印任务了。
关于作者:
Michael Eaton是位专攻Microsoft技术的独立顾问。自1994年来,他一直在从事软件开发工作,并于1995年获得MCSD。他主要用VB、SQL Server和ASP进行开发,但在.NET SDK发布后,他开始沉迷于用C#进行开发。他的Email是[email protected]。