最近做了几个项目,都有在产品贴标的需求
基本就是有个证卡类打印机,然后把产品的信息打印在标签上。
然后通过机器人把标签贴到产品上面
标签信息包括文本,二维码,条形码之类的,要根据对应的数据生成二维码,条形码。
打印标签的需求接到手后,开始了我的填坑之旅。
打印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 /// 4 /// A utility class that converts a document to XPS using Aspose.Words and then sends to the XpsPrint API. 5 /// 6 public class XpsPrintHelper 7 { 8 /// 9 /// No ctor. 10 /// 11 private XpsPrintHelper() 12 { 13 } 14 15 // ExStart:XpsPrint_PrintDocument 16 // ExSummary:Convert an Aspose.Words document into an XPS stream and print. 17 /// 18 /// Sends an Aspose.Words document to a printer using the XpsPrint API. 19 /// 20 /// 21 /// 22 /// Job name. Can be null. 23 /// True to wait for the job to complete. False to return immediately after submitting the job. 24 /// Thrown if any error occurs. 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 /// 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 /// 50 /// 51 /// 52 /// Job name. Can be null. 53 /// True to wait for the job to complete. False to return immediately after submitting the job. 54 /// Thrown if any error occurs. 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 // try110 // {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, // HANDLE173 IntPtr completionEvent, // HANDLE174 [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 /// 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 /// 201 [Guid("0C733A30-2A1C-11CE-ADE5-00AA0044773D")] // This is IID of ISequenatialSteam.202 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]203 interface IXpsPrintJobStream204 {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 IXpsPrintJob215 {216 void Cancel();217 void GetJobStatus(out XPS_JOB_STATUS jobStatus);218 }219 220 [StructLayout(LayoutKind.Sequential)]221 struct XPS_JOB_STATUS222 {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; // UInt32229 };230 231 enum XPS_JOB_COMPLETION232 {233 XPS_JOB_IN_PROGRESS = 0,234 XPS_JOB_COMPLETED = 1,235 XPS_JOB_CANCELLED = 2,236 XPS_JOB_FAILED = 3237 }238 239 enum WAIT_RESULT240 {241 WAIT_OBJECT_0 = 0,242 WAIT_ABANDONED = 0x80,243 WAIT_TIMEOUT = 0x102,244 WAIT_FAILED = -1 // 0xFFFFFFFF245 }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();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();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();55 var keyName = item.Key.Replace("Image:", "");56 if (result.ImageContent.ContainsKey(keyName))57 result.ImageContent[keyName] = imageModel;58 else59 result.ImageContent.Add(keyName, imageModel);60 continue;61 }62 if (item.Key.StartsWith("Table:"))63 {64 var table = item.Value.ToJson().ToObject();65 var keyName = item.Key.Replace("Table:", "");66 table.TableName = keyName;67 if (result.TableContent.ContainsKey(keyName))68 result.TableContent[keyName] = table;69 else70 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 else76 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 = false103 };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 = 2114 };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