news 2026/9/7 8:04:38

用Delphi自研一维条形码控件:从Code128原理到扫码枪实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Delphi自研一维条形码控件:从Code128原理到扫码枪实战

简介:这是一份面向Delphi 6开发者的条形码控件源码,目标是在不安装外部插件的前提下,为老版本IDE项目补充一维条形码生成能力。控件支持Code 39与Code 128等常见制式,前者可编码数字、大写字母与部分符号,后者则能覆盖完整ASCII字符集并按内容选择子集,适合库存标签、资产盘点、发货单打印等场景。资源共17个文件,以5个.pas源码文件为核心,配合.dcu编译单元、.dfm窗体定义、.dpr工程文件以及可直接运行的.exe演示程序;另含.bmp位图和说明文档,便于对照调试。压缩包仅239KB,整体结构精简,适合按需改写。已有1269人学习下载,对想深入理解条纹编码映射、校验码计算或Delphi GDI绘图的开发者而言,是一份较完整的入门与参考样例。 做进销存项目那会儿,客户要求在发货单上打一维条形码,我第一反应是上网找个现成的ActiveX控件。试了两三个,要么license文件折腾半天,要么控件体积比整个项目还大,还有的在Win10 64位下直接没法注册。后来一咬牙,干脆用Delphi自己写了一个轻量的一维条形码控件,编码加绘制全下来不到500行,稳定跑了几个版本,扫码枪扫得比那些商业控件还顺。

这篇文章就把这套实现思路完整拆给你看。从一维条形码的编码底层逻辑,到Delphi控件的封装方式,再到打印适配和扫码枪实测中的坑,适合正在做供应链、仓储、门店零售系统的Delphi工程师参考,也适合想理解条形码原理的初学者读一读。全程用Code128和EAN-13做例子,代码可以直接抄。

1. 为什么放着现成的条形码控件不用,非要自己写一个?

先说结论:如果你的需求只是"在单据上打印一维条形码",那自研控件在成本、可维护性和可定制性上,都能打赢绝大多数商业控件。

1.1 第三方控件那些让人头疼的授权和兼容问题

早年做Delphi项目的同行应该都体会过,VCL控件生态里的条形码控件大致分两类。一类是收费的商业库,功能确实全,但你是为了一个条形码功能去买它一个全家桶,licensing文件、注册表项、运行时授权,部署到客户机器上任何一个环节漏了就是"无效的授权说明"。另一类是免费的共享控件,能用,但代码质量参差不齐,有的只支持Code39,有的绘制出来扫码率很低,有的在D7时代就停止维护了,拖到D10/D11里编译直接报错。而且这些控件底层往往依赖专门的第三方库,出了问题你根本没法调试到细节。

1.2 自研一维条形码控件,难度其实被高估了

很多人一听"自己写条形码控件"就觉得是个大工程,实际上完全不是。一维条形码的本质就是宽度不一的条和空,编码规则全部是公开标准,绘制阶段也不过是往Canvas上画一系列矩形。核心工作量就在两件事:把字符串按标准转成条空序列,然后把条空序列按像素画出来。这两件事的逻辑都是直来直去的,没有复杂算法,Delphi自带的VCL绘图能力完全够用。

我做这个控件时给自己划了个清晰的边界:只做一维码,不做二维码。二维码涉及Reed-Solomon纠错、掩码、8比特字符分段这些复杂机制,做起来确实费劲。条形码只需要查表、算校验位、画矩形,边界清晰才不会过度设计。如果未来真需要二维码,直接集成成熟的开源库就好,和这个控件互不冲突。

很多人一听到"条形码"就默认包含二维码,这是概念混淆。一维条形码和二维码在编码原理和实现难度上差着一个量级,做技术选型时一定要分开考虑。

2. 一维条形码编码的底层逻辑:以Code128为例

条形码不是乱画的黑白条纹,每条码都有严格的数学规则。咱们先理解编码原理,再谈控件实现。

2.1 条空序列的基本单位:模块(Module)

一维条形码里,最细的那个条或空就是1个模块宽度,其他条和空都是模块宽度的整数倍。比如Code128里每个字符由11个模块组成,但被切割成3个条和3个空(6段),每段的宽度是1到4个模块。扫码枪通过测量这些条空的相对宽度来解码,所以条形码能不能被扫出来,核心就是条空宽度的比例必须绝对准确,这也是后面许多坑的根源。

