1、利用XML实现通用WEB报表打印(实现篇) -------------------------------------------------------------------------------- 摘要: 本文结合代码详细讲解了《利用XML实现通用WEB报表打印》(以下简称"《利》")一文中所介绍的报表打印中的.Net Web控件方案的实现及扩充过程。本文为《利》文的续作,文中所举的代码示例均为C#语言编写。 --------------------------------------------------------------------------------
2、 目录: 引言 软件原理 结构设计 代码实现 方案扩充 总结 -------------------------------------------------------------------------------- 引言: 在《利》刊出后,有大量的读者发E-Mail给我表示对该方案非常感兴趣,同时还询问具体如何实现报表格式的解析和打印细节并索取该程序的源代码。读者的热情让我始料未及,虽然我一一对来信进行解答和发送了源代码,但是还是深感抱歉和遗憾,因为时间和精力的关系,我不可能对每封信都作出很详细的答复,而且我写的源代码也很乱,事实上,是我花了两个小时
3、赶写出来的(原来的程序因为硬盘故障被销毁了),不但没有什么注解而且还不完善,包括一些标签还没有被实现。 为了弥补以前的缺憾,我花了一些时间改进了程序的结构,重写了全部的源代码,实现了所有标签的功能,下面就要开始讲解该程序的设计和编码过程,在看此文之前,强烈推荐您先阅读《利》一文来了解一下相关的概念,如果在该文中已经有清楚讲解的部分,本文将不再详细介绍,这里只将主要讲解《利》文没有提及或是介绍得不清楚和读者来信提问最多的部分。 --------------------------------------------------------------------------------
4、 软件原理: 该软件的原理其实很简单,就是要方便的解析出定义好的XML格式标记,解读出文件中标记的参数定义,最后将这些信息还原成打印机输出的图形格式。 为了能表达出复杂的报表样式,我们需要定义一些标记,在这些标记中附加上具体的样式信息,作用类似HTML的标签,而我们的解析程序就相当于IE浏览器,所不同的是IE将图形输出到屏幕,而我们是将图形输出到打印机,由于打印机相对于显示屏的特殊性(例如分页),因此我们不能直接采用网页浏览器的标签解析功能来打印,需要自己来做一个满足需要的"打印浏览器"。 针对大多数报表的功能需要,我只定义了两种格式标签:文本(text)和表格(table),它
5、们的具体属性定义和另外一些设置性的标签定义请参考《利》文,这里再补充一幅结构图帮助读者理解。如下所示: -------------------------------------------------------------------------------- 结构设计: 为了描述所有的样式标记,我先定义了一个抽象基类PrintElement,它拥有一个虚拟方法Draw,然后对应表格和文本,从PrintElement派生出两个子类,分别是Table和Text,我还创建了一个Parser类用来解析不同的样式标记和创建对应的对象,它拥有一个静态的方法CreateElement,用来
6、根据不同的格式标签创建出对应的对象。结构图如下所示: 读过《设计模式》的读者一定已经看出来了,这种设计应用了设计模式中的一个非常著名的模式:Abstract Factory。这里使用该模式的好处就是让标签对象和解析器都独立出来,降低了系统的耦合度,有利于今后在需要的时候可以很容易的增加其它的格式标签(下文将会举一个实例)和方便的更换不同的用户界面(图中Client表示Windows应用程序或者是网页插件)。 -------------------------------------------------------------------------------- 代码实现:
7、 首先,创建一个"Windows控件库"的新项目,在项目名称处写入RemotePrint,如下图所示: 然后把新建项目中的那个默认的UserControl1类,它的构造函数名和文件名都改成PrintControl。再将它的背景颜色设置为白色,添加三个按纽,并将它们的Enable属性都设置为false,Anchor属性设置为Bottom, Right,再添加一个Label控件用来显示程序状态,它的Anchor属性设置为Left。如下图所示: 再从控件栏中拖入三个打印对象:PrintDocument, PageSetupDialog, PrintPreviewDialog,如下图所示:
8、 将其中的pageSetupDialog1和printPreviewDialog1的Document属性均设置为printDocument1。 然后为项目添加一个PrintElement的新类,代码如下: using System; using System.Xml; using System.Drawing; namespace RemotePrint { public class PrintElement { public PrintElement() { } public virtual bool Draw(Graphi
9、cs g) { return false; } } } 该类中只有一个虚拟方法Draw,注意它规定需要返回一个bool值,这个值的作用是用来指示标签是否在页内打印完毕。 然后再添一个Table的新类,代码如下: using System; using System.Xml; using System.Drawing; namespace RemotePrint { public class Table : PrintElement { private XmlNode table; public stat
10、ic int count = 0, pc = 1; public Table(XmlNode Table) { table = Table; } public override bool Draw(Graphics g) { //表格坐标 int tableX = int.Parse(table.Attributes["x"].InnerText); int tableY = int.Parse(table.Attributes["y"].InnerText); int x = tableX, y = t
11、ableY; DrawTopLine(g, table);//画表格顶线 Pen pen = new Pen(Color.FromName(table.Attributes["bordercolor"].InnerText), float.Parse(table.Attributes["border"].InnerText)); int trheight = 0; //表头 foreach(XmlNode tr in table["tablehead"].ChildNodes) { trheight = int
12、Parse(tr.Attributes["height"].InnerText); DrawTR(x, y, tr, pen, g); y += trheight; } //表项 for(int i = 0; i < int.Parse(table.Attributes["maxlines"].InnerText); i++) { XmlNode tr = table["tablebody"].ChildNodes[count]; trheight = int.Parse(tr.Attributes["h
13、eight"].InnerText); DrawTR(x, y, tr, pen, g); y += trheight; count++; if(count == table["tablebody"].ChildNodes.Count) break; } x = tableX; //表底 foreach(XmlNode tr in table["tablefoot"].ChildNodes) { trheight = int.Parse(tr.Attributes["he
14、ight"].InnerText); DrawTR(x, y, tr, pen, g); y += trheight; } int currentpage = pc; pc++; bool hasPage = false; if(count < table["tablebody"].ChildNodes.Count - 1) { hasPage = true;//需要继续打印 } else { count = 0; pc = 1; h
15、asPage = false;//表格打印完毕 } return hasPage; } private void DrawTopLine(Graphics g, XmlNode table) { Pen pen = new Pen(Color.FromName(table.Attributes["bordercolor"].InnerText), float.Parse(table.Attributes["border"].InnerText)); int width = 0; foreach(XmlNode
16、 td in table.FirstChild.FirstChild) { width += int.Parse(td.Attributes["width"].InnerText); } int x = int.Parse(table.Attributes["x"].InnerText); int y = int.Parse(table.Attributes["y"].InnerText); g.DrawLine(pen, x, y, x + width, y); } //画表格行 private void
17、DrawTR(int x, int y, XmlNode tr, Pen pen, Graphics g) { int height = int.Parse(tr.Attributes["height"].InnerText); int width; g.DrawLine(pen, x, y, x, y + height);//画左端线条 foreach(XmlNode td in tr) { width = int.Parse(td.Attributes["width"].InnerText); DrawTD(
18、x, y, width, height, td, g); g.DrawLine(pen, x + width, y, x + width, y + height);//右线 g.DrawLine(pen, x, y + height, x + width, y + height);//底线 x += width; } } //画单元格 private void DrawTD(int x, int y, int width, int height, XmlNode td, Graphics g) { Brus
19、h brush = new SolidBrush(Color.FromName(td.Attributes["bgcolor"].InnerText)); g.FillRectangle(brush, x, y, width, height); FontStyle style = FontStyle.Regular; //设置字体样式 if(td.Attributes["b"].InnerText == "true") style |= FontStyle.Bold; if(td.Attributes["i"].InnerText
20、 "true") style |= FontStyle.Italic; if(td.Attributes["u"].InnerText == "true") style |= FontStyle.Underline; Font font = new Font(td.Attributes["fontname"].InnerText, float.Parse(td.Attributes["fontsize"].InnerText), style); brush = new SolidBrush(Color.FromName(td
21、Attributes["fontcolor"].InnerText)); StringFormat sf = new StringFormat(); //设置对齐方式 switch(td.Attributes["align"].InnerText) { case "center": sf.Alignment = StringAlignment.Center; break; case "right": sf.Alignment = StringAlignment.Near;
22、break; default: sf.Alignment = StringAlignment.Far; break; } sf.LineAlignment = StringAlignment.Center; RectangleF rect = new RectangleF( (float)x, (float)y, (float)width, (float)height); g.DrawString(td.InnerText, font, brush, rect, sf); } } }
23、 Table类将table标签内部的解析和打印独立出来,全部在类的内部完成,这样,我们在对顶层标签解析的时候只要是碰到table标签就直接交给Table类去完成,不需要再关心其实现细节。 再添加一个Text类,代码如下: using System; using System.Xml; using System.Drawing; namespace RemotePrint { public class Text : PrintElement { private XmlNode text = null; public Text(XmlNode
24、Text) { text = Text; } public override bool Draw(Graphics g) { Font font = new Font(text.Attributes["fontname"].InnerText, int.Parse(text.Attributes["fontsize"].InnerText)); Brush brush = new SolidBrush(Color.FromName(text.Attributes ["fontcolor"].InnerText
25、)); g.DrawString(text.InnerText, font, brush, float.Parse (text.Attributes["x"].InnerText), float.Parse(text.Attributes["y"].InnerText)); return false; } } } 同Table类一样,Text类完成对text标签的解析和打印,不过因为text的简单性,它的代码也少了很多。它们两者同样继承自PrintElement,都重载了Draw方法的实现。 最后,我们还需要一个解析器用来解
26、析顶层的标签和生成相应的对象,它在此模式中的作用就是一个"工厂类",负责生产出用户需要的"产品"。代码如下: using System; using System.Xml; namespace RemotePrint { public class Parser { public Parser() { } public static PrintElement CreateElement(XmlNode element) { PrintElement printElement = null; switch(el
27、ement.Name) { case "text": printElement = new Text(element); break; case "table": printElement = new Table(element); break; default: printElement = new PrintElement(); break; } return printElement; } } } 好了,核心的解析和标
28、签的具体打印方法已经完成了,现在我们回到PrintControl中编写一些代码来测试我们的成果。 首先,需要引用两个要用到的名称空间: using System.Xml; using System.Drawing.Printing; 然后,在打印之前,需要根据XML文件中的pagesetting标签来设置一下打印机的页面,所以我们先写一个方法来设置打印机。在PrintControl类中增加一个私有的方法: private void SettingPrinter(XmlNode ps) { //打印方向(纵/横) this.printDocument1.Def
29、aultPageSettings.Landscape = bool.Parse(ps["landscape"].InnerText); //设置纸张类型 string papername = ps["paperkind"].InnerText; bool fitpaper = false; //获取打印机支持的所有纸张类型 foreach(PaperSize size in this.printDocument1.PrinterSettings.PaperSizes) { if(papername == size.PaperName)//看该打印机是
30、否有我们需要的纸张类型 { this.printDocument1.DefaultPageSettings.PaperSize = size; fitpaper = true; } } if(!fitpaper) { //假如没有我们需要的标准类型,则使用自定义的尺寸 this.printDocument1.DefaultPageSettings.PaperSize = new PaperSize("Custom", int.Parse(ps["paperwidth"].InnerText), int.P
31、arse(ps["paperheight"].InnerText)); } } 接下来,我们类中添加一个XmlDocument的对象和一个静态变量计算页码: private XmlDocument doc = new XmlDocument(); public static int Pages = 1; 然后再控件的Load事件中为该对象加载XML报表数据,代码如下: private void PrintControl_Load(object sender, System.EventArgs e) { try { //装载报表XML数据
32、 this.label1.Text = "正在加载报表数据,请稍侯..."; doc.Load(""); this.label1.Text = "报表数据加载完毕!"; this.button1.Enabled = this.button2.Enabled = this.button3.Enabled = true; } catch(Exception ex) { this.label1.Text = "出现错误:" + ex.Message; } } 请注意,我们这里只是装入了一个本地的测试数据文件(该文件的编写请参考《利》文)
33、其实,完全可以改成装载网络上任何地方的静态或者动态的XML文件,例如以上的doc.Load("")可以改写成: doc.Load(""); doc.Load(""); doc.Load("?date=xxx"); 等等,只要装载的数据是符合我们规定的XML数据文档就可以。 然后在控件的构造函数中加入打印事件的委托: public PrintControl() { InitializeComponent(); this.printDocument1.PrintPage += new PrintPageEventHandler(this.pd_PrintPag
34、e); } 该委托方法的代码如下: private void pd_PrintPage(object sender, PrintPageEventArgs ev) { Graphics g = ev.Graphics; bool HasMorePages = false; PrintElement printElement = null; foreach(XmlNode node in doc["root"]["reporttable"].ChildNodes) { printElement = Parser.CreateElement(n
35、ode);//调用解析器生成相应的对象 try { HasMorePages = printElement.Draw(g);//是否需要分页 } catch(Exception ex) { this.label1.Text = ex.Message; } } //在页底中间输出页码 Font font = new Font("黑体", 12.0f); Brush brush = new SolidBrush(Color.Black); g.DrawString("第 " + Pages.ToStr
36、ing() + " 页", font,brush,ev.MarginBounds.Width / 2 + ev.MarginBounds.Left - 30, ev.PageBounds.Height - 60); if(HasMorePages) { Pages++; } ev.HasMorePages = HasMorePages; } 三个按纽的Click事件代码分别如下: //页面设置 private void button1_Click(object sender, System.EventArgs e) {
37、this.pageSetupDialog1.ShowDialog(); this.printDocument1.DefaultPageSettings = this.pageSetupDialog1.PageSettings; } //打印预览 private void button2_Click(object sender, System.EventArgs e) { try { this.printPreviewDialog1.ShowDialog(); } catch(Exception ex) { this.label
38、1.Text = ex.Message; } } //打印 private void button3_Click(object sender, System.EventArgs e) { try { this.printDocument1.Print(); } catch(Exception ex) { this.label1.Text = ex.Message; } } 好了,我们的打印控件到这里就全部做完了,选择生成一个Release的版本,然后到工程目录下将生成的PrintControl.dll文件拷贝到I
39、IS的虚拟根目录下,然后新建一个remoteprint.htm的HTML格式文件,在合适的地方加上:,为了更加形象和美观,还可以将需要打印的数据做成网页形式放在上面,如果需要获取的XML是动态数据源,则可以采用asp等动态脚本来生成该网页表格,如果需要获取的XML是一个静态的文本,则可以采用XSLT直接将XML文件转换成网页表格。 打开浏览器,输入:,如果您已经跟我一样,事
40、先做好了一个XML报表数据文件的话,您就可以看到下图所示的效果 请注意:该图示例中的所有数据均为笔者随意虚拟,网页中的表格数据和打印数据并非来自同一数据源,也没有刻意去对等,仅仅只是为了演示一下效果,因此网页显示报表跟打印预览中的报表有一些出入是正常的。在实际应用中可以让网页显示数据跟打印输出数据完全一致。 -------------------------------------------------------------------------------- 方案扩充: 有一部分读者在来信中问到如何打印一些特殊形态的图表,《利》文中已经提到,采用本方案可以非常方便的定义
41、出自己所需要的标签,在理论上可以打印出任何样式的特殊图表。因此本文打算详细介绍一下增加自己定义的标签扩充打印格式的具体过程。 先假设我们的客户看了打印效果后基本上满意,但是还有觉得一点不足,如果需要打印一些图表怎么办?例如折线图、K线图、饼状图、柱状图等等。使用我们现有的标签就不行了,所以我们首先要扩充我们的标签库,让它的表达能力更加强。在这里,我将只打算让我们的打印控件学会画简单的折线图,希望读者能举一反三,创造出其它各种各样的打印效果。 最基本的折线图是由X坐标轴、Y坐标轴和一系列点连接成的线构成的,因此,我定义了以下几种标签: 1. linechart:跟table,text
42、标签一样,为样式根标签。 属性:无 2. coordinate:坐标。 属性:无 3. xcoordinate:X轴坐标线 属性: # x:起点X坐标值 # y:起点Y坐标值 # length:长度值 # stroke:粗细 # color:颜色 # arrow:是否有箭头 4. ycoordinate:Y轴坐标线 属性:同xcoordinate。 5.scale:刻度线 标签内容:显示在刻度边的文字 属性: # length:距离起点长度值 # height:刻度线高度 # width:刻度线宽度 # color
43、颜色
# fontsize:字体大小
6.chart:图表根
属性:无
7.lines:线段
属性值:
# stroke:粗细
# color:颜色
8. point:点
属性值:
# x:X坐标值
# y:Y坐标值
# radius:半径
# color:颜色
其结构图如下所示:
下面是一段用刚才定义的标签制作的XML折线图示例:
44、ow="true">
45、"1" color="Black" fontsize="9">400 46、rdinate>
47、h="1" color="Black" fontsize="9">300 49、"400" radius="5" color="Black"/>
50、nt x="800" y="500" radius="5" color="Black"/>






