ZXing.Delphi实战:Delphi环境下二维码生成与识别完整指南

📅 发布时间:2026/10/16 20:16:30
ZXing.Delphi实战:Delphi环境下二维码生成与识别完整指南
简介ZXing.Delphi-3.9.0.zip是面向Delphi开发者的开源条形码扫描库支持从Delphi XE到10.3.3 Rio的多个版本主要解决在应用程序中快速集成扫码识别与二维码生成的需求。压缩包共263个文件以92个pas单元源码、8个dpr/dproj工程文件、6个fmx跨平台界面文件为核心另含107个png图片素材及res、xml、dfm等资源整体约15.5MB结构清晰便于直接编译与二次开发。库内置TZXingScanner可视化控件可针对摄像头实时视频流或静态图像识别Code 39、Code 128、QR Code、Data Matrix、EAN-13、UPC-A等主流条码并支持通过事件回调处理扫描结果方便将数据接入业务逻辑或继续生成新码同时可调整解码参数以适应不同打印质量和条码类型。配套示例工程覆盖基础窗体、复杂界面以及Android部署配置可帮助中高级开发者快速掌握跨平台集成要点和调参方法。目前已有505人学习适合需要为Delphi应用增加条码能力或研究条码解码原理的技术人员直接参考。1. 一张流传多年的 Delphi 条码库压缩包为什么到今天还有人在翻它的老底在 Delphi 里做一个带扫码功能的程序正常情况下你会经历一段相当难受的时光要么调系统的摄像头控件要么自己处理图像识别写出来的东西既慢又不稳定。ZXing.Delphi 之所以在 VCL 和 FMX 开发者里流传多年核心就一句话——它把二维码和一维码的生成、识别能力完整搬进了 Object Pascal 世界而且不依赖第三方运行时。3.9.0 这个版本对应的是对 Android 平台兼容性做过多轮修补后的一个稳定分支我在 Delphi 10.4 上打开它编译干净几乎没有报错。标题里这个 .zip 文件就是它的完整发布包源码、编译单元、示例工程全在里面。这篇文章的目标读者很明确你手上有一个 Delphi/C Builder 项目需要在桌面端或移动端扫二维码、扫描条形码或者把一段文本变成可打印的码图。下面我会从包内容、环境配置、最小实现、参数调优一直讲到踩坑记录保证你照着走能跑通而不是拿到压缩包只能干看。2. 先搞清楚 3.9.0 这个发布包里装了什么目录结构、核心单元和它们的依赖关系2.1 解压之后第一眼看哪里Source 目录才是真正的“发动机”ZXing.Delphi-3.9.0.zip 解压之后的布局和大多数开源 Delphi 库类似。根目录下通常能看到 lib、source、samples 这几个关键文件夹外加一个描述版本文档。很多人上来就找 .bpl 编译包装了半天发现自己的 Delphi 版本对不上最后又老老实实回到源码路径。我一般不会去碰预编译产物直接把 source 目录扔进 Library Path 是最省心的做法——原因后面会细说。source 目录里面按功能拆成了多个子单元你需要记住的大约有四块一是 ZXing.ScanManager这是识别入口封装了从 TBitmap 到解码结果的完整流程二是 ZXing.BarcodeWriter负责把字符串变成 TBitmap三是 ZXing.BarcodeFormat定义了 QR_CODE、CODE_128、EAN_13 等格式枚举四是各种 Decoder、Detector、Common 下的辅助单元正常使用时不直接引用它们但调试时要能定位进去。2.2 为什么 3.9.0 值得用版本之间的差异主要体现在移动端适配而非新功能ZXing 从 Java 版移植到 Delphi 的过程中最影响体验的并不是解码算法本身而是 Delphi 各版本对 Android/iOS 底层的支持差异。旧版本在 Android 相机相对布局、图像格式转换上有一堆历史包袱我在 FMX 工程里跑旧版扫描画面经常是颠倒的。3.9.0 这个版本主要解决的问题集中在更合理的图像亮度矩阵构造、对 Delphi 10.3 多平台编译的适配以及部分机型 ScanManager 在连续扫码时偶发内存访问异常的修复。我个人的判断是如果你不是必须兼容 Delphi 10.2 以下的古董环境直接用 3.9.0省掉自己打补丁的功夫。它的解码机制和 Java 版保持一致先把图像灰度化再经过二值化算法变成黑白矩阵最后根据码格式去定位、采样、纠错。这个机制决定了它在纯色背景、光线均匀的场景下表现最好也决定了某些坑是它天生避免不了的后面避坑章会讲。2.3 手写识别方案 vs. ZXing.Delphi为什么我不建议你自己造轮子每过一段时间就有人问我识别二维码而已我自己写个定位算法行不行。技术上可以但工程上不划算。二维码识别真正复杂的部分不是找三个定位角而是畸变校正、透视变换、版本信息解析、Reed-Solomon 纠错。ZXing 里这一整套代码经过十多年生产环境验证Delphi 移植版虽然不像 Java 原版那么活跃但核心逻辑完整保留了下来。对于 Delphi 生态里的开发者来说这是成本最低的成熟方案。再说生成码图如果你只调 Windows 的条码字体出来的二维码格式不标准手机扫不出来用 ZXing.Delphi 的 Writer 单元生成的二维码符合 ISO/IEC 18004 规范可以指定纠错级别和边距打印出来也稳定。选型这件事我建议遵循一个原则能用成熟库解决的不自己实现能下源码调试的不依赖黑匣子 exe。3. 把 ZXing.Delphi-3.9.0.zip 装进开发环境两条安装路径和一条验证命令3.1 标准安装解压、配置 Library Path、编译静默通过先说你拿到 zip 之后需要做的三步。解压到一个不带空格的纯英文路径这是 Delphi 的老规矩路径里有中文或者空格某些第三方包的编译脚本会间歇性发疯属于玄学但避开没坏处。然后打开 Delphi IDE进入 Tools Options Environment Delphi Options Library在 Library Path 里加入解压后的 source 目录。注意要同时覆盖 Win32、Win64 平台否则切到 64 位 Release 编译时提示找不到 ZXing.ScanManager。最后新建一个空工程在 uses 里挂上 ZXing.ScanManager 跑一次编译没有报错就是装好了。unit Unit1; interface uses System.SysUtils, System.Types, System.UITypes, System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, ZXing.ScanManager; type TForm1 class(TForm) private FScanManager: TScanManager; end; implementation {$R *.fmx} procedure TForm1.FormCreate(Sender: TObject); begin // 这一步会执行初始化逻辑如果路径配置错误 // 编译时会在此处报告找不到 ZXing.ScanManager。 FScanManager : TScanManager.Create(TBarcodeFormat.Auto, nil); end; procedure TForm1.FormDestroy(Sender: TObject); begin FScanManager.Free; end; end.这段代码里最关键的是构造 TScanManager 时传入的第一个参数。TBarcodeFormat.Auto 表示让解码器自动探测所有支持的格式日常使用最省事。传入 nil 作为第二个参数表示使用默认解码选项。如果 Library Path 没配好编译器会在 uses 子句处直接报找不到单元这是最简单的验证方式。如果编译通过但运行时报“无法找到 ZXingBitmap”通常是你把 source 目录加成了 lib 目录两个目录都要有只是 source 排在前面。3.2 免安装方式直接把源码写进工程适合快速验证和单文件分发不想动 IDE 全局配置或者你只想在一个小工具里试试效果可以把 source 目录里的单元文件直接加入当前工程。具体操作是在 Project Manager 窗口右键你的工程选择 Add to Project然后批量选中 Source 目录下的 .pas 文件。需要注意这个目录里的单元之间存在内部引用有些文件依赖另一些全部加进去最稳妥。这种方式的好处是你最终交付时不需要在目标机器上额外装什么。坏处是工程文件里会多出几十个单元管理起来有点乱。我通常在调研阶段用这种方式确认功能靠谱后再切换到 Library Path 方式。两种方式的运行效果没有区别因为它们最终编译进去的都是相同源码。3.3 安装后必须做的一个冒烟测试生成一张码再识别它装完之后我建议你先做一个闭环测试用 Writer 生成一个二维码再立刻用 ScanManager 把它识别出来。这样可以同时验证生成和识别两条链路也能确认源码里所有依赖都被正确编译。下面就是把这条链路压到最小可执行范围的代码。uses ZXing.BarcodeWriter, ZXing.ReadResult, ZXing.BarcodeFormat, ZXing.Common.BitMatrix; procedure TForm1.ButtonClick(Sender: TObject); var Writer: TBarcodeWriter; ScanManager: TScanManager; Bmp: TBitmap; Result: TReadResult; begin Bmp : TBitmap.Create; try Writer : TBarcodeWriter.Create; try Writer.Format : TBarcodeFormat.QR_CODE; Writer.Options.Width : 300; Writer.Options.Height : 300; Bmp : Writer.Write(Hello ZXing.Delphi 3.9.0); finally Writer.Free; end; ScanManager : TScanManager.Create(TBarcodeFormat.QR_CODE, nil); try Result : ScanManager.Scan(Bmp); if Result nil then ShowMessage(识别结果: Result.Text) else ShowMessage(识别失败); finally ScanManager.Free; end; finally Bmp.Free; end; end;这个冒烟测试里宽度和高度决定了生成的二维码像素尺寸。单位是像素300 对应在屏幕上约一英寸见方。扫描时我直接把格式限定为 QR_CODE避免了自动探测带来的额外开销。识别成功时 TReadResult 不为 nil读取它的 Text 属性就能拿到原始内容。如果这一步跑通了你的环境就没问题了后面所有功能都能在这个基础上叠加。4. 从识别到生成写出第一段生产级代码并搞懂 TryHarder 和 PossibleFormats4.1 从 TBitmap 到识别结果ScanManager.Scan 的完整调用链大多数实际场景里你的图像来源不是内存里的二维码而是摄像头抓帧、相册选图或者从文件加载。不管来源是哪种最终都要转成 TBitmap 再调用 Scan。下面这段代码是从文件加载图片并识别的标准写法。uses System.SysUtils, FMX.Graphics, ZXing.ScanManager, ZXing.ReadResult, ZXing.BarcodeFormat; function ScanBarcodeFromFile(const AFileName: string): string; var Bmp: TBitmap; ScanManager: TScanManager; ReadResult: TReadResult; begin Result : ; Bmp : TBitmap.Create; try Bmp.LoadThumbnailFromFile(AFileName, 1024, 1024); ScanManager : TScanManager.Create(TBarcodeFormat.Auto, nil); try ReadResult : ScanManager.Scan(Bmp); if ReadResult nil then Result : ReadResult.Text; finally ScanManager.Free; end; finally Bmp.Free; end; end;这里我用 LoadThumbnailFromFile 把图片缩放到最长边不超过 1024 像素这是个很关键的预处理。相机拍出来的照片可能在 3000 像素以上直接扔给解码器不仅慢还可能因为采样点太密导致定位失败。1024 是一个经过大量样本验证的经验值太小会丢失二维码细节太大会拖慢处理速度。另外注意 Scan 是同步阻塞方法放在 UI 线程里会卡界面。生产代码中应该用 TThread 包装一次 Scan 调用或者放到异步 Task 里执行这个我们放到进阶章再展开。4.2 生成条形码和二维码除了大小还要管好边距和纠错级别生成功能同样通过 TBarcodeWriter但不同格式对参数的敏感度差异很大。二维码推荐设置纠错级别为 H这样即使码图有 30% 区域被污染或遮挡依然可以识别。而 CODE_128 这类一维码没有纠错级别概念重点是留足两侧空白区。下面的代码演示了带完整参数设置的生成过程。uses ZXing.BarcodeWriter, ZXing.BarcodeFormat, ZXing.Common.EncodingOptions, ZXing.QrCode.Internal.ErrorCorrectionLevel; function GenerateQRCode(const AContent: string): TBitmap; var Writer: TBarcodeWriter; Options: TEncodingOptions; begin Writer : TBarcodeWriter.Create; try Options : TEncodingOptions.Create; try Options.Width : 500; Options.Height : 500; Options.Margin : 4; Options.ErrorCorrectionLevel : QRErrorCorrectionLevel.H; Options.CharacterSet : UTF-8; Writer.Options : Options; Writer.Format : TBarcodeFormat.QR_CODE; Result : Writer.Write(AContent); finally Options.Free; end; finally Writer.Free; end; end;参数层面我的建议是这样Width 和 Height 直接影响打印清晰度300 像素适合屏幕展示500 像素以上适合打印不要低于这个值否则扫码枪容易识别失败。Margin 表示静区宽度单位是模块个数不是像素。二维码规范要求四周至少有 4 个模块的静区所以 Margin 设置 4 是最低标准。CharacterSet 指定编码字符集如果内容里有中文务必显式设成 UTF-8否则扫码结果往往是乱码。Writer.Write 方法内部会按 Format 属性选择编码器返回一个干净的 TBitmap后续可以直接赋值给 TImage 显示或者调用 SaveToFile 保存。4.3 识别参数怎么调Auto 模式不是万能的PossibleFormats 和 TryHarder 是两大杀器ScanManager 的第二个构造函数参数是 TDecodeHints 对象很多人习惯传 nil这在简单场景下没问题但遇到复杂背景或图像模糊你会后悔没早点研究它。先看一个针对商品条码场景的典型配置。uses ZXing.ScanManager, ZXing.BarcodeFormat, ZXing.DecodeHint; function CreateProductScanManager: TScanManager; var Hints: TDecodeHints; begin Hints : TDecodeHints.Create; try Hints.TryHarder : True; Hints.PureBarcode : False; Hints.PossibleFormats : [TBarcodeFormat.EAN_13, TBarcodeFormat.EAN_8, TBarcodeFormat.CODE_128, TBarcodeFormat.CODE_39]; Hints.CharacterSet : UTF-8; Result : TScanManager.Create(TBarcodeFormat.Auto, Hints); finally Hints.Free; end; end;这段代码的精髓在于 PossibleFormats。扫码场景里把候选格式限制为四种常用一维码解码速度比 Auto 模式快出一个数量级因为不需要逐个尝试所有格式。TryHarder 设置为 True 会增加解码器额外扫描次数包括尝试旋转图像和反转颜色对模糊或小尺寸条码有明显帮助代价是耗时翻倍。PureBarcode 一般保持 False它的含义是告诉解码器“你看到的图像里只有条码没有背景”在完美渲染的截图上能提速在相机拍摄场景里反而容易干扰定位。CharacterSet 和生成端要保持一致我在实际项目中遇到中文乱码问题九成是因为两端字符集不一致。5. 避坑指南ZXing.Delphi 使用中的 5 个高频翻车现场5.1 现象Android 上运行直接闪退Logcat 只报 SecurityException这个坑几乎是每个从桌面端转到移动端的开发者都会遇见的。ZXing.Delphi 本身不处理相机权限它只接收 TBitmap 对象。你的 FMX 工程如果用的是系统的相机控件或者你试图直接从相机预览画面取帧那么 Android 6.0 以上的运行时权限检查必须由你的工程自己完成。原因很简单Android 的相机相关 API 在无权限状态下调用会直接抛异常而 ZXing.Delphi 没有捕获这个异常的逻辑于是整个 App 闪退。解决办法是申请 CAMERA 权限后再初始化任何取帧逻辑。我用的是 Delphi 自带的 Permissions 服务在 FormCreate 里申请权限确认授权后再打开相机预览。uses System.Permissions, Androidapi.Helpers, Androidapi.JNI.Os; procedure TForm1.RequestCameraPermission; begin PermissionsService.RequestPermissions([JStringToString(TJManifest_permission.JavaClass.CAMERA)], procedure(const APermissions: TArraystring; const AGrantResults: TArrayTPermissionStatus) begin if (Length(AGrantResults) 0) and (AGrantResults[0] TPermissionStatus.Granted) then FHasCameraPermission : True else ShowMessage(没有相机权限无法扫码); end); end;这里的关键是权限回调是异步的你不能在申请权限的同一条代码路径上立刻启动相机。正确做法是设置一个状态变量在回调里判断授权结果后再去初始化扫描界面。另外要注意Android 13 以上的细粒度权限虽然不强制但建议一并处理。这个坑的根源是对平台权限模型不熟悉和 ZXing.Delphi 本身无关但它会让你误以为是库出了问题。5.2 现象FMX 相机预览画面旋转了 90 度识别结果时而正常时而失败移动设备上摄像头传感器和屏幕方向不一致天然存在一个旋转角的问题。FMX 的相机控件在不同平台上的默认旋转行为不同你在 Android 上竖屏扫码预览画面经常是横过来的。ZXing.Delphi 的 Scan 方法不会自动校正方向它只会尝试在图像内部进行旋转匹配。预览画面是横的二维码在图像里也是横着的这种情况多试几次也许能识别但识别率极不稳定。我的处理方式是在取帧之后、调用 Scan 之前对 TBitmap 做一次明确的旋转。function RotateBitmap(const ASource: TBitmap; const ADegrees: Single): TBitmap; var Center: TPointF; begin Result : TBitmap.Create(ASource.Width, ASource.Height); Result.Canvas.BeginScene; try Center : TPointF.Create(ASource.Width / 2, ASource.Height / 2); Result.Canvas.Rotate(ADegrees, Center); Result.Canvas.DrawBitmap(ASource, RectF(0, 0, ASource.Width, ASource.Height), RectF(0, 0, ASource.Width, ASource.Height), 1); finally Result.Canvas.EndScene; end; end;旋转角度的计算公式是画面左下角到右下角的连线与水平线的夹角但不同机型上这个角度只有在运行时才能确定。我建议在设置界面加一个“测试旋转角度”按钮遇到画面颠倒就让用户手动切换角度。自动判断的方案有但在 Delphi 的 FMX 相机框架下做镜像和旋转的适配工作量不小投机取巧的方法是用加速计判断手机姿态但准确率不如手动切换高。记住再怎么自动旋转后的图像宽度和高度需要互换这点容易漏。5.3 现象识别出的中文串变成一串问号或者干脆是乱码二维码内容如果是 ASCII 字符基本不会出问题。但一旦内容包含中文、Emoji 或特殊符号识别端拿到的字符串就可能变成问号或者夹杂乱码。这个问题的根源在于读取结果时没有按正确的编码解析字节流。ZXing 在识别过程中会猜测内容的编码猜测逻辑对中文并不友好。解决方法是双管齐下生成端显式指定 UTF-8识别端的 DecodeHints 里也显式指定 UTF-8。Hints.CharacterSet : UTF-8; ReadResult : ScanManager.Scan(Bmp); if ReadResult nil then Result : ReadResult.Text;另外一个容易被忽略的坑是某些带 BOM 的 UTF-8 字符串在显示时头部会多一个不可见字符。如果你发现识别出来的字符串第 0 位是空格但怎么 Trim 都去不掉其实那可能是 BOM 字符需要用它的 Unicode 码点判断后再删除。顺便说个血泪经验测试时不要只测“你好”要测“你好世界#%……*”这种混合内容才能暴露字符集处理上的问题。5.4 现象连续扫码大概第 20 次左右程序开始卡顿内存持续增长这是另一个高发问题现象是内存在每次 Scan 调用后缓慢上涨。原因有两层一是 TBitmap 占用内存没有被及时释放二是 ScanManager 对象重复创建中间又夹着 TDecodeHints 的泄漏。Timer 或者相机帧事件里每次 new 一个 ScanManager 是不行的它内部会分配大量的解码器对象这些对象释放时依赖引用计数处理不当就会累积。正确做法是整个应用生命周期只创建一个 ScanManager在扫描循环里复用同一个实例。procedure TForm1.InitScanManager; begin if FScanManager nil then begin FHints : TDecodeHints.Create; FHints.TryHarder : False; FScanManager : TScanManager.Create(TBarcodeFormat.Auto, FHints); end; end;复用的前提是同一时刻不被多个线程同时调用。如果你有后台线程在识别同时主线程又来了一帧画面要先加锁或者串行化帧处理逻辑。另外识别完一帧后如果是单次扫码应用应当立刻停止取帧并释放相机资源如果是连续扫码应用至少要控制帧率不要每帧都跑识别。手机上的 TFrame 预览帧率通常能到 30fps但 ZXing 的识别速度根本跟不上这个节奏积压的 Bitmap 队列很快会把内存吃光。把帧率限到 5fps或者只在相邻两帧差异较大时才识别效果差别不大但崩溃概率大幅下降。5.5 现象某些打印出来的二维码手机能扫但运行 ZXing.Delphi 的应用扫不出来这个问题最容易引发对库的怀疑但根因往往在图像采样精度上。手机扫不出来时它其实做了多帧叠加、动态调焦等补偿。ZXing.Delphi 处理的是静止单帧图像如果二维码分辨率不够或者打印时有墨迹渗透导致模块边界不清晰它的二值化阈值可能直接把边角吃掉。遇到这种问题我第一反应是加大输入图像的分辨率而不是盲目打开 TryHarder。打印机渲染二维码时建议用 2 倍以上冗余即码图在屏幕上 300 像素打印时导出 600 像素的 PNG。还有一个容易忽略的细节部分打印机和显示器的色彩配置会让黑色区域不纯纯黑背景上带了 2% 的白色噪点这在视觉上不敏感但到二值化阶段就变成了干扰。处理方法是在生成二维码时把背景设为纯白、前景设为纯黑不要用透明背景。6. 进阶把扫码能力封装成可复用组件并用样本集量化识别率走到这一步你已经能把识别和生成跑通了。接下来值得做的事有两件一是把散落的代码整理成工程内的公共组件二是建立自己的识别率回归样本集。先说组件封装。我习惯把扫码功能封装成一个无 UI 的扫码服务输入是 TBitmap输出是异步回调结果。这样不管是 VCL 的 Form 还是 FMX 的 Frame 都能复用不绑定具体界面。异步化的核心是 TThread 加 TThread.Queue 回主线程。type TScanCallback reference to procedure(const AResultText: string; const ASuccess: Boolean); procedure TScanService.ScanAsync(const ABitmap: TBitmap; const ACallback: TScanCallback); var LBitmap: TBitmap; begin LBitmap : ABitmap.Clone; TThread.CreateAnonymousThread( procedure var LText: string; LOk: Boolean; begin LText : DoScan(LBitmap); LOk : LText ; TThread.Queue(nil, procedure begin ACallback(LText, LOk); end); end).Start; end;这个封装有几个细节值得注意。传入的 Bitmap 先在主线程里 Clone 一份避免后台线程读取的同时主线程销毁或修改原对象这是并发问题最常见的来源。DoScan 方法必须在后台线程内部执行因为它包含同步的 Scan 调用。回调里通过 TThread.Queue 回到主线程这样拿到结果后可以直接更新界面不需要再主线程里做繁琐的 Invoke 判断。这套封装跑通之后你的扫码代码从几十行缩到一行调用任何界面都能用。样本集这件事很多人前期懒得做等到现场反馈“识别率低”才被动去调参数那时候你已经没有数据支撑来对比前后差别了。我的做法是准备 20 张真实场景图覆盖室内灯光、强光直射、阴天、轻微倾斜、模糊失焦五种情况每张图对应一个正确的期望文本。每次改了解码参数就把这 20 张图跑一遍对比正确数和耗时。这个回归测试能快速暴露改动是否引入新问题。生成端同样要建样本不同纠错级别、不同边距、不同字符集各生成几张打印出来扫一遍。ZXing.Delphi 不是覆盖所有前沿条码格式的工具比如某些深度定制的彩码它不认但它把最常见、最通用的格式做到足够稳已经覆盖了绝大多数生产需求。就我自己的使用体验而言它最大的价值不是单一功能有多强而是让 Delphi 项目在几乎没有额外成本的情况下拥有了和主流生态同等级的扫码能力。几个夜晚的调试换来的经验是遇到问题先怀疑图像质量再怀疑参数配置最后才怀疑库本身。希望这篇东西能帮你少走我走过的弯路。本文还有配套的精品资源点击获取