续:利用XML实现通用WEB报表打印(实现篇) 卢彦

引言:
在《利》刊出后,有大量的读者发E-Mail给我表示对该方案非常感兴趣,同时还询问具体如何实现报表格式的解析和打印细节并索取该程序的源代码。读者的热情让我始料未及,虽然我一一对来信进行解答和发送了源代码,但是还是深感抱歉和遗憾,因为时间和精力的关系,我不可能对每封信都作出很详细的答复,而且我写的源代码也很乱,事实上,是我花了两个小时赶写出来的(原来的程序因为硬盘故障被销毁了),不但没有什么注解而且还不完善,包括一些标签还没有被实现。
为了弥补以前的缺憾,我花了一些时间改进了程序的结构,重写了全部的源代码,实现了所有标签的功能,下面就要开始讲解该程序的设计和编码过程,在看此文之前,强烈推荐您先阅读《利》一文来了解一下相关的概念,如果在该文中已经有清楚讲解的部分,本文将不再详细介绍,这里只将主要讲解《利》文没有提及或是介绍得不清楚和读者来信提问最多的部分。
软件原理:
该软件的原理其实很简单,就是要方便的解析出定义好的XML格式标记,解读出文件中标记的参数定义,最后将这些信息还原成打印机输出的图形格式。
为了能表达出复杂的报表样式,我们需要定义一些标记,在这些标记中附加上具体的样式信息,作用类似HTML的标签,而我们的解析程序就相当于IE浏览器,所不同的是IE将图形输出到屏幕,而我们是将图形输出到打印机,由于打印机相对于显示屏的特殊性(例如分页),因此我们不能直接采用网页浏览器的标签解析功能来打印,需要自己来做一个满足需要的"打印浏览器"。
针对大多数报表的功能需要,我只定义了两种格式标签:文本(text)和表格(table),它们的具体属性定义和另外一些设置性的标签定义请参考《利》文,这里再补充一幅结构图帮助读者理解。如下所示:

结构设计:
为了描述所有的样式标记,我先定义了一个抽象基类PrintElement,它拥有一个虚拟方法Draw,然后对应表格和文本,从PrintElement派生出两个子类,分别是Table和Text,我还创建了一个Parser类用来解析不同的样式标记和创建对应的对象,它拥有一个静态的方法CreateElement,用来根据不同的格式标签创建出对应的对象。结构图如下所示:

读过《设计模式》的读者一定已经看出来了,这种设计应用了设计模式中的一个非常著名的模式:Abstract Factory。这里使用该模式的好处就是让标签对象和解析器都独立出来,降低了系统的耦合度,有利于今后在需要的时候可以很容易的增加其它的格式标签(下文将会举一个实例)和方便的更换不同的用户界面(图中Client表示Windows应用程序或者是网页插件)。
首先,创建一个"Windows控件库"的新项目,在项目名称处写入RemotePrint,如下图所示:

然后把新建项目中的那个默认的UserControl1类,它的构造函数名和文件名都改成PrintControl。再将它的背景颜色设置为白色,添加三个按纽,并将它们的Enable属性都设置为false,Anchor属性设置为Bottom, Right,再添加一个Label控件用来显示程序状态,它的Anchor属性设置为Left。如下图所示:

再从控件栏中拖入三个打印对象:PrintDocument, PageSetupDialog, PrintPreviewDialog,如下图所示:

将其中的pageSetupDialog1和printPreviewDialog1的Document属性均设置为printDocument1。
然后为项目添加一个PrintElement的新类,代码如下:
using System;
using System.Xml;
using System.Drawing;

namespace RemotePrint
{
 public class PrintElement
 {
  public PrintElement()
  {
  }

  public virtual bool Draw(Graphics g)
  {
   return false;
  }
 }
}
该类中只有一个虚拟方法Draw,注意它规定需要返回一个bool值,这个值的作用是用来指示标签是否在页内打印完毕。
然后再添一个Table的新类,代码如下:
using System;
using System.Xml;
using System.Drawing;

namespace RemotePrint
{
 public class Table : PrintElement
 {
  private XmlNode table;
  public static 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 = tableY;
   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.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["height"].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["height"].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;
    hasPage = 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 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 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(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)
  {
   Brush 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 == "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.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;
     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);
  }
 }
}
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 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));
   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方法的实现。
最后,我们还需要一个解析器用来解析顶层的标签和生成相应的对象,它在此模式中的作用就是一个"工厂类",负责生产出用户需要的"产品"。代码如下:
using System;
using System.Xml;

