C# 打印条码标签实战:GDI+ 与 ZPL 指令选型及避坑指南 简介这是一份面向C#开发者的条码标签打印实战Demo适合从事物流、仓储、零售等桌面应用开发的技术人员参考学习。资源围绕条码生成与打印展开涵盖第三方条码库与ZPL、ITF等指令调用、GDI图形绘制、PDF与图像文档格式化、串口及网络打印机通信、WPF或WinForms界面设计以及异常处理、日志记录与单元测试等关键环节帮助读者理解从数据输入、条码预览到标签输出的完整流程。压缩包共630个文件约24.15MB以178个dll依赖库、154个xml配置与文档、142个无扩展名文件、52个txt说明及22个nupkg包为主另含cs源码、png示意图、exe可执行文件与sln解决方案结构完整便于直接运行调试。目前已有693人学习下载可作为条码打印项目的实践参考与排错思路来源。1. 从一张 40mm×30mm 的标签说起C# 打印条码标签 Demo 到底在做什么仓库里最常见的场景是这样的产线工人扫一下工单旁边那台小票机“滋”一声吐出一张 40mm×30mm 的标签上面有 Code128 条码、物料编码、批次号和一行二维码。很多人第一次接到这个需求以为就是Graphics.DrawString加个字体结果真机一跑条码扫不出来、边距对不上、换台打印机整版偏移。C# 打印条码标签 Demo 要解决的从来不是“怎么画一个条码”而是“怎么把一张标签稳定、可复现地送到指定打印机上并且扫得出来”。它适合两类人一类是刚接手 MES/WMS 周边打印模块的 .NET 开发者另一类是想把手工贴标换成自动打标的产线集成人员。这一篇不讲空泛概念只讲我实际落地时用的选型、代码和参数以及那些让我返工三次的坑。2. 先选对打印路线GDI 还是标签指令2.1 三种主流路线的适用边界在 C# 里打印条码标签绕不开三条路。第一条是 GDI 直接画到PrintDocument用Graphics画条码和文字优点是纯托管、不依赖驱动 SDK缺点是分辨率受打印机驱动影响小标签上容易糊。第二条是调用打印机厂商的指令集比如 ZPL、EPL、CPCL把整张标签拼成字符串发给打印机优点是打印快、条码锐利、不占 CPU缺点是换品牌就要改指令。第三条是走驱动提供的 Windows 打印队列用RawPrinterHelper把指令直接塞进 spooler兼顾速度和通用性。我的经验是标签尺寸小于 50mm、条码密度高、每天打印量上千张优先走指令路线如果只是偶尔打几张、标签内容经常变、又不想学指令GDI 足够。Demo 里我一般两条都留用配置切换。2.2 用 GDI 画一张最小标签的完整代码先看 GDI 路线因为它最容易跑通也最能暴露坐标系问题。using System; using System.Drawing; using System.Drawing.Printing; public class LabelPrinter { private readonly string _barcodeText; public LabelPrinter(string barcodeText) { _barcodeText barcodeText; } public void Print() { var doc new PrintDocument(); // 指定目标打印机名称必须和系统里完全一致 doc.PrinterSettings.PrinterName Zebra ZD421; // 标签纸在驱动里通常自定义为 40mm x 30mm doc.DefaultPageSettings.PaperSize new PaperSize(Label, 157, 118); // 单位 1/100 英寸 doc.DefaultPageSettings.Margins new Margins(2, 2, 2, 2); doc.PrintPage DrawLabel; doc.Print(); } private void DrawLabel(object sender, PrintPageEventArgs e) { var g e.Graphics; // 高分辨率下必须设置否则条码边缘锯齿严重 g.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.HighQuality; g.InterpolationMode System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; // 画 Code128 条码这里用字体方式演示实际项目建议用条码库 var barcodeFont new Font(Code 128, 28, FontStyle.Regular, GraphicsUnit.Pixel); g.DrawString(_barcodeText, barcodeFont, Brushes.Black, new PointF(4, 4)); // 画文字注意用等宽字体避免对不齐 var textFont new Font(Consolas, 9, FontStyle.Regular, GraphicsUnit.Pixel); g.DrawString(物料A-1024, textFont, Brushes.Black, new PointF(4, 46)); g.DrawString(批次20240521, textFont, Brushes.Black, new PointF(4, 60)); e.HasMorePages false; } }这段代码里最关键的不是画图而是PaperSize的单位。Windows 打印 API 用的是 1/100 英寸40mm 约等于 15730mm 约等于 118。很多人直接写 40 和 30结果打出来只有指甲盖大。Margins也要设否则默认边距会把内容挤到中间。条码字体方式只适合演示真实项目里 Code128 的校验位和宽窄比必须由条码库控制否则扫不出来。2.3 走指令路线把 ZPL 拼成字符串发出去如果打印机支持 ZPL我更推荐这条路线。下面是把一张标签拼成 ZPL 并通过 RawPrinterHelper 发送的最小实现。using System; using System.Runtime.InteropServices; using System.Text; public static class RawPrinterHelper { [StructLayout(LayoutKind.Sequential, CharSet CharSet.Unicode)] public struct DOCINFOA { [MarshalAs(UnmanagedType.LPWStr)] public string pDocName; [MarshalAs(UnmanagedType.LPWStr)] public string pOutputFile; [MarshalAs(UnmanagedType.LPWStr)] public string pDataType; } [DllImport(winspool.Drv, EntryPoint OpenPrinterW, SetLastError true, CharSet CharSet.Unicode)] public static extern bool OpenPrinter(string szPrinter, out IntPtr hPrinter, IntPtr pd); [DllImport(winspool.Drv, EntryPoint ClosePrinter, SetLastError true)] public static extern bool ClosePrinter(IntPtr hPrinter); [DllImport(winspool.Drv, EntryPoint StartDocPrinterW, SetLastError true, CharSet CharSet.Unicode)] public static extern bool StartDocPrinter(IntPtr hPrinter, int level, ref DOCINFOA di); [DllImport(winspool.Drv, EntryPoint EndDocPrinter, SetLastError true)] public static extern bool EndDocPrinter(IntPtr hPrinter); [DllImport(winspool.Drv, EntryPoint StartPagePrinter, SetLastError true)] public static extern bool StartPagePrinter(IntPtr hPrinter); [DllImport(winspool.Drv, EntryPoint EndPagePrinter, SetLastError true)] public static extern bool EndPagePrinter(IntPtr hPrinter); [DllImport(winspool.Drv, EntryPoint WritePrinter, SetLastError true)] public static extern bool WritePrinter(IntPtr hPrinter, IntPtr pBytes, int dwCount, out int dwWritten); public static bool SendStringToPrinter(string printerName, string zpl) { IntPtr hPrinter; if (!OpenPrinter(printerName, out hPrinter, IntPtr.Zero)) return false; var di new DOCINFOA { pDocName Label, pDataType RAW }; if (!StartDocPrinter(hPrinter, 1, ref di)) { ClosePrinter(hPrinter); return false; } if (!StartPagePrinter(hPrinter)) { EndDocPrinter(hPrinter); ClosePrinter(hPrinter); return false; } var bytes Encoding.UTF8.GetBytes(zpl); var pBytes Marshal.AllocCoTaskMem(bytes.Length); Marshal.Copy(bytes, 0, pBytes, bytes.Length); bool ok WritePrinter(hPrinter, pBytes, bytes.Length, out _); Marshal.FreeCoTaskMem(pBytes); EndPagePrinter(hPrinter); EndDocPrinter(hPrinter); ClosePrinter(hPrinter); return ok; } }调用时拼 ZPLstring zpl ^XA ^PW320 // 打印宽度 320 dots约 40mm203dpi ^LL240 // 标签长度 240 dots约 30mm ^FO20,20^A0N,28,28^FD物料A-1024^FS ^FO20,60^BCN,60,Y,N,N^FD barcode ^FS ^FO20,140^A0N,22,22^FD批次20240521^FS ^XZ; RawPrinterHelper.SendStringToPrinter(Zebra ZD421, zpl);^PW和^LL是标签宽高单位是点203dpi 下 1mm 约等于 8 点。^BCN,60,Y,N,N里第一个 60 是条码高度Y 表示打印可读文本。这段 ZPL 发出去打印机自己渲染条码边缘比 GDI 锐利得多而且不占主机 CPU。注意pDataType必须是RAW否则驱动会当成普通文本重新排版。3. 条码生成别用字体硬凑用库算校验位3.1 Code128 的宽窄比和校验位为什么不能省用条码字体画出来的条码看起来像那么回事但扫码枪经常读不出来。原因是 Code128 有严格的宽窄比和校验位字体只是把字符映射成条纹不负责计算校验位也不保证宽窄比符合规范。常见做法是用 ZXing.Net 或 BarcodeLib 生成位图再画到标签上。using ZXing; using ZXing.Common; using System.Drawing; public Bitmap GenerateBarcode(string content, int width, int height) { var writer new BarcodeWriterPixelData { Format BarcodeFormat.CODE_128, Options new EncodingOptions { Width width, Height height, Margin 2, // 静区太小扫不出来 PureBarcode true // 不显示下方文字文字自己画 } }; var pixelData writer.Write(content); return new Bitmap(pixelData.Width, pixelData.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); }Margin是静区Code128 左右各留 10 倍窄条宽度才稳实际项目里我一般设 8 到 10 个像素。PureBarcode设为 true 后条码下方不自动加文字方便自己控制排版。生成位图后再用g.DrawImage画到标签上位置和大小都可控。3.2 二维码和 DataMatrix 的选型差异二维码用 QR CodeDataMatrix 在极小尺寸下更有优势。ZXing.Net 两个都支持切换BarcodeFormat即可。QR 的纠错等级用Q或H标签有磨损时H更稳但同样尺寸下模块数更多打印分辨率不够就会糊。DataMatrix 在 3mm×3mm 以内还能扫适合芯片级标签。选型时先量标签可用面积再倒推模块尺寸203dpi 下每个模块至少 2 个点才可靠。4. 避坑与排查那些让我返工的参数4.1 条码扫不出来先查静区和宽窄比现象标签打出来肉眼看着正常扫码枪要贴很近才读得到。原因通常是静区不够或者条码被拉伸变形。解决把Margin调到 10 像素以上生成位图时保持原始宽高比不要用DrawImage强行拉伸到非整数倍。另外检查打印机 DPI 设置203dpi 的机器上按 300dpi 生成位图模块会被压缩。4.2 换一台打印机整版偏移现象在 A 打印机上位置刚好换到 B 打印机整体右移 3mm。原因是不同驱动的可打印区域和原点定义不同。解决不要用绝对坐标硬编码把标签内容做成相对布局或者统一走 ZPL 指令让打印机自己定位。如果必须用 GDI在PrintPage里先读e.PageBounds做偏移补偿。4.3 中文变成问号或方块现象标签上中文显示为问号。原因是 ZPL 默认字符集不支持中文或者 GDI 用的字体没有嵌入。解决ZPL 路线要加载中文字体到打印机或者把中文转成图片再发^GFGDI 路线确认字体在目标机器上存在服务账户下打印时字体可能不可见建议用PrivateFontCollection加载字体文件。4.4 打印任务卡在队列里不吐纸现象调用Print()后没反应队列里堆了一堆任务。原因多半是PrintDocument没有正确 Dispose或者 RawPrinterHelper 的句柄没关。解决PrintDocument用using包起来RawPrinterHelper 里每个失败分支都要 ClosePrinter。另外检查打印机是否处于脱机或缺纸状态spooler 会静默排队。4.5 高并发下打印错乱现象多线程同时打印标签内容串了。原因是PrintDocument不是线程安全的共享 Graphics 会互相覆盖。解决每次打印新建PrintDocument实例或者用队列串行化。RawPrinterHelper 的OpenPrinter每次调用独立句柄天然支持并发但同一台打印机建议加锁避免指令交错。5. 进阶把 Demo 变成可配置的标签模板引擎Demo 跑通之后真正要落地的是“换一张标签不用改代码”。我的做法是把标签定义成 JSON 模板字段位置、字体、条码类型都写在配置里运行时解析成绘制指令。下面是一个最小模板结构。{ widthMm: 40, heightMm: 30, dpi: 203, elements: [ { type: barcode, format: CODE_128, x: 2, y: 2, height: 8, field: sn }, { type: text, x: 2, y: 12, font: Consolas, size: 8, field: material }, { type: text, x: 2, y: 16, font: Consolas, size: 8, field: batch } ] }解析时把毫米换算成点dots mm * dpi / 25.4。203dpi 下 1mm 约 8 点300dpi 下约 11.8 点。这样同一份模板在不同 DPI 打印机上都能对齐。验证方法是先打一张校准标签画一个 10mm×10mm 的方框用卡尺量实际尺寸偏差超过 0.5mm 就调 DPI 参数。我踩过最深的一个坑是模板里写了dpi: 203但实际打印机是 300dpi结果所有元素缩小了三分之一条码密度过高直接扫不出来。后来养成习惯打印前先读PrinterSettings里的PhysicalResolution和模板 DPI 比对不一致就报警而不是硬打。另一个习惯是每换一批标签纸先打三张一张校准框、一张条码、一张满内容确认无误再批量。这些动作花不了两分钟但能省下整批返工的后悔药。希望帮到你。本文还有配套的精品资源点击获取