利用XML實現通用WEB報表打印(1) 盧彥
摘要
開發B/S結構的應用程序最頭疼的問題可能就是報表打印了,由于只能采用瀏覽器來作為用戶界面進行交互,所以不能精確控制客戶端的打印機。而很多B/S結構的應用程序常常需要完成非常復雜的報表打印任務。而靠IE自帶的頁面打印功能一般不能滿足需要。
采用Crystal Report是一種大型報表系統常用和推薦的解決方案,但是如果我們只需要進行一些小規模的報表打印的話,Crystal Report則顯得龐大麻煩了一點,可定制性也不太好,它的打印實際上也是利用了IE的打印功能,也不能精確控制打印效果,而且需要您對它進行注冊。
所以我們這里討論的是另外一種辦法,簡單來說,如果您有下列需求中的任何一條,那么就可以嘗試采用本方案。
目錄
- 方案適用性
- 方案原理
- 技術選擇
- 可行性分析
- 伸縮性和安全性
- 方案設計圖
- 格式定義
- 總結
- 作者
方案適用性
1. 遠程數據打印。需要打印的數據并不在本地,必須進行遠程讀取。
2. 需要精確控制打印效果,包括頁面格式,分頁,附加條目,表格等。
3. 出于安全性考慮,不能直接連接到數據庫。
方案原理
其實原理很簡單,通過XML強大的自定義功能,我們便能方便的自定義出我們所有需要的格式控制標簽,在服務器端進行動態編碼后通過WEB服務器傳到客戶端,然后在客戶端進行格式解析,根據服務器端定義的打印格式從客戶端直接控制打印機打印出我們需要的報表。
技術選擇
由于報表打印比較復雜,為了能夠精確控制打印格式,不能采用WEB瀏覽器頁面打印的方式進行報表打印工作,只能采取自編程控制客戶端的打印工作。由于.NET framework的winform可以直接嵌入到網頁中,我們在這里選用了該技術,但是請注意,我這么做并不代表.NET winform是唯一的選擇,其實您可以采用任何客戶端代替它,例如Java Applet或者ActiveX,甚至是一個普通的應用程序都能行。
不允許直接連接到數據庫,因此只能采用XML文件進行中間數據交換格式,通過普通WEB服務器的默認80端口進行數據傳輸。事實上,我簡直找不到其它更理想的方案了,當然,web service也許能算是一種,但是它采用的是SOAP傳輸數據,從原理上看,應該和我們采用的XML屬于同種類技術。
再補充說明一下我為什么要采用.NET編寫的受控組件,優點在于:
1. 它不需要進行客戶端注冊。相對于ActiveX的一個大優點。
2. 比ActiveX安全性高。在.NET Common Language Runtime的控制之下運行
3. 編寫方便。我喜歡C#和Visual Studio .NET。
4. 有很強大的打印控制功能。利用.NET framework類庫。
5. 直接支持XML技術。
6. 和IE兼容性高。同為Microsoft公司產品。
另外,需要注意一點就是,在.NET framework sp1和sp2中默認的安全級別是不能直接運行受控組件的,但是在.NET framework 1.1 beta中又改了回來,可以直接運行了。
服務器端您則可以采用現有的服務器系統和數據庫,不需要新添加任何新硬件設備和新的.NET服務器管理人員,他們往往是些要求拿高薪的家伙。 :)
服務器的工作流程為:
1. 接受客戶端的標準XML模版查詢。
2. 需要根據查詢要求將數據庫數據格式轉換成標準的XML數據格式。
3. 將XML數據通過80端口發送出去。
可行性分析
由于現在的大部分數據庫都支持XML格式的數據查詢和轉換,如SQL Server 2000,Oracle 9i,IBM DB2等大型關系型數據庫。只需要通過簡單的設置就能直接進行XML數據轉換工作。如果數據庫不能支持直接XML數據轉換,也可以籍由一些服務器端腳本程序進行腳本轉換工作,比如JSP,ASP,PHP等等。
客戶端也不需要任何特殊的設置工作,僅需要安裝一個大小為21M的.NET framework分發包,然后直接打開網頁就可以進行工作。也沒有操作系統限制,從windows 98到windows xp都能很好的支持。
伸縮性和安全性
伸縮性
由于采用的是XML標準數據格式作為中間數據交換,因此本解決方案具有非常好伸縮性,例如,客戶端的.NET控件可以采用JAVA APPLET、ACTIVX或者是VB,VC等編寫的客戶端應用程序直接替換。服務器也可以任意選擇采用IIS或APACHE等WEB服務器。數據庫也可以采用任意一種數據庫。包括SQL Server,Oracle或者是Access等。這點上文已經談到過,因為文章的長一點并不會使送給我的T恤大一號,這里再強調一遍只是為了加深讀者對XML的跨平臺性的認識。 :)
安全性
由于采用的是普通WEB服務器傳送數據,因此可以直接采用SSL安全套接字等已經成熟的WEB加密技術。同時還可以對XML進行數據算法加密,在客戶端再進行解密,保證了傳輸的安全性。
由于采用的是80端口,不需要再另外新增加專用端口,減少了安全漏洞的可能性,同時還能方便的穿過雙方的的網絡防火墻等保護設備。
方案設計圖

