三年来做制造行业,差不多做了近30个工厂,也由纯软件转入到了大量的硬件对接,包含厂房设计(这个目前还只是小菜鸟),硬件设计(只是提提意见),安装实施调试(软件和硬件撕逼操作),当然面向的对象也由计算机转向了和各种人员的对接,一直想做一个牛逼的技术人员,可是天不如人愿只能游走于各种琐事中,也很少写技术相关的文章了,今天分享一个小小的应用点《打印标签》
一 标签打印
在制造行业中,在仓库物流行业中标签打印是非常常用的需求,标签除了标识物体之外,我们还有一个比较重要的应用 就是在标签上附上 条码 或 二维码, 特别是二维码目前在我们生活中无处不在。 而标签中使用 条码或二维码,其实就是为了加快物体的识别效率。 当然市面上有很多打印的标签软件,各个打印机厂商也都有自带打印软件,而打印机厂商自带的软件大部分都是CS结构的程序,而且无法和BS网页交互。、
以上标签在生产制造行业中比较常见,除了以上在原料,生产过程中使用,还有成品仓库包装,物流发货等,当然还有一些特殊的情况比如 包装外箱等使用喷码机来喷码的情况,这种并不是使用打印来打印的。标签的应用很多,但是有一点不同的就是标签纸张规格大小的不一样,以及经常变动的表现形式。
二 为什么不使用浏览器打印
浏览器也是带有打印功能的,一般比较常用使用浏览器打印都是打印A4,A5 等这种比较规整的纸张,当然浏览器也能够打印各种小标签。但是浏览器打印有一个不好的就是清晰度不够,至于为什么清晰度不够这个我也说不清楚,然后在连续打印等方面都比较弱,可能很多人会问我们也经常见到连续打印的,一般这种打印都是使用浏览器插件或者和打印客户端相连接的,这也是在《吉特日化MES系统》中应用的方式。
另外目前市面上见到的浏览器打印客户端大部分都是收费的,虽然很多费用还不低,另外就是绑架销售,比如很多打印都是集成在报表组件里面的,目前的报表组件随随便便一个授权就是几十万,这也是客户无法接受的,我们自己做项目也无法接受。
三 自己开发的打印组件
其实算不上自己开发的打印组件,只是基于.NET 的documentprint 打印类做了一定的封装,在之间的文字中有分享过。具体可以看看如下文章
当然也还有一些其他的打印案例的分享,有兴趣的可以翻阅一下以前的文章。
关于自己的打印组件源代码如下: https://github.com/hechenqingyuan/gitprint
四 用网页打印怎么办
上位系统的开发使用BS架构,这也就必然遇到了打印头疼的地方,如何打印标签。其实思路很简单,那就是网页出发打印指令,然后发送到客户单软件来打印,这是我这边的一个基本思路。
如图是一个打印标签的操作界面,就目前而言操作还是相对比较方便了,要想达到如上效果要解决如下几个问题:
(1) 读取本地连接的打印机(客户端获取)
public class DBPaperSize { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] internal struct structPrinterDefaults { [MarshalAs(UnmanagedType.LPTStr)] public String pDatatype; public IntPtr pDevMode; [MarshalAs(UnmanagedType.I4)] public int DesiredAccess; }; [DllImport("winspool.Drv", EntryPoint = "OpenPrinter", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = false, CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurityAttribute()] internal static extern bool OpenPrinter([MarshalAs(UnmanagedType.LPTStr)]string printerName, out IntPtr phPrinter, ref structPrinterDefaults pd); [DllImport("winspool.Drv", EntryPoint = "ClosePrinter", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = false, CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurityAttribute()] internal static extern bool ClosePrinter(IntPtr phPrinter); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] internal struct structSize { public Int32 width; public Int32 height; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] internal struct structRect { public Int32 left; public Int32 top; public Int32 right; public Int32 bottom; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] internal struct structFormInfo1 { [MarshalAs(UnmanagedType.I4)] public int Flags; [MarshalAs(UnmanagedType.LPTStr)] public String pName; public structSize Size; public structRect ImageableArea; }; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] internal struct structDevMode { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public String dmDeviceName; [MarshalAs(UnmanagedType.U2)] public short dmSpecVersion; [MarshalAs(UnmanagedType.U2)] public short dmDriverVersion; [MarshalAs(UnmanagedType.U2)] public short dmSize; [MarshalAs(UnmanagedType.U2)] public short dmDriverExtra; [MarshalAs(UnmanagedType.U4)] public int dmFields; [MarshalAs(UnmanagedType.I2)] public short dmOrientation; [MarshalAs(UnmanagedType.I2)] public short dmPaperSize; [MarshalAs(UnmanagedType.I2)] public short dmPaperLength; [MarshalAs(UnmanagedType.I2)] public short dmPaperWidth; [MarshalAs(UnmanagedType.I2)] public short dmScale; [MarshalAs(UnmanagedType.I2)] public short dmCopies; [MarshalAs(UnmanagedType.I2)] public short dmDefaultSource; [MarshalAs(UnmanagedType.I2)] public short dmPrintQuality; [MarshalAs(UnmanagedType.I2)] public short dmColor; [MarshalAs(UnmanagedType.I2)] public short dmDuplex; [MarshalAs(UnmanagedType.I2)] public short dmYResolution; [MarshalAs(UnmanagedType.I2)] public short dmTTOption; [MarshalAs(UnmanagedType.I2)] public short dmCollate; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public String dmFormName; [MarshalAs(UnmanagedType.U2)] public short dmLogPixels; [MarshalAs(UnmanagedType.U4)] public int dmBitsPerPel; [MarshalAs(UnmanagedType.U4)] public int dmPelsWidth; [MarshalAs(UnmanagedType.U4)] public int dmPelsHeight; [MarshalAs(UnmanagedType.U4)] public int dmNup; [MarshalAs(UnmanagedType.U4)] public int dmDisplayFrequency; [MarshalAs(UnmanagedType.U4)] public int dmICMMethod; [MarshalAs(UnmanagedType.U4)] public int dmICMIntent; [MarshalAs(UnmanagedType.U4)] public int dmMediaType; [MarshalAs(UnmanagedType.U4)] public int dmDitherType; [MarshalAs(UnmanagedType.U4)] public int dmReserved1; [MarshalAs(UnmanagedType.U4)] public int dmReserved2; } [DllImport("winspool.Drv", EntryPoint = "AddForm", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = false, CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurityAttribute()] internal static extern bool AddForm(IntPtr phPrinter, [MarshalAs(UnmanagedType.I4)] int level, ref structFormInfo1 form); [DllImport("winspool.Drv", EntryPoint = "DeleteForm", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = false, CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurityAttribute()] internal static extern bool DeleteForm(IntPtr phPrinter, [MarshalAs(UnmanagedType.LPTStr)] string pName); [DllImport("winspool.Drv", EntryPoint = "SetForm", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = false, CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurityAttribute()] internal static extern bool SetForm(IntPtr phPrinter, [MarshalAs(UnmanagedType.LPTStr)] string pName, [MarshalAs(UnmanagedType.I4)] int level, ref structFormInfo1 form); [DllImport("kernel32.dll", EntryPoint = "GetLastError", SetLastError = false, ExactSpelling = true, CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurityAttribute()] internal static extern Int32 GetLastError(); [DllImport("GDI32.dll", EntryPoint = "CreateDC", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = false, CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurityAttribute()] internal static extern IntPtr CreateDC([MarshalAs(UnmanagedType.LPTStr)]string pDrive, [MarshalAs(UnmanagedType.LPTStr)] string pName, [MarshalAs(UnmanagedType.LPTStr)] string pOutput, ref structDevMode pDevMode); [DllImport("GDI32.dll", EntryPoint = "ResetDC", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = false, CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurityAttribute()] internal static extern IntPtr ResetDC(IntPtr hDC, ref structDevMode pDevMode); [DllImport("GDI32.dll", EntryPoint = "DeleteDC", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = false, CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurityAttribute()] internal static extern bool DeleteDC(IntPtr hDC); public System.Drawing.Printing.PaperSize GetPrintForm(string printerName, string paperName) { System.Drawing.Printing.PaperSize paper = null; System.Drawing.Printing.PrinterSettings printer = new System.Drawing.Printing.PrinterSettings(); printer.PrinterName = printerName; foreach (System.Drawing.Printing.PaperSize ps in printer.PaperSizes) { if (ps.PaperName.ToLower() == paperName.ToLower()) { paper = ps; break; } } return paper; } public void SetPrintForm(string printerName, string paperName, float width, float height) { if (PlatformID.Win32NT == Environment.OSVersion.Platform) { const int PRINTER_ACCESS_USE = 0x00000008; const int PRINTER_ACCESS_ADMINISTER = 0x00000004; structPrinterDefaults defaults = new structPrinterDefaults(); defaults.pDatatype = null; defaults.pDevMode = IntPtr.Zero; defaults.DesiredAccess = PRINTER_ACCESS_ADMINISTER | PRINTER_ACCESS_USE; IntPtr hPrinter = IntPtr.Zero; if (OpenPrinter(printerName, out hPrinter, ref defaults)) { try { structFormInfo1 formInfo = new structFormInfo1(); formInfo.Flags = 0; formInfo.pName = paperName; formInfo.Size.width = (int)(width * 100.0); formInfo.Size.height = (int)(height * 100.0); formInfo.ImageableArea.left = 0; formInfo.ImageableArea.right = formInfo.Size.width; formInfo.ImageableArea.top = 0; formInfo.ImageableArea.bottom = formInfo.Size.height; bool rslt = false; if (GetPrintForm(printerName, paperName) != null) { rslt = SetForm(hPrinter, paperName, 1, ref formInfo); } else { this.AddCustomPaperSize(printerName, paperName, width, height); rslt = true; } if (!rslt) { StringBuilder strBuilder = new StringBuilder(); strBuilder.AppendFormat("添加纸张{0}时发生错误!, 系统错误号: {1}", paperName, GetLastError()); MessageBox.Show(strBuilder.ToString()); } } finally { ClosePrinter(hPrinter); } } } } public void AddCustomPaperSize(string printerName, string paperName, float width, float height) { if (PlatformID.Win32NT == Environment.OSVersion.Platform) { const int PRINTER_ACCESS_USE = 0x00000008; const int PRINTER_ACCESS_ADMINISTER = 0x00000004; structPrinterDefaults defaults = new structPrinterDefaults(); defaults.pDatatype = null; defaults.pDevMode = IntPtr.Zero; defaults.DesiredAccess = PRINTER_ACCESS_ADMINISTER | PRINTER_ACCESS_USE; IntPtr hPrinter = IntPtr.Zero; if (OpenPrinter(printerName, out hPrinter, ref defaults)) { try { DeleteForm(hPrinter, paperName); structFormInfo1 formInfo = new structFormInfo1(); formInfo.Flags = 0; formInfo.pName = paperName; formInfo.Size.width = (int)(width * 100.0); formInfo.Size.height = (int)(height * 100.0); formInfo.ImageableArea.left = 0; formInfo.ImageableArea.right = formInfo.Size.width; formInfo.ImageableArea.top = 0; formInfo.ImageableArea.bottom = formInfo.Size.height; if (!AddForm(hPrinter, 1, ref formInfo)) { StringBuilder strBuilder = new StringBuilder(); strBuilder.AppendFormat("添加纸张{0}时发生错误!, 系统错误号: {1}", paperName, GetLastError()); throw new ApplicationException(strBuilder.ToString()); } } finally { ClosePrinter(hPrinter); } } else { StringBuilder strBuilder = new StringBuilder(); strBuilder.AppendFormat("打开打印机{0} 时出现异常!, 系统错误号: {1}", printerName, GetLastError()); throw new ApplicationException(strBuilder.ToString()); } } else { structDevMode pDevMode = new structDevMode(); IntPtr hDC = CreateDC(null, printerName, null, ref pDevMode); if (hDC != IntPtr.Zero) { const long DM_PAPERSIZE = 0x00000002L; const long DM_PAPERLENGTH = 0x00000004L; const long DM_PAPERWIDTH = 0x00000008L; pDevMode.dmFields = (int)(DM_PAPERSIZE | DM_PAPERWIDTH | DM_PAPERLENGTH); pDevMode.dmPaperSize = 256; pDevMode.dmPaperWidth = (short)(width * 2.54 * 10000.0); pDevMode.dmPaperLength = (short)(height * 2.54 * 10000.0); ResetDC(hDC, ref pDevMode); DeleteDC(hDC); } } } [DllImport("Winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] private static extern bool SetDefaultPrinter(string printerName); [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] private static extern bool GetDefaultPrinter(StringBuilder pszBuffer, ref int pcchBuffer); public static bool SetDefaultPrint(string printName) { return SetDefaultPrinter(printName); } /// <summary> /// 获取所有打印机名称 /// </summary> /// <returns></returns> public List<string> GetLocalPrinters() { List<string> fPrinters = new List<string>(); string defaultprint = DBPaperSize.GetDefaultPrinter(); if (!string.IsNullOrEmpty(defaultprint)) { fPrinters.Add(defaultprint); } foreach (string fPrinterName in PrinterSettings.InstalledPrinters) { if (!fPrinters.Contains(fPrinterName)) fPrinters.Add(fPrinterName); } //fPrinters = fPrinters.Where(item => item.ToLower().Contains("zdesigner")).ToList(); return fPrinters; } /// <summary> /// 获取默认的打印机 /// </summary> /// <returns></returns> public static string GetDefaultPrinter() { const int ERROR_FILE_NOT_FOUND = 2; const int ERROR_INSUFFICIENT_BUFFER = 122; int pcchBuffer = 0; if (GetDefaultPrinter(null, ref pcchBuffer)) { return null; } int lastWin32Error = Marshal.GetLastWin32Error(); if (lastWin32Error == ERROR_INSUFFICIENT_BUFFER) { StringBuilder pszBuffer = new StringBuilder(pcchBuffer); if (GetDefaultPrinter(pszBuffer, ref pcchBuffer)) { return pszBuffer.ToString(); } lastWin32Error = Marshal.GetLastWin32Error(); } if (lastWin32Error == ERROR_FILE_NOT_FOUND) { return null; } throw new Win32Exception(Marshal.GetLastWin32Error()); } }
(2) 网页获取本地连接打印机
其实这个比较简单,就是将客户端启动时候,获取本地连接的打印机,然后发送到服务端,然后服务端打开选择打印机时获取本地注册的打印机。注册的打印机信息使用Redis缓存,为什么使用Redis 是有用处的.
(3)将网页发送的执行传递给打印客户端
其实我们知道Redis除了缓存功能之外,还有简单的订阅发布的功能,将消息传递给打印客户端我们暂时就是使用的Redis,因为属于小量的消息发布订阅个人觉得Redis还比较合适,但是问题也会有,也有很多其他的方式解决。
(4) 打印机的上线和下线
这个其实就是取决于打印客户端是否启动了,其实是否真正连接到打印其实还是打印客户端说了算。当打印客户端启动的时候,将本地打印机信息发送到服务端,关闭的时候发送请求注销服务端缓存的打印机信息。所以为了能够完整的实现打印,打印客户端必须启动,这也是我们在实施过程中很难搞的部分,因为要打开浏览器又要打开客户端(目前我们的办法是设置开机自动启动打印客户端),客户觉得很麻烦。
另外Redis 掉线的问题,必须考虑重连,否则就无法接受到推送的信息,另外重复连接又导致消息订阅重复,导致一个指令可以打印多张标签出来。
五 将打印客户端注册成本地服务
上面说的这种方式是我们用的比较多的一种方式打印,但是有点就是如果有多个打印机站点,我选择的时候可以看到全工厂所有连接的打印机,这个选择就很头疼了。于是就出现了本机网页只调用本地的打印机服务。
这个时候我们就需要将本地的打印服务发布成一个可调用的http服务,IP地址指向127.0.0.1 ,由JS调用本地服务打印,至于如何转化为http服务,可以参考文章
<<吉特日化MES&WMS系统--三色灯控制协议转http>>
六 WebSocket 解决通讯问题
前面也提到了,解决问题的思路和方式都比较简单,也比较固话,只要解决操作端和打印机之间的通讯问题即可,我们还可以使用到WebSocket方式连接网页与打印客户端程序。
七 Windows 强大的注册表
你知道从淘宝网页上如何打开一个 阿里旺旺 聊天工具么,怎么在网页上唤醒 QQ 客服,这两个给了我一个比较不错的思路,我通过网页唤醒一个打印客户端程序,及时客户端未打开的情况下。
《吉特仓库管理系统(开源)-如何在网页端启动WinForm 程序》
只要能够启动打印客户端,就能够传递参数发送打印指令。
八 移动终端调用打印
说到这里绝大部分想到的可以使用蓝牙打印机,没错,蓝牙打印机的确是很不错的一个选择,但是有一点就是打印指令的编排问题,以及难以设置的打印格式。再就是我们开发移动终端的时候使用的是H5, 调用蓝牙通讯这个稳定性实在不敢恭维(当然也有可能是我们的技术能力太弱了,不求甚解)。
其实我们的方式还是采用 4 章节提到的思路,H5 ,http 协议这个不是很完美的方式么,H5 终端发送http指令,通过Redis推送打印消息到打印客户端
选择好打印机之后,然后确定就可以顺利打印出标签了。
以上的方式和方法在之前分享文字以及共享的代码中都有提到使用,只是这一次是集中汇总一次,虽然是小功能但是感觉是应用软件的刚需问题,说简单也简单说复杂也还有那么一些小的弯弯绕绕。
在这几年的工作中,虽然很多精力被分散到其他的事情上去了,但是也从其他领域学到了很多东西,希望有更多的时间能够记录这些经历和经验,这几年的工作领域跨度是非常大的,虽然年纪上来了也有些迷茫和担忧,但是一腔热血暂时还没有消退。世界之大,制造业领域的应用也跨越了众多学科,要学的东西还很多,虽然和高喊着 智能制造,数字化转型,工业互联网,5G+,大数据工业应用的专家和大佬比起来有很大的差距,自己更希望能够从工业制造的工艺应用上去有点突破,虽然要顺势而为但也要实事求是。
这几年主要工作都是从事 日化产品,化妆品类的工厂生产制造,包括 家用清洗剂,牙膏,香水,粉底彩妆,护肤,香水,保健品以及制药方面,希望有更多自己的时间来反思记录这些年遇到的问题。