资讯详情

资讯详情

Delphi纯代码实现一维条码控件:Code39与Code128编码原理及实战

简介这份资源面向仍在使用Delphi 6的开发者提供一套无需任何外部插件即可生成一维条形码的控件源码适用于库存管理、产品标识、标签打印等需要条码编码与输出的场景。压缩包共17个文件约239KB以pas源码、dcu编译单元、dfm窗体、dpr工程文件为主另含bmp位图、res资源、htm说明及可执行文件覆盖从编码逻辑到界面演示的完整结构。控件支持Code 39与Code 128等常见一维码制通过代码完成字符到条纹模式的转换并借助Delphi图形API绘制条码同时包含打印输出与校验码计算等处理便于直接集成到项目中。目前已有1272人学习下载读者可从中理解条码编码原理、图形绘制与打印布局的实现思路并参考源码完成二次开发与调试。1. 从一张打印歪掉的条码说起这个控件到底解决什么问题做过零售、仓储或者医疗耗材管理系统的朋友大概都遇到过这种场景客户现场用针式打印机打标签条码枪扫十次能错三次回头一看是条码宽度被拉伸了或者 Code128 的校验位算错了。更麻烦的是很多项目跑在内网机器上装不了第三方条码字体也不方便引入外部 DLL这时候一个能直接嵌进 Delphi 工程的纯代码条码控件就成了刚需。这份delphi代码实现一维条形码控件.rar就是干这个的——它用 Delphi 原生代码实现了 Code39 和 Code128 两种最常用的一维条码生成逻辑不依赖任何插件、不依赖条码字体编译进项目就能用。适合谁适合还在维护 Delphi6/Delphi7 老系统、需要在报表或标签上动态生成条码的开发者也适合想搞清楚一维条码编码原理、打算自己改造出 EAN13 或 ITF 的人。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这个包拆开讲一遍。2. 技术选型与目录结构为什么是纯代码而不是字体方案2.1 纯代码生成 vs 条码字体两条路线的取舍在 Delphi 里做一维条码业内常见做法无非三种一是买商业条码字体比如把 Code128 做成 TrueType二是调用外部 DLL 或 ActiveX 控件三是纯代码自己画。这个项目选了第三条路原因很实际。字体方案的问题在于Code128 是变长编码不同字符集切换需要动态计算校验位和起始符字体本身没法帮你算你得自己拼字符串再交给字体渲染一旦拼错就是扫不出来而且字体方案在针式打印机上容易因为字距被拉伸导致条码失真。外部 DLL 方案则带来部署问题老项目往往要求绿色免安装多一个 DLL 就多一份注册和位数匹配的麻烦。纯代码方案的核心思路是把每个字符映射成一组黑白条空宽度序列然后用 Canvas 或直接操作像素逐条画出来。这样做的好处是宽度完全可控你可以精确到 0.1mm 去设置窄条宽度打印出来不会变形坏处是代码量比调字体大得自己维护编码表。这个包里的实现就是典型的「编码表 绘制」结构Code39 相对简单43 个字符加起始结束符*每个字符固定 9 个元素5 条 4 空其中 3 个是宽元素Code128 复杂一些分 A/B/C 三个字符集需要处理起始符、校验位和停止符。提示如果你的项目只需要 Code39 且对宽度精度要求不高字体方案确实更省事但只要涉及 Code128 或者要精确控制模块宽度纯代码方案几乎是唯一稳妥的选择。2.2 目录结构一个单元文件撑起整个控件解压后目录不会太复杂典型结构是这样BarcodeControl/ ├── Barcode.pas // 核心编码与绘制单元 ├── BarcodeDemo.dpr // 演示程序工程文件 ├── BarcodeDemo.res // 工程资源 ├── Main.pas // 演示窗体逻辑 ├── Main.dfm // 演示窗体设计 └── README.txt // 简要说明核心就是Barcode.pas这一个单元。它里面一般会定义一个TBarcode类继承自TGraphicControl或者直接是一个非可视组件对外暴露Text、BarcodeType、BarWidth、Height、ShowText这些属性内部用procedure DrawBarcode(ACanvas: TCanvas; X, Y: Integer)完成绘制。演示程序Main.pas里通常放一个TEdit输入内容、一个TComboBox选类型、一个TPaintBox或直接重写FormPaint来显示结果。这种「一个单元 一个 Demo」的组织方式很符合 Delphi 老项目的习惯拷贝Barcode.pas到你的工程目录在uses里加上就能用不需要注册组件面板。2.3 编码表的数据结构数组还是常量字符串打开Barcode.pas你会看到编码表通常用常量数组实现。Code39 的编码表常见写法是const // Code39 编码表每个字符对应 9 位1 表示宽0 表示窄 // 顺序为 条、空、条、空、条、空、条、空、条 Code39Table: array[0..43] of record Ch: Char; Code: String[9]; end ( (Ch: 0; Code: 000110100), (Ch: 1; Code: 100100001), // ... 中间省略 (Ch: *; Code: 010010100) // 起始/结束符 );这里每个字符的Code是 9 位字符串1代表宽元素0代表窄元素。为什么是 9 位因为 Code39 规定每个字符由 5 个条和 4 个空交替组成共 9 个元素其中恰好 3 个是宽的。字符之间用一个窄空隔开。这种「固定 9 元素、3 宽 6 窄」的规则让 Code39 的密度比较低但容错性好老式条码枪识别率很高。Code128 的编码表就不能用这种简单字符串了因为它有 107 个符号含起始符、校验符、停止符每个符号是 11 个模块宽用 6 个元素3 条 3 空表示宽度值从 1 到 4 不等。常见做法是定义一个array[0..106] of array[0..5] of Byte每个元素存该条或空的模块数。比如字符0在 Code128B 下对应值 16其模式是(2,1,2,2,2,2)加起来正好 11 个模块。校验位的计算是起始符值 各字符值 × 位置权重然后对 103 取模。function CalcCode128CheckSum(const Data: array of Byte): Byte; var I, Sum: Integer; begin Sum : Data[0]; // 起始符值 for I : 1 to High(Data) do Sum : Sum Data[I] * I; // 位置权重从 1 开始 Result : Sum mod 103; end;这段校验逻辑是整个 Code128 里最容易翻车的地方。权重是从起始符之后第一个字符开始算 1不是从 0 开始也不是从起始符算 1。我见过有人把起始符也乘了权重结果生成的条码扫出来永远是乱码。参数Data是包含起始符的字节数组返回的Result就是校验符的值再查表拿到对应的 11 模块模式追加到输出序列末尾最后加上停止符(2,3,3,1,1,1,2)就完成了。3. 核心模块拆解从字符串到黑白条的完整链路3.1 Code39 编码流程起始符、数据、结束符三段式Code39 的编码逻辑相对直白整个流程可以拆成三步加起始符*、逐字符查表展开、加结束符*。下面是一个简化后的核心函数function TBarcode.EncodeCode39(const AText: string): string; var I, J: Integer; Ch: Char; Pattern: string; begin Result : ; // 起始符 Result : Result GetCode39Pattern(*) 0; // 末尾补窄空 // 数据字符统一转大写Code39 只支持大写 for I : 1 to Length(AText) do begin Ch : UpCase(AText[I]); Pattern : GetCode39Pattern(Ch); if Pattern then raise Exception.CreateFmt(字符 %s 不在 Code39 字符集中, [Ch]); Result : Result Pattern 0; // 字符间窄空 end; // 结束符 Result : Result GetCode39Pattern(*); end;GetCode39Pattern就是查前面那张常量表把 9 位字符串返回。注意每个字符后面补的0是字符间隔的窄空最后一个结束符后面不需要补。返回的Result是一个由0和1组成的长字符串1表示宽条或宽空0表示窄条或窄空。接下来绘制时遍历这个字符串遇到1就画宽遇到0就画窄同时根据当前位置是奇数位还是偶数位决定画黑条还是留白。这里有个容易忽略的点Code39 的宽窄比。标准建议宽元素是窄元素的 2 到 3 倍常见取 2.5 倍或 3 倍。如果窄条宽度设为 1 像素宽条就是 2 或 3 像素。比例太小条码枪可能区分不出来比例太大条码会过长。项目里一般把BarWidth作为窄条宽度属性暴露出来宽条宽度内部按BarWidth * 3计算这个 3 就是宽窄比。3.2 Code128 字符集选择A/B/C 怎么自动切换Code128 比 Code39 复杂的地方在于字符集。Code128A 支持大写字母和控制字符Code128B 支持大小写和符号Code128C 专门用于纯数字且成对压缩密度最高。一个成熟的实现应该能根据输入内容自动选择最优字符集但很多简易控件只固定用 Code128B。这个包里的实现我倾向于它至少支持 B 和 C 的自动切换因为纯数字场景用 C 能省一半长度。自动切换的常见策略是先判断字符串是否全为数字且长度为偶数如果是就用 Code128C 起始符值为 105否则用 Code128B 起始符值为 104。如果数字串长度是奇数可以先用 Code128B 发一个数字再切到 C。切换字符集需要插入切换符Code B 切 C 是值 99C 切 B 是值 100。下面是一个简化的字符集决策片段function TBarcode.ChooseCode128Start(const AText: string): Byte; var I: Integer; AllDigit: Boolean; begin AllDigit : Length(AText) 0; for I : 1 to Length(AText) do if not (AText[I] in [0..9]) then begin AllDigit : False; Break; end; if AllDigit and (Length(AText) mod 2 0) then Result : 105 // Start C else Result : 104; // Start B end;AllDigit判断是否纯数字Length mod 2 0保证 C 集能成对编码。如果纯数字但长度是奇数稳妥做法是退回 B 集或者用 B 发一个字符再切 C但后者实现复杂度高简易控件通常不做。选定起始符后把每个字符转成对应的值B 集下字符值 ASCII - 32C 集下每两位数字转成一个 0-99 的值依次查表得到 11 模块模式最后算校验位、加停止符。3.3 绘制逻辑Canvas 逐条绘制与宽度控制编码完成后得到的是一个模块序列比如(2,1,2,2,2,2)表示第一条宽 2 模块、第一空宽 1 模块、第二条宽 2 模块……绘制时用一个X游标从左往右推进procedure TBarcode.DrawModules(ACanvas: TCanvas; X, Y: Integer; const Modules: array of Byte); var I, W: Integer; IsBar: Boolean; begin IsBar : True; // 第一个元素是条 for I : 0 to High(Modules) do begin W : Modules[I] * FBarWidth; // 模块数 × 单模块宽度 if IsBar then begin ACanvas.Brush.Color : clBlack; ACanvas.FillRect(Rect(X, Y, X W, Y FHeight)); end; Inc(X, W); IsBar : not IsBar; // 条空交替 end; end;FBarWidth是单个模块的像素宽度通常取 1 或 2。FHeight是条码高度。IsBar初始为True每画完一个元素翻转一次。这里用FillRect而不是LineTo是因为FillRect在针式打印机和激光打印机上表现更稳定不会因为画笔宽度产生半像素偏移。如果你要在TPaintBox上预览记得在OnPaint里调用如果要输出到打印机把ACanvas换成Printer.Canvas并设置好Printer.BeginDoc即可。注意FBarWidth不要设成小于 1 的值Delphi 的 Canvas 坐标是整数小于 1 会被截断成 0导致条码消失。如果确实需要更窄的条应该整体放大绘制比例再缩放输出。4. 跑起来Demo 工程编译与第一个条码4.1 编译环境与工程打开这个包面向的是经典 Delphi 版本Delphi6 和 Delphi7 是主要目标更高版本2007 以后 Unicode 化可能需要微调字符串类型。打开BarcodeDemo.dprIDE 会加载Main.pas和Barcode.pas。如果你用的是 Delphi7直接Project → Build就能过如果用的是 Delphi10 以上可能会遇到String和AnsiString的警告把Barcode.pas里涉及字符处理的string改成AnsiString通常能解决。编译前检查一下Barcode.pas的uses子句一般只需要SysUtils、Classes、Graphics不涉及 Windows 单元所以跨版本移植性还不错。如果报「找不到单元」确认Barcode.pas和Main.pas在同一目录或者在Project → Options → Directories里把源码目录加进搜索路径。4.2 在窗体上放一个条码最小可用代码假设你不想用 Demo想在自己的窗体上快速验证可以这样做在uses里加上Barcode在窗体类里声明一个TBarcode字段然后在FormCreate里初始化在FormPaint里绘制。unit MyForm; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Barcode; type TForm1 class(TForm) procedure FormCreate(Sender: TObject); procedure FormPaint(Sender: TObject); private FBarcode: TBarcode; end; var Form1: TForm1; implementation procedure TForm1.FormCreate(Sender: TObject); begin FBarcode : TBarcode.Create(Self); FBarcode.BarcodeType : bcCode128; // 或 bcCode39 FBarcode.Text : ABC123456; FBarcode.BarWidth : 2; FBarcode.Height : 80; end; procedure TForm1.FormPaint(Sender: TObject); begin FBarcode.DrawBarcode(Canvas, 20, 20); end;BarcodeType枚举控制用哪种编码Text是待编码内容BarWidth是单模块像素宽Height是条高。DrawBarcode的X、Y是左上角坐标。这段代码跑起来后窗体上就会出现一个 Code128 条码。如果你要打印把Canvas换成Printer.Canvas并在DrawBarcode前后加Printer.BeginDoc和Printer.EndDoc。4.3 验证条码是否正确的三个土办法生成出来能不能扫别急着上专业检测仪先用这三个办法过一遍。第一用手机上的条码扫描 App 对着屏幕扫能读出你输入的原文就说明编码逻辑基本正确第二把条码打印出来用你项目实际要用的那把条码枪扫因为屏幕扫描和激光扫描对宽窄比的容忍度不一样第三拿尺子量一下窄条宽度确认打印出来的实际尺寸和BarWidth设置一致如果偏大偏小检查打印机 DPI 和绘制时的缩放。如果手机能扫但条码枪扫不出大概率是宽窄比问题把BarWidth调大一点或者把宽窄比从 2 调到 3。如果都扫不出回头检查校验位计算尤其是 Code128 的权重起始位置。5. 避坑与常见问题那些年条码扫不出来的原因5.1 现象条码能显示但扫描枪毫无反应原因通常有三个。一是宽窄比太小窄条 1 像素、宽条 2 像素激光枪在打印介质上分辨不出来二是条码周围没有足够的静区Quiet Zone标准要求左右各留至少 10 倍窄条宽度的空白很多人把条码画到边距只剩 2 像素三是条码高度不够一维条码虽然理论上一条线就能扫但实际扫描枪需要一定高度容错低于 10mm 很容易失败。解决办法是把BarWidth提到 2 以上绘制时左右各留BarWidth * 10的空白高度至少设 40 像素。5.2 现象Code128 扫出来内容多一位或少一位这是校验位或起始符处理错了。常见错误是把起始符的值也乘了位置权重或者停止符后面多补了模块。正确的校验计算是起始符值不乘权重第一个数据字符乘 1第二个乘 2以此类推累加后对 103 取模。停止符是固定的 7 个元素(2,3,3,1,1,1,2)不参与校验。如果你扫出来多一位检查是不是把校验符当数据符又编码了一次少一位则可能是某个字符的模块数查错了表。5.3 现象Delphi 高版本编译报字符串类型错误Delphi2009 以后string默认是UnicodeString而Barcode.pas里大量用String[9]这种短字符串和Char做编码表索引。短字符串在 Unicode 下行为变了Ord(Ch)返回的也不再是 ASCII 值。解决办法是把编码表相关的Char改成AnsiCharstring改成AnsiString或者在单元开头加{$H-}关闭长字符串。更彻底的做法是把输入Text先转成AnsiString再进编码流程。5.4 现象针式打印机打出来条码被拉宽针式打印机的走纸和字距控制比较粗放如果你用Canvas.TextOut或者依赖打印机驱动缩放条码宽度会被拉伸。正确做法是用FillRect按精确像素绘制并且在打印前设置Printer.Canvas.Pen.Width : 1避免画笔宽度影响。另外打印分辨率要和屏幕分辨率分开算BarWidth在打印时应该按 DPI 比例放大比如屏幕 96 DPI 下BarWidth2到 203 DPI 的针打上应该设成2 * 203 / 96 ≈ 4。5.5 现象纯数字用 Code128B 编码后条码特别长这是没用 Code128C 压缩。纯数字场景下Code128C 每两位数字用一个符号表示长度几乎减半。如果你的实现只固定用 B 集遇到长数字串比如 20 位序列号条码会非常长打印出来可能超出标签宽度。改造方法是按 3.2 节的逻辑判断纯数字且偶数长度时切到 C 集奇数长度时用 B 集发一位再切 C或者简单点直接退回 B 集但提醒用户补零成偶数。6. 二次开发从 Code39/128 扩展到 EAN13 与批量打印6.1 增加一种条码类型以 EAN13 为例这个包目前只有 Code39 和 Code128但结构是开放的加一种新类型并不难。以 EAN13 为例它固定 13 位数字前 12 位是数据第 13 位是校验位校验算法是奇数位乘 1、偶数位乘 3 求和后取模 10 补足。编码时左侧 6 位用 A 或 B 子集交替右侧 6 位统一用 C 子集起始符和中间分隔符是固定模式。你需要在TBarcodeType枚举里加一个bcEAN13在Encode方法里加一个分支再补一张 EAN13 的编码表。function CalcEAN13CheckDigit(const Digits12: string): Char; var I, Sum: Integer; begin Sum : 0; for I : 1 to 12 do begin if Odd(I) then Sum : Sum (Ord(Digits12[I]) - Ord(0)) else Sum : Sum (Ord(Digits12[I]) - Ord(0)) * 3; end; Result : Chr(Ord(0) (10 - (Sum mod 10)) mod 10); end;Digits12是前 12 位Odd(I)判断奇数位乘 1、偶数位乘 3最后(10 - Sum mod 10) mod 10得到校验位。加完编码逻辑后别忘了在绘制时把 EAN13 的左侧子集选择位第 1 位数字决定后续 6 位用 A 还是 B 子集也处理掉这是 EAN13 比 Code128 更容易写错的地方。6.2 批量打印把条码画到报表或标签上实际项目里很少只打一个条码通常是给一批商品或耗材打标签。改造思路是把DrawBarcode封装成一个可复用的过程接收ACanvas、坐标、内容和类型然后在循环里遍历数据集。如果用的是 QuickReport 或 FastReport可以在OnNeedData或自定义对象里调用如果是直接控制打印机就按标签纸的物理尺寸算好每个条码的X、Y逐行逐列绘制。procedure PrintLabels(ADataSet: TDataSet); var Row, Col: Integer; X, Y: Integer; begin Printer.BeginDoc; try Row : 0; Col : 0; ADataSet.First; while not ADataSet.Eof do begin X : 20 Col * 200; // 每列间隔 200 像素 Y : 20 Row * 120; // 每行间隔 120 像素 FBarcode.Text : ADataSet.FieldByName(SN).AsString; FBarcode.DrawBarcode(Printer.Canvas, X, Y); Inc(Col); if Col 3 then begin Col : 0; Inc(Row); end; if Row 8 then begin Printer.NewPage; Row : 0; end; ADataSet.Next; end; finally Printer.EndDoc; end; end;ADataSet是数据源Col和Row控制每页排布200和120是标签间距按实际标签纸调整。Printer.NewPage在排满一页后换页。这里要注意FBarcode的高度和宽度在打印前重新设一次因为屏幕预览和打印的 DPI 不同。6.3 一个我踩过的坑别在循环里反复 Create早期我图省事在打印循环里每次TBarcode.Create再Free结果打几百个标签后程序越来越慢最后直接卡死。原因是TBarcode内部如果持有TBitmap或TFont之类的 GDI 对象频繁创建销毁会造成句柄泄漏。后来改成在循环外创建一个实例循环内只改Text和BarcodeType性能立刻正常。从那以后我每次写批量绘制都强制走一遍「实例复用」的检查哪怕看起来只是改个属性。希望这个习惯也能帮到你少走一段弯路。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →