简介:这是一份面向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 约等于 157,30mm 约等于 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; } }调用时拼 ZPL:
string zpl = "^XA" + "^PW320" + // 打印宽度 320 dots,约 40mm(203dpi) "^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 Code,DataMatrix 在极小尺寸下更有优势。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 路线要加载中文字体到打印机,或者把中文转成图片再发^GF;GDI+ 路线确认字体在目标机器上存在,服务账户下打印时字体可能不可见,建议用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 比对,不一致就报警而不是硬打。另一个习惯是每换一批标签纸,先打三张:一张校准框、一张条码、一张满内容,确认无误再批量。这些动作花不了两分钟,但能省下整批返工的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取