由網頁呼叫本機端列印方式
※本篇會參考使用到 將 RDLC 報表不需預覽直接列印(Print RDLC Report without Preview) 部分程式實作方式,重覆部分不再說明。
現在,很多系統都是開發成為 WEB 型態(也稱雲端平台之類...)。
因此,只需要 Browser (瀏覽器) 即可進行系統操作。雖然有很大的便利程度,但是現實中還是會面臨到一些問題,像是 I/O控制等。這是因為受限於 瀏覽器的安全性不得直接讓程式碼介入系統控制,以免被駭客入侵等....
但這也造就許多問題 ...... 撇開其它不談,就以我們常見的列印來說好了。
也許大家都知道,瀏覽器也是可以列印的啊,不管是直接列印,或是輸出成PDF列印等等。
但所謂不在其位不知其苦,部分行業或是以目前台灣推行的電子發票的列印格式(奇特格式),基本上瀏覽器列印就做不到了。
目前,有接觸過幾家廠商在做這類的系統,遇到電子發票列印實的做法怎麼處理呢?
當然回歸老路囉........ IE + ActiveX ....... 說真的,我有點語塞了。
怎麼有種回歸到 IE 綁架的時代的 Feel 呢?
IE only 都會遇到 Windows 重大改版後導致 IE不支援繼續更新會始操作版面出現怪異現象 ,然後 ActiveX 有無法移植到其它瀏覽器(Chrome/FireFox/Safari/Opera...)的問題存在。
當然不是沒有辦法解決,只是台灣可能很多軟體公司面臨賺錢問題,不太願意花太多時間/金錢做研究是擺明事實(IT行業是個很燒錢的行業,由其它的RD部門更是如此),既有技術能快速上手開發賺錢,憑甚麼不做?
有天,在研究 MIT 開發的 Android App 開發(MIT app inventor) 介面得到了很棒的靈感(外國都已經行之有年了)
技術上,大致就像一開始的圖一般:
那個齒輪狀的東西就是一個本地(使用者電腦上的)一個程式或是服務,它提供了基本的 HTTP 通訊能力用來接收指令,並控制印表機。
通訊圖
所以,我有一台伺服器 www.myweb.com.tw 提供了作業頁面
localprint-test.html
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=big5" />
<title>呼叫本地列印服務</title>
<script type="text/javascript" src="jquery-1.7.2.min.js"></script>
<script type="text/javascript">
$(document).ready(function(){
$('#print').click(function(){
var dt=$.ajax({type:"GET",
url:"http://localhost:1300/printdata?doc=accup03",
async:false,
complete: function(e, xhr, settings){
if(e.status==200){
alert("列印已送出");
}
else
{
alert("列印服務無回應!");
}
}
});
});
})
</script>
</head>
<body>
本機必須啟動 SimpleHttpService1<br />
該程式會在本機啟用 port 1300 http 服務<br /><br />
<div>
<form id="f1" name="f1" >
<input id="print" name="print" type="button" value="列印資料" />
</form>
</div>
</body>
</html>
這裡的 Html使用了 JQuery 套件來做 Ajax 呼叫。
而當使用者按下 button 時就會觸發發出 http://localhost:1300/printdata?doc=accup03 請求。
然後等待請求是否回覆正確/錯誤。
伺服器內還有另一個檔案 accup03.txt 的文字檔,是用來列印的資料。
(內容隨便,當然也可以是其它類型檔案,但處理方式就要修改)
接下來就是本機的小型 Http 服務了,這程式是寫成 console 模式執行的,當然也可以寫成 Service的
program.cs
using System;
using System.Net;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Net.Http;
using Telerik.Reporting;
namespace SimpleHttpService1
{
class Program
{
//列印記數用的頁數紀錄
static private int m_currentPageIndex;
//列印用串流資料儲存區
static private IList<Stream> m_streams;
static void Main(string[] args)
{
HttpListener listener = null;
try
{
// 建立 HttpListener 來監聽指定埠
// 此案設定 port:1300 監聽 TCP 活動,並且有路徑具名 printdata/
listener = new HttpListener();
listener.Prefixes.Add("http://*:1300/printdata/");
listener.Start();
Console.WriteLine("WebServer Start Successed.......");
while (true)
{
Console.WriteLine("waiting ...");
// 阻塞模式等待請求,當有使用者呼叫才會繼續
HttpListenerContext context = listener.GetContext();
// 將請求放入處理過程線程池(Thread Pool)
// 此處以另一個 Thread 處理 Request Context
System.Threading.ThreadPool.QueueUserWorkItem(ProcessHttpClient, context);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
/// <summary>
/// 客户請求處理
/// </summary>
/// <param name="obj">傳遞的 context</param>
static void ProcessHttpClient(object obj)
{
string msg;
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(@"列印完成");
// 取得傳入的 Context
HttpListenerContext context = obj as HttpListenerContext;
// 設定回覆代碼 200 OK / 回覆編碼 Utf-8 / 回覆類型 text/html
context.Response.StatusCode = (int)HttpStatusCode.OK;
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.ContentType = "text/html";
// 由瀏覽器於從原本伺服器host位置,發到本機來請求服務會被 Same origin policy 檔住而視為 Fail
// 所以回覆時必須加上下面這行 Header 才不會被瀏覽器視作不合法的跨網域請求(CORS)
context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
msg = System.Text.Encoding.Default.GetString(buffer);
// 這段可以用來檢查路徑(目前沒有實做,只是拿來觀察)
string urlpath = context.Request.Url.LocalPath;
Console.WriteLine("localpath:" + urlpath);
try
{
// 取得參數指定的文件名稱
string qryKey = context.Request.QueryString["doc"];
if (qryKey != null)
{
Console.WriteLine("Target Document :" + qryKey);
}
// 去外部取得資料檔案 (等待回覆後才會繼續)
Task<string> html = GetURL(qryKey);
html.Wait();
// 轉換 Html 為結構化資料 DataModel
List<DataModel> data = new List<DataModel>();
var lines = html.Result.Split(new char[] { '\r' });
int ix = 0;
foreach (string line in lines)
{
string line0 = line.Trim();
if (!string.IsNullOrEmpty(line0))
{
DataModel d1 = new DataModel();
ix++;
d1.no = ix;
d1.line = line0;
data.Add(d1);
}
}
// 送出列印
RdlcPrint(data);
}
catch (Exception e)
{
Console.WriteLine("列印失敗:" + e.Message + "[" + e.InnerException.ToString() + "]");
msg = "Print document is abord or failure!";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
// 送出回覆訊息 Response Output
try
{
using (StreamWriter writer = new StreamWriter(context.Response.OutputStream))
{
writer.Write(msg);
writer.Close();
}
}
catch (Exception e)
{
Console.WriteLine("Response Error:" + e.Message);
}
context.Response.Close();
}
/// <summary>
/// 取得遠端伺服器上的資料
/// </summary>
/// <param name="docName">指定文件名稱</param>
/// <returns>html內容</returns>
static async Task<string> GetURL(string docName)
{
HttpClient client = new HttpClient();
string html;
var response = await client.GetAsync($"http://www.myweb.com.tw/documents/{docName}.txt");
// 如果 httpstatus code 不是 200 時會直接丟出 expection
response.EnsureSuccessStatusCode();
// 取得回傳內容
html = await response.Content.ReadAsStringAsync();
response.Dispose();
client.Dispose();
return html;
}
/// <summary>
/// 使用RDLC 直接列印
/// </summary>
/// <param name="data">結構化資料</param>
static void RdlcPrint(List<DataModel> data)
{
Microsoft.Reporting.WinForms.LocalReport localReport = new Microsoft.Reporting.WinForms.LocalReport();
localReport.ReportPath = "Report2.rdlc";
localReport.DataSources.Add(new Microsoft.Reporting.WinForms.ReportDataSource("DataSet1", data));
Export(localReport);
Print();
localReport.Dispose();
}
// 提供給 the report renderer 使用, 用來建立列印用的 image stream
static Stream CreateStream(string name, string fileNameExtension, Encoding encoding, string mimeType, bool willSeek)
{
Stream stream = new MemoryStream();
m_streams.Add(stream);
return stream;
}
// 把 RDLC 輸出成列印報表使用的 EMF(Enhanced Metafile file) 格式 的
static void Export(Microsoft.Reporting.WinForms.LocalReport report)
{
string deviceInfo =
@"<DeviceInfo>
<OutputFormat>EMF</OutputFormat>
<PageWidth>8.27in</PageWidth>
<PageHeight>11.69in</PageHeight>
<MarginTop>0.25in</MarginTop>
<MarginLeft>0.25in</MarginLeft>
<MarginRight>0.25in</MarginRight>
<MarginBottom>0.25in</MarginBottom>
</DeviceInfo>";
Microsoft.Reporting.WinForms.Warning[] warnings;
m_streams = new List<Stream>();
report.Render("Image", deviceInfo, CreateStream, out warnings);
foreach (Stream stream in m_streams)
stream.Position = 0;
}
// 提供給 PrintDocument 作業用的 PrintPageEvents
static void PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs ev)
{
//建立影像 Meta 同時把 Image Stream 倒入
System.Drawing.Imaging.Metafile pageImage = new System.Drawing.Imaging.Metafile(m_streams[m_currentPageIndex]);
// 調整繪製方塊大小同等印表機可列印空間
System.Drawing.Rectangle adjustedRect = new System.Drawing.Rectangle(
ev.PageBounds.Left - (int)ev.PageSettings.HardMarginX,
ev.PageBounds.Top - (int)ev.PageSettings.HardMarginY,
ev.PageBounds.Width,
ev.PageBounds.Height);
// 報表背景以白色塗刷
ev.Graphics.FillRectangle(System.Drawing.Brushes.White, adjustedRect);
// 繪製報表內容
ev.Graphics.DrawImage(pageImage, adjustedRect);
// 準備到下一頁繪製. 確保將資料繪製完畢
m_currentPageIndex++;
ev.HasMorePages = (m_currentPageIndex < m_streams.Count);
}
// 列印 m_streams 內的文件
static void Print(string printerName = null)
{
if (m_streams == null || m_streams.Count == 0)
throw new Exception("Error: no stream to print.");
System.Drawing.Printing.PrintDocument printDoc = new System.Drawing.Printing.PrintDocument();
// 有指定印表機就設定為指定印表機,反之不設定(使用預設)
if (!string.IsNullOrEmpty(printerName))
{
printDoc.PrinterSettings.PrinterName = printerName;
}
if (!printDoc.PrinterSettings.IsValid)
{
throw new Exception("Error: cannot find the printer [" + printerName + "] .");
}
else
{
printDoc.PrintPage += new System.Drawing.Printing.PrintPageEventHandler(PrintPage);
m_currentPageIndex = 0;
printDoc.Print();
}
}
}
}
將 RDLC 報表不需預覽直接列印(Print RDLC Report without Preview)
DataModel.cs
namespace SimpleHttpService1
{
class DataModel
{
public int no { get; set; }
public string line { get; set; }
}
}
Report2.rdlc
由於不需要太複雜的 HTTP 服務處理,所以基本上就 HttpListener 就可以完成。
再來透過 HttpClient 到指定網站索取資料,索取到的資料交由 RDLC 產生版面。
再透過 PrintDocument 去發送列印即可。
最後很重要的是, Response 的內容的 Header 必須帶有允許做『跨域資源共享CORS』的識別值,才能讓原本請求的瀏覽器不會被識別為錯誤。
關於CORS其實有很多定義的,由於Http服務器是自己寫的,對於請求也只使用簡單請求,因此也只會使用 Access-Control-Allow-Origin 字段的操作而已。
這樣寫的好處:
1、可以直接控制特殊印表機,例如很多標籤機有提供 API 開發可以加快列印度或是各種條碼列印,而不需要自己手刻條碼程式碼。
2、不只可以控制印表機,也可控制電腦其它 輸出/輸入設備 等。
3、索取資料來源可以多樣化。
缺點:
1、必須在本機安裝小型 HTTP 程式,這樣就跟不是 Work Anywhere 了
2、平台需要列印前必須先將它執行起來(如果是寫成 service 型態就不需要每次啟動了。
參考:
跨來源資源共用(CORS)-MDN
跨來源資源共用- 維基百科,自由的百科全書 - Wikipedia
跨域资源共享CORS 详解- 阮一峰的网络日志
留言
您好,看了您的這篇文章,讓我受益良多,我目前是用dotnet core 3.1 api + angular 9.0 開發,我是在前端用pdfMake套件產生電子發票pdf列印,所以console service 並不用再去後端撈資料來列印,請問這部份程式應該要怎麼改,可以幫我一下嗎?
如果不用去後端撈資料來列印,就是把
static async Task GetURL(string docName)
的內容改掉
換成你要怎麼去取得的資訊來源即可(Function 名稱也可以不用GetURL之類)
如果變更Function名稱,下面這段的呼叫也要一併改
Task html = GetURL(qryKey);
我在程式碼這一段卡住了,是否可以請教您要如何解決,
在函式 Print() :
System.Drawing.Printing.PrintDocument pd = new System.Drawing.Printing.PrintDocument();
pd.PrinterSettings.PrinterName = printerName;
pd.PrinterSettings.PrintFileName = filepath;
pd.DocumentName = "file.pdf";
pd.PrintController = new StandardPrintController();
pd.PrintPage += new System.Drawing.Printing.PrintPageEventHandler(pd_PrintPage);
pd.Print();
我在事件 pd_PrintPage(object sender, PrintPageEventArgs ev)這裏卡住了,我不知道這段程式碼要如何寫, 是否可以請教您,因為試過很多方法都不行
您那段CODE是想要輸出成PDF嗎?
看起來很像....
PrintDocumwnt 沒有原生支援 PDF
所以您必須用其他方法產生PDF
您可以參考其他網站提供的解決方式:
https://www.mdeditor.tw/pl/2N0Y/zh-tw
http://renjin.blogspot.com/2009/01/printing-pdf-documents-using-c.html
https://www.coder.work/article/2958753
等等
謝謝您的解答,我現在的解法就是用如下這段程式碼:
System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo();
startInfo.CreateNoWindow = true;
startInfo.UseShellExecute = true;
startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
startInfo.FileName = filepath; //.pdf filepath
startInfo.Verb = "Print";
startInfo.Arguments = String.Format("/h /t \"{0}\" \"{1}\"", filepath, printerName);
Console.WriteLine("Arguments: {0} ", startInfo.Arguments);
System.Diagnostics.Process process = new System.Diagnostics.Process();
process.StartInfo = startInfo;
process.Start();
process.WaitForExit();
系統要先要裝 adobe reader,並把 .pdf 預設打開程式設定為 adobe reader,
不過我就是很想用 PrintDocument 去直接列印 pdf,
但是我好像找不到可以用系統的PrintDocument直接列印pdf的範例,都好像是要透過第三方程式庫
其實不是喔,那個是可以讓你指定本機的host name用的
因為本機呼叫可以使用:
http://127.0.0.1:1300
http://localhost:1300
http://192.168.0.1:1300 <本機在內網的IP>
http://169.157.123.85 <本機如果有掛在外網的IP,記得防火牆要開規則>
http://myhome.net.idv <如果您有申請網域的話>
當你使用http://localhost:1300做為綁定時,其他應用或是服務呼叫http://127.0.0.1:1300 或 http://192.168.0.1:1300 都是不能使用的
諸如此類