☰
Delphi纯代码实现一维条码控件:Code39与Code128编码原理及实战
2026/10/20 14:10:32 网站建设 项目流程

简介:这份资源面向仍在使用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 是值 99,C 切 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 - 32,C 集下每两位数字转成一个 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.dpr,IDE 会加载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改成AnsiChar,string改成AnsiString,或者在单元开头加{$H-}关闭长字符串。更彻底的做法是把输入Text先转成AnsiString再进编码流程。

5.4 现象:针式打印机打出来条码被拉宽

针式打印机的走纸和字距控制比较粗放,如果你用Canvas.TextOut或者依赖打印机驱动缩放,条码宽度会被拉伸。正确做法是用FillRect按精确像素绘制,并且在打印前设置Printer.Canvas.Pen.Width := 1,避免画笔宽度影响。另外,打印分辨率要和屏幕分辨率分开算,BarWidth在打印时应该按 DPI 比例放大,比如屏幕 96 DPI 下BarWidth=2,到 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,性能立刻正常。从那以后我每次写批量绘制都强制走一遍「实例复用」的检查,哪怕看起来只是改个属性。希望这个习惯也能帮到你,少走一段弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询