基于 C# 实现样式与数据分离的打印方案

对于八月份的印象,我发现大部分都留给了出差。而九月初出差回来,我便立马投入了新项目的研发工作。因此,无论是中秋节还是国庆节,在这一连串忙碌的日子里,无不充满着仓促的气息。王北洛说,“活着不就是仓促,哪里由得了你我”。最近,我一直在忙着搞打印,我时常怀疑在“数字化转型”这件事情上,人们的口号大于实质,否则,人们便不会如此热衷于打印单据,虽然时间已过去许多年,可有些事情似乎从未改变过,无论是过去的 FastReport、FineReport,还是如今的 PrintDocument 以及基于 Web 的打印方案,它们只是形式在变化而已,真正的本质并未改变,就像业务可以从线下转移到线上一样,可人们试图控制和聚合信息流的意愿从未小腿。在变与不变这两者间,我们总强调“适应” 和 “向前看”,可每个人都在有意无意地,试图向别人兜售某种在“舒适圈”浸染已久的概念,这一刻,我觉得还是应该多一点变化。所以,我想以 “样式与数据分离的打印方案” 为主题,探索一种 “” 的玩法。

从 PrintDocument 说起

一切的故事都有一个起点,而对于 C# 或者 .NET 来说,PrintDocument 始终是打印绕不过去的一个点。虽然,在别人的眼里,打印无非是调用系统 API 向打印机发送指令,可如果考虑到针式、喷墨、激光、热敏…等等不一而足的打印机种类,以及各种尺寸的打印纸、三联单/五联单、小票纸,我觉得这个问题还是蛮复杂的。考虑到篇幅,我不打算在这里科普这些 API 的使用方法,下面这张思维导图展示了 PrintDocument 所具备的关于 “打印” 的能力。从这个角度来看,打印需要考虑的事情何其纷扰耶,甚至你还要考虑打印机缺/卡纸、切刀打印机是否正确地切割了纸张…等等的问题。此前,网络上流传着一个段子,大意是有人问如何解决打印时产生的空白页。此时,在职场打拼多年的前辈会语重心长地告诉你,只需要将其打印出来然后丢掉其中的空白页😺。
在这里插入图片描述

相信大家都见过类似下面这样的单据或者小票:

在这里插入图片描述

通常情况下,如果使用 C# 中的 PrintDocument 来实现打印,其基本思路是构造一个 PrintDocument 实例,同时注册 PrintPage 事件,而在该事件中,我们可以利用 Graphics 来绘制线条、文字、图片等元素:

var printDocument = new PrintDocument();
printDocument.PrintController = new StandardPrintController();// 设置打印机名称
printDocument.DefaultPageSettings.PrinterSettings.PrinterName = "HP LaserJet Pro MFP M126nw";// 设置纸张大小为 A5
foreach (PaperSize paperSize in printDocument.DefaultPageSettings.PrinterSettings.PaperSizes)
{if (paperSize.PaperName == "A5"){printDocument.DefaultPageSettings.PaperSize = paperSize;break;}
}// 注册 PrintPage 事件
printDocument.PrintPage += async (s, e) =>
{// ...// 绘制一个二维码var qrCodeWidth = 100;var qrCodeName = Guid.NewGuid().ToString("N");var qrCodePath = PathSourcecs.CaptureFace + @"\" + qrCodeName + ".png";QRCodeByZxingNet.NewQRCodeByZxingNet(qrCodePath, orderSaHwVo.saRecordId, qrCodeWidth, qrCodeWidth, ImageFormat.Png, BarcodeFormat.QR_CODE);var image = System.Drawing.Image.FromFile(qrCodePath);args.Graphics.DrawImage(image, marginLeft, totalHeight);// ...
};

当然,你还可以利用 BeginPrint 和 EndPrint 这组事件来处理打印开始和打印结束的逻辑,这里我们按下不表,下面是打印以及打印预览的代码实现,可以发现,这一切在微软 API 的加持下非常简单:

