十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

C#驱动Bartender实现标签打印:动态字段替换与批量打印实战

C#驱动Bartender实现标签打印:动态字段替换与批量打印实战 1. 为什么我要用 C# 去驱动 Bartender 做打印工厂里做 MES、WMS、上位机的人绕不开一个需求标签打印。产品下线要打条码入库要打库位标出库要打物流面单返修要打返工标。这些标签的模板十有八九是产线工程师或者品质部用 Bartender 画好的因为 Bartender 在标签排版这块确实顺手拖拖拽拽就能把条码、二维码、文本、图片、序列号排得明明白白。问题就出在“画好之后”这一步。模板是死的数据是活的。今天生产 A 型号 500 件明天生产 B 型号 300 件每件的序列号、批次号、生产日期都不一样。你不可能让操作员在 Bartender 软件里手动改一条打一条那效率低到没法看还容易改错。所以真正落地的做法是用 C# 写一个上位机程序把数据库或者界面上的数据动态塞进 Bartender 模板里然后触发打印甚至一次性批量打印几百上千张。这套东西我做过好几个项目从最简单的“单张动态打印”到“一枪扫进去自动打一整批”踩过的坑不算少。这篇就把整个链路拆开讲清楚Bartender 的引擎怎么调、C# 里怎么引用、动态字段怎么替换、批量打印怎么组织数据、序列号怎么保证不重复、出问题了怎么排查。文末我会把核心源码结构给出来你拿去改改就能用。适合谁看如果你正在做 C# 上位机、MES 对接、产线自动化或者单纯被“怎么让程序自动打标签”卡住了这篇应该能帮你省掉几天摸索的时间。哪怕你之前没碰过 Bartender 的二次开发跟着走一遍也能跑通。2. 整体方案设计与技术选型思路2.1 为什么是 Bartender 而不是纯代码画标签先回答一个很多人会问的问题既然都用 C# 了为什么不干脆用 ZPL 指令或者 GDI 直接画标签非要绕一层 Bartender原因很现实。第一标签模板通常不是程序员设计的是产线或品质部门设计的他们只会用 Bartender 这种可视化工具你让他们去写 ZPL 不现实。第二Bartender 对条码类型、字体、排版、打印机的适配做得非常成熟尤其是斑马、TSC、Godex 这些工业打印机Bartender 的驱动层处理了很多兼容性细节。第三改模板不用改代码。产线要调一下标签上文字的位置他们在 Bartender 里拖一下保存就行程序完全不用动。这三点加起来就决定了“Bartender 做模板 C# 做数据驱动”是最省事的组合。提示这里说的 Bartender 是正版授权软件实际项目里请使用合法授权的版本本文只讨论技术实现思路。2.2 两种集成方式命令行 vs SDK 引擎Bartender 对外提供二次开发能力常见的有两条路。一条是命令行方式用bartend.exe加参数去调用比如/F模板路径 /D数据文件。这种方式简单粗暴但灵活性差动态字段的替换要靠中间文件批量打印时性能也一般而且不好拿到打印结果反馈。另一条是SDK 引擎方式也就是引用 Bartender 安装目录下的.NET程序集主要是Seagull.BarTender.Print.dll在 C# 里直接创建Engine对象打开LabelFormat通过SubStrings替换动态字段再调用Print方法。这种方式是进程内调用性能好、可控性强、能拿到打印状态是正经项目该用的方式。我选的是 SDK 引擎方式。下面所有的代码和思路都基于这条路线。2.3 整体数据流设计把整个链路理一下大概是这么个流程C# 程序从数据库、Excel、扫码枪或者界面控件拿到业务数据。把数据组织成一条条“打印任务”每条任务对应一张标签。打开 Bartender 模板定位到需要动态替换的字段Bartender 里叫“子字符串” SubString。逐条把数据写进对应字段触发打印。批量场景下循环执行第 4 步或者用事务化的方式一次性提交。记录打印日志失败的重试或报警。这个流程看着简单但每一步都有细节。比如字段怎么命名才能和代码对上、序列号怎么递增、批量打印时引擎要不要复用、打印机脱机了怎么办。这些后面逐个讲。3. 环境准备与 Bartender 模板设计要点3.1 开发环境与依赖清单先把环境说清楚避免你卡在“引用都找不到”这种低级问题上。开发工具Visual Studio 2019 或 2022社区版就够。目标框架.NET Framework 4.6.1 及以上或者 .NET 6/8 的 Windows 桌面项目。注意 Bartender 的 SDK 程序集对框架版本有要求太老的 4.0 可能不兼容热词里也提到“c#不再支持 netframework 4.0”这个坑要避开。Bartender 版本本文以 Bartender 10.1 为基准2016、2019、2021 的 SDK 接口基本一致差异不大。关键程序集Seagull.BarTender.Print.dll一般位于 Bartender 安装目录下比如C:\Program Files (x86)\Seagull\BarTender Suite\。打印机任意支持 Windows 驱动的标签打印机调试阶段可以先用虚拟打印机或者“打印到文件”。引用程序集的时候在 VS 里右键项目 → 添加引用 → 浏览找到那个 dll 加进来。加完之后代码里using Seagull.BarTender.Print;就能用了。注意SDK 程序集是分 32 位和 64 位的你的 C# 项目平台目标要和它一致。如果报“BadImageFormatException”八成是位数对不上把项目改成 x86 或 x64 再试。3.2 模板里必须做的三件事在写代码之前模板本身要先设计好否则代码再对也白搭。我要求模板设计者必须做三件事。第一给每个动态字段起一个规范的“子字符串名称”。在 Bartender 里双击一个文本或条码对象找到“数据源”或者“子字符串”设置给它命名。比如产品序列号叫SN批次号叫LOT日期叫PROD_DATE。这个名字就是代码里替换的 key必须唯一、不能有中文和空格。第二模板里所有要变的内容都必须是子字符串不能是硬编码文本。有些设计者图省事直接把“批次”这种前缀写死只把后面的值做成子字符串这没问题。但如果整个字段都是死的代码就没法替换。第三保存为.btw格式记住完整路径。路径里尽量不要有中文和空格虽然理论上支持但实际项目里中文路径偶尔会出幺蛾子能避则避。3.3 字段命名与代码的对应关系我一般会维护一张对照表放在代码的配置里而不是散落在各处。比如模板字段名业务含义数据类型示例值SN产品序列号字符串SN20240101001LOT批次号字符串20240101APROD_DATE生产日期日期字符串2024-01-01QTY数量整数500BARCODE主条码内容字符串6901234567890这张表的好处是模板改了字段名你只需要改配置不用满代码找。后面讲批量打印的时候这张表就是数据映射的依据。4. C# 调用 Bartender 引擎的核心实现4.1 引擎的创建与生命周期管理Bartender 的Engine对象是整个操作的入口。它的创建和销毁是有成本的所以不要每打一张标签就 new 一个 Engine那样性能会很差。正确的做法是在程序启动或者打印任务开始时创建一个 Engine用完再关掉。using Seagull.BarTender.Print; private Engine _btEngine; private bool StartEngine() { _btEngine new Engine(true); // true 表示显示 Bartender 的打印对话框一般设 false _btEngine.Start(); return _btEngine.IsAlive; }Engine启动后是常驻的可以反复打开模板、打印。程序退出或者长时间不用时调用_btEngine.Stop()释放资源。我一般会在FormClosing或者服务的OnStop里做这件事。实操心得Engine 启动偶尔会慢尤其是第一次启动可能要一两秒。如果你的程序对响应速度敏感可以在程序初始化时就先把 Engine 启起来别等到用户点了打印才启动。4.2 打开模板与定位子字符串打开模板用Engine.Formats.Open方法返回一个LabelFormat对象。这个对象就是你对模板的操作句柄。LabelFormat format _btEngine.Formats.Open(D:\Labels\ProductLabel.btw, true);第二个参数true表示以可写方式打开因为我们要替换子字符串。打开之后通过format.SubStrings[字段名].Value 值来赋值。format.SubStrings[SN].Value SN20240101001; format.SubStrings[LOT].Value 20240101A; format.SubStrings[PROD_DATE].Value DateTime.Now.ToString(yyyy-MM-dd);这里有个细节如果字段名写错了format.SubStrings[XXX]会抛异常或者返回 null所以最好先判断一下字段是否存在。我一般会封装一个安全赋值的方法字段不存在就记日志而不是直接崩。4.3 触发打印与获取打印结果赋值完成后调用format.Print()或者format.Print(打印机名)触发打印。返回值是一个Result枚举能告诉你打印是成功、失败还是被取消。Result result format.Print(Zebra ZT410, 1); // 打印机名份数 if (result Result.Success) { // 打印成功记日志 } else { // 打印失败报警或重试 }打印份数这个参数单张动态打印时传 1批量打印时可以在循环里每张传 1也可以一次传多份。但要注意如果每张标签内容不同就不能靠份数参数必须循环替换再打印。4.4 关闭模板与异常处理打完一张或者一批之后要调用format.Close()关闭模板释放句柄。如果不关模板文件会被占用下次打开可能失败。format.Close(SaveOptions.DoNotSaveChanges);异常处理这块Bartender 的 SDK 抛的异常类型主要是PrintEngineException和PrintException。我一般会用一个 try-catch 包住整个打印流程捕获后记录详细信息包括模板路径、字段值、打印机名方便排查。5. 动态打印的完整实操流程5.1 单张动态打印的代码骨架把上面的片段串起来一个完整的单张动态打印方法大概长这样public bool PrintSingleLabel(Dictionarystring, string fieldValues, string printerName) { LabelFormat format null; try { format _btEngine.Formats.Open(_templatePath, true); foreach (var kv in fieldValues) { if (format.SubStrings[kv.Key] ! null) { format.SubStrings[kv.Key].Value kv.Value; } } Result result format.Print(printerName, 1); return result Result.Success; } catch (Exception ex) { // 记录日志 return false; } finally { format?.Close(SaveOptions.DoNotSaveChanges); } }调用的时候把业务数据组装成字典传进去就行。这种写法适合“扫一个码打一张”的场景比如产线工位扫码后立即打印。5.2 序列号连续递增且不重复的实现序列号是标签打印里最容易出问题的地方。热词里有人搜“c#中设计连续编号不重复的代码”说明这是刚需。核心要求是同一批次内序列号连续不同批次之间不重复程序重启后也不能重号。我的做法是序列号不靠 Bartender 自己生成而是由 C# 程序统一管理。程序里维护一个“当前序列号”的状态每次打印前先取号、加一、持久化再赋值给模板。private int _currentSerial 0; private readonly object _serialLock new object(); private string GetNextSerial(string prefix) { lock (_serialLock) { _currentSerial; SaveSerialToFile(_currentSerial); // 持久化到文件或数据库 return ${prefix}{_currentSerial:D6}; } }lock是为了防止多线程并发取号导致重号。持久化是为了程序重启后能接着上次的号继续。如果对可靠性要求高可以把序列号存在数据库里用事务保证取号和打印的原子性。注意千万不要用DateTime.Now.Ticks或者随机数去凑序列号那种做法在批量打印时几乎必然重号或者跳号产线会来找你麻烦的。5.3 日期、批次等字段的格式化处理日期字段要注意格式。Bartender 模板里如果设置了日期格式你传字符串进去可能会被二次解析。稳妥的做法是模板里把日期字段设成“文本”类型然后 C# 里格式化成你想要的字符串再传。format.SubStrings[PROD_DATE].Value DateTime.Now.ToString(yyyy-MM-dd); format.SubStrings[PROD_TIME].Value DateTime.Now.ToString(HH:mm:ss);批次号一般是“日期班次流水”这种组合比如20240101A01。这种拼接逻辑放在 C# 里做模板只负责显示。5.4 打印机的选择与切换多打印机场景很常见比如一条线有多个工位每个工位一台打印机。format.Print()的第一个参数就是打印机名传不同的名字就能打到不同的机器上。打印机名从哪来从System.Drawing.Printing.PrinterSettings.InstalledPrinters里能枚举出本机安装的所有打印机。我一般会在界面上做一个下拉框让用户选选完存到配置里。foreach (string printer in System.Drawing.Printing.PrinterSettings.InstalledPrinters) { comboBoxPrinter.Items.Add(printer); }实操心得打印机名必须和 Windows 里显示的完全一致包括大小写和空格。如果传错名字Print会返回失败但错误信息不一定明确容易让人以为是模板问题。我踩过这个坑排查了半天才发现是打印机名少了个空格。6. 批量打印的组织与性能优化6.1 批量打印的数据组织方式批量打印的本质是“一批数据循环替换逐张打印”。数据来源可能是数据库查询结果、Excel 导入、或者扫码枪连续扫描。不管来源是什么最终都要组织成一个列表每个元素是一张标签的字段字典。ListDictionarystring, string printTasks new ListDictionarystring, string(); // 从数据库或Excel填充 printTasks foreach (var task in printTasks) { PrintSingleLabel(task, printerName); }这种写法最直观但有个性能问题每张标签都Open和Close一次模板开销很大。批量几百张的时候能明显感觉到慢。6.2 复用模板句柄提升批量打印速度优化的思路是批量打印时只打开一次模板循环替换字段并打印最后统一关闭。这样省掉了反复打开关闭模板的开销。public int PrintBatch(ListDictionarystring, string tasks, string printerName) { int successCount 0; LabelFormat format null; try { format _btEngine.Formats.Open(_templatePath, true); foreach (var task in tasks) { foreach (var kv in task) { if (format.SubStrings[kv.Key] ! null) { format.SubStrings[kv.Key].Value kv.Value; } } Result result format.Print(printerName, 1); if (result Result.Success) successCount; } } finally { format?.Close(SaveOptions.DoNotSaveChanges); } return successCount; }实测下来这种写法比逐张打开关闭快好几倍。批量 500 张标签优化前可能要一两分钟优化后十几秒就完事。6.3 大批量打印的分批与进度反馈如果批量数据量特别大比如几千张一次性循环会让界面卡死用户以为程序挂了。这时候要分批处理并且给进度反馈。我的做法是把任务列表切成小批比如每批 50 张用Task.Run放到后台线程执行通过IProgress或者事件回调更新界面进度条。await Task.Run(() { for (int i 0; i tasks.Count; i 50) { var batch tasks.Skip(i).Take(50).ToList(); PrintBatch(batch, printerName); progress.Report(i batch.Count); } });注意Bartender 的 Engine 不是线程安全的多个线程同时操作同一个 Engine 会出问题。所以批量打印要么单线程串行要么每个线程用自己的 Engine 实例。我一般用单线程串行简单可靠。6.4 批量打印的失败重试机制批量打印最怕的是打到一半打印机卡纸或者脱机后面的全失败。所以要有失败重试和断点续打的能力。我的做法是每张标签打印后记录状态失败的收集到一个“失败列表”里。整批打完后如果有失败弹窗提示用户可以选择重试失败的那些。重试时只打失败列表不影响已成功的。ListDictionarystring, string failedTasks new ListDictionarystring, string(); foreach (var task in tasks) { if (!PrintSingleLabel(task, printerName)) { failedTasks.Add(task); } } if (failedTasks.Count 0) { // 提示用户提供重试入口 }这个机制在实际产线里非常有用因为打印机出状况是常态没有重试机制的话操作员只能整批重打浪费标签还容易重号。7. 常见问题与排查技巧实录7.1 打印失败的典型原因速查表现象可能原因排查方向Print 返回失败打印机名错误或脱机检查打印机名、电源、连接打开模板抛异常模板路径错误或被占用检查路径、是否被其他程序打开字段替换无效子字符串名称不匹配在 Bartender 里核对字段名打印内容乱码字体或编码问题检查模板字体、传值编码批量打印中途停止打印机卡纸或缓存满检查打印机状态、分批重试序列号重号并发取号未加锁检查取号逻辑的线程安全程序报 BadImageFormatException32/64 位不匹配调整项目平台目标这张表是我这些年遇到问题后总结的基本覆盖了八成以上的故障。遇到问题先对着表查一遍能省很多时间。7.2 模板被占用导致打开失败怎么办这个问题很常见。Bartender 软件本身如果开着某个模板你的程序再去打开就会失败。解决办法有两个一是确保 Bartender 设计器没开着同一个模板二是用SaveOptions.DoNotSaveChanges关闭避免程序自己占用。还有一种情况是程序异常退出没走到Close导致模板句柄泄漏。这种要在程序启动时做一次清理或者用using模式确保释放。7.3 打印内容与预期不符的调试方法有时候打印出来的标签字段位置对了但内容不对比如日期格式变了、条码扫不出来。这种问题光看代码看不出来要把实际传给模板的值打日志。我一般会在替换字段的地方加一行日志把字段名和值都记下来System.Diagnostics.Debug.WriteLine($字段 {kv.Key} {kv.Value});然后拿这个值和模板里设置的格式对比。条码扫不出来多半是条码内容里混入了不可见字符或者校验位没算对。这种要在传值前做一次清洗。7.4 性能瓶颈的定位与优化如果批量打印慢先定位瓶颈在哪。是打开模板慢还是替换字段慢还是打印机本身慢。可以在每个环节加计时。var sw System.Diagnostics.Stopwatch.StartNew(); format _btEngine.Formats.Open(_templatePath, true); sw.Stop(); System.Diagnostics.Debug.WriteLine($打开模板耗时 {sw.ElapsedMilliseconds}ms);实测下来打开模板通常是大头所以复用模板句柄的优化最有效。打印机本身的物理速度是改不了的那是硬件限制。实操心得如果打印机是网络打印机网络延迟也会影响批量打印速度。这种情况可以考虑先把打印任务发到本地打印队列让 Windows 慢慢往网络打印机送程序不用等。8. 源码结构与关键类设计8.1 整体类结构我把这套东西封装成了几个类职责分明方便复用。BartenderEngineManager负责 Engine 的启动、停止、单例管理。LabelPrinter负责单张和批量打印的核心逻辑。SerialNumberGenerator负责序列号的取号、持久化、线程安全。PrintTask打印任务的数据模型包含字段字典和状态。PrintLogger负责打印日志的记录和查询。这种拆分的好处是Engine 的生命周期和打印逻辑解耦序列号生成独立可测试日志独立可替换。8.2 核心类的关键方法LabelPrinter是核心它对外暴露两个方法PrintSingle和PrintBatch。内部共用一套字段替换逻辑。public class LabelPrinter { private readonly BartenderEngineManager _engineManager; private readonly string _templatePath; public bool PrintSingle(Dictionarystring, string fields, string printerName) { // 单张打印逻辑 } public int PrintBatch(ListDictionarystring, string tasks, string printerName) { // 批量打印逻辑复用模板句柄 } private void ApplyFields(LabelFormat format, Dictionarystring, string fields) { // 统一的字段替换逻辑 } }ApplyFields抽出来是为了单张和批量共用避免代码重复。字段不存在时的处理也在这里统一做。8.3 配置与模板路径管理模板路径、打印机名、字段映射这些配置我一般放在appsettings.json或者App.config里不硬编码在代码里。这样换模板、换打印机不用重新编译。appSettings add keyTemplatePath valueD:\Labels\ProductLabel.btw/ add keyDefaultPrinter valueZebra ZT410/ /appSettings读取的时候用ConfigurationManager.AppSettings[TemplatePath]。如果是 .NET Core 项目用IConfiguration注入。8.4 日志与异常记录日志这块我推荐用NLog或者Serilog比手写文件日志靠谱。关键要记录的信息包括打印时间、模板路径、打印机名、字段值、打印结果、异常堆栈。_logger.Info(打印任务开始模板{Template}打印机{Printer}, _templatePath, printerName); _logger.Error(ex, 打印失败字段{Fields}, string.Join(,, fields.Select(kv ${kv.Key}{kv.Value})));有了这些日志出问题的时候能快速定位是哪张标签、哪个字段、哪台打印机出的问题。9. 一些踩坑后的个人体会序列号这块我再强调一次因为它太容易出事了。我见过有项目用数据库自增主键当序列号结果测试环境打了一批正式环境接着打号就断了。还有项目用时间戳同一秒内批量打印直接重号。所以序列号一定要有独立的、持久化的、带锁的生成器别偷懒。批量打印的进度反馈也很重要。操作员点了“打印 500 张”如果界面没反应他会以为没点上然后再点一次结果打了 1000 张。所以要么禁用按钮要么给进度条让他知道程序在干活。最后说一个模板设计的小技巧。让产线设计模板的时候把所有动态字段集中放在一个区域命名统一加前缀比如F_SN、F_LOT。这样代码里遍历替换的时候可以按前缀筛选不容易漏字段也不容易误替换。这套方案我在几个项目里都用过从单机版到网络版从几十张到几千张批量基本都能扛住。核心就是那几件事Engine 复用、字段映射、序列号管理、失败重试。把这四点做扎实剩下的就是业务逻辑的适配了。
返回列表