基於Win服務的標籤列印(模板套打)
最近做了幾個專案,都有在產品貼標的需求
基本就是有個證卡類印表機,然後把產品的資訊列印在標籤上。
然後通過機器人把標籤貼到產品上面
標籤資訊包括文字,二維碼,條形碼之類的,要根據對應的資料生成二維碼,條形碼。
列印標籤的需求接到手後,開始了我的填坑之旅。
列印3.0原始碼:https://github.com/zeqp/ZEQP.Print
列印1.0
第一個專案開始,因為原來沒有研究過列印,所以在Bing上查了一下.Net印表機關的資料
發現基本上都是基於.net的
System.Drawing.Printing.PrintDocument
這個類來做自定義列印
大家都用這個做列印,我想按理也沒有問題。
所以開始了我的程式碼。
PrintDocument去做列印,無非就是設定好印表機名稱,
DefaultPageSettings.PrinterSettings.PrinterName
列印份數
DefaultPageSettings.PrinterSettings.Copies
紙張方向
DefaultPageSettings.Landscape
然後列印的具體的資訊就是事件PrintPage寫進去
然後呼叫
Graphics.DrawString,Graphics.DrawImage來寫入具體的文字與圖片
Graphics.Draw的時候要指定字型,顏色,位置等資料
然後1.0版本就成了。
下圖為位置的配置檔案
程式碼一寫完,用VS除錯的時候。跑得飛起。、
所有的字型,要列印資料的位置也通過配置檔案可以動態的調整。感覺還算完美。
但是現實很骨感,馬上就拍拍打臉了
PrintDocument類只能以WinForm的方式執行,不能以服務的方式執行。
具體可以參考:https://docs.microsoft.com/zh-cn/dotnet/api/system.drawing.printing?redirectedfrom=MSDN&view=netframework-4.8
幸好客戶方面沒有什麼要求,而且生產的時候會有一臺專門的上位機可以做這個事,所以做了一個無介面的WinForm。在電腦啟動的時候執行
從而解決了不能以服務的方式執行的問題。
列印2.0
做完列印1.0後,又接到了一個專案。又是有列印相關的功能,自然又分配到我這裡來了。
但是對於上一個版本的列印。不能做為服務執行,做為自己寫的一個程式,居然有這麼大的瑕疵。總感覺心裡不爽
想去解決這個問題,但是在Bing上找到.Net的所有列印都是這樣做的。也找不到什麼更好的方法。
只到問了很多相關的相關人士。最後給了我一個第三方的商業解決方案BarTender
相關參考:https://www.bartendersoftware.com/
這個有自己的模板編輯器,
有自己的SDK,有編輯器,功能也非學強大。不愧是商業列印解決方案。
根據他的SDK,同時安裝了相關程式,寫下幾句列印程式碼。一個基於Win服務的打印出來了
於是。列印2.0出來了。
列印3.0
但是對於一個基於第三方的商業列印方案,所有功能都是很強大。實現也簡單。
就是對於一般公司的小專案。掙的錢還不夠買這個商業套件的License
而且對於一個只會使用別人家的SDK的程式。不是一個有靈魂的程式。
因為你都不知道人家背後是怎麼實現的。原理是什麼都不知道。
對於我,雖然能把這個專案用BarTender完成。但是總是對這個列印方案不是很滿意。
因為我只在這個上面加了一層殼。不知道後面做了什麼。
所以我一直想自己開發一個可以基於Win服務執行的列印程式。最好也要有自己的模板編輯器。
只到有一天。無意找到一篇文章
https://docs.aspose.com/display/wordsnet/Print+a+Document
他這裡也解釋了有關基於服務的列印有關的問題不能解決。
並且他們已經找到了對應的解決方案。基於他的解決方案。寫了對應一個列印幫助類。
這個是基於Windows的XPS文件API列印。
XPS是在Win 7後就是windows支援的列印文件型別 類比PDF
基本 XpsPrint API 的相關說明
同時基本他的XPS列印幫助類。我做了測試。可以完美的在Windows服務裡面執行關列印。
1 namespace ZEQP.Print.Framework 2 { 3 /// <summary> 4 /// A utility class that converts a document to XPS using Aspose.Words and then sends to the XpsPrint API. 5 /// </summary> 6 public class XpsPrintHelper 7 { 8 /// <summary> 9 /// No ctor. 10 /// </summary> 11 private XpsPrintHelper() 12 { 13 } 14 15 // ExStart:XpsPrint_PrintDocument 16 // ExSummary:Convert an Aspose.Words document into an XPS stream and print. 17 /// <summary> 18 /// Sends an Aspose.Words document to a printer using the XpsPrint API. 19 /// </summary> 20 /// <param name="document"></param> 21 /// <param name="printerName"></param> 22 /// <param name="jobName">Job name. Can be null.</param> 23 /// <param name="isWait">True to wait for the job to complete. False to return immediately after submitting the job.</param> 24 /// <exception cref="Exception">Thrown if any error occurs.</exception> 25 public static void Print(string xpsFile, string printerName, string jobName, bool isWait) 26 { 27 Console.WriteLine("Print"); 28 if (!File.Exists(xpsFile)) 29 throw new ArgumentNullException("xpsFile"); 30 using (var stream = File.OpenRead(xpsFile)) 31 { 32 Print(stream, printerName, jobName, isWait); 33 } 34 //// Use Aspose.Words to convert the document to XPS and store in a memory stream. 35 //File.OpenRead 36 //MemoryStream stream = new MemoryStream(); 37 38 //stream.Position = 0; 39 //Console.WriteLine("Saved as Xps"); 40 //Print(stream, printerName, jobName, isWait); 41 Console.WriteLine("After Print"); 42 } 43 // ExEnd:XpsPrint_PrintDocument 44 // ExStart:XpsPrint_PrintStream 45 // ExSummary:Prints an XPS document using the XpsPrint API. 46 /// <summary> 47 /// Sends a stream that contains a document in the XPS format to a printer using the XpsPrint API. 48 /// Has no dependency on Aspose.Words, can be used in any project. 49 /// </summary> 50 /// <param name="stream"></param> 51 /// <param name="printerName"></param> 52 /// <param name="jobName">Job name. Can be null.</param> 53 /// <param name="isWait">True to wait for the job to complete. False to return immediately after submitting the job.</param> 54 /// <exception cref="Exception">Thrown if any error occurs.</exception> 55 public static void Print(Stream stream, string printerName, string jobName, bool isWait) 56 { 57 if (stream == null) 58 throw new ArgumentNullException("stream"); 59 if (printerName == null) 60 throw new ArgumentNullException("printerName"); 61 62 // Create an event that we will wait on until the job is complete. 63 IntPtr completionEvent = CreateEvent(IntPtr.Zero, true, false, null); 64 if (completionEvent == IntPtr.Zero) 65 throw new Win32Exception(); 66 67 // try 68 // { 69 IXpsPrintJob job; 70 IXpsPrintJobStream jobStream; 71 Console.WriteLine("StartJob"); 72 StartJob(printerName, jobName, completionEvent, out job, out jobStream); 73 Console.WriteLine("Done StartJob"); 74 Console.WriteLine("Start CopyJob"); 75 CopyJob(stream, job, jobStream); 76 Console.WriteLine("End CopyJob"); 77 78 Console.WriteLine("Start Wait"); 79 if (isWait) 80 { 81 WaitForJob(completionEvent); 82 CheckJobStatus(job); 83 } 84 Console.WriteLine("End Wait"); 85 /* } 86 finally 87 { 88 if (completionEvent != IntPtr.Zero) 89 CloseHandle(completionEvent); 90 } 91 */ 92 if (completionEvent != IntPtr.Zero) 93 CloseHandle(completionEvent); 94 Console.WriteLine("Close Handle"); 95 } 96 // ExEnd:XpsPrint_PrintStream 97 98 private static void StartJob(string printerName, string jobName, IntPtr completionEvent, out IXpsPrintJob job, out IXpsPrintJobStream jobStream) 99 { 100 int result = StartXpsPrintJob(printerName, jobName, null, IntPtr.Zero, completionEvent, 101 null, 0, out job, out jobStream, IntPtr.Zero); 102 if (result != 0) 103 throw new Win32Exception(result); 104 } 105 106 private static void CopyJob(Stream stream, IXpsPrintJob job, IXpsPrintJobStream jobStream) 107 { 108 109 // try 110 // { 111 byte[] buff = new byte[4096]; 112 while (true) 113 { 114 uint read = (uint)stream.Read(buff, 0, buff.Length); 115 if (read == 0) 116 break; 117 118 uint written; 119 jobStream.Write(buff, read, out written); 120 121 if (read != written) 122 throw new Exception("Failed to copy data to the print job stream."); 123 } 124 125 // Indicate that the entire document has been copied. 126 jobStream.Close(); 127 // } 128 // catch (Exception) 129 // { 130 // // Cancel the job if we had any trouble submitting it. 131 // job.Cancel(); 132 // throw; 133 // } 134 } 135 136 private static void WaitForJob(IntPtr completionEvent) 137 { 138 const int INFINITE = -1; 139 switch (WaitForSingleObject(completionEvent, INFINITE)) 140 { 141 case WAIT_RESULT.WAIT_OBJECT_0: 142 // Expected result, do nothing. 143 break; 144 case WAIT_RESULT.WAIT_FAILED: 145 throw new Win32Exception(); 146 default: 147 throw new Exception("Unexpected result when waiting for the print job."); 148 } 149 } 150 151 private static void CheckJobStatus(IXpsPrintJob job) 152 { 153 XPS_JOB_STATUS jobStatus; 154 job.GetJobStatus(out jobStatus); 155 switch (jobStatus.completion) 156 { 157 case XPS_JOB_COMPLETION.XPS_JOB_COMPLETED: 158 // Expected result, do nothing. 159 break; 160 case XPS_JOB_COMPLETION.XPS_JOB_FAILED: 161 throw new Win32Exception(jobStatus.jobStatus); 162 default: 163 throw new Exception("Unexpected print job status."); 164 } 165 } 166 167 [DllImport("XpsPrint.dll", EntryPoint = "StartXpsPrintJob")] 168 private static extern int StartXpsPrintJob( 169 [MarshalAs(UnmanagedType.LPWStr)] String printerName, 170 [MarshalAs(UnmanagedType.LPWStr)] String jobName, 171 [MarshalAs(UnmanagedType.LPWStr)] String outputFileName, 172 IntPtr progressEvent, // HANDLE 173 IntPtr completionEvent, // HANDLE 174 [MarshalAs(UnmanagedType.LPArray)] byte[] printablePagesOn, 175 UInt32 printablePagesOnCount, 176 out IXpsPrintJob xpsPrintJob, 177 out IXpsPrintJobStream documentStream, 178 IntPtr printTicketStream); // This is actually "out IXpsPrintJobStream", but we don't use it and just want to pass null, hence IntPtr. 179 180 [DllImport("Kernel32.dll", SetLastError = true)] 181 private static extern IntPtr CreateEvent(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName); 182 183 [DllImport("Kernel32.dll", SetLastError = true, ExactSpelling = true)] 184 private static extern WAIT_RESULT WaitForSingleObject(IntPtr handle, Int32 milliseconds); 185 186 [DllImport("Kernel32.dll", SetLastError = true)] 187 [return: MarshalAs(UnmanagedType.Bool)] 188 private static extern bool CloseHandle(IntPtr hObject); 189 } 190 191 /// <summary> 192 /// This interface definition is HACKED. 193 /// 194 /// It appears that the IID for IXpsPrintJobStream specified in XpsPrint.h as 195 /// MIDL_INTERFACE("7a77dc5f-45d6-4dff-9307-d8cb846347ca") is not correct and the RCW cannot return it. 196 /// But the returned object returns the parent ISequentialStream inteface successfully. 197 /// 198 /// So the hack is that we obtain the ISequentialStream interface but work with it as 199 /// with the IXpsPrintJobStream interface. 200 /// </summary> 201 [Guid("0C733A30-2A1C-11CE-ADE5-00AA0044773D")] // This is IID of ISequenatialSteam. 202 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 203 interface IXpsPrintJobStream 204 { 205 // ISequentualStream methods. 206 void Read([MarshalAs(UnmanagedType.LPArray)] byte[] pv, uint cb, out uint pcbRead); 207 void Write([MarshalAs(UnmanagedType.LPArray)] byte[] pv, uint cb, out uint pcbWritten); 208 // IXpsPrintJobStream methods. 209 void Close(); 210 } 211 212 [Guid("5ab89b06-8194-425f-ab3b-d7a96e350161")] 213 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 214 interface IXpsPrintJob 215 { 216 void Cancel(); 217 void GetJobStatus(out XPS_JOB_STATUS jobStatus); 218 } 219 220 [StructLayout(LayoutKind.Sequential)] 221 struct XPS_JOB_STATUS 222 { 223 public UInt32 jobId; 224 public Int32 currentDocument; 225 public Int32 currentPage; 226 public Int32 currentPageTotal; 227 public XPS_JOB_COMPLETION completion; 228 public Int32 jobStatus; // UInt32 229 }; 230 231 enum XPS_JOB_COMPLETION 232 { 233 XPS_JOB_IN_PROGRESS = 0, 234 XPS_JOB_COMPLETED = 1, 235 XPS_JOB_CANCELLED = 2, 236 XPS_JOB_FAILED = 3 237 } 238 239 enum WAIT_RESULT 240 { 241 WAIT_OBJECT_0 = 0, 242 WAIT_ABANDONED = 0x80, 243 WAIT_TIMEOUT = 0x102, 244 WAIT_FAILED = -1 // 0xFFFFFFFF 245 } 246 }XpsPrintHelper
到此,基於windows服務的列印已經解決。
就只有模板編輯器的事情了。
對於原來做過基於Word的郵件合併域的經驗。自己開發一個編輯器來說工程量有點大
所以選擇了一個現有的,功能又強大的文件編輯器。Word來做為我的標籤編輯器了。
Word可以完美的解決紙張,格式,位置等問題。只是在對應的地方用“文字域”來做佔位符
然後用自定義的資料填充就可以了。
下圖為Word模板編輯
編輯佔位符(域)
這樣的話。一個模板就出來了
如果是圖片的話。就在域名前加Image:
如果是表格的話。在表格的開始加上TableStart:表名
在表格的未尾加上TableEnd:表名
協議的話。走的是所有語言都支援的http,對於以後開發SDK也方便
對於上面的模板,只要傳送這樣的請球POST
對於Get請求
然後打印出來的效果
到此,列印3.0已經完成。
關鍵程式碼
根據請求資料生成列印實體
1 private PrintModel GetPrintModel(HttpListenerRequest request) 2 { 3 var result = new PrintModel(); 4 result.PrintName = ConfigurationManager.AppSettings["PrintName"]; 5 result.Template = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Template", "Default.docx"); 6 result.Action = PrintActionType.Print; 7 8 var query = request.Url.Query; 9 var dicQuery = this.ToNameValueDictionary(query); 10 if (dicQuery.ContainsKey("PrintName")) result.PrintName = dicQuery["PrintName"]; 11 if (dicQuery.ContainsKey("Copies")) result.Copies = int.Parse(dicQuery["Copies"]); 12 if (dicQuery.ContainsKey("Template")) 13 { 14 var tempPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Template", dicQuery["Template"]); 15 if (File.Exists(tempPath)) 16 result.Template = tempPath; 17 } 18 if (dicQuery.ContainsKey("Action")) result.Action = (PrintActionType)Enum.Parse(typeof(PrintActionType), dicQuery["Action"]); 19 20 foreach (var item in dicQuery) 21 { 22 if (item.Key.StartsWith("Image:")) 23 { 24 var keyName = item.Key.Replace("Image:", ""); 25 if (result.ImageContent.ContainsKey(keyName)) continue; 26 var imageModel = item.Value.ToObject<ImageContentModel>(); 27 result.ImageContent.Add(keyName, imageModel); 28 continue; 29 } 30 if (item.Key.StartsWith("Table:")) 31 { 32 var keyName = item.Key.Replace("Table:", ""); 33 if (result.TableContent.ContainsKey(keyName)) continue; 34 var table = item.Value.ToObject<DataTable>(); 35 table.TableName = keyName; 36 result.TableContent.Add(keyName, table); 37 continue; 38 } 39 if (result.FieldCotent.ContainsKey(item.Key)) continue; 40 result.FieldCotent.Add(item.Key, item.Value); 41 } 42 43 if (request.HttpMethod.Equals("POST", StringComparison.CurrentCultureIgnoreCase)) 44 { 45 var body = request.InputStream; 46 var encoding = Encoding.UTF8; 47 var reader = new StreamReader(body, encoding); 48 var bodyContent = reader.ReadToEnd(); 49 var bodyModel = bodyContent.ToObject<Dictionary<string, object>>(); 50 foreach (var item in bodyModel) 51 { 52 if (item.Key.StartsWith("Image:")) 53 { 54 var imageModel = item.Value.ToJson().ToObject<ImageContentModel>(); 55 var keyName = item.Key.Replace("Image:", ""); 56 if (result.ImageContent.ContainsKey(keyName)) 57 result.ImageContent[keyName] = imageModel; 58 else 59 result.ImageContent.Add(keyName, imageModel); 60 continue; 61 } 62 if (item.Key.StartsWith("Table:")) 63 { 64 var table = item.Value.ToJson().ToObject<DataTable>(); 65 var keyName = item.Key.Replace("Table:", ""); 66 table.TableName = keyName; 67 if (result.TableContent.ContainsKey(keyName)) 68 result.TableContent[keyName] = table; 69 else 70 result.TableContent.Add(keyName, table); 71 continue; 72 } 73 if (result.FieldCotent.ContainsKey(item.Key)) 74 result.FieldCotent[item.Key] = HttpUtility.UrlDecode(item.Value.ToString()); 75 else 76 result.FieldCotent.Add(item.Key, HttpUtility.UrlDecode(item.Value.ToString())); 77 } 78 } 79 return result; 80 }GetPrintModel
文件郵件合併域
1 public class MergeDocument : IDisposable 2 { 3 public PrintModel Model { get; set; } 4 public Document Doc { get; set; } 5 private PrintFieldMergingCallback FieldCallback { get; set; } 6 public MergeDocument(PrintModel model) 7 { 8 this.Model = model; 9 this.Doc = new Document(model.Template); 10 this.FieldCallback = new PrintFieldMergingCallback(this.Model); 11 this.Doc.MailMerge.FieldMergingCallback = this.FieldCallback; 12 } 13 public Stream MergeToStream() 14 { 15 if (this.Model.FieldCotent.Count > 0) 16 this.Doc.MailMerge.Execute(this.Model.FieldCotent.Keys.ToArray(), this.Model.FieldCotent.Values.ToArray()); 17 if (this.Model.ImageContent.Count > 0) 18 { 19 this.Doc.MailMerge.Execute(this.Model.ImageContent.Keys.ToArray(), this.Model.ImageContent.Values.Select(s => s.Value).ToArray()); 20 }; 21 if (this.Model.TableContent.Count > 0) 22 { 23 foreach (var item in this.Model.TableContent) 24 { 25 var table = item.Value; 26 table.TableName = item.Key; 27 this.Doc.MailMerge.ExecuteWithRegions(table); 28 } 29 } 30 this.Doc.UpdateFields(); 31 32 var fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PrintDoc", $"{DateTime.Now.ToString("yyMMddHHmmssfff")}.docx"); 33 var ms = new MemoryStream(); 34 this.Doc.Save(ms, SaveFormat.Xps); 35 return ms; 36 } 37 38 public void Dispose() 39 { 40 this.FieldCallback.Dispose(); 41 } 42 43 private class PrintFieldMergingCallback : IFieldMergingCallback, IDisposable 44 { 45 public HttpClient Client { get; set; } 46 public PrintModel Model { get; set; } 47 public PrintFieldMergingCallback(PrintModel model) 48 { 49 this.Model = model; 50 this.Client = new HttpClient(); 51 } 52 public void FieldMerging(FieldMergingArgs args) 53 { 54 } 55 56 public void ImageFieldMerging(ImageFieldMergingArgs field) 57 { 58 var fieldName = field.FieldName; 59 if (!this.Model.ImageContent.ContainsKey(fieldName)) return; 60 var imageModel = this.Model.ImageContent[fieldName]; 61 switch (imageModel.Type) 62 { 63 case ImageType.Local: 64 { 65 field.Image = Image.FromFile(imageModel.Value); 66 field.ImageWidth = new MergeFieldImageDimension(imageModel.Width); 67 field.ImageHeight = new MergeFieldImageDimension(imageModel.Height); 68 }; 69 break; 70 case ImageType.Network: 71 { 72 var imageStream = this.Client.GetStreamAsync(imageModel.Value).Result; 73 var ms = new MemoryStream(); 74 imageStream.CopyTo(ms); 75 ms.Position = 0; 76 field.ImageStream = ms; 77 field.ImageWidth = new MergeFieldImageDimension(imageModel.Width); 78 field.ImageHeight = new MergeFieldImageDimension(imageModel.Height); 79 }; break; 80 case ImageType.BarCode: 81 { 82 var barImage = this.GenerateImage(BarcodeFormat.CODE_128, imageModel.Value, imageModel.Width, imageModel.Height); 83 field.Image = barImage; 84 }; break; 85 case ImageType.QRCode: 86 { 87 var qrImage = this.GenerateImage(BarcodeFormat.QR_CODE, imageModel.Value, imageModel.Width, imageModel.Height); 88 field.Image = qrImage; 89 }; break; 90 default: break; 91 } 92 } 93 private Bitmap GenerateImage(BarcodeFormat format, string code, int width, int height) 94 { 95 var writer = new BarcodeWriter(); 96 writer.Format = format; 97 EncodingOptions options = new EncodingOptions() 98 { 99 Width = width, 100 Height = height, 101 Margin = 2, 102 PureBarcode = false 103 }; 104 writer.Options = options; 105 if (format == BarcodeFormat.QR_CODE) 106 { 107 var qrOption = new QrCodeEncodingOptions() 108 { 109 DisableECI = true, 110 CharacterSet = "UTF-8", 111 Width = width, 112 Height = height, 113 Margin = 2 114 }; 115 writer.Options = qrOption; 116 } 117 var codeimg = writer.Write(code); 118 return codeimg; 119 } 120 121 public void Dispose() 122 { 123 this.Client.Dispose(); 124 } 125 } 126 }MergeDocument