简介: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 和 PossibleFormats
4.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: TArray<string>; const AGrantResults: TArray<TPermissionStatus>) 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 项目在几乎没有额外成本的情况下,拥有了和主流生态同等级的扫码能力。几个夜晚的调试换来的经验是:遇到问题先怀疑图像质量,再怀疑参数配置,最后才怀疑库本身。希望这篇东西能帮你少走我走过的弯路。
本文还有配套的精品资源,点击获取