所以编码阶段的产物,不应该是一堆像素坐标,而是一个描述模块序列的字符串。比如某个字符的编码是"212222",意思就是:第1段条宽2模块,第2段空宽1模块,第3段条宽2模块,第4段空宽2模块,第5段条宽2模块,第6段空宽2模块。绘制阶段再去把这个模块序列换算成像素,逻辑就清晰了。

2.2 Code128:为什么它是事实上的通用码制

常见的一维码有Code39、Code128、EAN-13、UPC-A、Interleaved 2 of 5等。我做控件时第一个支持的就是Code128,原因是它密度高、支持全部128个ASCII字符、字符集可切换,在物流、制造业、内部管理场景里几乎通吃。

Code128包含三个字符集:

  • Code A:支持大写字母、数字、控制字符
  • Code B:支持大小写字母、数字、常用标点
  • Code C:只支持0-9数字,但密度翻倍,每两个数字用一个码元表示,适合纯数字长串

每个字符对应一个0到106的值,其中0-95是可视字符,96-102是功能符,103、104、105分别是Code A、Code B、Code C的起始符,106是终止符。

2.3 校验位是硬规则,算错一位整条码作废

Code128校验位的算法是:起始符的值 + 每个数据字符的值乘以它在数据中的位置序号,加总后对103取模,得到的余数就是校验字符值。用伪代码表示:

function CalcCheckDigit(StartChar: Integer; Data: string): Integer; var i, Sum: Integer; begin Sum := StartChar; // 103, 104 或 105 for i := 1 to Length(Data) do Sum := Sum + (Ord(Data[i]) - 32) * i; // Code B模式下值 = ASCII-32 Result := Sum mod 103; end;

比如编码字符串"ABC123",用Code B起始符104,那么校验和 = 104 + 65×1 + 66×2 + 67×3 + 49×4 + 50×5 + 51×6,取模103后得到校验值,再去查表找到对应字符。

有了编码值和校验位,最后一步就是查表。Code128的标准字符表网上能搜到,每个值对应一个6位数字序列,我贴一段最关键的查询逻辑:

const // Code128 符号表值,索引0-106,对应条空模式 CODE_128_PATTERNS: array[0..106] of string = ( '212222', '222122', '222221', '121223', '121322', '131222', '122213', '122312', '132212', '221213', '221312', '231212', '112232', '122132', '122231', '113222', '123122', '123221', '223211', '221132', '221231', '213212', '223112', '312131', '311222', '321122', '321221', '312212', '322112', '322211', '212123', '212321', '232121', '111323', '131123', '131321', '112313', '132113', '132311', '211313', '231113', '231311', '112133', '112331', '132131', '113123', '113321', '133121', '313121', '211331', '231131', '213113', '213311', '213131', '311123', '311321', '331121', '312113', '312311', '332111', '314111', '221411', '431111', '111224', '111422', '121124', '121421', '141122', '141221', '112214', '112412', '122114', '122411', '142112', '142211', '241211', '221114', '413111', '241112', '134111', '111242', '121142', '121241', '114212', '124112', '124211', '411212', '421112', '421211', '212141', '214121', '412121', '111143', '111341', '131141', '114113', '114311', '411113', '411311', '113141', '114131', '311141', '411131', '211412', '211214', '211232', '2331112' ); // 最后一项是STOP终止符

每个字符串的字符依次代表:条宽模块数、空宽模块数、条宽模块数、空宽模块数、条宽模块数、空宽模块数。最后一个终止符'2331112'是7段13个模块,这是Code128的特殊之处。

把起始符、数据字符、校验符、终止符对应的模式串拼接起来,就得到一整个条形码的模块序列:

function BuildCode128Pattern(const Data: string): string; var StartIdx: Integer; i, CheckSum: Integer; begin StartIdx := 104; // Code B起始符 Result := CODE_128_PATTERNS[StartIdx]; CheckSum := StartIdx; for i := 1 to Length(Data) do begin Result := Result + CODE_128_PATTERNS[Ord(Data[i]) - 32]; CheckSum := CheckSum + (Ord(Data[i]) - 32) * i; end; Result := Result + CODE_128_PATTERNS[CheckSum mod 103]; Result := Result + CODE_128_PATTERNS[106]; // STOP end;

2.4 EAN-13:商品零售场景的必选项

如果做门店POS相关系统,EAN-13就绕不开了。它的结构是固定的:前12位是数据(国家码+厂商码+商品码),第13位是校验位,和Code128完全不同。

EAN-13校验位算法是交替乘以1和3:从第1位到第12位,奇数位乘1、偶数位乘3,加总后取个位数,用10减去个位数再对10取模,得到校验位。比如前12位是"690123456789",计算过程是:

(6*1 + 9*3 + 0*1 + 1*3 + 2*1 + 3*3 + 4*1 + 5*3 + 6*1 + 7*3 + 8*1 + 9*3) = 129 校验位 = (10 - (129 mod 10)) mod 10 = (10 - 9) mod 10 = 1 完整码为 6901234567891

EAN-13的绘制规则比Code128复杂一些,左侧6位和右侧6位的编码方式不同,还涉及A/B/C三套字符集切换。但核心思路一样,先将数字字符映射到对应的条空模式,再按左侧和右侧规则分别处理,最后把校验位也编码进去。

3. Delphi控件落地:从编码到可拖拽组件的关键设计

有了编码函数,剩下就是把模块序列画到界面上,并封装成标准的VCL控件。这里有几个设计决策直接影响控件的可用性,逐个说一下。

3.1 基类选择:TCustomControl还是TGraphicControl

Delphi里自定义控件有两个常用基类:TCustomControl带窗口句柄,可以响应焦点和消息;TGraphicControl纯绘制,没有句柄、不占系统资源。条形码控件只需要显示,不需要焦点,所以我选了TGraphicControl作为父类,然后在继承链里把TPersistent的标准机制用起来,让它出现在组件面板上即可。

控件需要暴露的属性不多,核心就这几个:

  • BarCodeType:枚举,目前支持Code128、EAN-13
  • DataText:要编码的字符串
  • BarHeight:条形码高度(像素)
  • ModuleWidth:模块宽度(像素)
  • ShowText:是否在人眼可读区域显示文本
  • Color/BarColor:背景色和条色

用Published属性声明出来,写setter时记得调用Invalidate

type TBarCodeType = (bcCode128, bcEAN13); TOneDBarCode = class(TGraphicControl) private FBarCodeType: TBarCodeType; FDataText: string; FBarHeight: Integer; FModuleWidth: Integer; FShowText: Boolean; FBarColor: TColor; procedure SetDataText(const Value: string); procedure SetBarHeight(Value: Integer); procedure SetModuleWidth(Value: Integer); procedure SetShowText(Value: Boolean); procedure SetBarColor(Value: TColor); protected procedure Paint; override; public constructor Create(AOwner: TComponent); override; function CalculateTotalWidth: Integer; function GeneratePattern: string; published property BarCodeType: TBarCodeType read FBarCodeType write FBarCodeType; property DataText: string read FDataText write SetDataText; property BarHeight: Integer read FBarHeight write SetBarHeight default 50; property ModuleWidth: Integer read FModuleWidth write SetModuleWidth default 2; property ShowText: Boolean read FShowText write SetShowText default True; property BarColor: TColor read FBarColor write SetBarColor default clBlack; end;

3.2 Paint方法:画出真正的可扫描条形码

绘制阶段一个容易忽略的关键点是静区(Quiet Zone),也就是条形码左右两侧必须留出的空白区域。Code128标准要求左右至少各10个模块宽的空白,这是扫码枪识别的基础,很多人自己画条码扫不出来,第一嫌疑就是静区没留够。

Paint方法的完整逻辑是这样:

procedure TOneDBarCode.Paint; var Pattern: string; TotalModules, X, Y, I, W: Integer; IsBar: Boolean; begin inherited; Canvas.Brush.Color := Color; Canvas.FillRect(ClientRect); if (FDataText = '') then Exit; Pattern := GeneratePattern; // 总模块数 = 条空模式字符数求和(每字符代表该段模块数) TotalModules := 0; for I := 1 to Length(Pattern) do TotalModules := TotalModules + StrToInt(Pattern[I]); // 自动适应:如果控件的Width不够,自动缩小模块宽度 // 但不要小于1像素,否则扫码枪很难识别 X := 10; // 静区起始,至少10像素,标准要求按模块算 Y := 1; IsBar := True; // 每个符号的第1位永远是条 for I := 1 to Length(Pattern) do begin W := StrToInt(Pattern[I]) * FModuleWidth; if IsBar then begin Canvas.Brush.Color := FBarColor; Canvas.FillRect(Rect(X, Y, X + W, Y + FBarHeight)); end; Inc(X, W); IsBar := not IsBar; end; // 绘制人眼可读文本 if FShowText then begin Canvas.Font := Font; Canvas.Brush.Style := bsClear; Canvas.TextOut(ClientWidth div 2 - Canvas.TextWidth(FDataText) div 2, Y + FBarHeight + 2, FDataText); end; end;

绘制时有一个非常实操的小细节:模块宽度最好保持整数,在OnPaint里根据ClientWidth自动反算。比如控件拉到200像素宽,总共120个模块,那就让模块宽度为200 div 120得到1像素,再以1像素为基准、以静区起始为对齐点去绘制,这样条空比例严格一致,不会因为浮点取舍导致后面积累误差。

3.3 双缓冲和缩放:别让屏幕绘制拖后腿

TGraphicControl自带简单的绘制机制,但在Windows下窗体刷新时很容易产生闪烁。我用的办法是重写Paint时做一个内存位图,画完一次性BitBlt到Canvas上:

procedure TOneDBarCode.Paint; var Bmp: TBitmap; begin Bmp := TBitmap.Create; try Bmp.Width := Width; Bmp.Height := Height; // 所有绘制逻辑画到Bmp.Canvas上 // 最后一下子画到控件上 Canvas.Draw(0, 0, Bmp); finally Bmp.Free; end; end;

一个中等长度的Code128条码大概100多个模块,画200个矩形对CPU来说微不足道,瓶颈其实在频繁的Invalidate上。所以我在SetDataText里只在数据真正变化时才触发Invalidate,不搞无意义重绘。

这个控件我用了双缓冲之后,在Win10高分屏下缩放窗口也没有闪烁。如果不用双缓冲,拖动窗口边缘时条码区域会留下一串残影。

4. 实测中的踩坑记录:扫码枪不认账的几种原因

代码写得再漂亮,最终要过扫码枪这一关。以下是实际调试中碰到的问题,以及对应的排查链路。

4.1 静区不足,扫码枪就是不出声

第一个版本我偷懒,把静区做成了固定10像素。结果用超市那种老式激光枪扫很顺,换成客户仓库里的国产CCD扫码枪,成功率只有六成。后来把Code128左侧静区按10个模块宽度来算,问题立刻消失。

原因在于不同扫码枪的激光头光斑大小不同,CCD式扫描器需要一个"起始判定区域"来锁定条空比例。如果左侧空白太窄,扫描器把金属支架、纸张边缘误判成条的一部分,解码就直接失败。

4.2 打印出来的条码模糊:DPI换算的教训

屏幕上看很清晰的条码,用针式打印机打出来扫不了,这是另一个经典问题。做控件时我只考虑了屏幕的96DPI,打印时Windows GDI会做缩放,导致条和空的边界出现灰度渐变,1像素宽的细条直接被墨水糊掉了。

解决办法是打印时单独走一条路径:把ModuleWidth按打印机的DPI换算,比如屏幕96DPI下2像素的模块,在300DPI打印机上对应6像素多,要取整并保证奇数偶数关系稳定。我最后用了一个简单策略:在Form的OnPaint和打印代码里分别维护两套绘制函数,打印版直接以1/300英寸为单位绘制,不经过屏幕DPI换算。

如果只是偶尔打标签,用FastReport这类报表工具时,建议把条形码控件放在一个独立Frame里,用Canvas.StretchDraw做整数倍放大,而不是让GDI自动做非整数缩放,糊的概率会小很多。

4.3 中文字符串怎么处理

Code128的标准字符集只覆盖ASCII,虽然有个FNC4扩展可以切换成Latin-1字符集,但对CJK字符无能为力。实际项目里如果需要在条码里体现中文,常见的做法是对中文字符串做GBK编码后转成十六进制文本,再把这个十六进制字符串编码进Code128。

比如"批次A01"这样一个字符串,你先用GBK得到字节序列,把每个字节转成两位十六进制,得到一串纯ASCII字符,再用Code128编码。扫码枪扫出来的是十六进制文本,你自己在系统里转回中文。注意这个方案要避开Code128里字符串中出现EAN-13那种纯13位数字的情况,否则会被扫成Code C造成歧义。

4.4 高度不够、条宽过细,别等上了产线才后悔

条形码高度的经验值是不低于15mm,实际项目我一般做20mm以上。扫码枪的扫描线是一条线性光,如果条码太矮,光斑扫到上方或下方的空白区域,解码就中断了。

模块宽度的经验值是不小于0.25mm,也就是300DPI下约3像素。条宽小于这个阈值,普通扫码枪基本就瞎了。这些参数不是标准强制规定的,而是大量实测总结出来的经验边界。

下面是一个快速排查表,我在项目里直接贴在测试工位上:

现象最常见根因快速验证方法
扫码枪永远没反应静区不足左右各加10模块空白再试
远距离扫不了,贴很近才能扫模块宽度太小把ModuleWidth上调0.1mm
打印件扫不了,屏幕件能扫DPI换算错误用300DPI位图先打印测试页
部分码能扫,部分码不能扫校验位算错用标准扫码App查解码内容
扫出来一串数字和原串不一致Code C字符集误判检查纯数字自动切Code C的逻辑

5. 后续还能怎么扩展这个控件

这个条形码控件的核心价值在于它是自包含、无外部依赖的,所以扩展起来也很便利。

5.1 增加Code39和Interleaved 2 of 5

Code39的实现复杂度更低,字符表只有43个字符,校验位可选,逻辑比Code128简单一个档次,适合企业内部做固定资产标签。Interleaved 2 of 5则是把两个数字交叉编码,密度高,适合印刷在纸箱上的外箱码。扩展方式都类似:写一个GeneratePattern的分支函数,在TBarCodeType枚举里加两个值,再补上对应的查表数据即可。

5.2 导出矢量图

画到屏幕或位图上,放大后必然失真。对需要印刷Logo和条码在同一张图上的场景,可以加一个SaveToEMF方法,用TCanvasMetafile绘制,这样在AI里无限放大也是清晰的。核心代码就几行,但客户满意度提升是肉眼可见的。

5.3 和报表工具协同工作

自绘控件直接放在Form上没有问题,但放到FastReport或Rave Reports里作为组件就会出现兼容性问题。折中方案是:生成模块序列后保存成字符串,报表里用一个条形码形状或图片来承接,用控件生成一张BMP或EMF图片,再塞进报表。这样保持了报表设计的所见即所得,也避开了报表引擎对VCL控件的渲染限制。

6. 最后再分享一个我摸出来的真实经验

整套控件写下来,我最深的体会是:条形码控件90%的价值都在编码正确性上,绘制只是锦上添花。编码标准里每一个细节——校验位、起始符、终止符、字符集切换时机、静区最小宽度——全是硬规则,漏一项,扫码枪的识别率就打折。而绘制再炫酷,条空比例不对也是白搭。

我自己习惯在正式发布前,拿一次实际业务里最长的字符串、最纯的数字串、带特殊符号的字符串,分别打到热转印纸、A4纸和普通标签纸上,用三把不同厂家的扫码枪各扫50次记录成功率。这个测试用例一直留在工程里,每次改动编码逻辑都会重新跑一遍,比什么单元测试都好使。

你如果正在写类似的控件,留好这个测试用例,后面省心程度不是一点半点。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 8:02:25

C#上位机调用VisionPro实战:ToolBlock加载、扫码触发与九点标定

简介:一份面向C#机器视觉开发者的源码示例,演示如何调用VisionPro库实现图像圆形检测。方案基于Cognex.VisionPro_dotNET,通过CogFindCircleTool工具完成读取图像、设定半径范围、执行查找并显示结果,适用于制造质检、尺寸测量等自…

作者头像 李华
网站建设 2026/9/7 7:57:42

数模竞赛AI智能体搭建:从知识库向量检索到代码生成全流程

华数杯数学建模竞赛有一个很真实的情况:比赛时间紧张,赛题发散,论文要求高。很多队伍不是不会建模,而是卡在“怎么把思路快速落地成代码,再快速整理成论文素材”。我这次想分享的,是一个可以随手用的AI智能…

作者头像 李华
网站建设 2026/9/7 7:57:37

糖类NMR数据处理实战:峰拾取、相位校正与耦合常数估算工具解析

简介:面向网络管理员与运维人员的SugarNMSTool 2.0是一款针对华为交换机SNMP管理监控的实用工具,核心价值在于通过OID查询快速定位网络中开启SNMP服务的华为设备,从而简化设备发现与状态监控流程。资源包共8个文件,以Java程序包&a…

作者头像 李华
网站建设 2026/9/7 7:55:19

Holonic Asset开源解析:2D像素风素材生成平台选型、使用与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华