ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Delphi自绘一维条形码控件:从编码算法到打印适配完整实战

Delphi自绘一维条形码控件:从编码算法到打印适配完整实战 简介面向 Delphi 6 开发者的一维条形码控件源码包可在不依赖任何插件的情况下生成 Code 39、Code 128 等常见一维码并支持打印输出适用于库存管理、产品标识、数据追踪等场景。压缩包共包含 17 个文件涵盖 5 个 pas 源码、4 个 dcu 编译单元、1 个 dfm 窗体文件、1 个 dpr 工程文件等并附有可直接运行的演示程序 bctest.exe 与说明文档整体体积仅 239KB。目前已有 1269 人学习这份资源。通过阅读源码可深入理解条形码字符到条纹模式的转换原理、Code 128 的子集选择与校验码计算、Delphi 图形 API 的绘图用法以及打印布局与缩放处理等关键实现对于正在维护老项目或需要离线生成条码的开发者这套代码能直接移植复用也可作为学习条形码控件设计的完整范例。 我前阵子有个项目需要在 Delphi 里生成并打印一维条形码翻遍了开源社区和商业控件要么体积太大要么 License 卡得死。最后干脆自己写了一个一维条形码控件用原生 VCL 实现不依赖任何第三方库Code39、Code128、EAN-13 都能出扫描枪实测一次性通过率很高。这篇就把完整思路、编码算法、绘制代码、踩坑记录都捋一遍希望能帮你少走弯路。如果你也想在 Delphi 里做条形码生成、打印、屏幕显示又不想被商业控件捆绑这篇就是给你准备的。1. 整体方案设计为什么自绘控件而不是用现成的先说结论条形码的生成逻辑本身并不复杂核心就是把字符转成“条”和“空”的宽度序列再绘制出来。真正麻烦的是细节校验位、起始符、终止符、静区宽度、缩放质量、打印适配。这些细节决定了扫描枪能不能稳定识别。1.1 为什么不用第三方条形码控件确实有很多现成方案比如 TBarCode、ZXing 的 Delphi 移植版、FastReport 内置的条码对象。但我这次的需求有几个约束需要集成到一套老的 Delphi 7 代码库里部分第三方控件不支持。客户环境不允许额外安装运行时需要纯 VCL 静态编译。打印场景多需要精细控制条码尺寸和分辨率。需要同时输出到屏幕预览和打印机高分DPI要求绘制引擎能复用。商业控件功能全但黑盒居多出了问题不好排查。自绘则每一个像素的行为都可控调起来更顺手。1.2 编码方案选型Code39、Code128、EAN-13 到底怎么选一维条形码的编码标准很多实际项目中 90% 的场景用下面三种就够码制字符集长度应用场景特点Code39大写字母、数字、部分符号不定长工业、资产管理、内部标签实现简单无强校验要求但密度低Code128ASCII 全字符128个不定长物流、电商、贸易密度高支持全字符需计算校验位EAN-13纯数字固定13位零售、商品条码需严格校验位结构固定我这套控件一开始就做了多码制抽象顶层接口统一是Generate(Code: string): TBarcodeBitmap内部按码制分发到不同编码器。这样后续加新码制比如 ITF-14、UPC-A不需要改绘制引擎。1.3 控件架构TCustomControl 子类化控件基类继承自TCustomControl核心职责是三个接收输入的字符串和码制参数。调用编码模块生成条空宽度数组。在Paint中根据宽度数组进行绘制同时处理缩放、颜色、静区。接口设计大概这样type TBarcodeType (btCode39, btCode128, btEAN13); TBarCodeCtrl class(TCustomControl) private FBarcodeType: TBarcodeType; FCode: string; FBars: array of Byte; // 每个元素表示一个模块的宽度0空1条 procedure GenerateBars; protected procedure Paint; override; public property Code: string read FCode write FCode; property BarcodeType: TBarcodeType read FBarcodeType write FBarcodeType; end;这里最核心的FBars数组是条空序列的“中间表示”绘制引擎只认这个数组不关心具体码制。这样做的好处是编码和渲染彻底解耦后面如果想支持 2D 码只要扩展编码模块即可。2. 核心编码算法从字符到条空的完整推导2.1 Code39 的编码映射Code39 的逻辑相对直白每个字符由 9 个模块组成其中 5 个条、4 个空条和空各有 3 个宽、6 个窄。宽条/宽空表示二进制的 1窄条/窄空表示 0。以字符A为例它的 Code39 模式是110100001011这是条空序列1 宽 0 窄。要用代码表达我建了一张映射表const Code39Table: array[0..43] of string ( 110110101, // 0 110100101, // 1 110100110, // 2 110101100, // 3 110101001, // 4 110011010, // 5 110011001, // 6 110010110, // 7 110010101, // 8 110001101, // 9 101101101, // A // ... 省略中间 101001001 // * );注意表中的1代表宽元素0代表窄元素。但光有宽窄还不够实际绘制时还要把每个宽元素换算成像素。用一个变量ModuleWidth控制窄条的宽度宽条就是ModuleWidth * 2或ModuleWidth * 3不同标准有差异。2.2 Code39 的起始符和终止符Code39 规定条码串必须以*开头和结尾。这个*字符本身有对应的编码模式同时它不参与数据内容。我在这里踩过一次坑一开始没加终止符扫描枪能读出来一部分但偶尔会漏字符。后来补全了起始和终止符识别率立刻上来了。这属于条码规范里的细节但直接影响成败。2.3 Code128 的编码原理Code128 比 Code39 复杂它的字符集有三个版本Code A、Code B、Code C。分别对应大写字母控制符、大小写字母符号、纯数字对。条码中间可以切换字符集用特殊的 SHIFT 或 CODE 切换符。Code128 的每个字符由 11 个模块组成条和空交替排列。编码表有 107 个条目103 个数据字符 3 个起始符 1 个切换符 1 个终止符。这个表在 Delphi 里直接用常量数组存即可不用自己推导。校验位算法起始符的值为 103Code A、104Code B或 105Code C。校验位 起始符值 第1位数据值×1 第2位数据值×2 ... 第N位数据值×N mod 103。实现代码function CalcCode128CheckSum(const Values: array of Integer): Integer; var I, Sum: Integer; begin Sum : Values[0]; // 起始符 for I : 1 to High(Values) do Sum : Sum Values[I] * I; Result : Sum mod 103; end;注意这里的索引从 1 开始乘很多初次实现的人会把索引从 0 开始乘导致校验位算错。我用上面的代码在几个不同长度字符串下对比过 ZXing 的校验值完全一致。2.4 条空序列的宽度数组生成无论哪种码制最终都要生成一个形如条2、空1、条3、空2、条1...的序列。我是这样处理的procedure AppendBars(var Bars: array of Byte; Pattern: string; const ModuleWidth: Integer); var I: Integer; Count: Integer; begin Count : 0; for I : 1 to Length(Pattern) do begin if Pattern[I] 1 then Count : ModuleWidth * 2 // 宽元素2倍模块宽 else Count : ModuleWidth; // 窄元素1倍模块宽 SetLength(Bars, Length(Bars) 1); Bars[High(Bars)] : Count; end; end;这里的关键决策是数组里存的是“宽度值”而不是“条/空状态”。这样绘制时只需要顺序切换颜色不用每次判断当前是条还是空。2.5 EAN-13 的校验位细节EAN-13 的校验位算法是标准的模10算法奇数位权重1、偶数位权重3。比如690123456789的前12位校验位计算是function EAN13CheckDigit(const 12Digits: string): Integer; var I, Sum: Integer; begin Sum : 0; for I : 1 to 12 do begin if (I mod 2) 0 then Sum : Sum StrToInt(12Digits[I]) * 3 else Sum : Sum StrToInt(12Digits[I]) * 1; end; Result : (10 - (Sum mod 10)) mod 10; end;EAN-13 的条空结构是每个数字由 7 个模块组成分为左侧和右侧两套编码左侧奇偶校验位决定右侧全偶。这个细节比较多我建议初学时先跑通 Code39再上 EAN-13。EAN-13 的绘制并不难难的是左侧 6 位数字的奇偶组合规则由第一位数字决定这部分对照标准表映射即可。3. 绘制引擎的实现与优化3.1 基于 TCanvas 的自绘实现绘制部分的核心就是在Paint方法里遍历宽度数组交替画黑条和留白procedure TBarCodeCtrl.Paint; var X, I: Integer; BarWidth: Integer; IsBar: Boolean; begin inherited; if Length(FBars) 0 then Exit; X : FLeftMargin; IsBar : True; // 第一个元素是条 for I : 0 to High(FBars) do begin BarWidth : FBars[I] * FScale; if IsBar then begin Canvas.Brush.Color : clBlack; Canvas.FillRect(Rect(X, 0, X BarWidth, Height)); end; X : X BarWidth; IsBar : not IsBar; end; end;这段代码的关键点用一个布尔变量IsBar切换当前是条还是空。因为条码的条和空总是交替出现所以这里用一个翻转标志就行。空的部分不需要画直接X往后偏移。3.2 绘制质量的几个关键参数条形码绘制不是简单画几根线就完事以下几个参数影响识别率静区Quiet Zone条码左侧和右侧必须留白宽度至少是 10 倍窄条宽度。否则扫描枪会误判起始位置。我的控件里默认加了 12 倍窄条宽度实测识别率最高。条高一维码的条高通常要求不低于条宽的 15 倍针对 Code39 而言否则部分扫描枪在倾斜角度下会丢线。缩放屏幕预览用 1:1 可能太小打印时需要按 DPI 缩放。我的做法是暴露一个Scale属性绘制时统一乘上去。Scale的计算方式在打印场景很重要。打印机 300 DPI 和 600 DPI 下同样的物理尺寸对应的像素数差一倍。我写了一个方法按目标打印 DPI 自动计算缩放倍数function GetPrintScale(const DPI: Integer): Integer; begin // 假设窄条的基础宽度是 0.01 英寸 Result : Round(DPI * 0.01); end;如果 300 DPI 下窄条宽度是 3 像素那么 600 DPI 下就是 6 像素实际打印出来的物理宽度才一致。3.3 高分屏和打印场景的适配Delphi 7 时代的控件在 Windows 高分屏下经常出现模糊、错位。我的处理方式是设置TForm.Scaled : False坐标系走物理像素。绘制时通过GetDeviceCaps获取当前 DC 的 DPI动态计算缩放。屏幕预览时按窗体 DPI 适配打印时重新按打印机 DPI 生成。这里有个细节TControl 的Width、Height在 Windows Form Designer 里是以逻辑像素为单位但在不同 DPI 下实际物理尺寸会变。我干脆不在设计期固定条码尺寸而是在Resize事件里根据 DPI 动态调整。3.4 输出到位图方便嵌入报表除了直接画在控件上我还提供了一个RenderToBitmap(Dpi: Integer): TBitmap方法这样条码可以嵌到 FastReport、Rave Report 或者其他报表工具里。function TBarCodeCtrl.RenderToBitmap(const Dpi: Integer): TBitmap; var Scale: Integer; BmpWidth, BmpHeight: Integer; begin Scale : GetPrintScale(Dpi); BmpWidth : (TotalModules QuietZoneModules * 2) * ModuleWidth * Scale; BmpHeight : BarHeight * Scale; Result : TBitmap.Create; Result.Width : BmpWidth; Result.Height : BmpHeight; Result.Canvas.Brush.Color : clWhite; Result.Canvas.FillRect(Rect(0, 0, BmpWidth, BmpHeight)); DrawBarcode(Result.Canvas, Scale); end;生成的位图可以直接赋值给报表的TImage或者存成 BMP 文件。4. 实测问题与排查记录扫描枪为什么读不出来写控件容易调通识别率才是坑。我把测试中遇到的高频问题和排查方法整理了一下。4.1 问题一缺少静区现象条码打印出来后扫描枪有时能读有时不能读且条码越靠近标签边缘越难扫。排查看条码左右两侧的空白区域。很多标签设计者为了省空间把条码贴着标签边缘放两侧静区不足。解决在设计界面强制预留静区。我在Paint里先画一个白底矩形再在水平方向留出QuietZone的像素宽度。实测静区宽度设为窄条宽的 12 倍时误读率降为 0。4.2 问题二Code39 的宽窄比例不当现象条码扫描枪贴近了能读距离 10cm 以上就完全读不出来。或者条码很窄的时候能读放大后反而读不出。排查Code39 对宽窄比有要求一般在 2:1 到 3:1 之间。如果实际绘制时窄条 1 像素、宽条 3 像素比例是 3:1扫描枪的算法能接受。但如果窄条 1 像素、宽条 2 像素某些老式扫描枪会卡在阈值判断上。解决我把窄条定义为 1 个模块宽宽条定义为 2 个模块宽模块宽度本身可调。代码里默认ModuleWidth : 2宽条就是 4 像素宽窄比正好 2:1。4.3 问题三Code128 校验位错误现象Code128 条码打印出来后扫描枪显示“无效的条形码”或干脆不识别。排查拿一个已知正确的 Code128 条码手动计算校验位对比程序输出的结果。我在这里犯过的典型错误起始符的值计算进去时索引从 0 开始。Code128 的校验公式规定起始符的权重为 1即乘 1第一个数据字符权重为 2依次递增。如果把起始符也按 0 权重算校验位就会不对。另外还有字符集切换符的处理。Code128 在字符串中间遇到无法用当前字符集表示的字符时必须插入切换符。这个逻辑如果漏了生成出来的条码长度对但内容完全错。4.4 问题四像素对齐模糊现象条码打印出来边缘发虚扫描枪识别率低。排查缩放不是整数倍时部分线条宽度变成了小数GDI 绘制时做了抗锯齿。解决在打印场景下禁用抗锯齿并确保所有宽度值都是整数像素。我的做法是先将缩放比例算成一个有理数然后用Round落到整数而不是让 GDI 去处理小数坐标。Canvas.Pen.Style : psSolid; Canvas.Pen.Width : 1;核心代码里还有一个细节FillRect的矩形边界要用Rect(X, 0, X BarWidth, Height)这里右边界不包含X BarWidth本身所以多个条之间的间隔不会重叠也不会漏缝。4.5 问题五低 DPI 下条码过密现象屏幕上看很清楚保存成 96 DPI 的图片后放大到 200% 全是锯齿且扫描枪对密集条码识别率下降。排查96 DPI 下 1 像素对应的物理宽度很大如果条码总长度太短条的物理宽度可能超过 0.5mm部分扫描枪反而识别不了因为条空比例变形。解决我用一个简单的规则计算窄条的物理宽度PhysicalWidthMm : ModuleWidthPx / Dpi * 25.4;如果结果小于 0.25mm 或大于 0.6mm就调整ModuleWidth重新生成。通常窄条物理宽度在 0.33mm 左右识别率最优。5. 工具选型与性能优化实测5.1 Delphi 版本兼容性我最早在 Delphi 7 上跑通后面迁移到 Delphi 10.4、11.3 也没出问题。代码里只用了 TCustomControl、TCanvas、TBitmap 这些基础 VCL 类不涉及任何新语法特性所以老项目拿来即用。如果你用的是 Delphi 11.3 之后的版本注意TCanvas.FillRect的行为在 Per-Monitor V2 DPI 下可能受主题影响建议在窗体OnCreate里手动设置TForm.Scaled : False。5.2 常用第三方条形码控件对比参考自绘虽然是本次的选择但在某些快速交付场景下用成熟控件能省不少时间。这里顺便整理几个市面上常见方案的对比供你参考方案支持的码制许可证部署难度适用场景TBarCode SDKCode39/128/EAN/UPC/二维码等商业授权需注册 DLL商用系统、复杂需求ZXingDelphi 移植条码二维码Apache 2.0需编译进工程偏解码场景FastReport 内置条码对象多种商业授权依赖 FastReport报表打印场景自绘控件本文方案按需扩展完全自主零依赖任意定制化场景如果你只用一个码制比如 Code39自绘是性价比最高的如果需要全量码制且商用发行商业控件反而更省心因为条码标准和专利问题已经被它们处理过了。5.3 性能优化长字符串批量生成有次测试遇到一个需求一次生成 5000 个 Code128 条码物流面单场景要控制总耗时在 2 秒内。最初的实现每次调用都重新计算校验位、重新生成条空数组、再构建位图纯 CPU 部分耗时约 3.5 秒。优化方案对相同的字符串做缓存用TDictionarystring, TBitmap重复内容直接复用。字符串转条空数组的过程不建位图先存到一个TBarcodeData结构里。最后批量绘制时使用 Canvas 的批量绘制模式锁定窗口 DC一次性输出。Delphi 的TBitmap.Canvas.Lock能避免 GDI 频繁切换实测批量生成耗时从 3.5 秒降到 1.2 秒。5.4 打印时的条码质量用矢量输出而不是位图如果条码是直接传到打印机建议用 GDI 画到打印机 DC 上而不是先生成位图再拉伸。位图在 600 DPI 下放大到 300 DPI 的打印机会糊反过来则可能出现锯齿。核心思路是让打印机 DC 自己处理缩放。我在打印模块里直接调用DrawBarcode(Printer.Canvas, PrintScale)打印机驱动会按物理分辨率渲染出来的效果最锐利。6. 扩展思路条码控件还能怎么玩写完这个基础控件后我又顺手做了几个扩展如果你有时间可以参照试试。6.1 支持 Code128 的字符集自动切换Code128 的 Code A、Code B、Code C 可以混排让条码长度更短。比如字符串ABC123用纯 Code B 编码是起始B ABC123 校验 终止长度较长改成Code B起始 ABC 切换Code C 12 30 校验 结束长度更短。这个切换逻辑需要动态规划或贪心算法。Delphi 里用 for 循环贪心选择就能做到不需要上复杂算法。实测效果数字较多的字符串比如 20 位以上用 Code C 自动切换后条码长度减少 40% 左右打印密度和识别率都更好。6.2 在字符串里直接显示内容在条码下方加一行可读文本这个在物流标签里几乎必备。实现很简单在Paint方法末尾用TextOut输出即可。Canvas.Font.Name : Arial; Canvas.Font.Size : 9; Canvas.TextOut(X, BarHeight 2, FCode);注意文字不要覆盖条码区域这里我把条码高度单独空了出来文字放在条码下方。6.3 多码制混合的自动检测做一个Auto模式让控件自己判断输入内容适合用哪种码制纯数字且长度 13 且符合 EAN 规格则用 EAN-13纯数字且长度较长则用 Code128C含大小写字母则用 Code128B。这个判断逻辑很简单加上之后调用方就完全不用操心码制选择。写在最后我在这个控件的实际开发过程中最大的体会是条形码根本不是“画线”那么简单编码规则、校验位、静区、缩放、DPI 适配每一步都直接影响终端扫描枪的识别效果。如果你只是临时用一下直接调 ZXing 省事但如果这个条码控件要长期集成在自己的产品里自绘一次后面所有定制都能自己说了算这个投入是值得的。另外再提醒一句在做 Code128 的时候校验位的索引真的真的从 1 开始乘别问我是怎么知道的。希望能帮上你。本文还有配套的精品资源点击获取
返回列表