做過bs開發的同志應該都深有體會,在web程式中列印不再象應用程式中那樣便於控制了,web程式天生的一些特性造成了這個缺點,如:印表機在本地,而檔案確可能在伺服器上;格式如何控制和定製等等。都給我們開發中帶來了很多問題,雖說有水晶報表等控制項來解決但總歸是不方便。當然有了問題就會有人來研究解決,這裡我先對目前流行的幾種方式做個簡單介紹:
1、IE直接列印
這個不用多說,直接調用window.print或者webrower控制項的ExecWB方法來列印。方便快捷,用戶端無需任何設定即可。利用一些辦法也可以實現簡單的定製,比如做一個模板htm檔案,然後在js中動態建立一個隱藏幀來,用指令碼來產生其中的資料,再把最後的結果檔案寫入到隱藏幀列印處理。如果處理的好,實際上效果也是不錯。對於簡單的列印需求應該是夠了。這裡我舉個實際中的例子來說明這種方式:
開發中經常需要列印一些統計的結果給使用者,比如說一個常見的功能是營業報表類的列印:操作員先輸入查詢條件,然後提交得到查詢的結果,點擊列印後,按照定義好的格式列印報表。
我們實現上大部分情況會把查詢的結果綁定到DataGrid上來,然後列印DataGrid。這種情況的列印一般來說格式比較固定簡單,確定後基本不會再作更改。所以可以採用IE直接列印,但若直接調用window.print來列印結果頁面,頁面上別的無關元素也會被列印出來,頁頭頁尾的格式也不好控制,所以採用把需要列印的資料動態寫入到隱藏幀中列印的方式來實現
程式碼範例:ASP.NET中列印指定的DataGrid內容
其中借用來自微軟的一段js代碼,整個js代碼如下:
//以下指令碼實現:列印指定的datagrid
var vGridContent; //DataGrid的內容
var vHeaderInfo; //列印的表頭
var vTailerInfo; //列印的表尾
/*
目的:在頁面寫入隱藏幀並列印
參數:
vDataGrid 所要列印的DataGrid控制代碼
備忘:
代碼中調用如下
btPrint.Attributes.Add("onclick","return PrintDataGrid(document.all('SheetList'))");
SheetList為待列印的DataGrid的ID
*/
function PrintDataGrid(vDataGrid)
{
PickupHeaderInfo();
document.body.insertAdjacentHTML("beforeEnd",
"<iframe name=printHiddenFrame width=0 height=0></iframe>");
var doc = printHiddenFrame.document;
doc.open();
doc.write("<body onload=\"setTimeout('parent.onprintHiddenFrame()', 0)\">");
doc.write("<iframe name=printMe width=0 height=0 ></iframe>");
doc.write("</body>");
doc.close();
CreateHtmlReport(printHiddenFrame.printMe,vDataGrid);
return false;
}
/*
目的:在隱藏幀中寫入DataGrid的內容,並重寫DataGrid的格式
參數:
vHideFrame 隱藏幀的控制代碼
vDataGrid 所要列印的DataGrid控制代碼
備忘:
*/
function CreateHtmlReport(vHideFrame,vDataGrid)
{
vGridContent = vDataGrid.outerHTML;
// 輸出報表頭資訊及抽取過來的表格
var doc = vHideFrame.document;
doc.open();
doc.write("<html><body>");
doc.write(vHeaderInfo);
doc.write(vGridContent);
doc.write("</body></html>");
doc.close();
// 重新設定表格樣式
vDataGrid.borderColor = "#000000";
vDataGrid.width = "100%";
vDataGrid.style.fontFamily = "Verdana";
vDataGrid.style.fontSize = "12px";
vDataGrid.style.borderRight = "2px solid #000000";
vDataGrid.style.borderTop = "2px solid #000000";
vDataGrid.style.borderLeft = "2px solid #000000";
vDataGrid.style.borderBottom = "2px solid #000000";
vDataGrid.style.borderCollapse = "collapse";
// 重新設定表格頭樣式
var TBody = vDataGrid.children(0);
TBody.children(0).style.fontWeight = "bold";
TBody.children(0).bgColor = "#E7E7E7";
// 替換原表格底部的頁碼資訊
var pageInfo = "<td>第 " + ((4 - 3) / 1 + 1) + " 頁 / 共 " + "1" + " 頁 </td>";
}
//建立表頭 表尾
function PickupHeaderInfo()
{
try
{
// 提取報表標題字型大小
var ReportTitleWithSizeInfo = "<font size='" + "+2" + "'>" + "無費用使用者統計" + "</font>"
var reportDate = "";
var reportWriter = "";
var nowdate=new Date();
reportDate = "<b>統計時間</b>:" +nowdate.toLocaleString() + "<br>";
reportDate +="<b>營業廳</b>:測試而已<br>";
// 產生報表頭資訊
vHeaderInfo = "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=gb2312\">";
vHeaderInfo += "<title>無費用使用者統計</title></head>" +
"<body bgcolor='#FFFFFF' style='color: #000000; font-family: Verdana; font-size:12px; cursor: default'>";
vHeaderInfo += "<br><p align='center'><b>" + ReportTitleWithSizeInfo + "</b></p>";
vHeaderInfo += "<p>" + reportDate;
vHeaderInfo += reportWriter + "</p>";
}
catch (e)
{
alert("提取報表公用資訊失敗,列印操作被取消!");
self.close();
}
}
//下面的指令碼來自msdn
// The code by Captain <cerebrum@iname.com>
// Mead & Company, http://www.meadroid.com/wpm/
// fake print() for IE4.x
if ( !printIsNativeSupport() )
window.print = printFrame;
// main stuff
function printFrame(frame, onfinish) {
if ( !frame ) frame = window;
if ( frame.document.readyState !== "complete" &&
!confirm("The document to print is not downloaded yet! Continue with printing?") )
{
if ( onfinish ) onfinish();
return;
}
if ( printIsNativeSupport() ) {
/* focus handling for this scope is IE5Beta workaround,
should be gone with IE5 RTM.
*/
var focused = document.activeElement;
frame.focus();
frame.self.print();
if ( onfinish ) onfinish();
if ( focused && !focused.disabled ) focused.focus();
return;
}
var eventScope = printGetEventScope(frame);
var focused = document.activeElement;
window.printHelper = function() {
execScript("on error resume next: printWB.ExecWB 6, 1", "VBScript");
printFireEvent(frame, eventScope, "onafterprint");
printWB.outerHTML = "";
if ( onfinish ) onfinish();
if ( focused && !focused.disabled ) focused.focus();
window.printHelper = null;
}
document.body.insertAdjacentHTML("beforeEnd",
"<object id=\"printWB\" width=0 height=0 \
classid=\"clsid:8856F961-340A-11D0-A96B-00C04FD705A2\"></object>");
printFireEvent(frame, eventScope, "onbeforeprint");
frame.focus();
window.printHelper = printHelper;
setTimeout("window.printHelper()", 0);
}
// helpers
function printIsNativeSupport() {
var agent = window.navigator.userAgent;
var i = agent.indexOf("MSIE ")+5;
return parseInt(agent.substr(i)) >= 5 && agent.indexOf("5.0b1") < 0;
}
function printFireEvent(frame, obj, name) {
var handler = obj[name];
switch ( typeof(handler) ) {
case "string": frame.execScript(handler); break;
case "function": handler();
}
}
function printGetEventScope(frame) {
var frameset = frame.document.all.tags("FRAMESET");
if ( frameset.length ) return frameset[0];
return frame.document.body;
}
function onprintHiddenFrame() {
function onfinish() {
printHiddenFrame.outerHTML = "";
if ( window.onprintcomplete ) window.onprintcomplete();
}
printFrame(printHiddenFrame.printMe, onfinish);
}
程式中在Page_Load裡面加上:btPrint.Attributes.Add("onclick","return PrintDataGrid(document.all('SheetList'))");
註:SheetList為需要列印的DataGrid ID,在查詢後,btPrint為頁面上列印按鈕的ID
可以將上述指令碼代碼寫在一個js檔案中,然後再aspx檔案中引用,如<script srcenter.js"></script> ,上述代碼的原理比較簡單,我不在多說。上述代碼可以實現直接列印頁面上指定控制項的內容,當然最多還是列印table的內容,如果需要先預覽後列印。需要作一個空的html檔案,然後動態寫入需要列印的內容:
var preDlg = window.open("PrintList.htm");
CreateHtmlReport(preDlg, true);
2、ActiveX控制項
自己開發控制項。這種方式很多商用軟體採用這種方式,寫成控制項後已經無所謂是在web中使用還是應用程式中使用了。列印方式非常靈活,基本上程式能做到的web也能做得到。但用戶端需要安裝組件,部署不是很方便。
3、.NET組件
盧彥寫過一篇很好的文章《利用XML實現通用WEB報表列印》,相信大家都看過了。思路新穎,實現簡單,確實不失為一種通用WEB列印解決辦法,尤其利用XML來描述列印檔案的方法給以後的格式的拓展留下很好的介面,非常容易擴充。這種列印方式對于格式變化大,資料量小的應用來說非常合適。這種思路也給了ASP.NET上列印的一種新的思路:自訂一些組件來實現靈活的列印功能。當然缺點也是顯而易見:1、需要用戶端安裝NET framework1.0組件。2、XML的解析上,如果檔案較大速度上不是很理想。3、頁面首次載入時會有明顯的延時。當然最大的問題在於用戶端需要安裝組件,因為大部分採用BS架構的系統,用戶端配置都不會太高,9x的作業系統居多,如果採用這種方式必將給工程的實施造成很多麻煩,所以最好能有一種方式:既能利用xml這種好的方式來描述列印檔案,而且用戶端也無需安裝任何組件。
在研究了盧大俠的代碼後,俺有了一個想法:事實上代碼裡別的功能我們並不關心,最重要的關鍵在於xml的解析部分和列印的部分。先來看看XmlDocument的命名空間System.Xml,並非winform特有,webform也可以使用,再看看PrintDocument的命名空間System.Drawing.Printing,查詢了MSDN後發現這個命名空間下的類庫webform中依然可以使用。好了,我們最關心的兩點WebForm中都可以使用,我們可以把這個列印控制項寫成一個類庫,然後在ASP.NET中直接調用而用戶端無需再安裝任何組件了。
但隨後問題出來了:盧彥的.NET組件是在頁面請求的首次下載到用戶端執行的,所以組件中可以直接使用各種本地資源,如印表機,網路等,但我們的列印控制項寫成類庫由ASP.NET程式調用時,實際上組件是在服務端上運行,它訪問服務端的資源不會有問題,但我們更希望:運行在服務端的組件可以訪問用戶端的資源,如訪問用戶端的印表機列印指定內容,當然列印的內容可能是在服務端產生的。
這又引出一個新的問題:ASP.NET如何不受限制的訪問各種資源。由於安全原因,ASP.NET程式預設以ASPNET 本機使用者帳戶運行。由於該帳戶不具有任何網路憑據,因此在網路看來,它是 Windows 匿名帳戶 (NT AUTHORITY\ANONYMOUS LOGON),不具有訪問本地資源的許可權,所以必須採用類比使用者的方式讓APS.NET程式以別的帳戶形式運行。
思路已經整理清楚,簡單說一些實現的步驟:
A、編寫列印組件
參考盧彥的代碼,去除無關部分,只保留xml解析部分和列印部分,PrintControl類中增加三個成員資料:
public string FileName=""; //需要列印的檔案名稱
public string PrinterName=""; //印表機名稱
public string ClientIP=""; //用戶端IP地址
增加一個成員函數:
public void PrintPage()
{
try
{
doc.Load(FileName);
// set the printer name
this.printDocument1.PrinterSettings.PrinterName = ClientIP+PrinterName;
// add print page event handler
this.printDocument1.PrintPage += new PrintPageEventHandler(this.pd_PrintPage);
// print the page
//string tm=User.Identity.Name;
this.printDocument1.Print();
error_msg="列印成功";
}
catch(Exception ex)
{
error_msg = ex.Message;
}
}
注意:用戶端的印表機必須是共用
別的xml解析部分不用動,編譯成類庫後,在ASP.NET引用RemotePrint.dll,並在需要列印功能的頁面放上一個列印按鈕,代碼中引用RemotePrint命名空間,編寫Click事件如下:
PrintControl print=new PrintControl();
print.PrintPath =Request.PhysicalApplicationPath;
print.ClientIP=Request.ServerVariables["REMOTE_ADDR"]
print.PrinterName="printer";
print.PrintPage();
ASP.NET項目中Web.Config開啟使用者類比:
<identity impersonate="true" userName="1234" password="1234" />
上述解決辦法必須基於一個前提,服務端和用戶端是運行在同一個網段內,不過仔細想想,有上述列印需求的BS系統一般都運行在企業的內網上,所以基本上滿足要求