// 打印
printDocument.Print()// 打印预览
var printPreviewDialog = new PrintPreviewDialog();
printPreviewDialog.Document = printDocument;
printPreviewDialog.TopLevel = true;
printPreviewDialog.ShowDialog();

如下图所示,下面是通过 PrintPreviewDialog 组件实现的打印预览效果:

在这里插入图片描述

如果从这个角度来审视 PrintDocument,它毫无疑问是一个非常完美的解决方案!

样式与数据分离的尝试

历史经验告诉我们,凡事没有绝对,使用这个方案来打印最大的问题在于,样式和数据没有分离开来,甚至严重耦合在一起。这就导致每次只要更换打印格式,整个代码基本上等于全部重写。时过境迁,一个项目里存在着各种版本的 PrintPage 代码更是家常便饭。作为一名程序员,我一直呼吁大家努力去抓住那些不变的东西,可对于人生而言,适应变化、拥抱变化、创造变化的心态显然更具有普适性。所以,这世上是否会有一种 “以不变应万变” 的方案来解决这个问题呢?所以,下面来探索打印样式与数据的分离问题。

在这里插入图片描述

作为一个前/后端都写的伪・全栈工程师,我有时候甚至觉得,人类或许是是一遍遍地重复循环着自身,从原生到 Web 的演化过程中,我看到的是人们周而复始地在用新技术 “重制” 过去的旧业务。譬如,前端同样有单据打印的需求,通常可以使用 vue-print-nb 或者 vue3-print-nb 来实现。诚然,前端的打印方案自始至终都摆脱不了浏览器自身的特性限制,可我们还是能从中找到某种共性。如图所示,在此前的前端项目中,我使用 EJS 这个模板引擎来编写和渲染 HTML模板,再通过 vue-print-nb 将其打印出来。所以,在这里我想继续沿用这个方案,下面是整体的实现思路:

在这里插入图片描述

如图所示,我们的思路是利用此前博主介绍过的 Liquid 来渲染 HTML 模板。此时,打印样式可以通过前端三件套搞定,我们只需要在模板文件中完成字段绑定即可,这样就可以实现数据和样式的分离。当然,这一切还不足以传递给 PrintDocument 来使用,所以,还需要将其进一步转化为图片或者 PDF 文件。在 IE 浏览器还没有寿终正寝的时间线里,你可以使用 WebBrowser 来实现图片的转化,可如果 2023 年我们还固执地着 WebBrowser 不愿放手,这何尝不是一种莫名的执念呢?下面采用全新的 WebView2 方案的一种实现:

