ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Delphi生成二维码:PaintBox绘制、PNG保存与Image显示

Delphi生成二维码:PaintBox绘制、PNG保存与Image显示 这段时间陆续给两三个 Delphi 项目做了二维码生成模块需求都差不多离线状态下录入一串编号程序生成二维码展示在界面上能保存成图片文件归档。第一个项目我图省事调了某个云接口把图片拉回来结果部署环境没外网项目差点延期。后面老老实实自己生成把整个流程跑通以后发现Delphi 里做这件事并不复杂难点反而在几个容易被忽略的细节上——比如 PaintBox 绘制完以后怎么不闪保存时怎么保证扫得出来还有放到 Image 里显示会不会莫名其妙被拉伸变形。这篇文章就把这套流程完整说一遍从二维码生成的原理、PaintBox 画图、保存 PNG再到 Image 控件加载显示连带我在项目里踩过的坑一起写出来。适合正在用 Delphi 做桌面客户端、需要在本地生成和展示二维码的小伙伴也适合想搞懂二维码生成底层概念再自己动手写的人。读的时候可以开着 IDE 跟着敲代码片段都是可以直接粘进项目的。1. 项目背景与需求拆解桌面端二维码到底怎么用1.1 三种典型应用场景二维码在桌面程序里出现最多的场景我总结下来就三类。第一类是设备资产标签操作员录入设备编号、型号、序列号程序生成二维码要么直接打印到标签纸上要么导出图片归档。第二类是授权激活常见于工业软件或专业工具程序在离线环境下根据授权码生成二维码用户拿手机 App 扫一下完成激活绑定这里数据完全不能出内网。第三类是工序流转产线操作员选择工单号二维码贴在物料流转单上下一个工位扫码读取不需要额外的扫码枪硬件手机就能顶上去。我做的某制造企业 MES 客户端就属于第一类和第三类的结合体。操作员在界面上选中设备编号点击按钮之后二维码立刻显示在右侧的 PaintBox 里同时保存成一个 PNG 文件放到共享目录最后在下方 Image 控件里再预显一次。整个流程看起来简单但里面每一步都有选择可以做错尤其是后面要讲的绘制和显示问题。1.2 本地生成和外接接口怎么选做二维码功能的第一道选择题就是到底调用别人的接口还是自己本地生成。在线接口的优势很明显代码少图片美观人家还帮你处理了各种参数。但劣势同样明显必须有外网、有接口限流、数据内容会经过第三方服务器这三条在工控和政企项目里通常都是硬伤。我第一版就是调接口开发时一切正常到了客户现场发现生产网段根本没有外网所有二维码都拉不回来。后来改成本地生成才意识到二维码生成本身就是一个纯计算过程不涉及 OCR、不涉及大模型普通 CPU 计算一个 V40 的二维码也就是几十毫秒的事完全没有必要依赖外部服务。所以我现在的建议很简单桌面程序里做二维码默认走本地生成除非你有批量生成几万张并且需要统一模板设计的需求才考虑外部服务。1.3 为什么是 PaintBox Image 这个组合Delphi 的 VCL 里能显示图形的东西很多但 PaintBox 和 Image 是分工不同的两个控件。TPaintBox 是轻量级绘图控件它本身不保存图像数据只是在 OnPaint 事件触发时把内容画到 Canvas 上适合动态计算出来的画面。TImage 则是一个图像容器有 Picture 属性可以加载磁盘文件、TBitmap、TPngImage 等等适合静态展示。不少人会偷懒直接在 TImage.Canvas 上画二维码我一开始也这么干过结果窗体一缩放、一遮挡TImage 上的内容就被系统擦掉了因为它并不负责持久化绘制内容。用 PaintBox 就清楚很多数据变了就调用 Invalidate 触发重绘OnPaint 里统一处理逻辑上是闭环的。而 TImage 用来做最终结果展示和文件预览分工干净利落。2. 二维码生成的底层逻辑先把矩阵搞清楚2.1 二维码本质上是一张位图矩阵理解了底层再看代码很多坑就能提前避开。QR Code 这个东西表面上是一张黑白相间的方块图实际上是一个行列固定的布尔矩阵黑色模块代表 1白色模块代表 0。这个矩阵里既有我们输入的数据内容也有定位图形、校正图形、时序图形这些专门用于扫描定位的元素。所以二维码生成库的工作本质就两步一是把你的字符串编码成符合 QR 规范的数据位流二是在内存里拼出一个 rows * columns 的二维布尔数组。绘制环节完全不需要关心里面的编码细节只需要拿着这个布尔数组把每个为 true 的格子画成一个带颜色的矩形就得到了肉眼可见的二维码。这也是为什么 PaintBox 很适合做这件事它的 Canvas 画矩形是基本操作。2.2 生成器在中间做了四件事如果你用封装好的库这些事都是透明的但知道一下没有坏处至少调试的时候不会瞎猜。第一件事是数据分析检查待编码内容属于纯数字、字母数字、字节还是汉字模式选择最紧凑的编码方式。第二件事是数据编码把内容变成比特流加入模式指示符、字符计数符和终止符。第三件事是生成纠错码这里用的是 Reed-Solomon 算法把原始数据按照设定的纠错级别拆块每块生成纠错码这样二维码局部被遮挡或污损时依然能解出来。第四件事是把数据码和纠错码按规则填入矩阵再执行掩码运算并写入版本信息和格式信息。掩码这一步可能很多人不知道它的目的不是加密而是让生成出来的二维码黑色模块分布尽量均匀避免出现大面积的空白或密集方块导致扫描器无法定位。整个过程标准文档写了几十页但你完全不需要自己实现开源社区早就把这块固化成稳定的库了。2.3 版本、纠错级别和参数选择二维码版本从 1 到 40版本越高矩阵越大容量越大。V1 是 21x21 的模块每增加一个版本矩阵一边增加 4 个模块到 V40 是 177x177。同一个版本下容量还受纠错级别影响。纠错级别分 L、M、Q、H分别能容忍约 7%、15%、25%、30% 的码字损坏。级别越高能存的有效内容越少但抗污损能力越强。我的经验是普通业务场景选 M 级别就够内容比较短的比如链接、编号直接用 H 也不会亏。但是要注意内容长度决定版本号版本越高矩阵越大如果显示尺寸固定高版本的每个模块像素就越小扫码设备识别难度会上升。所以不要盲目追求大容量能短则短。另外还有一个参数容易被忽略就是静区也就是二维码周围一圈空白的宽度。规范要求至少 4 个模块宽实际测试下来少于这个值手机还能扫但工业扫码枪很容易翻车。这个参数后面绘制和保存时会反复用到。3. 核心实现PaintBox 上把二维码画出来3.1 生成库的选型和引入Delphi 生态里现成的二维码生成方案不算多但有一款基于 ZXing 移植的开源库相当能打纯 Delphi 实现没有第三方动态库依赖可以在旧版本的 Delphi 里直接用。市面上还有 Delphi XE 自带的某些组件以及各种个人封装的单元但对比下来ZXing 移植版胜在编码规范、支持多版本多纠错级别、矩阵输出接口清晰非常适合我们把绘制逻辑自己做一遍。引入方式很简单下载源码后把相关单元所在目录加入 Project Manager 里的 Search Path 就行然后在你自己的单元 uses 里加上声明。整个过程不装包、不注册组件和写普通代码完全一样。我建议不要用那种直接把整张二维码图片生成好的重量级封装因为项目里后续需求往往会有变动比如加 Logo、改静区尺寸、只生成矩阵自己排版打印矩阵输出的接口比图片输出接口好用得多。3.2 从字符串到矩阵再到 Canvas核心代码看下面这段。首先生成器对象设置 Data 为要编码的内容Encoding 用字节模式这样中文和特殊字符都能按 UTF-8 正确处理。QuietZone 设置为 4对应静区 4 个模块。ErrorCorrectionLevel 这里设成 1表示 M 级纠错。随后遍历矩阵所有 IsBlack 为 true 的格子都画成黑色矩形。type TFormQRCode class(TForm) PaintBox1: TPaintBox; btnGenerate: TButton; procedure btnGenerateClick(Sender: TObject); procedure FormCreate(Sender: TObject); private QRCore: TDelphiZXingQRCode; public procedure DrawMatrix(PBox: TPaintBox); end; implementation procedure TFormQRCode.FormCreate(Sender: TObject); begin QRCore : TDelphiZXingQRCode.Create; end; procedure TFormQRCode.btnGenerateClick(Sender: TObject); begin QRCore.Data : edtContent.Text; QRCore.Encoding : TQRCodeEncoding.qrEncodingByte; QRCore.QuietZone : 4; QRCore.ErrorCorrectionLevel : 1; PaintBox1.Invalidate; end; procedure TFormQRCode.DrawMatrix(PBox: TPaintBox); var Row, Col: Integer; ModuleSize, Margin: Integer; DrawWidth, DrawHeight: Integer; X, Y: Integer; begin ModuleSize : 8; Margin : ModuleSize * QRCore.QuietZone; DrawWidth : QRCore.Columns * ModuleSize Margin * 2; DrawHeight : QRCore.Rows * ModuleSize Margin * 2; PBox.Canvas.Brush.Color : clWhite; PBox.Canvas.FillRect(Rect(0, 0, DrawWidth, DrawHeight)); PBox.Canvas.Brush.Color : clBlack; for Row : 0 to QRCore.Rows - 1 do begin for Col : 0 to QRCore.Columns - 1 do begin if QRCore.IsBlack[Row, Col] then begin X : Margin Col * ModuleSize; Y : Margin Row * ModuleSize; PBox.Canvas.FillRect(Rect(X, Y, X ModuleSize, Y ModuleSize)); end; end; end; PBox.Width : DrawWidth; PBox.Height : DrawHeight; end;这段代码里最关键的一个点就是绘制尺寸的计算。很多人直接把 PaintBox 撑满整个窗体然后从左上角开始画结果二维码挤在角落静区也没有扫起来特别费劲。正确的做法是先根据矩阵列数和模块尺寸算出总宽度再把整个绘制逻辑和 PaintBox 的宽高绑定。这样无论内容长短、版本高低二维码都能居中占满控件且四周留有标准的白边。3.3 绘制参数调试模块尺寸与静区模块尺寸选多大取决于二维码最终要在哪看。如果只是在屏幕上展示模块尺寸 6 到 8 像素很合适画面干净。如果还要打印到小标签上那模块尺寸建议按打印分辨率换算通常至少 4 个打印像素。模块尺寸过小的直接后果是模块与模块之间几乎没有间隔黑白边缘容易被打印机糊成一团。静区在上面的代码里由 QRCore.QuietZone 参数控制我固定设为 4。这里要提醒一点如果你自己实现绘制不要只在矩阵外边留几个像素就算静区必须按模块尺寸乘以静区模块数来计算。还有颜色选择我坚持用纯黑和纯白不要为了 UI 美观把前景改成深蓝、深绿很多扫码引擎对非标准黑色边缘的容错很差尤其是低端扫码枪。如果想要“彩色二维码”的效果更稳妥的做法是先保存黑白底图再通过位图后处理给白色区域染色黑色模块保持不动。4. 保存本地文件并加载到 Image 控件4.1 为什么要绕一层文件操作有朋友问既然已经画在 PaintBox 上了直接 Image1.Picture.Assign(PaintBox1.Canvas) 这种思路行不行答案是不行Canva 不是图形对象不能直接赋值给 Picture。另外标题里的需求本来就要求“保存为本地文件并添加至 Image 控件中”这个文件是要拿来归档、打印、上传服务端的不是临时展示用。所以正确的链路是PaintBox 绘制完成后把它 Canvas 上的内容拷贝到一个 TBitmap再由 TBitmap 转成 TPngImage 保存成磁盘文件。保存完之后Image 控件再从磁盘加载这个 PNG。这样文件有了展示也有了后面想对这个文件做二次处理比如加水印、压缩、合并都有明确的操作对象。4.2 保存为 PNG 的具体实现下面的代码把 PaintBox 的内容拷贝到一个 24 位色的 Bitmap然后转成 PNG 保存。注意拷贝之前一定要确保 PaintBox 已经完成了一次完整的重绘否则 Canvas 上可能是旧数据或者空白。我习惯在生成函数里显式调用一下 PaintBox1.Update()强制刷新整个窗口消息队列确保画布内容落定。uses System.SysUtils, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.ExtCtrls, Vcl.Imaging.pngimage; procedure SavePaintBoxToPng(PBox: TPaintBox; const AFileName: string); var Bmp: TBitmap; Png: TPngImage; W, H: Integer; begin W : PBox.Width; H : PBox.Height; if (W 0) or (H 0) then Exit; Bmp : TBitmap.Create; try Bmp.Width : W; Bmp.Height : H; Bmp.PixelFormat : pf24bit; Bmp.Canvas.Brush.Color : clWhite; Bmp.Canvas.FillRect(Rect(0, 0, W, H)); Bmp.Canvas.CopyRect( Rect(0, 0, W, H), PBox.Canvas, PBox.ClientRect); Png : TPngImage.Create; try Png.Assign(Bmp); Png.SaveToFile(AFileName); finally Png.Free; end; finally Bmp.Free; end; end;为什么不直接存 BMP 而转成 PNG因为 PNG 是无损压缩白底黑字的二维码压缩率极高存出来可能只有几 KB而 BMP 一张 300x300 的图就有二百多 KB。还有一点PNG 自带 alpha 通道和像素格式信息后续如果用其他平台处理兼容性比 BMP 好太多。JPG 格式我是强烈不建议用的JPG 是有损压缩黑色模块边缘会出现颜色扩散和模糊一旦模块较小扫码识别率断崖式下降后面讲踩坑会细说。4.3 Image 控件显示参数设置与优化文件保存好之后加载进 Image 很简单一行代码Image1.Picture.LoadFromFile(AFileName);但这里有个新手高频问题加载进去之后显示只露出左上角一小块二维码其他部分都是空白。原因很简单TImage 控件的 Stretch 属性默认是 FalsePicture 多大它就按原始像素尺寸显示如果控件比图片小内容就会被裁剪。解决办法是把 Stretch 设为 TrueProportional 设为 TrueAutoSize 保持 False。Stretch True图片自动缩放填满控件区域。Proportional True缩放时保持宽高比不会把二维码压扁。Center True缩放后如果还有留白让图片居中。这三个属性配合起来不管 Image 控件在窗体里被拉大还是缩小二维码始终保持正方形且居中显示。如果你想把二维码放大预览按钮里动态设置 Image1.Width 和 Height 就行不需要重新生成文件。5. 我踩过的坑与排查速查5.1 绘制闪烁问题第一次把绘制代码塞进 OnPaint 之后每次鼠标划过窗体二维码区域就疯狂闪烁。原因是 PaintBox 背景先被清成白色再画黑色模块重绘频率高的时候人眼就能捕捉到闪烁。解决方案有两个一是 Form 的 DoubleBuffered 属性设为 True让 VCL 在内存画布上完成整套绘制后再一次性复制到屏幕二是减少不必要的重绘只有数据改变或控件尺寸改变时才调用 Invalidate。最稳的做法是把绘制逻辑封装成一个独立函数按钮点击时设置矩阵数据、计算新尺寸最后才 Invalidate。不要在 OnPaint 里去读取 Edit 框内容、去请求网络、去做 IO 操作。VCL 的绘制事件频率远比你想象的高窗口遮挡恢复、窗体拖动、最小化还原都会触发干重活会导致界面明显卡顿。还有一个小技巧如果某个界面里二维码和其他控件布局比较复杂可以把 PaintBox 的 Enabled 设为 False。这样它不响应鼠标交互从根本上减少无意义的刷新。5.2 DPI 缩放导致的锯齿和虚化高 DPI 屏幕上VCL 窗体如果启用了缩放控件会被整体放大。这时 PaintBox 的 Width、Height 是物理像素还是逻辑像素会直接影响绘制清晰度。我遇到过 150% 缩放下模块边缘出现灰边、扫码时识错的情况原因是绘制时用了物理 Canvas但坐标计算基于逻辑尺寸黑色矩形被系统插值拉伸边缘就虚了。解决思路是按当前 PixelsPerInch 动态调整 ModuleSize保证每个模块的物理宽度是一个整数。简单实现就是在绘制前读取 Form 的 PixelsPerInch 和设计时基准值换算出实际模块尺寸。另外保存文件的过程不受屏幕影响因为 Bitmap 尺寸完全由我们自己指定和屏幕分辨率无关。所以我建议屏幕展示图稍虚没关系保存出去的 PNG 一定要按固定逻辑尺寸生成保证扫描稳定。5.3 保存出来的图片扫不出来这个坑最隐蔽曾经让我排查了一个下午。现象是 PaintBox 上扫码正常保存成 PNG 后手机能扫但工业扫码枪怎么都扫不到。后面对比两张图发现PaintBox 上显示时周围有空白的窗体背景实际上发挥了静区作用而保存出来的 PNG 因为绘制时 Margin 设置为 0黑白矩阵贴边生成扫码枪读不了。处理办法就是本文前面反复提到的静区参数QuietZone 至少 4保存时最外层 4 圈模块宽度必须是空白。另外一个导致扫不出来的原因是保存尺寸太小如果二维码是 V10 以上的版本模块数量多但你把图片整体压到 100x100 像素那每个模块平均只有不到 3 像素很多扫码引擎直接放弃。我通常保证最终保存的图片边长不低于 400 像素模块尺寸不小于 6 像素。5.4 二次加载与文件占用问题有业务需求是保存后立即把文件删掉只让图片保留在 Image 控件里。实际操作时发现LoadFromFile 之后立刻 DeleteFile 会失败有一部分原因是杀毒软件还在扫描文件另一部分是 Image 控件内部还没有完全释放文件句柄。稳妥做法是先用 Try 包装删除确认延迟几百毫秒如果真的必须马上删可以先把 Picture 保存到内存里的 TBitmap再加载到 Image之后删除文件就毫无障碍。另外一个相关问题是PaintBox 绘制的二维码在窗体最小化再恢复之后OnPaint 触发时会重新绘制这个前提下代码里必须保证矩阵对象还在或者至少数据源还在。我见过有人直接把循环画在 FormCreate 里结果窗口一刷新二维码就消失了。正确做法是把矩阵生成结果缓存成一个字段OnPaint 只做绘制。5.5 常见问题速查表问题现象可能原因解决办法二维码绘制区域闪烁PaintBox 频繁重绘未开双缓冲Form.DoubleBuffered : True数据变化再 Invalidate高 DPI 下边缘虚化逻辑尺寸与物理像素不匹配动态调整 ModuleSize用整数像素绘制保存后扫码枪识别失败静区不足或模块太小QuietZone 设为 4图片边长不低于 400 像素JPG 保存后无法扫描有损压缩导致模块边缘模糊改用 PNG 保存Image 控件只显示一角Stretch 为 False设置 Stretch / Proportional / Center 为 True窗体恢复后二维码消失绘制逻辑不在 OnPaint 里缓存矩阵在 OnPaint 中统一绘制DeleteFile 删除失败图片文件句柄未释放加载到内存后再删除或延迟删除内容包含中文扫出来乱码编码模式或字符集不一致Encoding 使用字节模式UTF-8扫码端约定一致6. 扩展从“能扫出来”到“扫得快、不翻车”6.1 给二维码加 Logo 的注意事项很多客户会要求在二维码中间放一个企业 Logo外观上确实更有辨识度。实现上并不复杂在生成好的 Bitmap 正中央把 Logo 图片按比例缩小后绘制上去。但核心问题是Logo 区域会遮挡一部分矩阵模块QR 码的 Reed-Solomon 纠错能力能扛住一定程度的遮挡H 级别最高可以容忍约 30% 的码字损坏。所以加 Logo 时必须把纠错级别提升到 H设置 ErrorCorrectionLevel 为 3。Logo 尺寸建议不要超过二维码边长的四分之一也就是中心 1/4 区域内的遮挡会被纠错抵消。还有一个小细节Logo 图案最好不要带大面积的纯黑或纯白背景尤其是不能带有与黑色模块相似的规则纹理否则扫描器会误判为数据模块。我一般会把 Logo 周围留一圈白边再放到二维码上。6.2 批量生成与文件命名策略如果业务上要一次生成几十上百个二维码循环调用生成库即可每个二维码生成计算量很小不需要额外开线程。但文件命名和日志管理值得提前规划。我习惯用“前缀 日期 业务ID 随机短码”的方式比如 DEV20250615-0001-X7K3.png这样归档时按前缀分类、按日期检索、按业务ID关联非常清晰。批量场景还有一个常见需求是连同二维码一起导出 Excel 或 SQLite 记录表生成后把文件名、内容、生成时间、版本、纠错级别一并记录。千万不要在文件名里放中文和特殊符号有些共享目录和打印系统的编码处理很弱极易出错。文件名里的信息越少越稳真正的内容全部放在关联表里。6.3 二维码内容协议与数据安全二维码能承载的内容有限但很多人会往里塞一长串 JSON结果版本飙到 V20 以上扫码变慢打印变小纯属自找麻烦。我建议在系统设计阶段就约定一个紧凑的内容协议比如DEV|SN20250615|A1B2C3这种管道符分隔格式扫描端按固定顺序拆分即可。如果是 URL就尽量用短链接服务或者干脆放一个跳转参数别把完整参数往二维码里塞。涉及敏感信息时直接在二维码里放明文不可取。二维码本身没有任何加密能力贴在产品标签上谁都能扫。要保护的内容先做加密再编码例如用 AES 加密后转 Base64扫描端再反向解密。如果项目允许离线验签可以在内容尾部加一段 HMAC 签名扫码后校验签名确认数据未被篡改。这些内容看起来超出“画二维码”的范畴但实际项目里往往和二维码功能绑定出现提前设计能省掉后面改协议的痛苦。我自己在实际项目里踩得最惨的一次就是把二维码当成普通图片处理最后因为格式和静区问题耽误了上线。以后的习惯是内容先定协议生成用矩阵接口绘制时算好模块尺寸和静区保存永远走 PNG显示用 Image 的三件套属性。这套流程顺下来不管是画在 PaintBox、保存到本地文件、加载到 Image 控件还是后续加 Logo、批量导出、加密签名都只是在这个骨架上加东西而已。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表