简介:面向Delphi 6开发环境的一套一维条形码生成控件,无需外部插件即可在数据跟踪、库存管理、产品标识等场景中集成条码功能。控件支持Code 39、Code 128等标准格式,既能生成条形码图像,也内置打印能力,方便输出到标签或文档。压缩包共17个文件,以pas源码、dcu编译单元、dfm窗体、dpr工程文件为主,另含exe演示程序、d32/d16运行文件与res资源,整体239KB,结构精炼。目前已有1269人学习下载。通过阅读源码,可掌握字符到条纹模式的编码原理、校验位算法、Delphi图形API绘制以及打印布局处理等关键环节,是一份适合中级开发者的实用学习范例。 做MES、WMS、仓储物流这类项目,条形码几乎躲不掉。用Delphi做桌面端,条形码的生成、显示、打印是个绕不开的活儿:车间要打箱唛,仓库要扫枪录入,上位机界面上还要实时显示条码。很多人拿到一份delphi代码实现一维条形码控件.rar,习惯性先找第三方控件库,结果装一圈下来,不是版本对不上,就是授权弹窗烦人。我的习惯是把这类包拆开看,抓住编码、绘制、封装三条线,自己就能把一维条码控件从零实现一遍。这篇文章就按这个思路,把Code 128、Code 39、EAN-13的编码规则、TCanvas绘制、设计期安装和打印输出逐个讲透。适合刚开始写Delphi控件的朋友,也适合被各种条码库坑过、想彻底搞明白原理的老手。
1. 先想清楚:自研条形码控件前,市面上的方案有多坑
1.1 商业控件和开源库都差在哪
先讲结论:条形码控件在工程里通常不是“技术难点”,而是“依赖管理难点”。商业控件我最早也用过TBarCode,功能确实全,Code 128、EAN、QR、DataMatrix都有,问题在授权和分发。公司电脑环境乱,Delphi版本从D7到D11.x都有,商业控件绑注册码、绑机器,客户现场装完还要激活,页面上再弹个“无效的授权说明”,光处理这个就够呛。开源方案也不是省心:ZXing在Delphi下偏QR code,一维码支持的也不全;VCLZip、Mitov这类控件单看还好,但引入一个就多一层依赖,跟项目里其他组件版本打架是常有的事。更要命的是一堆老控件在Win10/Win11高DPI下绘制发虚,打印到热敏标签纸上发糊,扫枪根本不认。所以后来我倾向于自己维护一个小而精的条形码控件,不依赖第三方包,编译出来一个BPL就完事。
1.2 控件架构怎么划分,才能让Code 128、Code 39、EAN-13共用一套绘制逻辑
如果只为一个项目写死一个编码函数,后面加码制会非常痛苦。我的做法是拆成三层:
- 编码层:每种码制一个Encoder类,输出统一的“条空序列”。这里的条空序列不是像素,而是“模块数组”,数组元素记录每个模块是黑还是白,或者更省内存地记录每条黑条的开始位置和宽度。
- 绘制层:只接收模块数组,按ModuleWidth、BarHeight、Margin等参数换算成矩形坐标,画到TCanvas上。绘制层不关心内容是Code 128还是EAN-13。
- 控件外观层:继承TCustomControl,暴露BarCode、BarcodeType、BarHeight、ModuleWidth、ShowText等属性,负责触发布局、重绘和设计期交互。
这样后续哪怕要加Code 93、Interleaved 2 of 5,只需要新增一个Encoder类,绘制和控件属性几乎不用动。这个拆分听起来简单,但很多人一开始就图方便把编码和绘制写在一个Paint方法里,等要扩展码制时才发现代码没法看了。我个人的经验是:宁可前期多定义两个类,也别让编码逻辑和Canvas搅在一起。
2. 编码层实现:条码不是画出来的,是算出来的
2.1 Code 128:字符集切换、校验位计算与查表编码
先讲Code 128,因为它是工业场景出现频率最高的码制,支持全ASCII,密度又高。Code 128其实有三种起始字符集:Start A对应大写字母和控制字符,Start B对应大写小写字母,Start C对应00到99的两位数字,编码时就是一组一组的数字对。很多初学实现只做Start B,遇到纯数字串时会比Start C长出不少,但依然能扫。校验位算法是固定的:把起始符号的值作为累加初值,从第一个数据符号开始加权,权重从1递增,所有值求和后对103取余,得到校验符号值。
我自己常用的写法是维护一张符号表,下标对应Code 128的符号值0到106,每个符号存一个由11个模块组成的位模式。编码时先把字符串映射成符号值数组,加上起始符号、校验符号、停止符号,最后把每个符号的位模式按顺序拼接成最终的模块数组。这里有个容易漏的点:停止符号占用13个模块,算总宽时别按11算,否则最后一条线会被裁掉。
简单示意代码:
function TCode128Encoder.Encode(const AData: string): TByteArray; var Symbols: array of Integer; I, Sum, Check: Integer; begin SetLength(Symbols, 0); AddSymbol(Symbols, CODE128_START_B); // 简化:先用Start B for I := 1 to Length(AData) do AddSymbol(Symbols, FCharToSymbol[AData[I]]); Sum := Symbols[0]; for I := 1 to High(Symbols) do Sum := Sum + Symbols[I] * I; // 权重从1开始 Check := Sum mod 103; AddSymbol(Symbols, Check); AddSymbol(Symbols, CODE128_STOP); // STOP为13模块 Result := SymbolsToModules(Symbols); end;需要注意,这个代码里的FCharToSymbol是查表得到每个ASCII字符对应的符号值,和直接Ord(Char)并不一样,网上很多简化版都用Ord - 32之类公式,那只适用于部分字符,遇到扩展ASCII就翻车。
2.2 Code 39和EAN-13:什么时候用它们,代码怎么写
如果不想处理Code 128的三种字符集切换,Code 39是很好的备选。它一共43个字符,支持大写字母、数字和几个符号,用星号作为起始和终止符,每个字符9个模块、5条4空,其中3个宽单元6个窄单元。优点是自校验、无需校验位也能扫,缺点是密度低。什么时候用Code 39?比如企业内部固定资产标签,数据基本是大写字母+数字,条码区域又足够大,用Code 39最省事。它的编码也是查表,比Code 128简单很多。
EAN-13则是零售商品条码的标准,13位数字,前12位是数据,第13位是校验位。校验位算法是奇数位权1、偶数位权3,求和后取10的补数。编码结构是起始符101、左侧6位数字、中间分隔符01010、右侧6位数字、终止符101。左侧6位会根据第一位数字选择A组或B组编码,右侧6位统一用C组编码。第一次写EAN-13时的经验是:别自己推导A/B的排列表,找张现成的奇偶编码表对着抄,半天就能写完。
三种码制的对比我整理了一张表:
| 码制 | 字符集 | 密度 | 校验 | 典型场景 | 实现复杂度 |
|---|---|---|---|---|---|
| Code 128 | 全ASCII | 高 | 固定1位 | 物流、MES、WMS | 中 |
| Code 39 | 大写字母数字等43字符 | 低 | 可选Mod 43 | 资产标签、工票 | 低 |
| EAN-13 | 13位数字 | 中 | 固定1位 | 零售商品条码 | 中 |
2.3 编码层边界处理:非法字符、Unicode与常见规范
这一块最容易被忽视。Code 128能编码全ASCII,但如果你传中文进去,标准码表里根本没有对应符号,直接查表就会得到错误结果。实际项目里很多设备标签包含中文,我的处理方式是:给编码器一个字符映射表,把需要的中文按预先定义好的编号编码,或者干脆限定输入字符集,界面上提前用正则拦截。这里用Delphi里的正则表达式就够了,没必要引第三方OCR库——OCR是识别方向,和生成条码完全是两码事。
另一个坑是Delphi的字符串类型。D2009之前String是AnsiString,逐字节处理没问题;之后String是UnicodeString,直接用S[I]取出的是Char,再配合Ord取码值,很容易跟旧查表逻辑冲突。稳妥做法是内部统一转成字节数组,比如用TEncoding.ASCII.GetBytes,再走编码逻辑。GS1-128也值得提一句,它在Code 128数据区最前面加了FNC1(符号值102),用于标识GS1应用标识体系,实现时要能支持插入这个特殊符号,而不是把它当成普通可打印字符。
2.4 把编码结果缓存起来,批量生成才不卡
单个条码编码很快,但批量打印标签时,几百上千个条码如果在Paint里现算,UI会明显卡顿。我的做法是给控件加一个编码缓存:以“码制 + 条码内容”作为Key,把编码后的模块数组存进TDictionary。由于字符串天然适合做字典Key,这个操作非常自然——但要注意Delphi的TDictionary默认区分大小写,如果希望“ABC”和“abc”共用缓存,可以给构造函数传一个不区分大小写的比较器。缓存不用做太复杂,在条码内容变化时清空对应项即可。
3. 绘制层实现:从条空序列到屏幕、图片和打印机
3.1 用TCanvas画条码的三种姿势,我最后选了哪种
拿到模块数组后,画法有好几种。最直观的是遍历每个模块,黑模块就FillRect,白模块跳过,这种方式逻辑简单,但一个Code 128条码动辄两三百个模块,重复调用FillRect性能一般。第二种是把相邻的黑模块合并成一条黑条,先扫描数组生成“黑条矩形列表”,再一次绘制,性能好很多,代码也不复杂。第三种是生成矢量轮廓,用Polygon直接把条码画成闭合路径,好处是放大缩小不变形,打印到矢量MetaFile里效果最好。
我实际项目中混用第二种和第三种:屏幕预览用第二种,因为Windows GDI对大量FillRect有优化,条码数量不大时肉眼感知不到区别;导出到EMF或直接打印时用第三种,矢量路径在打印机里更干净。别直接拿控件窗体截图转成位图再打印,那个方案在热敏打印机上十有八九是糊的,这个问题后面细说。
3.2 静区、人眼可读字符与坐标计算
绘制时除了条码本身,还有两个容易漏的因素:静区和人眼可读字符。静区就是条码左右必须留白的区域,Code 128规范要求左侧至少10个模块宽度、右侧至少7个模块宽度,实际我统一按10个模块留白。静区不是装饰,扫描枪要靠它判断条码边界,留不够是扫不出最常见的硬件原因。
人眼可读字符一般显示在条码下方,和条码对齐。这里有个小细节:字符区域的高度应该单独计算,不要挤占BarHeight。我的默认布局是:总高度 = BarHeight + TextGap + FontHeight,条码底部和文本基线之间留2到3个像素。字体建议用等宽字体,比如Courier New,条码下方内容在视觉上更整齐。字体太小也不行,热敏标签打出来只有12号以下,人工核对时眼睛很难受,我一般最小给14号。
坐标计算统一走“模块→像素”换算:水平位置 = MarginX + 静区模块数 * ModuleWidth + 当前黑条起始模块序号 * ModuleWidth。所有绘制参数都由一个布局类统一计算,不要太散落在Paint里。
3.3 打印模糊问题:为什么屏幕清晰、扫描枪却总不认
这个问题我踩过太多次。屏幕上看条码清清楚楚,打印出来一扫描,要么不认,要么要凑很近才认。根因有两个:一个是模块宽度小于打印机的最小可分辨单元,另一个是用了位图方式输出。300dpi热敏打印机上,1个点约0.085mm,理论上一个模块至少3个点(约0.25mm)才能保证黑条之间有明确的空白间隔,但实际经验是别低于0.33mm,也就是300dpi下至少4个点。代码里ModuleWidth如果是像素值,屏幕显示和打印要分开算,不能一套参数两端用。
更靠谱的做法是直接用Printer.Canvas按打印机DPI重算条码矩形,再用矢量方式绘制。打印代码如下:Printer.BeginDoc,设置好Printer.Orientation和纸张尺寸后,用Printer.PixelsPerInch把毫米换算成点,再把黑条列表画到Printer.Canvas上。这样模块宽度可以用毫米定义,屏幕显示时按Screen.PixelsPerInch换算,打印时按打印机DPI换算,一套逻辑两端适配。
3.4 输出图片与设计期预览
除了屏幕和打印机,还经常需要把条码导出成图片,比如拼到Excel报表或者生成HTML预览。导出时我会新建一个TBitmap,设好PixelFormat := pf24bit,按目标DPI绘制一遍,然后SaveToFile。分辨率我一般按300dpi生成:假设条码宽40mm,图片宽度就是Round(40 / 25.4 * 300),这样客户拿去印刷或喷绘不会虚。
设计期预览其实不用额外写代码。控件只要在Paint里根据属性绘制,Delphi窗体的设计器就会自动调用Paint显示效果。要注意的是别在Paint里写耗资源的逻辑,比如每次都重新查表编码——用2.4的缓存方案后,设计器里拖动窗体也不会卡顿。
4. 控件封装:属性、事件与设计期安装
4.1 属性设计与自动重绘机制
控件要提供给使用者的核心属性,我用一张表列出来:
| 属性 | 类型 | 说明 |
|---|---|---|
| BarCode | string | 条码内容,Setter中触发编码与重绘 |
| BarcodeType | TBarcodeType | 枚举:Code128、Code39、EAN13等 |
| ModuleWidth | Integer | 模块宽度,单位像素(屏幕预览) |
| BarHeight | Integer | 条码区高度 |
| ShowText | Boolean | 是否显示人眼可读字符 |
| TextFont | TFont | 人眼可读字符字体 |
| MarginX | Integer | 左右留白 |
| QuietZoneEnabled | Boolean | 是否自动加静区 |
设计上最重要的是Setter统一走一个Changed方法,属性变化时先重新生成/清理缓存,再Invalidate重绘。比较经典的做法是拦截CMTextChanged或者直接在Set方法里判断值是否真的变化,没变化直接退出,避免设计器里无意义的重复编码。
4.2 事件、异步生成与数据绑定
控件留一个OnChange事件,在BarCode内容变化并编码成功后触发,方便外部联动,比如界面上的文本框跟条码控件绑定,用户每改一个字,条码实时刷新。这个场景在MES的报工界面很常见。因为编码本身很快,不推荐在OnChange里做耗时的事。
但批量生成标签页就不同了,一次可能生成几百个条码。这种情况我不会在UI线程里循环编码,而是用匿名TTask放进线程池,每完成一个就通过TThread.Queue把结果丢回主线程更新显示。这里顺带说下TThread和TTask的区别:TThread更偏底层,句柄生命周期要自己管;TTask是线程池调度的轻量任务,适合“丢个任务进去,回调里收结果”的场景。很多Delphi初学者会把两者混着用,写出来的代码既没有利用线程池,又白白维护了一堆线程句柄。
4.3 打包成组件安装到IDE
要把控件变成IDE组件栏里的图标,需要建一个Runtime Package和一个Design Package。Runtime包requires rtl, vcl,放控件实现单元;Design包requires rtl, vcl, BarcodeRun,放注册单元。注册单元最简单:
unit BarcodeReg; interface procedure Register; implementation uses System.Classes, BarcodeControl; procedure Register; begin RegisterComponents('Barcode', [TBarcodeControl]); end; end.编译完成后,在IDE里Component -> Install Packages添加BPL,工具栏就会出现“Barcode”分组。这里有个经验:Design包一定要和IDE主版本深度绑定,Delphi 11.x装的BPL不能拿给D7用,源码包维护两套版本是常态。安装过程如果提示找不到包单元,多半是Library路径没加,把源码目录加进IDE的Library Path就行。
4.4 使用场景扩展:表格内显示与批量打印标签
控件装在IDE后,最常见的扩展是放进DBGrid或ListView里显示条码。DBGrid的列上放DBImage不方便,我自己是写一个绘制委托:在OnDrawColumnCell里调用控件的绘制函数,把条码画到表格单元格上。不用非得把控件拉到表格里,只要把绘制层做成一个公开函数,任何TCanvas上都能画。
批量打印标签则是另一个高频需求。我通常做一个“标签模板”窗体,把固定字段和可变条码区分开,打印时循环设置BarCode属性,每设置一次就打印一页。用模板方式的好处是客户要改LOGO、改备注字段时,只需要调窗体布局,完全不用动条码生成逻辑。打印前建议把热敏标签纸的宽度高度按毫米配好,别用默认A4,不然条码比例会被纸张拉伸搞乱。
5. 踩坑实录:常见问题与排查表
5.1 扫不出、扫错、扫倒:扫码终端行为对照
最后把我在现场遇到最多的情况整理成一张对照表:
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 扫不出,红点无反应 | 静区不足、模块太窄、对比度低 | 检查静区留白,调大ModuleWidth,深色条+白底 |
| 扫出来但结果是乱码 | Code 128字符集切换错误、校验位算错 | 先用在线工具核对符号序列,再逐段检查编码逻辑 |
| 扫出结果但末尾多回车 | 扫码枪被设置为键盘仿真+回车后缀 | 在上位机接收处做回车过滤 |
| 打印出来要凑很近才扫 | 模块宽度低于0.33mm | 打印DPI不变时,调大物理模块宽度 |
| 屏幕清晰但导出图片发虚 | 位图分辨率太低 | 按300dpi生成位图,不要直接用控件截屏 |
扫码枪的“键盘仿真模式”是最隐蔽的坑。很多USB扫码枪默认模拟键盘输入,扫完自动敲一下回车,这本身不是条码控件的问题,但会让现场人员误以为“条码生成错了”。遇到扫码结果总带奇怪字符,先怀疑扫描枪配置,别急着改编码代码。
5.2 跨Delphi版本编译:从D7到D11.x都要过的坎
如果控件要兼容老项目,跨版本编译是绕不开的。D2007及之前的版本String是AnsiString,Delphi 11.x下String是UnicodeString,直接对字符串下标取字符再Ord的做法,在不同版本下行为完全不一样。我自己写编码器时,内部数据统一用TBytes或PAnsiChar处理,字符串只在控件边界做一次转换。
另一个跨版本问题是TPngImage、TVclStyles等单元在各版本中的可用性有差异,条形码控件尽量只依赖Vcl.Graphics、System.Classes这些基础单元,别为了一点小功能引入额外包。我在实测中,同一个控件源码从XE8编到11.3基本零改动,前提是没用高版本才有的语法特性。
5.3 字体与DPI:细线条宽度控制的经典迷思
Delphi 10.4以后屏幕DPI自适应问题变多了。如果Form没有正确声明PerMonitorV2,在高分屏上条码控件的ModuleWidth按像素算会显得特别小。我的做法是属性里ModuleWidth以0.1mm为单位存储,绘制时根据当前Canvas的DPI换算像素。屏幕预览和打印输出都用同一套换算,不再区分“屏幕像素”和“打印点”。
字体方面,条形码下方的人眼可读字符用OCR-A或Courier New比宋体、雅黑更稳,因为等宽字符在扫描枪人工核对时不容易看串行。字体大小建议用物理字号,不要用Font.Height := -12这种像素值,不同DPI下会忽大忽小。
5.4 一个可以直接用的最小实现思路
如果手头没有现成代码,又想最快落地,我建议先别急着编译安装BPL,而是只写一个TBarcodeView = class(TGraphicControl)的轻量控件,放在单个单元里,动态创建到窗体上。TGraphicControl没有窗口句柄,绘制快,内存开销小,放几十个在界面上也不心疼。编码逻辑单独放一个单元,先用Code 128一个码制跑通,再慢慢往里面加Code 39和EAN-13。等稳定了,再把单元包装成Design Package装进IDE组件栏。这个演进路线我在几个项目里都验证过,前期投入小,后期扩展也顺,不会一上来就被BPL编译、包依赖这些问题劝退。
本文还有配套的精品资源,点击获取