ARTICLE DETAIL

资讯详情

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

Delphi自研一维条形码控件:Code 128/39/EAN-13编码绘制与打印实战

Delphi自研一维条形码控件:Code 128/39/EAN-13编码绘制与打印实战 简介面向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-1313位数字中固定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 属性设计与自动重绘机制控件要提供给使用者的核心属性我用一张表列出来属性类型说明BarCodestring条码内容Setter中触发编码与重绘BarcodeTypeTBarcodeType枚举Code128、Code39、EAN13等ModuleWidthInteger模块宽度单位像素屏幕预览BarHeightInteger条码区高度ShowTextBoolean是否显示人眼可读字符TextFontTFont人眼可读字符字体MarginXInteger左右留白QuietZoneEnabledBoolean是否自动加静区设计上最重要的是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是AnsiStringDelphi 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编译、包依赖这些问题劝退。本文还有配套的精品资源点击获取
返回列表