简介:面向需要对接Zebra打印机的开发人员,这套演示项目压缩包提供了完整的C#示例工程与使用说明,适合物流、仓储、零售等条码标签打印场景。包内含系统打印demo源码,ZebraUnity与Form1两个核心代码文件展示了关键打印交互逻辑,配合Visual Studio解决方案、属性资源及程序配置等文件,可帮助快速理解打印机SDK调用流程。资源共89个文件,以源程序、示例、配置类文件为主,附带README说明文档及无积分付费使用条款,压缩包整体仅348KB,轻量便于下载和部署。目前已有1135人学习关注,项目内置Git版本管理信息,目录结构清晰,从源码、配置到说明文档一应俱全。通过阅读README并运行演示程序,读者既能掌握对Zebra打印机进行条码或标签输出的方法,也能了解如何合规使用免费服务,减少集成阶段的踩坑成本。
1. 一个打印demo,为什么值得翻源码
接手物流标签打印的人,大多会在“用驱动打印”和“直接发ZPL指令”之间纠结。驱动方式适合办公文档,但要控制标签尺寸、浓度、剥离位置、批量连续打印时,驱动对话框反而成了瓶颈。Zebra打印demo这套C# WinForms工程,核心价值在于展示了一条不经驱动、直接与打印机通信的链路:从界面输入数据,到组装ZPL指令,再通过USB/网口发给Zebra机器。压缩包里那份“无积分付费.txt”只是下载说明,真正值得读的是ZebraUnity.cs和Form1.cs。适合正在做WMS、ERP标签打印集成,或者刚拿到Zebra机器不知道从哪下手的开发人员。
这套demo的写法比较老派,没有依赖SDK包,而是用System.IO.Ports和Socket直接打指令。它的好处是依赖少、逻辑透明,坏处是很多边界情况没有处理。下面按“连接方式 → 核心代码 → 参数调优 → 排错进阶”的顺序拆开讲,顺手把坑标出来。
2. ZebraUnity.cs与打印链路设计
2.1 为什么demo选择绕过SDK直发ZPL
Zebra官方提供两种开发路线:一种是安装驱动后通过Windows打印队列输出,另一种是使用Zebra SDK(比如Link-OS Multiplatform SDK)。但这个demo两个都没用,它直接向打印机发送ZPL(Zebra Programming Language)纯文本指令。这么做有几个实际原因:第一,ZPL是打印机固件原生解释的语言,响应速度最快;第二,驱动打印会经过Windows图形引擎进行DPI转换,标签上的一维码容易出现边缘模糊,直发ZPL则完全由打印机栅格化;第三,SDK和驱动的版本升级频繁,Demo工程要长期维护,依赖越少越不容易坏。
这个选择也影响了代码结构。整个工程只有一个主窗体Form1和一个辅助类ZebraUnity,ZebraUnity负责底层通信,Form1负责把用户输入拼成ZPL字符串。没有Model层,没有数据库,属于典型的“桩工程”。但对学习的人来说,这种结构恰好把最关键的通信环节暴露了出来。
2.2 连接方式选型:USB、串口还是网口
看ZebraUnity.cs里的枚举和初始化方法,会发现它对三种连接都做了预留。USB连接在Windows下会虚拟成一个串口或打印机端口,常见做法是用System.IO.Ports.SerialPort打开。网口连接则适合车间多台打印机共享,用TcpClient或Socket向打印机9100端口发送数据。
public enum ConnectionType { Serial, Network, Usb } public class ZebraUnity { private SerialPort _serialPort; private TcpClient _tcpClient; public bool Connect(ConnectionType type, string target, int port = 9100) { switch (type) { case ConnectionType.Serial: _serialPort = new SerialPort(target, 9600, Parity.None, 8, StopBits.One); _serialPort.Open(); return _serialPort.IsOpen; case ConnectionType.Network: _tcpClient = new TcpClient(target, port); return _tcpClient.Connected; case ConnectionType.Usb: // USB虚拟串口模式下,target填设备管理器中的COM口号 _serialPort = new SerialPort(target, 19200, Parity.None, 8, StopBits.One); _serialPort.Open(); return _serialPort.IsOpen; } return false; } }串口波特率这里需要说明:老款Zebra打印机(如GK420t)默认是9600,但新款ZT230、ZD421默认可能是115200。如果连接后发送指令没有反应,第一件事就是查打印机的“通讯设置”菜单,把波特率改成代码里的数值,或者反过来改代码。USB虚拟串口模式下,Windows会忽略实际波特率,可以随便填,但这只是假象,不代表所有型号都这样。
网口连接时,TcpClient的目标端口固定是9100,这是Zebra的RAW打印端口,直接承载字节流。注意不要用telnet测试后忘记退出,因为多客户端同时连接时,部分旧型号固件会锁死打印任务。
2.3 ZPL指令在demo中的组织方式
ZPL不是XML那种结构化语言,而是按^XA开头、^XZ结尾的段来组织的。demo里把标签内容拼成一个多行字符串,再用Encoding.Default转成字节数组发送。这里有个坑:中文字段如果打印机没有加载中文字库,^A字体命令无法正常渲染,所以ZebraUnity.cs里往往需要配合^CI28来切换编码。
private string BuildZpl(string sku, string name, string qty) { var sb = new StringBuilder(); sb.Append("^XA"); sb.Append("^CI28"); // 启用UTF-8/Unicode编码,避免中文乱码 sb.Append("^PW832"); // 标签宽度832点,对应104mm标签 sb.Append("^LL406"); // 标签高度406点,对应50.8mm sb.Append("^FO20,20^A0N,32,32^FD").Append(sku).Append("^FS"); sb.Append("^FO20,70^A0N,28,28^FD").Append(name).Append("^FS"); sb.Append("^FO20,120^BQN,2,10^FDQA,").Append(qty).Append("^FS"); sb.Append("^XZ"); return sb.ToString(); }^FO是字段原点坐标,单位是点(1点=1/203英寸,对于203dpi机型);^A0N表示使用内置字体A,后面的32,32是横向和纵向的字体放大倍数;^BQN是二维码指令,QA,表示数据采用二进制安全编码。这些参数在Zebra编程手册里都能查到,但demo把它们封装在了一个方法里,方便界面层直接调用。
3. 核心实现:从Form1到打印机
3.1 Form1里的事件驱动流程
打开Form1.cs,你会发现界面逻辑很朴素:几个TextBox、一个ComboBox选择打印数量、一个Button触发打印。核心事件是btnPrint_Click,它读取输入框内容,调用ZebraUnity的Send方法发送ZPL字节流。值得注意的是,demo没有用异步发送,这在大批量打印时会导致UI卡死。
private void btnPrint_Click(object sender, EventArgs e) { try { string zpl = ZebraHelper.BuildZpl( txtSku.Text.Trim(), txtName.Text.Trim(), txtQty.Text.Trim()); ZebraUnity zebra = new ZebraUnity(); zebra.Connect(ConnectionType.Network, txtIp.Text.Trim(), 9100); zebra.Send(zpl); zebra.Disconnect(); lblStatus.Text = $"已发送 {txtQty.Text} 张"; } catch (Exception ex) { MessageBox.Show($"打印失败: {ex.Message}"); } }这段代码里的Send方法需要自己实现,demo里的实现是NetworkStream.Write再加Flush。这里最容易被忽略的是:Zebra打印机收到^XZ之后就开始物理打印,但NetworkStream.Write只保证数据进入系统TCP缓冲区,不保证数据已经到达打印机。如果发送后立刻断开连接,打印机可能只收到半个ZPL段,吐出一张空白标签。
3.2 发送方法中缺失的回报机制
Zebra打印机支持^HS(主机状态)指令来查询打印机当前状态,demo里没有封装这个。实际集成时,应该在批量打印前发送一次^HS,读取返回的ASCII码,判断是否处于“打印机就绪”状态。
public string QueryStatus(string ip, int port = 9100) { using (var client = new TcpClient(ip, port)) using (var stream = client.GetStream()) { byte[] cmd = Encoding.ASCII.GetBytes("^XA^HH^XZ"); stream.Write(cmd, 0, cmd.Length); Thread.Sleep(300); // 等待固件返回 byte[] buffer = new byte[1024]; int read = stream.Read(buffer, 0, buffer.Length); return Encoding.ASCII.GetString(buffer, 0, read); } }返回的字符串中,第一个ASCII字符代表状态,常见值:0表示正常,1表示打印头抬起,2表示缺纸,3表示暂停。拿到状态码后,界面上就可以提前提示,而不是等标签打了一半才报警。这个能力在原文demo里没写,但属于Zebra集成中必须具备的环节。
3.3 打印队列与批量任务的常用做法
demo的发送是一次性把所有ZPL拼成一个大字符串,然后一次性写入。如果打印1000张,这种写法会占用大量内存,而且中途打印机卡纸时无法定位到具体标签。更稳妥的做法是循环发送单张ZPL,并在每张之间监听状态。
| 方式 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 一次性发送全部ZPL | 网络开销小,吞吐高 | 错误定位难,内存占用大 | 标签内容相同且数量少 |
| 循环发送单张ZPL | 可逐张确认状态,及时中止 | 网络往返增加,速度稍慢 | 内容动态变化、需要计数 |
| 使用^SN指令自动编号 | 标签内容自动递增,无需逐张生成 | 只能处理数字变量 | 流水号、批次号打印 |
实际项目中,我一般会采用循环发送单张的方式,把每一张的ZPL放在一个List<string>里,用一个后台线程逐条发送。发送前用^PQ指令设置打印数量,但^PQ对循环模式没有意义,所以正确的做法是仅在单张ZPL内使用^PQ1,循环次数由C#控制。
4. 打印效果控制:标签尺寸、浓度与校准
4.1 按标签尺寸精确计算点位参数
Zebra打印机的分辨率有203dpi、300dpi和600dpi三种。demo里写死^PW832和^LL406,显然是为203dpi、104mm×51mm的标签准备的。但实际使用中,标签宽度和高度必须根据实际耗材换算,否则会出现内容偏出标签边界或者连续打在同一位置的情况。
^XA ^PW832 ; 标签宽度 = 104mm × 8点/mm ^LL406 ; 标签高度 = 50.8mm × 8点/mm ^LT10 ; 标签顶部偏移10点,用于补偿撕纸位置 ^MNY ; 手动模式,关闭自动切纸 ^JUS ; 重置打印头灵敏度 ^XZ这里的^MNY很重要。demo里没有设置^MN,默认是^MNN(非连续纸模式),适合有间隙的标签。如果用的是连续标签纸,必须改成^MNC,否则打印机每次走纸都会因为找不到间隙而报错。^LT是垂直方向偏移,当标签内容明显偏上或偏下时,用这个参数微调,而不是重新计算每个字段的^FO坐标。
4.2 浓度和速度的ZPL指令调整
打印浓度和打印速度是标签质量的两大核心。浓度太低,条码扫描枪扫不出;浓度太高,二维码边缘糊在一起导致解码失败。Zebra的^MD指令控制深度,范围是-30到30,默认0。速度用^PR指令,单位是英寸/秒,常见范围是2到6。
^XA ^MD10 ; 浓度增加10,适合碳带较淡的情况 ^PR4 ; 打印速度4ips,兼顾速度和黑度 ^POI ; 反转打印方向,用于顶部出纸的机型 ^XZ调试时我建议先用^MD0和^PR2打一张测试标签,观察条码边缘是否锐利。如果条码有明显的拖尾(ghosting),优先降低速度而不是增加浓度。如果空白地方有灰雾,说明浓度过高或碳带不匹配。切记:^MD是全局参数,会一直生效直到下次设置,调试完不要忘记复位。
4.3 测纸与校准操作
更换标签规格后,打印机内部对标签间隙的检测值还是旧的,需要做自动校准。手动校准的步骤是:关机,按住暂停键开机,直到状态灯闪烁再松手。代码里也可以通过ZPL指令触发:
^XA ^JC ; 校准标签间隙传感器 ^XZ^JC会在下次送纸时重新测量标签长度和间隙位置。但有些固件对^JC支持不完整,更可靠的命令是^JUS(重置传感器)和^MF(设置介质类型)。如果校准后仍然每隔几张就报错一次“媒体无法校准”,就要检查标签是否安装过紧、纸张是否卷曲挡住传感器。
4.4 中文字体与图形打印的常见选择
demo里用了^A0N内置字体,它只能打印ASCII字符。如果标签上必须显示中文,有三个方案:一是使用^A@命令调用打印机内部的软字体;二是把中文转成图片再打印;三是使用^CI28切换编码后,配合支持中文的字体(如Zebra的simplified chinese字体包)。前两种比较稳定。
Bitmap bmp = DrawTextToBitmap("商品名称", 200, 30); byte[] imgData = BmpToZpl(bmp); string zpl = "^XA^FO20,20^GFA," + imgData.Length + "," + imgData.Length + ",20," + imgDataAsAscii + "^FS^XZ";^GFA指令传输的是Zebra的二进制图片数据,需要把位图逐行转换成十六进制字符串。这个方法不受打印机有没有中文字库的限制,但数据量比文字指令大得多。对于静态中文内容,可以先在Zebra Designer里做好模板,导出ZPL之后只替换动态字段,性能和兼容性最好。这在网络热词中提到的“zebra designer2打印excel”是类似的场景:先用设计器排好版,再通过变量字段灌入Excel数据。
5. 排错技巧与工程化收尾
5.1 打印乱码
乱码是老款Zebra机器最常被问的问题,原因基本出在编码上。demo里使用Encoding.Default发送数据,在中文Windows系统下这个编码是GB2312,但打印机固件默认按CP437解析。解法是在ZPL中加入^CI28,并把C#发送编码改成Encoding.UTF8。
byte[] data = Encoding.UTF8.GetBytes(zpl); stream.Write(data, 0, data.Length);如果改了^CI28仍然乱码,检查打印机固件版本,2015年之前的固件对UTF-8支持不完整,需要升级固件或者改用^FH^FD十六进制编码传输中文。例如“宁波”对应的UTF-8十六进制是E5AE81E6B3A2,ZPL里写作^FH^FD_E5AE81_E6B3A2^FS。
5.2 标签内容偏移且撕纸位置不对
内容整体偏移,优先检查^LT和^FO的坐标原点,还要确认^PO(打印方向)是否和出纸方向匹配。撕纸位置不对,则是^LL与实际标签高度不一致导致的。一个实用的验证方法是打印一个纯色块:
^XA ^LL406 ^FO0,0^GB832,406,10^FS ^XZ色块如果恰好覆盖标签且没有上下错位,说明尺寸参数正确;如果色块超出标签或留白,按实际偏差调整^LL。注意,撕纸模式下标签长度最好比实际略小1-2mm,否则打印机每次都多走一小段,累计后出现偏移。
5.3 如何验证打印结果是否合格
只用肉眼看标签是否完整是不够的。二维码和条码需要用扫描枪做分级验证,Zebra打印机的^MD和^PR对条码等级影响很大,建议用打印机的镜像页功能,或者用Zebra的“条码质量报告”工具扫描测试样张。如果没有专业工具,至少保证二维码边缘没有毛刺,条码的空白区(静区)不小于1.5mm。
5.4 让demo代码可维护化改造
原始demo把所有逻辑都堆在Form1里,我拿到手里的第一件事是把ZPL生成、连接管理、状态解析分别拆成独立类。其次是增加配置文件,把IP地址、端口、标签尺寸、浓度这些经常变动的值放进App.config,而不是硬编码。做完这两步,这个demo就从一个教学示例变成了可上线的打印服务基础。
用的时候还建议加一层重试机制。网络闪断时TcpClient.Write不会立刻抛异常,而是进入超时状态。设置client.SendTimeout = 3000,并在Send方法里捕获IOException后重连重发。这样整个打印任务的健壮性会明显提高,不会再出现“明明发了,但标签没打出来”的糊涂账。
本文还有配套的精品资源,点击获取