本文对指令打印和驱动打印做了一个简要的介绍,分享了在开发客户端打印组件时的一些过程并提出了一个新轮子用于解决老的问题并引出更多的新问题。全文大概 3500 字无图,阅读大概需要 7 分钟。
驱动打印是指:使用 PrintDocument 进行打印。通过注册其 PrintPage 方法拿到 Graphics 对象使用 GDI+ 画图打印。
指令打印是指:利用打印机厂商提供的指令协议控制打印机直接打印。
驱动打印和 Windows 平台关联紧密,所以不能做到跨平台。驱动打印大部分情况不能即插即用,在第一次将某打印机链接到电脑时,可能需要安装对应的驱动程序系统才能正确的识别到该打印机。
绝大部分小票打印机都支持 ESC 指令,除了 ESC 外常见的还有 TSC、TSPL,PPLA等这与打印机厂商和型号相关。指令打印可以跨平台,且在不同的平台要向硬件发出的指令是相同的,无论链接方式是 USB、串口还是蓝牙。
从开发的角度来说,如果我们想兼容市面上大多数打印机并且想支持跨平台,那么这就会是一件需要仔细斟酌和权衡的事情:
1、仅采用驱动打印。那么我们不得不放弃对跨平台的支持。如果遇到过老的设备,它很可能没有提供对最新的操作系统(比如 Windows 10)的支持,所以单纯的驱动打印是玩儿不赢的。
2、仅采用指令打印。我们可以做到跨平台,无惧系统升级,但仍有无解的情况:如果客户的打印机没有指令打印或者指令协议很小众没必要做支持怎么办?这是真实发生的事情,有客户需要用传统的办公用打印机打印小票,真·谜一般的操作。
3、驱动打印和指令打印并行。这当然会解决上述问题,但同时会引入新的问题:你不得不写出多套不同的代码去完成一件相同的事情,更可怕的是在修改一个问题时很可能会改了这一套忘了那一套。
在项目起初,因为对各种打印方案并不熟悉所以带你部分经过了上述三个阶段的演变。当支持的打印机和打印格式越来越多,维护这部分代码就成为一件苦力活儿,而且非常容易出错。接手这部分代码的人会被怀疑是否能力有问题,毕竟开始的时候时那么的简单。
大概 2019 年 7 月份时,项目组对驱动打印进行了封装,该封装参考了网上的开源组件,构建出了一个名为 TicketDocument 的类型,并添加了一些基础操作:
public int Top { get; set; } public PaperWidth PaperWidth { get; set; } public double Width { get; } public int Height { get; set; } public string FontFamily { get; set; } public FontSize FontSize { get; set; } public String Printer { get; set; } public TicketElementCollection Elements { get; } public void Render(Graphics graphics); public void Print(String printerName); public Image ToImage(); public string ToJson(); public override string ToString(); private T Add<T>() where T : TicketElement, new(); public void AddNewRow(); public void AddEmptyRow(int height); public void AddLine(FontSize? fontSize = null); public TextTicketElement AddText(String content, FontSize? fontSize = null, StringAlignment alignment = StringAlignment.Near, float width = 1, float offset = 0, bool borderBottom = false, bool truncate = false ); public void AddImage(Image image, StringAlignment alignment = StringAlignment.Near, bool isBlock = true); public SealTicketElement AddSeal(List<String> texts, FontSize? fontSize = null, StringAlignment alignment = StringAlignment.Near, float width = 1, float offset = 0); public void AddPageBorder(); public void AddBarcode(String content, Int32 height, float offset = 0f, Action<BarcodeTicketElement> action = null);
TicketDocument 可以序列化为 JSON 字符串用于在网络间传输。所以可以将 TicketDocument 的生成放置在服务端,这样对打印格式进行微调时不需要更新客户端。
项目中对 TicketDocument 的调用类似如下,其中 doc 变量即 TicketDocument 实例:
doc.AddText($"来源:{g.SName}"); doc.AddNewRow(); doc.AddText($"出厂时间:{g.CommandDate:yyyy/MM/dd}"); doc.AddNewRow(); doc.AddText($"产品:{g.Items.Count(i => i.FXashId == 0)}件", width: 0.4f); doc.AddText($"附件:{g.Items.Count(i => i.FXashId != 0)}件", width: 0.3f, offset: 0.4f, alignment: StringAlignment.Center); doc.AddText($"共计:{g.Items.Count}件", width: 0.3f, offset: 0.7f, alignment: StringAlignment.Far);
当项目不得不支持指令打印时, TicketDocument 的抽象定义就不能满足需求了:因为指令打印并不能提供类似于 GDI+ 这种强大的控制力。
驱动打印和指令打印并行的事情必须上马。因为指令各不相同,所以就编写了不同的代码对应不同的打印机,业务应用调用打印宿主时也采用多种不同的协议格式,因项目不同没有使用 TicketDocument 。这对驱动打印部分造成了影响,满天飞的硬编码,写死的数组下标,接着在对打印格式进行调整时,驱动打印罢工了。
于是,我们需要一个新的轮子:
- 它应该满足跨平台打印的需求,在 Windows、Android、iOS 中有相同的行为表现。
- 它应该同时支持驱动打印和指令打印。
- 在满足前两条的同时,它应该尽量减少新增格式时的工作量。
All problems in computer science can be solved by another level of indirection .
计算机科学中的所有问题都可以通过间接的另一个层次来解决。
出自:David Wheeler
这是软件工程学中的一个真理,我们可以引入一种新的自定义指令来决绝上述的问题:
- 这种指令是一种高级指令,它对驱动打印和大部分目前受支持的指令打印行为进行了封装。
- 这种高级指令最终会被翻译成对 Graphics 的操作或打印机指令。
- 这种高级指令由业务系统生成并可以在网络中进行传播。
- 这种高级指令可以使用目前的主流编程语言生成,比如 C#、Java、Python、PHP、JavaScript 等。
- 这种高级指令应该易于识别,并尽量减少在网络传输中的流量消耗。
TicketDocument 似乎是一个不错的先驱者,目前为止它满足了 3、4、5 这三个条件。但设计一种高级指令并不是唯一需要的事情,仍有许多工作要做比如这种高级指令的解析和转换等。
目前为止我并没有完成对这个轮子的全部设计,以上是对这个轮子的设想。这个轮子在设计上还不完整,有许多空白的部分需要填上。如果您对这个轮子感兴趣,可以收藏本站,在文章下留言或打赏作者,谢谢支持!
这个倒是真没遇到过。打印部分丢失的情况倒是见到过,比如打印纸整齐的左边或者右边丢失,这种情况一般是打印纸没装好。
再用驱动打印的时候 使用printDocument时 会出现打印元素丢失的问题 后来发现是设置后台打印会影响到 不知道您遇见过吗
有什么问题?
你好 请问可以有些问题向您咨询嘛?