首页 / 资讯中心 / 文章详情

Delphi绘制二维码:从PaintBox到TImage的完整流程

Delphi绘制二维码:从PaintBox到TImage的完整流程 ★ FEATURED ARTICLE
做Delphi开发这几年经常被人问起“二维码怎么做”。给设备打铭牌、给活动现场生成推广链接、给内部系统加个扫码登录页面需求不大但绕不开。像标题里这种“用PaintBox画二维码、存成本地文件、再加载到TImage控件里”的链路我一开始觉得简单真正动手才发现坑不少。这篇文章就把这套流程完整拆一遍方案怎么选、PaintBox怎么画清楚、文件怎么存对、Image怎么加载没问题以及我踩过的那些典型坑。照着做基本能一次跑通。1. 方案选型在Delphi中生成二维码的几条路1.1 几种实现方案横向对比在开始写代码之前先回答一个关键问题为什么非要“在程序里生成二维码”而不是找个在线接口、装个商业控件轻松搞定我的判断标准很简单看你的运行环境和使用频率。车间、内网、现场部署的机器上不能保证外网批量生成几百个带序列号的二维码时也不能逐个请求网络接口数据安全上更不踏实。所以本地生成几乎是唯一可靠的选择。Delphi下常见的有三条路方案优点缺点适合场景开源二维码单元免费、代码可控、离线可用需要自己引入并处理绘制细节大部分常规项目调用在线接口实现最快依赖网络、有安全风险、受接口调用限制临时演示、低频少量商业二维码控件功能全、开箱即用收费、体积大、可能有授权约束预算充足且不想折腾我推荐第一种引入一个开源的二维码编码单元。它内部完成了真正复杂的编码和纠错计算开发者只需要把返回的布尔矩阵画到界面上。这也是下面所有代码的基础。关于版本兼容这里给个经验开源单元通常是一个或者几个纯.pas文件对Delphi版本要求不高从老的Delphi 7到新版的Delphi 11都能编译但要注意Unicode字符串类型的变化。老代码里Data属性如果是AnsiString在新版本里遇到中文或特殊字符就可能乱码所以引入库的时候优先选择支持Unicode的分支。1.2 为什么选PaintBox当绘制载体Delphi里能“画图”的控件不少TImage能显示图片也能在上面画TBitmap是内存里的画布TPaintBox是专门用来接收自定义绘制代码的控件。用户点名要用PaintBox我理解是有道理的。TImage的本质是“图片容器”它更适合显示一张已经存在的图片而不是在每次变化时执行绘制逻辑。TBitmap虽然灵活但它本身不可见必须借助别的控件把它呈现出来。PaintBox正好处在中间位置它没有默认图片所有内容都来自OnPaint事件这意味着你可以在事件里动态决定画什么、什么时候画。配合一个TBitmap做缓冲既能在界面上实时预览又能把同一份绘制结果保存成文件一举两得。还有个很重要的细节TImage和TPaintBox在窗口重绘时的行为不一样。TImage的Picture内容一般会自己重绘而PaintBox需要你在OnPaint里把绘制逻辑写清楚。如果项目里需要反复生成不同内容的二维码PaintBox这种“每次重绘都执行代码”的特性反而比TImage更灵活因为你不必手动维护“当前图片是什么”的状态。1.3 开工前的准备工作既然选定开源单元先把依赖加进来。通常做法是把单元文件复制到你的工程目录或者在Tools-Options的Library路径里加上源码目录然后在uses里引入对应单元。我习惯专门建一个公共单元比如QRTool.pas把二维码生成、保存、缩放等函数都封装在里面界面代码只调用接口。这样无论以后界面是放在Form上还是嵌入到别的面板核心逻辑都能复用。封装的时候需要注意编码模式一般设成自动识别即可比如qrAuto避免中文、网址、数字混合时出现意外。纠错级别按需设置。普通的链接、文本用M级15%纠错就够如果要在二维码中央放Logo最好用H级30%纠错不然Logo会遮掉太多关键数据。静区建议固定为4个模块这是二维码规范的标准值别改成0。准备工作做完下一步就是理解二维码的数据结构因为绘制方式完全取决于它。2. 核心实现用PaintBox绘制二维码的关键步骤2.1 二维码的数据结构与绘制原理网上很多教程一上来就给代码结果读者改个参数就懵了。先明白原理后面怎么写都顺。二维码本质上是一个由黑白模块组成的矩阵。版本1是21乘21个模块之后每增加一个版本行列各增加4个模块。模块越多能装的字节越多但画面也越密。数据被编码后会分布在矩阵的特定位置同时按照纠错级别加入冗余信息。常见纠错级别分四档L约能恢复7%的数据M恢复15%Q恢复25%H恢复30%。纠错级别越高二维码越“抗脏”、越耐遮挡但同样数据量下模块会更密集。除了数据区域二维码四周必须保留一圈空白模块叫做静区Quiet Zone。标准要求静区至少4个模块宽。很多“扫不出来”的案例问题都出在绘制时把静区裁掉了或者把二维码画得贴边太近。这个细节我后面会专门提。开源单元完成编码后返回的是一个二维布尔矩阵。True代表黑色模块False代表白色模块。绘制时的核心任务就是把这个矩阵按一定比例放大画到目标画布上。你不需要关心每个模块背后的编码规则但你需要知道矩阵的行列数决定了二维码的“原始分辨率”显示时每个模块占几个像素则决定了它清不清晰。2.2 OnPaint中绘制二维码的完整代码上面说到PaintBox的所有绘制代码都应该写在OnPaint事件里。原因很简单窗口一旦被遮挡、移动、最小化再恢复系统会通知控件重绘此时只有OnPaint会触发。如果你把绘制逻辑放在别的地方可能只在初始化时画了一次后面就白屏了。下面是一版能直接跑的绘制代码。把二维码对象FQR和缓冲位图FBitmap都声明成窗体成员变量FormCreate里创建FormDestroy里释放。这样每次重绘都能复用同一个位图避免反复创建对象type TFormMain class(TForm) PaintBoxQR: TPaintBox; EditContent: TEdit; btnGenerate: TButton; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure btnGenerateClick(Sender: TObject); procedure PaintBoxQRPaint(Sender: TObject); private FQR: TDelphiZXingQRCode; FBitmap: TBitmap; end; implementation procedure TFormMain.FormCreate(Sender: TObject); begin FQR : TDelphiZXingQRCode.Create; FBitmap : TBitmap.Create; end; procedure TFormMain.FormDestroy(Sender: TObject); begin FBitmap.Free; FQR.Free; end; procedure TFormMain.btnGenerateClick(Sender: TObject); begin FQR.Data : EditContent.Text; FQR.Encoding : TQRCodeEncoding(qrAuto); FQR.ErrorCorrection : TQRCodeErrorCorrection(qrLevelM); FQR.QuietZone : 4; PaintBoxQR.Invalidate; // 请求系统重绘 end; procedure TFormMain.PaintBoxQRPaint(Sender: TObject); var Row, Col: Integer; ModuleSize: Integer; begin if FQR.Bitmap.Width 0 then Exit; // 每个模块放大几个像素让二维码占满PaintBox宽度 ModuleSize : PaintBoxQR.Width div FQR.Bitmap.Width; if ModuleSize 3 then ModuleSize : 3; FBitmap.Width : FQR.Bitmap.Width * ModuleSize; FBitmap.Height : FQR.Bitmap.Height * ModuleSize; // 用FillRect逐块画矩形避免直接SetPixel带来的性能问题 for Row : 0 to FQR.Bitmap.Height - 1 do begin for Col : 0 to FQR.Bitmap.Width - 1 do begin if FQR.Bitmap[Row, Col] then FBitmap.Canvas.Brush.Color : clBlack else FBitmap.Canvas.Brush.Color : clWhite; FBitmap.Canvas.FillRect( Rect(Col * ModuleSize, Row * ModuleSize, (Col 1) * ModuleSize, (Row 1) * ModuleSize)); end; end; PaintBoxQR.Canvas.Draw(0, 0, FBitmap); end;这里有几个关键选择值得解释。为什么先画到FBitmap再一次性Draw到PaintBox因为如果直接在PaintBox.Canvas上逐块画会频繁触发画布操作界面可能闪烁而且重绘时残留的痕迹很难清理。先在内存位图里把整张图画好最后一次性呈现界面干净利落。为什么用FillRect画矩形而不是用Pixels[i, j]设置颜色因为Pixels每次访问都要走一次GDI颜色转换当矩阵是50乘50、放大倍数是5的时候就是250乘250共62500个点用Pixels会明显卡顿。FillRect是按区域填充效率高得多。如果矩阵很大、要求更极致性能还可以用ScanLine直接操作内存像素但那属于进阶优化普通场景没必要。2.3 清晰度与像素缩放细节二维码不是分辨率越高越好而是模块要清晰、边界要锐利。绘制时如果矩阵是25乘25直接画成25乘25像素每个模块只有1像素绝大多数扫码设备会扫不出来反过来如果用一个位图拉伸中间会产生灰色插值边缘黑白边界发虚同样影响识别。推荐做法按模块放大整数倍。例如矩阵25乘25ModuleSize取4生成100乘100的位图边缘就是清晰的矩形锯齿扫码器最喜欢这种图。实际操作中还要注意PaintBox宽高比例和矩阵宽高不一定整除。比如矩阵是29乘29PaintBox宽度是300300除以29约等于10.34按整数向下取整得到10画面右侧会留有空隙这通常不影响扫码。但如果你想做到完全铺满可以用Canvas.Draw的偏移参数居中绘制或者把PaintBox的Width在设计阶段就调整成能被矩阵边长整除的值。另外在二维码中央嵌Logo是常见需求。建议绘制完黑白矩阵之后再在FBitmap上画Logo而不是后期用图片编辑软件裁切否则容易破坏静区。Logo边长一般不要超过矩阵宽度的四分之一同时记得把纠错级别调高到Q或H这样即使Logo遮住一部分模块扫码设备也能通过纠错信息还原数据。这个度我实测下来Logo占四分之一时H级纠错基本无压力Q级勉强M级就有点悬了。3. 保存与展示写本地文件再加载进TImage3.1 从PaintBox到本地文件很多场景下二维码不只在界面上看一眼还需要落到硬盘上导出一批设备标签、生成报表附件、上传到服务器。保存的关键在于你保存的必须是内存缓冲位图FBitmap而不是PaintBox本身。PaintBox没有Picture属性它只在画布上临时呈现因此保存时要读FBitmap。保存为PNG是比较理想的选择。PNG是无损压缩文件体积小二维码上的黑白块能保持锐利边界而且支持透明通道。代码如下uses System.SysUtils, Vcl.Imaging.PNGImage; procedure TFormMain.SaveBitmapToFile(const AFileName: string); var Png: TPNGImage; begin Png : TPNGImage.Create; try Png.Assign(FBitmap); Png.SaveToFile(AFileName); finally Png.Free; end; end;注意这里要先创建TPNGImage再通过Assign把FBitmap内容复制进去。反过来直接把FBitmap当成TPNGImage用是错误的。还有Assign是“复制内容”不是“转移所有权”所以执行完Png.Assign(FBitmap)之后FBitmap依然可以继续画图或者再次保存你不需要担心FBitmap被“掏空”。如果你只是临时用保存成BMP最简单FBitmap.SaveToFile(qrcode.bmp)。但BMP体积大一张二维码可能几百KB不适合大量保存或网络传输。PNG通常只有几KB到几十KB效果差距明显。3.2 TImage加载本地图片的两种方式保存成文件之后加载进TImage就有很多选择。最直接的是LoadFromFileImage1.Picture.LoadFromFile(qrcode.png);LoadFromFile会根据扩展名自动选择解码器PNG、JPG、BMP都能处理。它背后是先创建合适的图片类型再内部解码成位图。好处是代码简单坏处是每次都要从磁盘读一遍。如果图片本身就是内存里的FBitmap完全没必要绕一圈写盘再读回来Image1.Picture.Bitmap.Assign(FBitmap);这行代码把内存中的位图直接复制给TImage的Picture.Bitmap瞬间完成不产生临时文件。两种方式哪种好取决于你是否需要文件落地。如果你既要在界面上显示又要导出文件那就先保存成文件再LoadFromFile流程统一。如果只是界面上看一眼用Assign更干脆。还有一个容易忽略的坑TImage默认不自动缩放图片比控件大时会显示不全。要让图片自适应控件可以把Stretch设为True或者把AutoSize设为False再配合Center居中。但这里我要劝一句做二维码预览时最好把Stretch关掉用原始像素看。因为一旦拉伸图像被插值模糊你就分不清到底是二维码画得不行还是TImage显示拉伸的锅。等确认二维码本身没问题再按界面需要调整显示属性。3.3 内存释放与对象管理注意事项Delphi里最烦的问题之一就是对象释放。这里有几个容易踩的点FBitmap被Assign给Png之后仍然可以继续使用因为Assign是复制内容而不是转移所有权。Png.Free之前必须先SaveToFile否则文件没写进去。如果你用TMemoryStream做中间环节记得在finally里Free别让流对象一直占着资源。在长时间运行的程序里循环创建TBitmap却忘记FreeGDI句柄会越占越多几个小时后程序就会卡死甚至崩溃。我自己的习惯是把“生成二维码”和“保存文件”拆成两个独立方法各管各的对象生命周期。生成方法只负责返回TBitmap保存方法负责从TBitmap写出文件界面方法负责调这两个并更新控件。这样排查问题时哪一步出了问题一眼就能定位。4. 常见问题与排查技巧实录4.1 界面白屏或二维码不刷新这是新手最容易遇到的。症状是点按钮之后PaintBox上还是老样子或者干脆是空白。排查思路从三个点入手OnPaint事件有没有绑定点按钮之后有没有调用InvalidateInvalidate触发的重绘是不是被后续某个操作覆盖了解决办法是在按钮点击里加一行PaintBoxQR.Invalidate不要直接调用PaintBoxQRPaint。因为手动调用只画一次而Invalidate会请求系统在合适的时机重绘窗口被遮挡后再恢复也能保证画面还在。这是我第一次做这个功能时犯的错后来统一用Invalidate后才彻底消停。另外还要提一个场景如果把生成逻辑写在了FormCreate里FormCreate之后窗体一旦被系统重绘画面就会被清空。所以无论初始化时要不要显示二维码OnPaint里都必须有完整的绘制代码这样才能保证任何时刻重绘都在。4.2 二维码扫不出来好不容易画出来了拿手机一扫没反应。原因通常集中在三个地方。第一静区不足。QuietZone设成0或者绘制时把矩阵整个铺满PaintBox边界模块紧贴外框扫码器就找不到“哪里是二维码的开头”。建议QuietZone至少4绘制时四周留白。第二尺寸太小。模块如果小于2像素手机摄像头在正常距离下很难对焦。尤其在低DPI屏幕上1个像素的模块可能被系统直接忽略。建议每个模块不小于3像素预览区保证200乘200以上。第三颜色对比不足。有人为了好看把黑色模块改成深灰或者把白色区域改成米黄都会导致扫码失败。二维码的标准方案就是黑配白。配色可以玩但对比度一定要足够最好拿多个手机实测。深蓝配白、黑配浅灰这种常见搭配我测过能用但灰配白、红配黄这类低对比组合就别试了。4.3 保存的文件是纯黑或纯白块出现这个现象时先检查保存之前FBitmap是不是已经被释放。常见错误写法是Bitmap.Free; Png.Assign(Bitmap); // 这里要么崩溃要么得到无效数据顺序反了。另一个隐性问题是你在OnPaint里每次重绘都新建一个局部TBitmap保存时拿到的却是另一个对象里面可能是旧数据或注定失败的悬空引用。按我前面的设计把FBitmap作为成员变量绘制时只改尺寸和内容不每次新建保存时直接用当前对象就不会有这个问题。还有一个比较隐蔽的情况如果生成二维码的矩阵宽度为0比如用户输入了空字符串FBitmap宽高被设成0后续再画矩形、再保存就会得到一张空白或异常图片。所以生成之前要先判断数据是否为空为空时直接在界面提示不要进入绘制分支。4.4 常见问题速查表为了方便排查我把常见问题整理成一张表现象常见原因解决办法PaintBox空白OnPaint未绑定或未调用Invalidate绑定事件按钮里调用Invalidate扫码无反应静区不足、尺寸太小、颜色对比差静区设4模块不小于3像素黑配白保存纯黑或空白对象释放顺序错误或宽高为0先Assign再释放生成前判空图片在TImage显示异常Stretch、AutoSize设置不当预览关Stretch按需调整属性程序跑久了卡死循环创建位图未释放GDI句柄泄漏用成员变量或try/finally保证Free生成内容一直重复Data没更新或编码模式固定错误每次生成前赋值Data用qrAuto自动识别中文显示成乱码老库Data类型是AnsiString换支持Unicode的分支或做编码转换4.5 一点实战心得做这种功能最忌讳代码全部堆在按钮事件里。我后来把整套逻辑拆成三块二维码生成器、文件仓储、控件绑定。生成器只负责从字符串到TBitmap文件仓储负责PNG文件读写绑定层负责把结果摆到PaintBox和TImage上。这样不管界面怎么改核心逻辑始终能复用。如果要批量生成我建议加一个“尺寸系数”参数统一控制模块放大的倍数再给导出过程加一个进度提示毕竟几百个文件一起导出时界面卡死是常事。实测下来把SaveToFile放进单独线程主界面基本无感二维码的生成和保存都很耗CPU和磁盘IO能异步就不要同步。另外提一下如果你要生成的内容包含中文、换行、特殊符号建议在编码前做一次字符串清洗去掉不可见字符。有些扫码App对特殊字符处理不友好会导致明明画对了却扫出乱码。别问我怎么知道的都是踩出来的经验。我个人在实际操作中最大的体会是二维码这种东西看着是“画图”本质上是个编码问题。只要数据编码正确、静区留够、模块边界清晰画到哪里都能扫反过来只顾外观不管编码细节画出来就是一坨只能看不能用的装饰画。希望这篇文章能帮你少走几步弯路。后续如果要做批量导出、带Logo样式、切换不同编码模式在这套骨架上加就非常顺手了。
阅读完成 · 觉得有帮助?
咨询建站