最近做了几个项目,都有在产品贴标的需求

基本就是有个证卡类打印机,然后把产品的信息打印在标签上。

然后通过机器人把标签贴到产品上面

标签信息包括文本,二维码,条形码之类的,要根据对应的数据生成二维码,条形码。

打印标签的需求接到手后,开始了我的填坑之旅。

打印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服务里面运行关打印。

 namespace ZEQP.Print.Framework
{
/// <summary>
/// A utility class that converts a document to XPS using Aspose.Words and then sends to the XpsPrint API.
/// </summary>
public class XpsPrintHelper
{
/// <summary>
/// No ctor.
/// </summary>
private XpsPrintHelper()
{
} // ExStart:XpsPrint_PrintDocument
// ExSummary:Convert an Aspose.Words document into an XPS stream and print.
/// <summary>
/// Sends an Aspose.Words document to a printer using the XpsPrint API.
/// </summary>
/// <param name="document"></param>
/// <param name="printerName"></param>
/// <param name="jobName">Job name. Can be null.</param>
/// <param name="isWait">True to wait for the job to complete. False to return immediately after submitting the job.</param>
/// <exception cref="Exception">Thrown if any error occurs.</exception>
public static void Print(string xpsFile, string printerName, string jobName, bool isWait)
{
Console.WriteLine("Print");
if (!File.Exists(xpsFile))
throw new ArgumentNullException("xpsFile");
using (var stream = File.OpenRead(xpsFile))
{
Print(stream, printerName, jobName, isWait);
}
//// Use Aspose.Words to convert the document to XPS and store in a memory stream.
//File.OpenRead
//MemoryStream stream = new MemoryStream(); //stream.Position = 0;
//Console.WriteLine("Saved as Xps");
//Print(stream, printerName, jobName, isWait);
Console.WriteLine("After Print");
}
// ExEnd:XpsPrint_PrintDocument
// ExStart:XpsPrint_PrintStream
// ExSummary:Prints an XPS document using the XpsPrint API.
/// <summary>
/// Sends a stream that contains a document in the XPS format to a printer using the XpsPrint API.
/// Has no dependency on Aspose.Words, can be used in any project.
/// </summary>
/// <param name="stream"></param>
/// <param name="printerName"></param>
/// <param name="jobName">Job name. Can be null.</param>
/// <param name="isWait">True to wait for the job to complete. False to return immediately after submitting the job.</param>
/// <exception cref="Exception">Thrown if any error occurs.</exception>
public static void Print(Stream stream, string printerName, string jobName, bool isWait)
{
if (stream == null)
throw new ArgumentNullException("stream");
if (printerName == null)
throw new ArgumentNullException("printerName"); // Create an event that we will wait on until the job is complete.
IntPtr completionEvent = CreateEvent(IntPtr.Zero, true, false, null);
if (completionEvent == IntPtr.Zero)
throw new Win32Exception(); // try
// {
IXpsPrintJob job;
IXpsPrintJobStream jobStream;
Console.WriteLine("StartJob");
StartJob(printerName, jobName, completionEvent, out job, out jobStream);
Console.WriteLine("Done StartJob");
Console.WriteLine("Start CopyJob");
CopyJob(stream, job, jobStream);
Console.WriteLine("End CopyJob"); Console.WriteLine("Start Wait");
if (isWait)
{
WaitForJob(completionEvent);
CheckJobStatus(job);
}
Console.WriteLine("End Wait");
/* }
finally
{
if (completionEvent != IntPtr.Zero)
CloseHandle(completionEvent);
}
*/
if (completionEvent != IntPtr.Zero)
CloseHandle(completionEvent);
Console.WriteLine("Close Handle");
}
// ExEnd:XpsPrint_PrintStream private static void StartJob(string printerName, string jobName, IntPtr completionEvent, out IXpsPrintJob job, out IXpsPrintJobStream jobStream)
{
int result = StartXpsPrintJob(printerName, jobName, null, IntPtr.Zero, completionEvent,
null, , out job, out jobStream, IntPtr.Zero);
if (result != )
throw new Win32Exception(result);
} private static void CopyJob(Stream stream, IXpsPrintJob job, IXpsPrintJobStream jobStream)
{ // try
// {
byte[] buff = new byte[];
while (true)
{
uint read = (uint)stream.Read(buff, , buff.Length);
if (read == )
break; uint written;
jobStream.Write(buff, read, out written); if (read != written)
throw new Exception("Failed to copy data to the print job stream.");
} // Indicate that the entire document has been copied.
jobStream.Close();
// }
// catch (Exception)
// {
// // Cancel the job if we had any trouble submitting it.
// job.Cancel();
// throw;
// }
} private static void WaitForJob(IntPtr completionEvent)
{
const int INFINITE = -;
switch (WaitForSingleObject(completionEvent, INFINITE))
{
case WAIT_RESULT.WAIT_OBJECT_0:
// Expected result, do nothing.
break;
case WAIT_RESULT.WAIT_FAILED:
throw new Win32Exception();
default:
throw new Exception("Unexpected result when waiting for the print job.");
}
} private static void CheckJobStatus(IXpsPrintJob job)
{
XPS_JOB_STATUS jobStatus;
job.GetJobStatus(out jobStatus);
switch (jobStatus.completion)
{
case XPS_JOB_COMPLETION.XPS_JOB_COMPLETED:
// Expected result, do nothing.
break;
case XPS_JOB_COMPLETION.XPS_JOB_FAILED:
throw new Win32Exception(jobStatus.jobStatus);
default:
throw new Exception("Unexpected print job status.");
}
} [DllImport("XpsPrint.dll", EntryPoint = "StartXpsPrintJob")]
private static extern int StartXpsPrintJob(
[MarshalAs(UnmanagedType.LPWStr)] String printerName,
[MarshalAs(UnmanagedType.LPWStr)] String jobName,
[MarshalAs(UnmanagedType.LPWStr)] String outputFileName,
IntPtr progressEvent, // HANDLE
IntPtr completionEvent, // HANDLE
[MarshalAs(UnmanagedType.LPArray)] byte[] printablePagesOn,
UInt32 printablePagesOnCount,
out IXpsPrintJob xpsPrintJob,
out IXpsPrintJobStream documentStream,
IntPtr printTicketStream); // This is actually "out IXpsPrintJobStream", but we don't use it and just want to pass null, hence IntPtr. [DllImport("Kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateEvent(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName); [DllImport("Kernel32.dll", SetLastError = true, ExactSpelling = true)]
private static extern WAIT_RESULT WaitForSingleObject(IntPtr handle, Int32 milliseconds); [DllImport("Kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
} /// <summary>
/// This interface definition is HACKED.
///
/// It appears that the IID for IXpsPrintJobStream specified in XpsPrint.h as
/// MIDL_INTERFACE("7a77dc5f-45d6-4dff-9307-d8cb846347ca") is not correct and the RCW cannot return it.
/// But the returned object returns the parent ISequentialStream inteface successfully.
///
/// So the hack is that we obtain the ISequentialStream interface but work with it as
/// with the IXpsPrintJobStream interface.
/// </summary>
[Guid("0C733A30-2A1C-11CE-ADE5-00AA0044773D")] // This is IID of ISequenatialSteam.
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IXpsPrintJobStream
{
// ISequentualStream methods.
void Read([MarshalAs(UnmanagedType.LPArray)] byte[] pv, uint cb, out uint pcbRead);
void Write([MarshalAs(UnmanagedType.LPArray)] byte[] pv, uint cb, out uint pcbWritten);
// IXpsPrintJobStream methods.
void Close();
} [Guid("5ab89b06-8194-425f-ab3b-d7a96e350161")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IXpsPrintJob
{
void Cancel();
void GetJobStatus(out XPS_JOB_STATUS jobStatus);
} [StructLayout(LayoutKind.Sequential)]
struct XPS_JOB_STATUS
{
public UInt32 jobId;
public Int32 currentDocument;
public Int32 currentPage;
public Int32 currentPageTotal;
public XPS_JOB_COMPLETION completion;
public Int32 jobStatus; // UInt32
}; enum XPS_JOB_COMPLETION
{
XPS_JOB_IN_PROGRESS = ,
XPS_JOB_COMPLETED = ,
XPS_JOB_CANCELLED = ,
XPS_JOB_FAILED =
} enum WAIT_RESULT
{
WAIT_OBJECT_0 = ,
WAIT_ABANDONED = 0x80,
WAIT_TIMEOUT = 0x102,
WAIT_FAILED = - // 0xFFFFFFFF
}
}

XpsPrintHelper

到此,基于windows服务的打印已经解决。

就只有模板编辑器的事情了。

对于原来做过基于Word的邮件合并域的经验。自己开发一个编辑器来说工程量有点大

所以选择了一个现有的,功能又强大的文档编辑器。Word来做为我的标签编辑器了。

Word可以完美的解决纸张,格式,位置等问题。只是在对应的地方用“文本域”来做占位符

然后用自定义的数据填充就可以了。

下图为Word模板编辑

编辑占位符(域)

这样的话。一个模板就出来了

如果是图片的话。就在域名前加Image:

如果是表格的话。在表格的开始加上TableStart:表名
在表格的未尾加上TableEnd:表名

协议的话。走的是所有语言都支持的http,对于以后开发SDK也方便

对于上面的模板,只要发送这样的请球POST

对于Get请求

然后打印出来的效果

到此,打印3.0已经完成。

关键代码
根据请求数据生成打印实体

 private PrintModel GetPrintModel(HttpListenerRequest request)
{
var result = new PrintModel();
result.PrintName = ConfigurationManager.AppSettings["PrintName"];
result.Template = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Template", "Default.docx");
result.Action = PrintActionType.Print; var query = request.Url.Query;
var dicQuery = this.ToNameValueDictionary(query);
if (dicQuery.ContainsKey("PrintName")) result.PrintName = dicQuery["PrintName"];
if (dicQuery.ContainsKey("Copies")) result.Copies = int.Parse(dicQuery["Copies"]);
if (dicQuery.ContainsKey("Template"))
{
var tempPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Template", dicQuery["Template"]);
if (File.Exists(tempPath))
result.Template = tempPath;
}
if (dicQuery.ContainsKey("Action")) result.Action = (PrintActionType)Enum.Parse(typeof(PrintActionType), dicQuery["Action"]); foreach (var item in dicQuery)
{
if (item.Key.StartsWith("Image:"))
{
var keyName = item.Key.Replace("Image:", "");
if (result.ImageContent.ContainsKey(keyName)) continue;
var imageModel = item.Value.ToObject<ImageContentModel>();
result.ImageContent.Add(keyName, imageModel);
continue;
}
if (item.Key.StartsWith("Table:"))
{
var keyName = item.Key.Replace("Table:", "");
if (result.TableContent.ContainsKey(keyName)) continue;
var table = item.Value.ToObject<DataTable>();
table.TableName = keyName;
result.TableContent.Add(keyName, table);
continue;
}
if (result.FieldCotent.ContainsKey(item.Key)) continue;
result.FieldCotent.Add(item.Key, item.Value);
} if (request.HttpMethod.Equals("POST", StringComparison.CurrentCultureIgnoreCase))
{
var body = request.InputStream;
var encoding = Encoding.UTF8;
var reader = new StreamReader(body, encoding);
var bodyContent = reader.ReadToEnd();
var bodyModel = bodyContent.ToObject<Dictionary<string, object>>();
foreach (var item in bodyModel)
{
if (item.Key.StartsWith("Image:"))
{
var imageModel = item.Value.ToJson().ToObject<ImageContentModel>();
var keyName = item.Key.Replace("Image:", "");
if (result.ImageContent.ContainsKey(keyName))
result.ImageContent[keyName] = imageModel;
else
result.ImageContent.Add(keyName, imageModel);
continue;
}
if (item.Key.StartsWith("Table:"))
{
var table = item.Value.ToJson().ToObject<DataTable>();
var keyName = item.Key.Replace("Table:", "");
table.TableName = keyName;
if (result.TableContent.ContainsKey(keyName))
result.TableContent[keyName] = table;
else
result.TableContent.Add(keyName, table);
continue;
}
if (result.FieldCotent.ContainsKey(item.Key))
result.FieldCotent[item.Key] = HttpUtility.UrlDecode(item.Value.ToString());
else
result.FieldCotent.Add(item.Key, HttpUtility.UrlDecode(item.Value.ToString()));
}
}
return result;
}

GetPrintModel

文档邮件合并域

 public class MergeDocument : IDisposable
{
public PrintModel Model { get; set; }
public Document Doc { get; set; }
private PrintFieldMergingCallback FieldCallback { get; set; }
public MergeDocument(PrintModel model)
{
this.Model = model;
this.Doc = new Document(model.Template);
this.FieldCallback = new PrintFieldMergingCallback(this.Model);
this.Doc.MailMerge.FieldMergingCallback = this.FieldCallback;
}
public Stream MergeToStream()
{
if (this.Model.FieldCotent.Count > )
this.Doc.MailMerge.Execute(this.Model.FieldCotent.Keys.ToArray(), this.Model.FieldCotent.Values.ToArray());
if (this.Model.ImageContent.Count > )
{
this.Doc.MailMerge.Execute(this.Model.ImageContent.Keys.ToArray(), this.Model.ImageContent.Values.Select(s => s.Value).ToArray());
};
if (this.Model.TableContent.Count > )
{
foreach (var item in this.Model.TableContent)
{
var table = item.Value;
table.TableName = item.Key;
this.Doc.MailMerge.ExecuteWithRegions(table);
}
}
this.Doc.UpdateFields(); var fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PrintDoc", $"{DateTime.Now.ToString("yyMMddHHmmssfff")}.docx");
var ms = new MemoryStream();
this.Doc.Save(ms, SaveFormat.Xps);
return ms;
} public void Dispose()
{
this.FieldCallback.Dispose();
} private class PrintFieldMergingCallback : IFieldMergingCallback, IDisposable
{
public HttpClient Client { get; set; }
public PrintModel Model { get; set; }
public PrintFieldMergingCallback(PrintModel model)
{
this.Model = model;
this.Client = new HttpClient();
}
public void FieldMerging(FieldMergingArgs args)
{
} public void ImageFieldMerging(ImageFieldMergingArgs field)
{
var fieldName = field.FieldName;
if (!this.Model.ImageContent.ContainsKey(fieldName)) return;
var imageModel = this.Model.ImageContent[fieldName];
switch (imageModel.Type)
{
case ImageType.Local:
{
field.Image = Image.FromFile(imageModel.Value);
field.ImageWidth = new MergeFieldImageDimension(imageModel.Width);
field.ImageHeight = new MergeFieldImageDimension(imageModel.Height);
};
break;
case ImageType.Network:
{
var imageStream = this.Client.GetStreamAsync(imageModel.Value).Result;
var ms = new MemoryStream();
imageStream.CopyTo(ms);
ms.Position = ;
field.ImageStream = ms;
field.ImageWidth = new MergeFieldImageDimension(imageModel.Width);
field.ImageHeight = new MergeFieldImageDimension(imageModel.Height);
}; break;
case ImageType.BarCode:
{
var barImage = this.GenerateImage(BarcodeFormat.CODE_128, imageModel.Value, imageModel.Width, imageModel.Height);
field.Image = barImage;
}; break;
case ImageType.QRCode:
{
var qrImage = this.GenerateImage(BarcodeFormat.QR_CODE, imageModel.Value, imageModel.Width, imageModel.Height);
field.Image = qrImage;
}; break;
default: break;
}
}
private Bitmap GenerateImage(BarcodeFormat format, string code, int width, int height)
{
var writer = new BarcodeWriter();
writer.Format = format;
EncodingOptions options = new EncodingOptions()
{
Width = width,
Height = height,
Margin = ,
PureBarcode = false
};
writer.Options = options;
if (format == BarcodeFormat.QR_CODE)
{
var qrOption = new QrCodeEncodingOptions()
{
DisableECI = true,
CharacterSet = "UTF-8",
Width = width,
Height = height,
Margin =
};
writer.Options = qrOption;
}
var codeimg = writer.Write(code);
return codeimg;
} public void Dispose()
{
this.Client.Dispose();
}
}
}

MergeDocument

 

最新文章

  1. Jquery实现一组复选框单选
  2. SNMP报文抓取与分析(二)
  3. golang——channel笔记
  4. 对比其它软件方法评估敏捷和Scrum
  5. 洛谷P2751 [USACO4.2]工序安排Job Processing
  6. 读Flask源代码学习Python--config原理
  7. Redis 提供的好的解决方案 实例
  8. Linux 高性能服务器编程——高级I/O函数
  9. Apache启动不了httpd: apr_sockaddr_info_get() failed xgp
  10. js canvas获取图片base64 dataUrl
  11. golang for 循环变量取内存地址
  12. [转帖]技术盛宴 | 关于PoE以太网供电技术详解
  13. 第13月第16天 ios keywindow
  14. Python 高性能并行计算之 mpi4py
  15. 无法设置主体sa的凭据
  16. python x[:] x[::]用法总结
  17. 【BZOJ3769】BST again [DP]
  18. Laravel 集合的处理
  19. Fiddler 发送post 请求失败
  20. ES02 变量、数组、对象、方法

热门文章

  1. MAC配置JAVA环境变量
  2. nginx篇高级用法之基于TCP/UDP的四层调度
  3. Mybaits 源码解析 (十)----- 全网最详细,没有之一:Spring-Mybatis框架使用与源码解析
  4. python学习之【第四篇】:Python中的列表及其所具有的方法
  5. 【algo&amp;ds】0.数据结构和算法入门
  6. tensorflow中的学习率调整策略
  7. linux下制作linux系统盘(光盘、U盘)
  8. SQLite性能 - 它不是内存数据库,不要对IN-MEMORY望文生意。
  9. websocket可以做什么
  10. zip的压缩和解压命令