基于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 }
到此,基于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 }
文档邮件合并域
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 }