// 确保 WebView2 内核可用
await webView.EnsureCoreWebView2Async();// 加载并渲染模板
var htmlContent = File.ReadAllText("HtmlTemplate.html");
var template = DotLiquid.Template.Parse(htmlContent);
htmlContent = template.Render(Hash.FromAnonymousObject(new { Remark = "这是通过打印模板渲染的内容" })// 加载网页并截图
webView.Reload();
this.webView.NavigateToString(htmlContent);
using var fileStream = File.OpenWrite("snapshot.jpg")
await webView.CoreWebView2.CapturePreviewAsync(CoreWebView2CapturePreviewImageFormat.Jpeg, fileStream);

此时,我们只需要为 PrintDocument 注册 PrintPage 事件即可:

printDocument.PrintPage += async (s, e) =>
{// 以下两行代码可以显著提升打印效果e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half;// 按实际打印区域缩放、绘制图片// 当然,这里会牵涉到像素、毫米、页边距等等的问题,还需要做进一步的研究,我这里表达的是一种可行性var image = System.Drawing.Image.FromFile("snapshot.jpg");var printableArea = printDocument.DefaultPageSettings.PrintableArea;var ratio = image.Width / image.Height;e.Graphics.DrawImage(image, new System.Drawing.RectangleF(printableArea.Left, printableArea.Top, printableArea.Width, printableArea.Width / ratio));
};

实际上,打印通常会牵扯到页边距、分页、纸张大小等等的问题,而采用前端三件套来渲染内容,自然不可避免地牵连出诸如像素、毫米、英寸、DPI …等等一堆的名词。我不得不承认,这一切非常复杂,即便在普通用户眼中,打印就像变魔术一般,只需要轻轻地点击一下鼠标。如果考虑到图片放缩导致的变形问题,理论上 HTML 模板的宽高比应该与实际打印纸张的宽高比相同,可事实是每次处理打印问题总不免要花点时间来做调试。我个人觉得,如果使用 PDF 作为打印的载体,效果应该会比图片稍微好一点。

在这里插入图片描述

考虑 PDF 的理由主要有两个方面,其一是基于 Webkit 内核的浏览器天然地对 PDF 格式友好,其二是可以复用浏览器自身的打印能力。如图所示,我们可以利用全新的 WebView2 组件去调用浏览器自带的打印对话框。从某种意义上来讲,这和前端常用的 vue-print-nb 或者 vue3-print-nb 插件并没有任何区别,本文的一切碎碎念似乎都在这一刻流向了同一个地方。可这种方案的缺陷在于,它无法跳过打印浏览器自带的打印对话框,甚至你连用户点击了打印还是取消都无从判断,更不必说要去判断打印机是否打印完成。实际上,即便是 PrintDocument,它同样无法“准确”地获得打印进度。一旦人们提出静默打印的诉求,这一切的一切终将重新回到 PrintDocument 的方案。

在这里插入图片描述

事实上,微软还提供了一种 RDLC 报表的方案,这种方案更贴近传统的报表类业务,它可以通过定义实体类、创建数据集、添加数据源、设计模板等一系列流程完成报表设计,如果你使用过 FastReport 这类产品,自然会觉得这一切似曾相识,甚至连 DataSet、DataTable 这种偏底层的 API 都会倍感亲切。微软的 RDLC 以及 FastReport 的报表模板,本质上都是一个 XML 文件,其底层应该都是利用了 PrintDocument 这套 API。如果你看到相关的代码片段,就会明白一件事情,即:太阳底下没有新鲜事,无外乎是将每一页渲染为图片,再通过 DrawImage() 方法绘制出来。当一个人越来越接近本质,就会天然地厌倦外在的装饰或者形式,可惜生活中好像到处都是这样的事情。

本文小结

在无数次纠结下,我终于写完了这篇没什么技术含量的文章。首先,打印这个话题非常零散,这些难以形成体系的内容,属实无法达到一篇文章的篇幅。其次,打印在业务中的价值非常低,有或者没有并不会影响主线流程,更多的情况下是一种聊胜于无的点缀。从这两个角度来看的话,我这篇文章甚至都没有什么价值,因为在人们的印象中,打印终归是一件非常简单的事情,哪怕有的人连装纸这件事情都能搞砸,可这丝毫不会影响人们心目中对它的定位,人们唯一能记住的就是接上电源、按下开关、点击鼠标。如果我永远改变不了这一点,我唯一能做的就是将这些碎碎念记录下来,无论是殊途同归还是独辟蹊径,我在意的是此时此刻坐在电脑前的我的感受,仅此而已。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/149974.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【软件安装】Windows系统中使用miniserve搭建一个文件服务器

这篇文章,主要介绍如何在Windows系统中使用miniserve搭建一个文件服务器。 目录 一、搭建文件服务器 1.1、下载miniserve 1.2、启动miniserve服务 1.3、指定根目录 1.4、开启访问日志 1.5、指定启动端口 1.6、设置用户认证 1.7、设置界面主题 (…

RabbitMQ的交换机(原理及代码实现)

1.交换机类型 Fanout Exchange(扇形)Direct Exchange(直连)opic Exchange(主题)Headers Exchange(头部) 2.Fanout Exchange 2.1 简介 Fanout 扇形的,散开的&#xff1…

IDEA 2023.2.2 使用 Scala 编译报错 No scalac found to compile scala sources

一、问题 scala: No scalac found to compile scala sources 官网 Bug 链接 二、临时解决方案 Incrementality Type 先变成 IDEA 类型 Please go to Settings > Build, Execution, Deployment > Compiler > Scala Compiler and change the Incrementality type to …

Ubuntu 内核降级到指定版本

reference https://www.cnblogs.com/leebri/p/16786685.html 前往此网站,找到所需的内核 https://kernel.ubuntu.com/~kernel-ppa/mainline/ 查看系统架构 dpkg --print-architecture 二、下载安装包 注意:下载除lowlatency以外的deb包 三、安装内核 3…

【AICFD案例教程】锥形燃烧器燃烧仿真

AICFD是由天洑软件自主研发的通用智能热流体仿真软件,用于高效解决能源动力、船舶海洋、电子设备和车辆运载等领域复杂的流动和传热问题。软件涵盖了从建模、仿真到结果处理完整仿真分析流程,帮助工业企业建立设计、仿真和优化相结合的一体化流程&#x…

前端开发技术栈(工具篇):2023深入了解webpack的安装和使用以及核心概念和启动流程(详细) 63.3k stars

目录 Webpack简介 Entry Module Chunk Loader Plugin Output Webpack的启动流程 Webpack的优缺点 Webpack的使用 1. 安装Webpack 2. 创建Webpack配置文件 3. 编写代码 4. 运行Webpack 5. 在HTML中引入打包后的文件 6. 执行编译命令 Webpack其他功能介绍 1. 使…

京东平台数据分析:2023年9月京东空气净化器行业品牌销售排行榜

鲸参谋监测的京东平台9月份空气净化器市场销售数据已出炉! 9月份,空气净化器的销售同比上年增长。根据鲸参谋平台的数据显示,今年9月,京东平台空气净化器的销量将近15万,同比增长约1%;销售额将近2亿元&…

C#,数值计算——分类与推理,基座向量机(SVM,Support Vector Machines)的计算方法与源程序

把 Support Vector Machines 翻译成 支持向量机 是书呆子翻译。基座向量机 不好吗。 1 文本格式 using System; namespace Legalsoft.Truffer { /// <summary> /// Support Vector Machines /// </summary> public class Svm { priv…

bug:Chrome插件SwitchyOmega安装时程序包无效:“CRX_HEADER_INVALID“问题

bug&#xff1a;Chrome插件SwitchyOmega安装时程序包无效:“CRX_HEADER_INVALID“问题 1 解决 先说解决办法&#xff1a; 将下载的crx重命名为xxx.zip&#xff0c;然后解压打开chrome的开发者模式 点击加载已解压的应用程序&#xff0c;然后选择我们解压后的文件夹即可 安装成…

Android APK瘦身实践:二次瘦身如何再减少大小?(4M—2.9M)

瘦身前 因为平时就考虑到大小的限制&#xff0c;所以很多工作已经做过了&#xff0c;如下列举现在的状态&#xff1a; 7.3M&#xff08;Debug版本&#xff09;和6.5M&#xff08;Release版本&#xff09; 开启minifyEnabled 开启shrinkResources 已经去除不相关的大型库 图片和…

k8s中kubectl陈述式资源管理

1、 理论 1.1、 管理k8s核心资源的三种基本方法 &#xff1a; 1.1.1陈述式的资源管理方法&#xff1a; 主要依赖命令行工具kubectl进行管理 1.1.1.1、优点&#xff1a; 可以满足90%以上的使用场景 对资源的增、删、查操作比较容易 1.1.1.2、缺点&#xff1a; 命令冗长&…

贝锐花生壳内网穿透推出全新功能,远程业务连接更安全

贝锐旗下内网穿透兼动态域名解析品牌花生壳目前推出了全新的“访问控制”功能&#xff0c;可精确设置访问权限&#xff0c;充分保障信息安全&#xff0c;满足更多用户安全远程访问内网服务的需求。 通过这一功能&#xff0c;可实现指定时间、IP、地区等条件下才能远程访问映射的…