格式定義
為了能自己控制打印的格式,我們定義了下列的格式標簽,其中在命名上參考了HTML的命名辦法,所以基本上熟悉HTML的都能一看就能明白標簽的具體含義。如果您覺得這些標簽的表達能力還不夠強,您還可以自己定義一些更多更精確的格式標簽。
主要標簽說明:
text:文本字符串
屬性:
x:打印輸出的X坐標
y:打印輸出的Y坐標
fontname:字體
fontsize:字體大小
fontcolor:顏色
b:是否為粗體
i:是否為斜體
u:是否有下劃線
table:表
屬性:
x:打印輸出的x坐標
y:打印輸出的y坐標
border:邊框粗細
bordercolor:邊框顏色
maxlines:每頁最大行數
tr:行
屬性:
height:高度
td:列
屬性:
width:寬度
align:對齊方式
fontname:字體
fontsize:字體大小
fontcolor:字體顏色
b:是否粗體
i:是否斜體
u:是否下劃線
bgcolor:背景顏色
next:下一頁
tablehead:表頭
tablebody:表項
tablefoot:表底
page:頁設置
PrintWard:橫/縱向打印
PageType:紙張類型
PageLeft:左邊距
PageRight:右邊距
PageTop:上邊距
PageBottom:下邊距
標簽應用示例:
<root> <pagesetting> <Landscape>true</Landscape> <paperkind>A4</paperkind> <paperwidth>210</paperwidth> <paperheight>297</paperheight> <pageleft>0</pageleft> <pageright>0</pageright> <pagetop>0</pagetop> <pagebottom>0</pagebottom> </pagesetting> <reporttable> <text x="450" y="40" fontname="黑體" fontsize="24" fontcolor="Black" b="true" i="false" u="true">最新成交合同信息</text> <text x="70" y="100" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="true">制表時間:2002年0月10日</text> <text x="910" y="100" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="true">單位:元</text> <table x="65" y="130" border ="1" bordercolor="Black" maxlines="28"> <tablehead> <tr height="25"> <td width="90" align="center" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">合同號</td> <td width="90" align="center" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">產品名稱</td> <td width="50" align="center" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">成交量</td> <td width="50" align="center" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">成交價</td> <td width="50" align="center" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">成交金額</td> <td width="50" align="center" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">掛單量</td> <td width="50" align="center" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">起始價</td> <td width="330" align="center" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">賣方</td> <td width="330" align="center" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">買方</td> </tr> </tablehead> <tablebody> <tr height="25"> <td width="100" align="left" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">20021010015</td> <td width="100" align="left" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">CNR</td> <td width="70" align="left" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">93</td> <td width="70" align="left" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">6680</td> <td width="70" align="left" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">621240</td> <td width="70" align="left" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">93</td> <td width="70" align="left" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">6680</td> <td width="200" align="left" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">湖北省國營新星拖拉機廠</td> <td width="200" align="left" fontname="宋體" fontsize="12" fontcolor="Black" b="true" i="false" u="false" bgcolor="White">中化國際貿易股份有限公司</td> </tr> ………. </tablebody> <tablefoot> </tablefoot> </table> </reporttable> </root>
注意事項:
a) 如果采用服務器腳本動態生成XML文檔時,發送內容類型應該設置為text/xml(普通html頁面為text/html),字符編碼應該為UTF-8,否則會出現編碼錯誤問題。
b) 應該嚴格按照XML規定的格式來生成文件,否則XML解析器將不會予以解析。
2. 客戶端
可以采用任意應用程序來讀取服務器端生成的XML文件,如果采用VB、DELPHI等桌面應用軟件開發工具,則可以使用MSXML的COM解析器。推薦采用.NET,內部已經集成了XML解析器,直接就可以通過使用.NET類庫調用。既可以做成桌面應用程序形式,通過遠程調用;也可以嵌入到IE瀏覽器中,直接在網頁中運行。

效果示例圖

打印預覽
注意事項:
1. 如果采用.NET,客戶端必須先安裝.NET framework1.0運行環境,下載地址為:http://download.microsoft.com/download/.NETframesdk/Redist/1.0/W98NT42KMeXP/EN-US/dotnetredist.exe
2. 如果采用嵌入到網頁中的形式,那么本程序需要編譯成一個控件形式(一個擴展名為dll的文件),然后在網頁中插入以下標記:
<object id="print" classid="http:print.dll#Print.UserControl1" Width="728" Height="460"></object>
將控件嵌入到一個靜態或動態網頁中。然后將該控件文件拷貝到和該網頁相同的目錄中(標記中Print.dll為生成的控件文件名,Print.UserControl1為該控件的命名空間NAMESPACE)。











軟件原理:
該軟件的原理其實很簡單,就是要方便的解析出定義好的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區域的安全級別設置為"完全信任",如下圖所示:
