在制造业、仓储物流、质检打标这些场景里标签打印几乎是每天都要面对的刚需。我最早接触标签打印是给一家做汽配的客户做产线追溯系统当时他们的做法是操作员在 Bartender 里手动改模板里的几个字段然后点打印一天几百个批次下来手都点酸了还经常改错。后来我把整套流程改成 C# 程序驱动 Bartender 自动打印操作员只需要在界面上选批次、点一下按钮剩下的全自动完成。这套方案跑了三年多稳定性很好今天就把完整思路和源码拆开讲清楚。这篇内容主要面向有 C# 基础、需要对接 Bartender 做自动化打印的开发者尤其是做上位机、MES、WMS、产线追溯系统的朋友。核心讲三件事C# 怎么调用 Bartender 的打印引擎、动态数据怎么传进去、批量打印怎么设计才不卡不崩。文中所有代码都是我在实际项目里跑过的可以直接拿去改。1. 先搞清楚 Bartender 到底提供了哪几种调用方式很多人一上来就搜Bartender 二次开发结果看到一堆方案反而不知道选哪个。我先把 Bartender 10.1 对外提供的几种集成方式摆出来说清楚各自的适用场景你才能判断自己该走哪条路。1.1 三种主流集成方式对比Bartender 本身是一个标签设计与打印软件但它同时提供了多种对外接口让外部程序可以驱动它打印。常见的有这三种集成方式原理优点缺点适用场景命令行调用用bartend.exe加参数启动简单无需引用组件无法获取打印状态交互弱简单单次打印ActiveX 自动化COM通过BarTender.Application对象操作功能全可读写模板、控制打印需要装 BartenderCOM 注册复杂业务、动态改模板.NET SDKSeagull 官方库引用官方 .NET 程序集类型安全性能好需要对应版本 SDK新项目、.NET 环境我实际项目里用得最多的是ActiveX 自动化也就是 COM 方式。原因很直接客户现场装的就是 Bartender 10.1 标准版没有额外买 SDK而 COM 接口是标准版就自带的不需要额外授权。命令行方式虽然简单但它没法拿到打印结果也没法在打印前动态改数据源做批量追溯根本不够用。1.2 为什么我最终选了 COM 而不是命令行命令行调用的典型写法是这样bartend.exe /FC:\Labels\demo.btw /P /X/F指定模板/P直接打印/X打印后退出。看起来很方便但它有几个致命问题第一你没法在打印前把数据库里的动态数据塞进模板第二你拿不到打印成功还是失败的反馈第三批量打印时每调一次就启动一个进程几百个标签下来系统资源直接爆掉。COM 方式就不一样了。它是在当前进程里创建一个 Bartender 应用对象打开模板、改数据、发打印指令全程可控。批量打印时只需要打开一次应用对象循环改数据、循环打印效率高得多。这也是为什么做上位机、MES 集成的同行基本都走 COM 这条路。1.3 环境准备里最容易忽略的两个细节在动手写代码之前有两个坑我必须提前说不然你代码写得再对也跑不起来。第一个是Bartender 的版本和位数。Bartender 10.1 有 32 位和 64 位之分你的 C# 程序目标平台必须和它匹配。如果你装的是 32 位 Bartender而你的程序编译成 x64创建 COM 对象时会直接报类未注册或者无法创建对象。我一般建议把 C# 项目的目标平台显式设成x86兼容性最好。第二个是模板里的数据源命名。COM 方式改数据靠的是模板里数据源的名称。你在 Bartender 设计器里给每个动态字段起什么名字代码里就得用什么名字去匹配。我见过太多人代码没问题就是名字对不上结果打印出来全是空的。建议命名规范统一比如SN、BatchNo、ProductName别用中文名避免编码问题。提示在 Bartender 设计器里双击某个文本对象看它的数据源属性那个名字就是代码里要用的名字。改之前先在设计器里确认一遍。2. 用 C# 创建 Bartender 应用对象并打开模板环境确认好之后就可以开始写代码了。这一节讲最基础的一步怎么在 C# 里把 Bartender 拉起来并且打开一个指定的模板文件。这一步是所有后续操作的地基地基不稳后面全白搭。2.1 引用 COM 组件与创建对象在 Visual Studio 里右键项目 → 添加引用 → COM → 找到BarTender 10.1相关的类型库勾选后确定。VS 会自动生成互操作程序集Interop之后你就能在代码里用BarTender.Application这个类了。创建应用对象的代码很简洁using BarTender; public class BtEngine { private Application btApp; public void Start() { btApp new Application(); btApp.Visible false; // 后台运行不弹界面 btApp.Formats.Open(C:\Labels\demo.btw, false, true); } }这里有几个参数要解释清楚。Visible false是让 Bartender 在后台静默运行用户看不到它的窗口适合集成到自己的系统里。Formats.Open的三个参数分别是模板路径、是否以只读方式打开、是否显示打印对话框。第二个参数传false表示可写第三个传true表示不弹对话框直接打印。这两个参数如果搞反了要么改不了数据要么每次打印都弹窗体验很差。2.2 打开模板时的路径与权限问题模板路径这块我踩过最典型的坑是相对路径。开发时用相对路径没问题因为工作目录就是项目目录但部署到客户现场程序可能被做成服务或者被别的进程拉起工作目录就变了相对路径直接找不到文件。所以模板路径一定要用绝对路径或者用AppDomain.CurrentDomain.BaseDirectory拼出来。另一个坑是权限。如果程序以服务方式运行或者运行账户权限不够打开模板时会报拒绝访问。解决办法是把模板放在程序自己的目录下或者给运行账户授予模板目录的读权限。我一般把模板和程序放同一个目录省去很多麻烦。2.3 一个容易忽略的释放问题COM 对象用完必须释放否则 Bartender 进程会一直挂在后台越积越多。很多人只调btApp.Quit()但这样并不彻底。正确的做法是显式释放public void Stop() { if (btApp ! null) { btApp.Quit(BtSaveOptions.btDoNotSaveChanges); System.Runtime.InteropServices.Marshal.ReleaseComObject(btApp); btApp null; } GC.Collect(); GC.WaitForPendingFinalizers(); }Quit的参数表示退出时是否保存模板改动批量打印场景下我们不需要保存传btDoNotSaveChanges就行。后面手动ReleaseComObject加GC.Collect是为了确保 COM 引用计数归零进程能真正退出。这一步不做跑一天下来任务管理器里全是bartend.exe。3. 动态数据怎么塞进模板SubStrings 的正确用法模板打开之后最关键的一步就是把业务数据写进去。Bartender COM 接口里改数据靠的是SubStrings集合。这一节把它的用法、命名匹配、常见错误讲透。3.1 SubStrings 的定位逻辑在 Bartender 里每个动态文本对象背后都有一个数据源数据源的名字就是SubStrings的键。代码里这样写Format format btApp.Formats[0]; format.SubStrings[SN].Value SN20240101001; format.SubStrings[BatchNo].Value B20240101; format.SubStrings[ProductName].Value 刹车片A型;SubStrings[SN]里的SN必须和模板里数据源的名字完全一致大小写敏感。如果名字写错运行时会抛异常提示找不到该子字符串。我建议在代码里把这些名字定义成常量避免手写拼错private const string FIELD_SN SN; private const string FIELD_BATCH BatchNo; private const string FIELD_PRODUCT ProductName;3.2 数据源类型不同赋值方式也不同这里有个很多人不知道的细节Bartender 的数据源分好几种类型有文本、数据库字段、计数器、日期时间等。如果你在模板里把某个字段设成了数据库字段类型那它可能不接受直接赋值而是跟着数据库走。这时候你在代码里改SubStrings是无效的。解决办法是在设计模板时把需要动态赋值的字段统一设成文本类型或者设成命名数据源。命名数据源是最灵活的它允许外部程序直接赋值。我在做项目时所有需要 C# 动态填的字段一律用命名数据源这样代码里改起来最省心。3.3 中文乱码与特殊字符处理中文乱码是另一个高频问题。如果你的数据里有中文而模板字体不支持中文打印出来就是方块或者问号。这不是代码问题是模板字体问题。解决办法是在 Bartender 设计器里把对应文本对象的字体设成支持中文的字体比如宋体、微软雅黑。特殊字符也要注意。比如数据里带了、、这类字符某些情况下会被当成标记语言解析。虽然 Bartender 的 SubStrings 一般不会但保险起见如果数据来源不可控建议做一次转义或者过滤。我在处理客户名称这类字段时会先做一次清洗去掉控制字符。注意赋值前最好判断一下SubStrings里是否存在该键避免模板改版后代码直接崩。可以用 try-catch 包住或者先遍历检查。4. 批量打印的性能设计与防崩策略单张打印跑通之后真正的考验是批量。一次打几百上千张标签如果设计不当轻则慢得让人抓狂重则程序卡死、内存溢出。这一节讲我在实际项目里总结的批量打印方案。4.1 为什么不能打开一次模板打一张最直观的写法是循环里每次都打开模板、改数据、打印、关闭。这种写法在数据量小的时候看不出问题一旦上到几百条性能直接崩盘。因为每次Formats.Open和Formats.Close都是重量级操作涉及文件 IO 和对象创建。正确的做法是打开一次模板循环改数据、循环打印Format format btApp.Formats.Open(templatePath, false, true); foreach (var item in dataList) { format.SubStrings[FIELD_SN].Value item.SN; format.SubStrings[FIELD_BATCH].Value item.BatchNo; format.PrintOut(false, false); // 不弹框不等待 } format.Close(BtSaveOptions.btDoNotSaveChanges);这样模板只打开一次循环里只做数据赋值和打印指令效率提升非常明显。我实测过一千张标签这种写法比每次打开快好几倍。4.2 PrintOut 的参数与打印队列PrintOut有两个常用参数第一个是是否显示打印对话框第二个是是否等待打印完成。批量场景下第一个传false不弹框第二个传false不等待。不等待的意思是指令发给打印队列就返回程序继续处理下一条。这里要注意不等待不等于打印完成。如果你需要确认每张都打出来了就得传true等待但那样速度会慢很多。我的做法是批量打印时用不等待模式把速度拉满然后在程序里记录已发送状态实际是否打出来由操作员目视确认。如果业务要求严格可以配合打印机状态查询来做。4.3 分批处理与内存控制即使打开了模板只循环赋值数据量特别大时比如上万条内存还是会涨。原因是 Bartender 内部会缓存打印任务。我的经验是分批处理比如每 200 条一批一批打完让引擎喘口气int batchSize 200; for (int i 0; i dataList.Count; i batchSize) { var batch dataList.Skip(i).Take(batchSize).ToList(); PrintBatch(format, batch); System.Threading.Thread.Sleep(500); // 给打印队列缓冲时间 }这个Sleep不是随便加的。打印机本身有缓冲区如果程序发得太快打印机处理不过来会出现丢标签或者错位。加个几百毫秒的间隔实际总耗时增加不多但稳定性提升很大。4.4 异常处理与断点续打批量打印最怕的是打到一半出错比如某条数据格式不对或者打印机卡纸。如果没做异常处理整个循环就断了前面的打了后面的没打操作员也不知道打到哪了。我的做法是逐条 try-catch记录成功和失败的索引Listint failedIndexes new Listint(); for (int i 0; i dataList.Count; i) { try { format.SubStrings[FIELD_SN].Value dataList[i].SN; format.PrintOut(false, false); } catch (Exception ex) { failedIndexes.Add(i); LogError($第{i}条打印失败{ex.Message}); } }这样即使中间有失败循环也不会断最后把失败列表返回给界面操作员可以针对失败的重打。这个设计在实际项目里救过我好几次尤其是数据来源不规范的时候。5. 完整源码与工程结构前面几节把关键点都拆开了这一节把完整代码整合起来给你一个可以直接用的工程结构。代码我做了封装分成引擎类、数据模型、调用示例三部分。5.1 引擎类完整实现using System; using System.Collections.Generic; using BarTender; namespace BtPrintDemo { public class BartenderEngine : IDisposable { private Application _btApp; private Format _format; private const string FIELD_SN SN; private const string FIELD_BATCH BatchNo; private const string FIELD_PRODUCT ProductName; public void Open(string templatePath) { _btApp new Application(); _btApp.Visible false; _format _btApp.Formats.Open(templatePath, false, true); } public void PrintOne(LabelData data) { _format.SubStrings[FIELD_SN].Value data.SN; _format.SubStrings[FIELD_BATCH].Value data.BatchNo; _format.SubStrings[FIELD_PRODUCT].Value data.ProductName; _format.PrintOut(false, false); } public Listint PrintBatch(ListLabelData list, int batchSize 200) { var failed new Listint(); for (int i 0; i list.Count; i) { try { PrintOne(list[i]); } catch (Exception ex) { failed.Add(i); Console.WriteLine($第{i}条失败{ex.Message}); } if ((i 1) % batchSize 0) { System.Threading.Thread.Sleep(500); } } return failed; } public void Dispose() { if (_format ! null) { _format.Close(BtSaveOptions.btDoNotSaveChanges); System.Runtime.InteropServices.Marshal.ReleaseComObject(_format); _format null; } if (_btApp ! null) { _btApp.Quit(BtSaveOptions.btDoNotSaveChanges); System.Runtime.InteropServices.Marshal.ReleaseComObject(_btApp); _btApp null; } GC.Collect(); GC.WaitForPendingFinalizers(); } } public class LabelData { public string SN { get; set; } public string BatchNo { get; set; } public string ProductName { get; set; } } }5.2 调用示例using (var engine new BartenderEngine()) { engine.Open(C:\Labels\demo.btw); var list new ListLabelData(); for (int i 1; i 500; i) { list.Add(new LabelData { SN $SN20240101{i:D4}, BatchNo B20240101, ProductName 刹车片A型 }); } var failed engine.PrintBatch(list); Console.WriteLine($打印完成失败{