namespace RemotePrint
{
 public class Parser
 {
  public Parser()
  {
  }
  public static PrintElement CreateElement(XmlNode element)
  {
   PrintElement printElement = null;
   switch(element.Name)
   {
    case "text":
     printElement = new Text(element);
     break;
    case "table":
     printElement = new Table(element);
     break;
    default:
     printElement = new PrintElement();
     break;
   }
   return printElement;
  }
 }
}
好了,核心的解析和标签的具体打印方法已经完成了,现在我们回到PrintControl中编写一些代码来测试我们的成果。
首先,需要引用两个要用到的名称空间:
using System.Xml;
using System.Drawing.Printing;
然后,在打印之前,需要根据XML文件中的pagesetting标签来设置一下打印机的页面,所以我们先写一个方法来设置打印机。在PrintControl类中增加一个私有的方法:
private void SettingPrinter(XmlNode ps)
{
 //打印方向(纵/横)
 this.printDocument1.DefaultPageSettings.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)//看该打印机是否有我们需要的纸张类型
  {
   this.printDocument1.DefaultPageSettings.PaperSize = size;
   fitpaper = true;
  }
 }
 if(!fitpaper)
 {
  //假如没有我们需要的标准类型,则使用自定义的尺寸
  this.printDocument1.DefaultPageSettings.PaperSize =
   new PaperSize("Custom", int.Parse(ps["paperwidth"].InnerText),
   int.Parse(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数据
  this.label1.Text = "正在加载报表数据,请稍侯...";
  doc.Load("http://localhost/report.xml");
  this.label1.Text = "报表数据加载完毕!";
this.button1.Enabled = this.button2.Enabled = this.button3.Enabled = true;
 }
 catch(Exception ex)
 {
  this.label1.Text = "出现错误:" + ex.Message;
 }
}
请注意,我们这里只是装入了一个本地的测试数据文件(该文件的编写请参考《利》文),其实,完全可以改成装载网络上任何地方的静态或者动态的XML文件,例如以上的doc.Load("http://localhost/report.xml")可以改写成:
doc.Load("http://www.anywhere.com/report.xml");
doc.Load("http://www.anywhere.com/report.asp");
doc.Load("http://www.anywhere.com/report.jsp?date=xxx");
等等,只要装载的数据是符合我们规定的XML数据文档就可以。
然后在控件的构造函数中加入打印事件的委托:
public PrintControl()
{
InitializeComponent();
this.printDocument1.PrintPage += new PrintPageEventHandler(this.pd_PrintPage);
}
该委托方法的代码如下:
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(node);//调用解析器生成相应的对象
  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.ToString() + " 页",
  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)
{
 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.label1.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文件拷贝到IIS的虚拟根目录下,然后新建一个remoteprint.htm的HTML格式文件,在合适的地方加上:<object id="print" classid="http:RemotePrint.dll#RemotePrint.PrintControl" Width="100%" Height="60"> </object>,为了更加形象和美观,还可以将需要打印的数据做成网页形式放在上面,如果需要获取的XML是动态数据源,则可以采用asp等动态脚本来生成该网页表格,如果需要获取的XML是一个静态的文本,则可以采用XSLT直接将XML文件转换成网页表格。
打开浏览器,输入:http://localhost/remoteprint.htm,如果您已经跟我一样,事先做好了一个XML报表数据文件的话,您就可以看到下图所示的效果


请注意:该图示例中的所有数据均为笔者随意虚拟,网页中的表格数据和打印数据并非来自同一数据源,也没有刻意去对等,仅仅只是为了演示一下效果,因此网页显示报表跟打印预览中的报表有一些出入是正常的。在实际应用中可以让网页显示数据跟打印输出数据完全一致。
方案扩充:
有一部分读者在来信中问到如何打印一些特殊形态的图表,《利》文中已经提到,采用本方案可以非常方便的定义出自己所需要的标签,在理论上可以打印出任何样式的特殊图表。因此本文打算详细介绍一下增加自己定义的标签扩充打印格式的具体过程。
先假设我们的客户看了打印效果后基本上满意,但是还有觉得一点不足,如果需要打印一些图表怎么办?例如折线图、K线图、饼状图、柱状图等等。使用我们现有的标签就不行了,所以我们首先要扩充我们的标签库,让它的表达能力更加强。在这里,我将只打算让我们的打印控件学会画简单的折线图,希望读者能举一反三,创造出其它各种各样的打印效果。
最基本的折线图是由X坐标轴、Y坐标轴和一系列点连接成的线构成的,因此,我定义了以下几种标签:
1. linechart:跟table,text标签一样,为样式根标签。
属性:无
2. coordinate:坐标。
属性:无
3. xcoordinate:X轴坐标线
属性:
# x:起点X坐标值
# y:起点Y坐标值
# length:长度值
# stroke:粗细
# color:颜色
# arrow:是否有箭头
4. ycoordinate:Y轴坐标线
属性:同xcoordinate。
5.scale:刻度线
标签内容:显示在刻度边的文字
属性:
# length:距离起点长度值
# height:刻度线高度
# width:刻度线宽度
# color:颜色
# fontsize:字体大小
6.chart:图表根
属性:无
7.lines:线段
属性值:
# stroke:粗细
# color:颜色
8. point:点
属性值:
# x:X坐标值
# y:Y坐标值
# radius:半径
# color:颜色
其结构图如下所示:

下面是一段用刚才定义的标签制作的XML折线图示例:
<linechart>
<coordinate>
<xcoordinate x="200" y="600" length="800" stroke="2" color="Black" arrow="true">
<scale length="100" height="10" width="1" color="Black" fontsize="9">100</scale>
<scale length="200" height="10" width="1" color="Black" fontsize="9">200</scale>
<scale length="300" height="10" width="1" color="Black" fontsize="9">300</scale>
<scale length="400" height="10" width="1" color="Black" fontsize="9">400</scale>
<scale length="500" height="10" width="1" color="Black" fontsize="9">500</scale>
<scale length="600" height="10" width="1" color="Black" fontsize="9">600</scale>
<scale length="700" height="10" width="1" color="Black" fontsize="9">700</scale>
</xcoordinate>
<ycoordinate x="200" y="600" length="-400" stroke="2" color="Black" arrow="true">
<scale length="-100" height="10" width="1" color="Black" fontsize="9">100</scale>
<scale length="-200" height="10" width="1" color="Black" fontsize="9">200</scale>
<scale length="-300" height="10" width="1" color="Black" fontsize="9">300</scale>
</ycoordinate>
</coordinate>
<chart>
<lines stroke="1" color="Blue">
<point x="200" y="600" radius="5" color="Black"/>
<point x="300" y="300" radius="5" color="Black"/>
<point x="400" y="400" radius="5" color="Black"/>
<point x="500" y="500" radius="5" color="Black"/>
<point x="600" y="300" radius="5" color="Black"/>
<point x="700" y="300" radius="5" color="Black"/>
<point x="800" y="600" radius="5" color="Black"/>
<point x="900" y="500" radius="5" color="Black"/>
</lines>
<lines stroke="1" color="Red">
<point x="200" y="400" radius="5" color="Black"/>
<point x="300" y="500" radius="5" color="Black"/>
<point x="400" y="600" radius="5" color="Black"/>
<point x="500" y="300" radius="5" color="Black"/>
<point x="600" y="400" radius="5" color="Black"/>
<point x="700" y="400" radius="5" color="Black"/>
<point x="800" y="500" radius="5" color="Black"/>
<point x="900" y="300" radius="5" color="Black"/>
</lines>
</chart>
</linechart>
完成了标签的定义,下一步就要来修改我们的程序,让他能"读懂"这些标签。
首先,我们先给工程增加一个LineChart的新类,跟Table,Text类一样,它也是继承自PrintElement类,同样重载了Draw虚方法。代码如下:
using System;
using System.Xml;
using System.Drawing;
using System.Drawing.Drawing2D;
 
namespace RemotePrint
{
 public class LineChart : PrintElement
 {
  private XmlNode chart;
  public LineChart(XmlNode Chart)
  {
   chart = Chart;
  }
  public override bool Draw(Graphics g)
  {
   DrawCoordinate(g, chart["coordinate"]);//画坐标轴
   DrawChart(g, chart["chart"]);
   return false;
  }
  private void DrawCoordinate(Graphics g, XmlNode coo)
  {
   DrawXCoor(g, coo["xcoordinate"]);//画X坐标
   DrawYCoor(g, coo["ycoordinate"]);//画Y坐标
  }
  private void DrawXCoor(Graphics g, XmlNode xcoo)
  {
   int x = int.Parse(xcoo.Attributes["x"].InnerText);
   int y = int.Parse(xcoo.Attributes["y"].InnerText);
   int length = int.Parse(xcoo.Attributes["length"].InnerText);
   bool arrow = bool.Parse(xcoo.Attributes["arrow"].InnerText);
   int stroke = int.Parse(xcoo.Attributes["stroke"].InnerText);
   Color color = Color.FromName(xcoo.Attributes["color"].InnerText);
   Pen pen = new Pen(color, (float)stroke);
   if(arrow)//是否有箭头
   {
    AdjustableArrowCap Arrow = new AdjustableArrowCap(
     (float)(stroke * 1.5 + 1.5),
     (float)(stroke * 1.5 + 2), true);
    pen.CustomEndCap = Arrow;
   }
   g.DrawLine(pen, x, y, x + length, y);//画坐标
   //画刻度
   foreach(XmlNode scale in xcoo.ChildNodes)
   {
    int len = int.Parse(scale.Attributes["length"].InnerText);
    int height = int.Parse(scale.Attributes["height"].InnerText);
    int width = int.Parse(scale.Attributes["width"].InnerText);
    int fontsize = int.Parse(scale.Attributes["fontsize"].InnerText);
    Color clr = Color.FromName(scale.Attributes["color"].InnerText);
    string name = scale.InnerText;
    
    Pen p = new Pen(clr, (float)width);
    g.DrawLine(p, x + len, y, x + len, y - height);
    Font font = new Font("Arial", (float)fontsize);
    g.DrawString(
     name, font, new SolidBrush(clr),
     (float)(x + len - 10), (float)(y + 10));
   }
  }
  private void DrawYCoor(Graphics g, XmlNode ycoo)
  {
   int x = int.Parse(ycoo.Attributes["x"].InnerText);
   int y = int.Parse(ycoo.Attributes["y"].InnerText);
   int length = int.Parse(ycoo.Attributes["length"].InnerText);
   bool arrow = bool.Parse(ycoo.Attributes["arrow"].InnerText);
   int stroke = int.Parse(ycoo.Attributes["stroke"].InnerText);
   Color color = Color.FromName(ycoo.Attributes["color"].InnerText);
   Pen pen = new Pen(color, (float)stroke);
   if(arrow)//是否有箭头
   {
    AdjustableArrowCap Arrow = new AdjustableArrowCap(
     (float)(stroke * 1.5 + 2),
     (float)(stroke * 1.5 + 3),
     true);
    pen.CustomEndCap = Arrow;
   }
   g.DrawLine(pen, x, y, x, y + length);//画坐标
   //画刻度
   foreach(XmlNode scale in ycoo.ChildNodes)
   {
    int len = int.Parse(scale.Attributes["length"].InnerText);
    int height = int.Parse(scale.Attributes["height"].InnerText);
    int width = int.Parse(scale.Attributes["width"].InnerText);
    int fontsize = int.Parse(scale.Attributes["fontsize"].InnerText);
    Color clr = Color.FromName(scale.Attributes["color"].InnerText);
    string name = scale.InnerText;
    Pen p = new Pen(clr, (float)width);
    g.DrawLine(p, x, y + len, x + height, y + len);
    Font font = new Font("Arial", (float)fontsize);
    StringFormat sf = new StringFormat();
    sf.Alignment = StringAlignment.Far;
    RectangleF rect = new RectangleF(
     (float)(x - 100),
     (float)(y + len - 25),
     90f,
     50f);
    sf.LineAlignment = StringAlignment.Center;
    g.DrawString(name, font, new SolidBrush(clr), rect, sf);
   }
  }
  private void DrawChart(Graphics g, XmlNode chart)
  {
   foreach(XmlNode lines in chart.ChildNodes)
   {
    DrawLines(g, lines);
   }
  }
  private void DrawLines(Graphics g, XmlNode lines)
  {
   int Stroke = int.Parse(lines.Attributes["stroke"].InnerText);
   Point[] points = new Point[lines.ChildNodes.Count];
   Color linecolor = Color.FromName(lines.Attributes["color"].InnerText);
   for(int i = 0; i < lines.ChildNodes.Count; i++)
   {
    XmlNode node = lines.ChildNodes[i];
    points[i] = new Point(
     int.Parse(node.Attributes["x"].InnerText),
     int.Parse(node.Attributes["y"].InnerText));
    int Radius = int.Parse(node.Attributes["radius"].InnerText);
    Color pointcolor = Color.FromName(node.Attributes["color"].InnerText);
    if(Radius != 0)//画点
    {
     g.FillEllipse(new SolidBrush(pointcolor),
      points[i].X - Radius,
      points[i].Y - Radius,
      Radius * 2,
      Radius * 2);
    }
   }
   Pen pen = new Pen(linecolor);
   g.DrawLines(pen, points);//画线
  }
 }
}
然后,为Parser类的CreateElement方法增加一个小case,代码如下:
switch(element.Name)
   {
    case "text":
     printElement = new Text(element);
     break;
    case "table":
     printElement = new Table(element);
     break;
    case "linechart"://新增加的linechart
     printElement = new LineChart(element);
     break;
    default:
     printElement = new PrintElement();
     break;
   }
将原来的XML文件中的table标签和其子标签都替换成刚才写的那段linechart,然后编译程序,运行后效果如下所示:

现在,我们的打印控件就能打印折线图了,由于我们采用了Abstract Factory的设计模式,将报表的打印和格式的解析分开,使得本程序有着非常方便的扩充能力,如果需要再增加一种新形式的图表,那么需要定义出标签,写一个解析类,再到Paser中为这个类增加一个case就搞定了,PrintControl内部的代码一行都不需要改写。
总结:
以上就是如何制作打印控件的详细介绍,基本上解答了读者来信中的大部分问题,另外还有几个被问得很多的问题这里再集中解答一下:
Q:这种方案是否一定需要客户端装有.Net Framework?
A:是肯定的,这也是算是本方案一个缺陷。不过我可以肯定,在不远的将来,微软一定会将.Net Framework以升级或者是补丁的形式安装到我们的大多数Windows甚至是Linux操作系统当中。那时便不会有现在的这个遗憾存在。
Q:我采用Winform应用程序的形式,那么是不是存在着一个部署的问题?例如我增加了一种新的图表格式,那么是否所有的打印客户端都需要升级到新的版本?
A:是的,不过理论上可以采用.Net Remoting的设计来避免这个问题:因为Graphics类也是从System.MarshalByRefObject继承下来的,因此同样可以通过Remoting序列化,这样我们就可以把解析类(Table,Text,Chart等)和厂类(Paser)都放到服务器端通过Remoting提供远程调用方法,而只把打印控制(PrintControl)放到客户端,那么,当我们新增加图表的时候,就可以不需要对客户端进行任何升级。
Q:打开网页控件不会运行,只显示一个白框,怎么办?
A:这个是因为你安装了.Net Framework SP1或者SP2,它们默认的安全策略是不允许控件运行的,这时需要进行以下修改:打开Microsoft .NET Framework Wizards,在"程序"里有,也可以在"管理工具"里面找到它,点击"调整.NET安全性",如下图所示:

再将Internet区域的安全级别设置为"完全信任",如下图所示:

整个方案到这里就介绍完了,读者如果在使用中出现什么问题,请发电子邮件到nluyan@msn.com, nluyan@163.net,或者到微软的中文DotNet新闻组来进行详细讨论。谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一、应用场景与出发点 同一个系统中,为了解决不同的客户可能需要设计不同的单据打印模板,实现此方法可能是: 1、设计不同的自带RDLC报表文件,根据当前客户加载不同的报表打印 2、GDI+绘图 和 打印组件 ,不同的客户创建不同的绘图XML格式的模板内容 3、其他第三方组件 主要对比一下前两种方法,第一种方法不好之处在于,不灵活,开发者必须地每个客户制订一个报表,不推荐采用。第二种方法,修改对应的模板内容就可以了, 模板内容可以是Xml文件,也可以是存放在数据库中的Xml格式字符串。推荐采用这种方法。然后这种方法的也有一个棘手问题 :如何让用户快速、方便地设计打印模板,本示例就是为了解决这个问题。 二、实现思路与原理 功能概要:设计一个界面,支持用户自由添加 要打印的项,文本,直线,图片 等,并且可以方便改变打印项的 字体、颜色、粗细、位置,设计时支持效果预览。 技术要点:GDI+绘图、拖动控件、XML解析、自定义控件 三、相关类介绍 绘图工具类:DrawHelper 实现 xml格式模板 与 打印项 之间进行互相转换,在目标画板中绘制 拖动工具类:WinHelper 实现控件的鼠标拖动,键盘移动 自定义控件:用于显示文字的文本框 TextBoxExt、用于显示直线的标签 LabelExt 主窗体代码:用于用户操作,添加,删除,编辑,打印项 详细介绍请参照我的博文:http://de.cel.blog.163.com/blog/static/51451236201472215450939/

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值