资讯动态

C#调用BarTender实现标签自动打印与序列号生成

发布时间:2026/9/16 21:47:26 来源:尧图企业网站定制
做上位机开发的朋友大概率都接过“把标签打印接进系统”这种需求产线工人在电脑前打开BarTender手动改模板里的型号、批次、序列号再点一下打印。如果一天只有几十张还好遇到批量出货或者多品种混线人工改数不仅慢还容易把条码打错等到后道扫描识别不了才开始返工。我当时手上是一个VS2012的C#上位机项目现场要求做一个“扫码/选单/自动打印”的闭环标签里除了品名日期还要带一个与数据库关联的条形码。研究了一圈下来最稳的方案就是用C#调用BarTender的自动化接口让程序替人干这些重复活。这篇文章就把整个思路和实操要点拆开讲清楚给做MES、WMS和自动化设备集成的朋友做个参考。1. 为什么要把BarTender的打印动作交给C#1.1 人工打印的瓶颈不止是慢很多人觉得打印标签而已慢几秒怕什么。但放到产线上算一笔账一台设备一天产几百件每件一个序列号人工在BarTender里改号、对模板、点打印一件至少要十几秒中间还要防止把序列号改错、漏打、重复打。更麻烦的是现场的操作工并不一定熟悉BarTender一旦模板被无意中改乱恢复起来又得折腾半天。所以需求本质不是“打印”而是“把打印变成系统里一个可控的、有记录的、可追溯的接口”。C#上位机里做一个按钮按下之后程序自动从数据库取数据、填充标签、发送打印指令然后记录日志这才是自动化该有的样子。1.2 为什么选BarTender而不是自己画条码也有人问条码不就是黑白条吗我用C#的GDI画一个不行吗从技术上讲行但从工程角度看非常不建议。第一条形码不是随便画几个矩形就能扫的。Code128、EAN-13、QR码这些都有编码规则和校验位自己实现一遍虽然能跑但字体、缩放、静区稍微处理不好就会造成扫描设备识别失败。而BarTender这类专业软件把这些都封装好了模板里选好符号体系出来的条码质量是经过大量工业现场验证的。第二标签模板会频繁调整。客户今天要在右下角加个公司Logo明天要加个“产地”字段Excel表格数据源也时不时要动。如果用代码里硬画的方式每次改动都要重新编译部署改用BarTender模板直接在模板设计器里改程序代码完全不用动维护成本直线下降。第三BarTender自带打印机驱动设置和打印浓度校准能适配很多工业条码打印机。相比直接用ZPL指令逐条发送BarTender的方式更接近“所见即所得”现场调试也方便得多。1.3 自动化调用的核心价值把打印动作交给程序之后整个业务闭环就顺了系统从ERP/MES拿到订单或批次数据程序自动生成序列号塞进模板变量。条形码内容和数据库记录绑定后面扫描才能反查出这批货的来源、工序、检验结果。每次打印的结果、时间、操作员、数据内容都写入日志谁在什么时候打过什么标签一查便知。我在项目里最深的体会是BarTender的自动化接口让“打印”这件事从一个独立的手工动作变成了可以编排的系统能力。你可以在扫码枪扫完后触发打印可以在PLC信号到位后触发打印甚至可以每天凌晨自动打印固定数量的批次标签。这种可控性是人工操作给不了的。2. 环境准备VS2012与BarTender的版本适配细节2.1 工控现场为什么还在用VS2012这几年新项目很多已经上VS2019、VS2022了但工厂里的老上位机平台往往还是VS2012甚至更老。工厂通常不愿意为了一个打印功能把整个开发工具链都换掉因为上位机软件还牵涉到老驱动、老控件、现场Windows 7系统等一系列兼容问题。所以VS2012C#这套组合在老产线里非常常见完全够用。我用的开发机是64位Windows 10目标框架是.NET Framework 4.5项目类型就是普通的WinForm程序。只要系统里装了BarTender引用方式正确跑起来没什么问题。2.2 BarTender版本与两种引用方式BarTender从10.x、11.x到2016、2019、2022版本很多自动化接口也一直在变化。老版本10.x、11.x主要提供ActiveX/COM方式用起来是在VS里添加COM引用新版本2016之后除了保留COM还提供了.NET SDK方式。这里先重点讲与VS2012最匹配的COM方式。在装有BarTender的机器上打开VS2012在解决方案资源管理器里右键项目选择“添加引用”切到“COM”选项卡找类似“BarTender”或“Seagull BarTender”的条目添加进去。添加成功后引用列表里会出现Interop.BarTender.dll。如果找不到这个COM项多半是安装BarTender时没有勾选Automation相关组件需要重新安装或修复把“Automation Interface / SDK”这类选项勾上。还有一点要注意有些新版BarTender的.NET SDK并不兼容VS2012的旧工具链所以对于老项目老老实实用COM引用是最不折腾的方案。新的开发项目另说后面会单独说。2.3 位数匹配问题这是最容易踩的坑必须提前说。BarTender的COM组件有32位和64位之分而VS2012默认的“Any CPU”编译方式在64位系统上跑起来就是64位进程。如果BarTender装的是32位组件64位进程去调用就会报“CLSID未注册”网上搜半天都是答非所问。解决办法很简单打开项目属性切到“生成”页把“平台目标”改成“x86”。不要觉得回到32位是倒退工业现场很多打印驱动和中间件都是32位的统一x86反而省心。这个配置改完之后把重新编译出来的程序放到工控机上基本上COM调用就能正常了。3. 集成方案选型COM组件、命令行还是.NET SDK3.1 COM/ActiveX方式功能最全推荐优先选COM方式是老项目里最常用的调用手段。程序里直接new一个BarTender.Application对象然后通过它打开模板、赋值、打印整个过程都是进程内调用能够拿到详细的返回结果。比如打印失败时你能知道是文件打不开还是打印机连接不上这对系统集成来说很重要。COM方式唯一麻烦的是资源释放BarTender的Application对象如果不好好释放会在任务管理器里留下一堆bartend.exe进程把内存慢慢吃光。后面踩坑部分我会专门讲释放问题。3.2 命令行方式简单但可控制性弱如果只是打印一个固定的标签模板不想写一堆COM代码也可以直接启动BarTender主程序带上命令行参数让它打印。大致形式是启动bartend.exe然后用/F指定模板文件路径/P表示打印/C指定份数。不同版本的命令行参数有差异要以安装目录下自带的Command Line Guide为准。命令行方式的好处是部署简单程序里用Process.Start启动外部进程就行。但坏处也很明显你很难拿到打印结果也不好往里传变量顶多打印固定模板。对于需要动态数据、需要判断结果、需要写日志的系统命令行方式有点太“裸”了我不推荐在正经项目里用它做核心功能。3.3 .NET SDK方式新项目的选择BarTender 2016之后的版本提供了.NET SDK比如可以通过NuGet引用BarTender的官方类库调用方式和COM类似但类型安全、线程模型和资源管理上都更现代。如果项目是用新版本Visual Studio开发的而且现场BarTender版本也比较新用.NET SDK是更好的选择。不过对VS2012这种老平台来说.NET SDK的兼容性不一定理想。我当时的判断标准很简单现场已经是老产线、老系统最优先的目标是稳定跑通用COM方式最合适新项目则可以用SDK省去COM释放的一些麻烦。3.4 我的选型结论没有绝对最好的方案只有当前环境里最不折腾的方案。我在VS2012这个项目里最终选了COM方式原因就三条VS2012对COM支持最成熟现场BarTender版本恰好支持网上能查到的老案例最多踩坑时容易找到参考。如果你是全新项目建议直接看官方文档评估.NET SDK。4. 打印主流程拆解模板、数据绑定、打印三步走4.1 整体流程概览C#调用BarTender打印核心流程就是三步打开模板往模板里塞数据执行打印。但具体到代码还要做好释放和异常处理。下面这个序列是标准的实例化BarTender.Application。调用Documents.Open打开一个.btw模板文件得到LabelFormatDocument对象。给模板里的命名变量赋当前要打印的内容。设置打印机名称或者沿用模板里配置好的默认打印机。调用PrintOut方法执行打印。关闭文档释放COM对象必要时退出Application。4.2 打开模板几秒钟的背后发生了什么BarTender.Application btApp new BarTender.Application(); BarTender.LabelFormatDocument btFormat btApp.Documents.Open(labelFilePath, false, , );Documents.Open的第一个参数是模板文件的完整路径第二个参数是是否显示BarTender窗口一般自动化场景都传false避免打印时弹窗打断流程。Open过程会做模板加载和打印机状态初始化第一次打开会稍慢几秒钟都正常。这里有个容易忽略的点路径一定要用绝对路径。BarTender的COM接口对相对路径的支持不友好有时候它在某个默认目录找半天找不到文件然后返回一个让人摸不着头脑的错误。直接用Path.GetFullPath转一下能省掉很多排查时间。4.3 给模板传数据命名变量是关键BarTender模板里所有的动态内容都应该做成“命名变量”。具体做法是在BarTender设计器里选中文本对象或者条码对象打开数据源属性把固定文本改成“命名数据源”并起个名字比如ProductName、BatchNo、SerialNo。这样程序就能按名字往里填值。C#端的赋值代码很直观btFormat.SubStrings[ProductName].Value 精密轴承-6204; btFormat.SubStrings[BatchNo].Value B20250607; btFormat.SubStrings[SerialNo].Value 240618001;需要说明的是不同版本BarTender的COM接口里这个变量集合的名字可能叫SubStrings也可能叫Variables或者Identifications具体以你安装版本的开发文档为准。但逻辑是一样的模板里提前定义变量程序按名称赋值模板负责渲染。二维码关联数据也是同一个套路。在BarTender模板里把QR码的数据源绑定到一个命名变量比如把多个字段拼接成“订单号|批次号|序列号”这串文本程序只需要给这几个字段分别赋值条码内容就会自动拼接并渲染。不用在C#里手工拼二维码内容更不用自己计算二维码编码省事又不容易错。4.4 执行打印与结果判断BarTender.PrintResults result btFormat.PrintOut( 1, BarTender.PrintMethods.Printer, , BarTender.PrintCompletionOptions.WaitForSpoolerComplete);PrintOut第一个参数是打印份数第二个参数是打印方式通常用Printer表示输出到打印机第三个参数是方式附带的参数对Printer方式留空就行第四个参数比较关键它决定方法什么时候返回。如果只想确认打印指令已经发出去可以用DoNotReturnUntilPrintSpawned但要想确保这次打印真的进了打印队列再返回就用WaitForSpoolerComplete。我实际调试中发现用WaitForSpoolerComplete返回的Success基本可以认为这次打印是可靠的。如果从代码层面连打印结果都不要那程序只要发现调用没抛异常就继续跑但那样出了问题不好定位不建议这样做。调用完成后根据返回值判断是否成功if (result BarTender.PrintResults.Success) { // 打印成功写日志 } else { // 打印失败处理异常 }4.5 释放资源这个坑必须认真对待COM对象的释放比普通对象讲究得多。官方文档说的FinalReleaseComObject用的时候也有一堆细节。finally { if (btFormat ! null) { btFormat.Close(BarTender.BtSaveOptions.btDoNotSaveChanges); Marshal.FinalReleaseComObject(btFormat); btFormat null; } }关闭文档时要明确选择不保存修改否则程序给模板变量赋的值可能会被覆盖回模板文件里造成模板被“污染”。如果每次打印都打开一次Application用完还要记得QuitbtApp.Quit(BarTender.BtSaveOptions.btDoNotSaveChanges); Marshal.FinalReleaseComObject(btApp); btApp null; GC.Collect(); GC.WaitForPendingFinalizers();GC.Collect在这地方不是性能洁癖而是切实在帮COM对象清理内部引用。很多人的bartend.exe进程残留就是因为少写了后两行。5. 序列号与条码数据自动生成的核心逻辑5.1 序列号的两种生成方式自动打印条形码离不开序列号。序列号可以放在两条路上生成模板里序列化或者程序里自己生成。两者各有适用场景。模板内序列化的做法是在BarTender模板的文本或条码对象上把数据源类型改成“序列号”设置起始值、步长、是否需要补零、是否到一定数量后重置。程序只需要传入第一个序列号模板在连续打印多份时会自动按步长递增。这种方式适合“连续打印一段连续编号”的场景逻辑简单打印多少份由程序控制。程序生成的方式则是C#拿到数据库里已有的最大编号加1之后作为字符串变量传给模板。这种方式更适合跟业务数据严格对应的场景比如订单号、批次内序号、设备编号序列号与数据库里的记录一一对应后续扫描追溯时不容易乱。对比项模板内序列化程序生成序列号实现复杂度低模板里配置即可中需要写取号和更新逻辑与数据库一致性弱模板自己递增强数据以数据库为准适合场景简单连续编号订单/批次绑定、追溯要求高并发安全单模板内安全需要数据库层控制我在项目里用的是程序生成的方式因为这就是为了做追溯打出去的序列号必须能在数据库里查到对应生产记录。5.2 从数据库取下一号如何避免重号很多人一上来就写“查最大编号1”这在单线程测试时没问题但产线只要两个工位同时打印就可能取出同一个号码。避免重号最简单的思路是专门建一张序列号表取号时用事务加锁BEGIN TRANSACTION; SELECT TOP 1 nextNo CurrentNo 1 FROM SerialSequence WITH (UPDLOCK, ROWLOCK) WHERE SequenceKey PROD-2025; UPDATE SerialSequence SET CurrentNo nextNo WHERE SequenceKey PROD-2025 AND CurrentNo nextNo - 1; COMMIT TRANSACTION;这只是一个示意实际要结合你的数据库类型和访问方式调整。核心要领是取号动作和更新动作必须在同一个事务里并且加行锁才能保证并发时不会拿到同一号。还有一个实操细节是打印动作是可能失败的。如果先取号再打印打印失败后这个号码就“丢了”会造成序列号断号。处理方式有两种断号不管它标签序列号本来就有断号容忍度或者做一个待打印任务表取号后先标记“待打印”等打印成功回写日志后再标记“已打印”失败则释放号码回池。后者更严谨但实现的复杂度也更高。5.3 条码可扫描性的几个检查点条码打出来扫不出来是最让人火大又最常见的问题。根据我的经验检查顺序应该是先看模板里的条码类型和内容再看打印机浓度最后才怀疑代码。C#这边的数据格式要注意这几点Code128适合变长、字母数字混编的内容EAN-13适合零售单件商品QR码适合需要塞进几十个字符的场景。模板里选错符号体系扫不出来是正常的。条码内容里不要混入不可见字符比如换行、制表符这些容易在字符串拼接时悄悄进去。如果条码内容包含校验位BarTender模板会自动计算不需要程序端处理。程序端只管业务数据条码规范交给模板。打印机浓度和标签材质也值得检查。热转印打印机浓度太低条会变浅导致扫描不良浓度太高又会糊成一团。这个要靠现场试经验值是先打成默认扫不动再逐步往上调。6. 真实踩坑记录COM资源泄漏与打印丢失的排查链路这一节是全文最想让你认真看的部分。下面每个问题都是我在项目联调时真真切切遇到过的排查过程比结果更有参考价值。6.1 坑1任务管理器里的bartend.exe越来越多现象是程序跑了半天之后系统变得很卡打开任务管理器一看bartend.exe躺着十几个进程。原因就是COM对象没有彻底释放。刚开始我写的是btApp.Quit(BarTender.BtSaveOptions.btDoNotSaveChanges);以为Quit之后就结束了但Quit只是告诉BarTender应用“该退了”托管侧的RCWRuntime Callable Wrapper还握着COM引用进程不一定退。正确的做法是在Quit之后对btApp调用Marshal.FinalReleaseComObject然后置空再GC一次。这里要注意同时也要把从Documents.Open得到的btFormat先Close和释放顺序不能反否则文档还握着应用上的引用应用也退不掉。我最后把释放逻辑全部收进一个Dispose方法里不管打印成不成功finally里统一执行public void Dispose() { try { if (_btFormat ! null) { _btFormat.Close(BarTender.BtSaveOptions.btDoNotSaveChanges); Marshal.FinalReleaseComObject(_btFormat); _btFormat null; } if (_btApp ! null) { _btApp.Quit(BarTender.BtSaveOptions.btDoNotSaveChanges); Marshal.FinalReleaseComObject(_btApp); _btApp null; } } finally { GC.Collect(); GC.WaitForPendingFinalizers(); } }这里还有一个心得如果你打算让Application对象常驻就不需要每次都Quit只需要每次释放文档对象。常驻实例对性能帮助很大一次启动可以用一整天打印速度比反复创建快得多。6.2 坑2PrintOut返回Success打印机却没出纸这个最迷惑程序判断打印成功打印机纹丝不动。排查了一阵子才发现问题出在PrintCompletionOptions上。我第一次用的是DoNotReturnUntilPrintSpawned意思是“打印任务一产生就返回”但这时候任务可能还在BarTender内部排队还没真正发给打印机。我又紧接着释放了文档甚至退出了Application任务就跟着丢了。换成WaitForSpoolerComplete之后程序会一直等到打印任务完全进入Windows打印队列并交给打印机驱动再返回结果。换句话说Succcess才是真正靠谱的“已受理”。如果你的现场打印机很慢这个等待可能让界面卡一两秒但换来的是可靠我觉得值。要更流畅就把打印放到后台线程去等不要堵UI线程。6.3 坑3两个线程同时打印COM直接报错项目后期加了一个功能扫码枪扫完一个件后台线程自动打印同时操作员又手动点了一下另一个打印按钮两路并发COM对象直接抛“对象正在被使用”的错。BarTender的COM对象线程亲和性很强同一个Application实例不能在多线程里随便用。解决办法很粗暴但有效给所有打印操作加同一把锁把并发变成串行。private static readonly object PrintLock new object(); public void PrintSafely(string templatePath, Dictionarystring, string data) { lock (PrintLock) { // 打开模板、赋值、打印、释放文档 } }如果并发的量很大锁竞争严重可以考虑用一个任务队列后台单线程消费打印请求。这样UI线程只管把打印请求丢进队列打印线程一个一个处理既不会冲突界面也不会卡。6.4 坑4部署到工控机上直接CLSID未注册本机调试得好好的部署到工控机一运行就报“CLSID未注册”。第一反应是工控机没装BarTender但查了一圈装得好好的。最后发现是平台目标的问题。本机项目设置是Any CPU跑出来是64位进程工控机上的BarTender COM组件是32位所以找不着。这个前面环境准备那节说过解决办法就是项目属性里把平台目标改成x86重新发布。以后遇到“本机能跑别的机器不能跑”的COM问题第一反应就查位数。这个经验适用于大部分COM组件不光是BarTender。6.5 坑5打印机名称对不上赋值就报错给PrinterSetup.PrinterName赋予了打印机名字结果报“打印机不存在”。后来用程序枚举系统中已安装的打印机发现实际名称是“Zebra ZT410 (副本 1)”多了个尾巴。比对之后才知道系统里的打印机名称经常和直觉有出入。稳妥的做法是要么在模板里就把默认打印机设置好程序不覆盖打印机名称要么程序动态枚举系统打印机做一个下拉框供现场选择再把选中的名称传给PrinterSetup。后者的容错性更好换打印机不用重编译。6.6 坑6模板路径里带中文和空格BarTender的模板路径放在中文目录或者有空格目录下Open偶尔会出问题。这个出现概率不高但一旦出现搜索资料都不一定好找。我的处理办法是统一用Path.GetFullPath生成绝对路径并且在传入之前先检查File.Exists。如果文件不存在直接把完整路径打到日志里现场一看就知道是哪个路径对不上。另外路径变量不要用相对路径拼接COM接口的工作目录不一定和程序当前目录一致相对路径很容易翻车。7. 批量打印与业务系统联动的完整设计7.1 封装一个打印管理器单次打印跑通只是第一步真正好用的是一个打印管理器让业务代码不用关心COM细节。我给自己的类定了几个功能初始化时创建唯一Application实例每次打印时打开文档、填数据、打印、关闭文档内部加锁保证并发安全同时提供日志回调。用的时候业务侧只需要这样调用_printManager.PrintOne(D:\Labels\ProductLabel.btw, new Dictionarystring, string { { ProductName, 精密轴承-6204 }, { BatchNo, B20250607 }, { SerialNo, 240618001 } });界面和逻辑完全分离以后要换BarTender版本或者从COM切到.NET SDK只需要改封装类内部界面代码几乎不用动。7.2 与扫码枪和数据库的联动实际场景里操作员用扫码枪扫一个工单条码WinForm的文本框收到一长串字符并自动回车程序拦截回车事件拿这个工单号去数据库查有哪些产品需要打印标签查到后逐条填充到模板里打印。这个流程可以把人工干预降低到“只扫一下”的程度。打印成功后往数据库打印记录表里写入模板路径、数据内容、打印时间、操作员。因为序列号与订单数据已经通过命名变量传给BarTender所以最后标签上的条码内容可以被扫描枪反查回溯到这条记录。有一个细节是写日志和打印最好在同一个事务边界里考虑至少不能出现“打印成功但日志没写”的情况否则后面追溯会出现孤品。7.3 大批量打印时的性能与界面卡顿一次性打印几百张标签如果直接在UI线程调用PrintOut界面会原地假死几十秒。正确做法是用Task.Run把打印放到后台线程或者把打印请求丢进队列有专门的线程消费。我更喜欢后台线程方式简单直接配合lock也能保证单实例访问。大批量打印时注意不要循环几百次打开/关闭模板。一次PrintOut可以传几百份模板序列号会自动递增即使数据要变化也尽量一批数据整理好后连续打印减少模板加载次数。BarTender的Application冷启动要几秒但常驻实例之后单张打印只需要几十毫秒到几百毫秒对产线节拍完全够用。最后再分享一个小经验如果打出来的条码扫不出来先回模板里看符号体系、DPI和打印机浓度再回来翻C#代码。数据内容和条码质量的分界线就在这里别在错误的方向使劲。老产线、老系统、VS2012这些条件听上去制约很多但COM调用BarTender的方案足够成熟先把链路跑通后面再考虑要不要迁移到新版SDK那是优化的事不是拦路的事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价