惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Hacker News
The Hacker News
C
Cisco Blogs
P
Privacy & Cybersecurity Law Blog
Cloudbric
Cloudbric
S
Security Affairs
PCI Perspectives
PCI Perspectives
The Last Watchdog
The Last Watchdog
AWS News Blog
AWS News Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
N
News and Events Feed by Topic
W
WeLiveSecurity
T
Tenable Blog
L
LINUX DO - 最新话题
T
Tor Project blog
Help Net Security
Help Net Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
P
Proofpoint News Feed
爱范儿
爱范儿
O
OpenAI News
Hacker News - Newest:
Hacker News - Newest: "LLM"
Y
Y Combinator Blog
I
Intezer
C
Check Point Blog
Stack Overflow Blog
Stack Overflow Blog
Recent Announcements
Recent Announcements
Google DeepMind News
Google DeepMind News
S
Securelist
P
Privacy International News Feed
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
V
Vulnerabilities – Threatpost
Schneier on Security
Schneier on Security
量子位
SecWiki News
SecWiki News
L
Lohrmann on Cybersecurity
T
Threat Research - Cisco Blogs
Recent Commits to openclaw:main
Recent Commits to openclaw:main
M
MIT News - Artificial intelligence
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Scott Helme
Scott Helme
H
Help Net Security
Vercel News
Vercel News
云风的 BLOG
云风的 BLOG
Spread Privacy
Spread Privacy
Know Your Adversary
Know Your Adversary
I
InfoQ
TaoSecurity Blog
TaoSecurity Blog
Blog — PlanetScale
Blog — PlanetScale
N
News | PayPal Newsroom
小众软件
小众软件
C
CERT Recently Published Vulnerability Notes

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
基于 C# 实现样式与数据分离的打印方案 - 元视角
飞鸿踏雪 · 2023-09-20 · via 元视角

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

从 PrintDocument 说起

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

PrintDocument 思维导图 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 组件实现的打印预览效果:

通过 PrintPreviewDialog 实现打印预览 通过 PrintPreviewDialog 实现打印预览

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

样式与数据分离的尝试

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

通过 HTML 模板实现打印 通过 HTML 模板实现打印

作为一个前/后端都写的伪・全栈工程师,我有时候甚至觉得,人类或许是是一遍遍地重复循环着自身,从原生到 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 报表设计器 RDLC 报表设计器

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

本文小结

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