简介:这是一份由达摩老生出品的C#源码项目,用于驱动TSC标签打印机打印二维码标签,解决标签打印功能开发中的指令调用与界面交互问题。项目以ShenMaPrint为核心,包含主窗体及Form1至Form6在内的多个功能窗体、程序入口、辅助类,同时集成log4net日志记录、INI文件读取、资源图片管理等配套设施,代码逻辑完整,可直接运行,也便于二次扩展。压缩包共79个文件,主要包含23个cs源代码、11个DLL依赖库、7个resources和7个resx资源文件,以及exe、config、txt等配置与可执行程序,整体体积仅2.12MB,目录结构清晰,方便按模块查阅学习。源码经过作者亲测校正,从界面布局到二维码打印指令调用均有完整实现,适合新手入门学习,也可供有一定经验的开发人员快速理解TSC打印机的工作原理并移植到实际项目中。目前已有1218人学习下载,对于需要对接TSC标签打印机、开发仓库或商品标签打印功能的技术人员,是一份值得参考的实用资料。
1. 这次直接用C#把TSC标签打印机跑起来:从逐条指令到二维码标签落地
很多C#上位机项目做到一半,就会遇到一个绕不开的需求:用TSC标签打印机打二维码。要么是固定资产贴着二维码做盘点,要么是仓储拣货单上打一个含货号信息的二维码,要么是小料盒上打一个唯一批次码。网上关于TSC打印的代码很多,但大多只贴一句“调用驱动打印图片”,或者只给出一个PRINT指令让人自己拼,根本没法直接落到产线。这一篇我按自己常用的工程做法,把链路完整拆开:用TSPL指令直接直发到TSC打印机,在C#里生成二维码、排版、连续打印,并附上能直接抄的通信类和避坑记录。
选择TSPL直发而不是Windows驱动,原因很直接:驱动打印是走操作系统打印队列和GDI,标签内容稍有变化就要重新渲染,速度慢、位置容易偏移;TSPL是指令级,打印机收到原始命令后自己解析、自己栅格化,连打印机都能看到打印内容,重启后还能继续打。产线上要求每张标签都稳、准、快,指令直发是更可控的方案。别被“程序源码”四个字吓到,真正写起来不到一百行核心代码,难点全在坐标换算和几条容易反直觉的指令上。
2. 先看TSPL指令是怎么工作的:为什么直发指令比驱动打印更稳
2.1 TSPL指令的最小完整流程:初始化、定位、画图、打印
TSC标签打印机虽然也有Windows驱动和SDK,但只要走TSPL,它就是一个“文本命令解析器”。你发过去一串ASCII字符,它按命令执行,最终把内容输出到标签纸上。一个最精简的TSPL打印流程,只有四行核心指令:
SIZE 50 mm,30 mm GAP 3 mm,0 CLS QRCODE 10,10,H,4,0,2,"https://example.com" PRINT 1,1先用SIZE告诉打印机标签纸物理尺寸是50mm宽、30mm高;再用GAP说明两张标签中间间隙为3mm,第二个参数是偏移量,连续纸或圆形标签场景会用到;CLS清空图片缓冲区,避免上一次打印内容残留;然后QRCODE在坐标(10,10)处生成一个二维码,最后PRINT 1,1表示打印1份、从当前缓冲取第1份内容。这四步就是TSPL的最小闭环,所有标签打印都围绕它展开。
SIZE和GAP是打印机走纸校准的根基。很多新手第一张标签打出来是歪的,不是代码写错,而是这两个参数和实际标签纸不一致。GAP告诉打印机两纸之间的间隙长度,打印机用反射传感器去找这个间隙,找对了才能避免打印头和标签错位。如果标签纸没有间隙,比如连续热敏纸,就写GAP 0;如果是黑标纸,TSPL里要改用专用的BLINE命令。
CLS这个指令最容易漏。生产环境里,上一条指令发送的数据会留在图片缓冲区,如果不清理,下一张标签会把旧内容和新内容叠在一起。打印头扫描一遍,旧图还在缓冲区里,结果是整张标签出现半透明的重影。所以每次打印新内容前,必须先CLS再发图元命令,最后PRINT。
2.2 二维码的两种生成路线:打印机原生QRCODE指令 vs 上位机生成位图
同样是打二维码,工程上有两条路线。第一条就是我上面用QRCODE命令,让打印机自己生成二维码。TSC的固件里内置了QR码生成器,只要给它坐标、纠错等级、模块宽度、方向、数据,它就在打印缓存里画好二维码。优点是不占上位机CPU,不依赖任何二维码类库,打印速度极快。同时因为二维码是打印机自己的渲染引擎画出来的,锯齿控制和点阵对齐都针对打印头做过优化,扫描识别率通常更高。
第二条路线是上位机先用QRCoder或ZXing生成二维码位图,再通过TSPL的BITMAP命令把图片点阵数据发给打印机。这样做的灵活性大,可以在二维码中央加Logo、换颜色、调整边距样式,甚至把二维码和图片、表格合成一张整图。缺点是打印前要把位图转换成打印机认识的单色点阵数据,转换逻辑稍有不慎就会得到一张全黑或反白的标签,而且数据量比一行指令大得多,网络传输和打印机解析都更耗时。
| 对比项 | 打印机原生QRCODE | 上位机生成位图 |
|---|---|---|
| 上位机依赖 | 无,仅拼字符串 | 需要二维码库和图像处理 |
| 打印速度 | 快,指令短 | 慢,图片数据量大 |
| 样式定制 | 只能控制基础参数 | 可加Logo、改配色 |
| 抗锯齿 | 打印机硬件优化 | 取决于位图转换算法 |
| 适合场景 | 产线、批量、物联网 | 需要品牌定制的内外标签 |
我的选择原则是:没有Logo需求的场景一律用原生QRCODE指令,只有像产品包装标签这种要对二维码做视觉定制时,才用上位机生成位图。后面第3章的源码会同时给出这两条路线的实现,生产上可以按需切换。
2.3 打印机连接方式:网口、串口、USB,哪个适合你的工位
TSPL指令发到打印机,首先要打通物理连接。TSC标签打印机常见三种接口:网口(RJ45,通常监听9100端口)、串口(RS232)、USB。网口是现在工位环境的首选,一台打印机配一个固定IP,同一局域网里多台C#上位机都能连它,走TCP协议,命令发送简单,还能把打印状态通过打印机自己的网络状态页查出来。
串口在老旧设备或纺织车间里还能见到,波特率从9600到115200不等,TSC打印机一般默认9600或38400。用串口的好处是抗干扰强、实时性高,但一根串口线只能连一台打印机,而且C#里串口写命令要注意Handshake和DtrEnable的设置,否则会出现偶尔发不出去的情况。USB接口在桌面级TSC型号上最常见,Windows驱动装完后会映射成虚拟串口,也可以用TSC提供的专用通讯库,但如果你没有二次开发SDK,直接用串口API会发现命令发送和网口没有本质区别。
实际项目里,我一般优先要求设备组提供网口标签打印机,因为后续维护IP比维护串口USB线容易得多。如果现场只有USB,那就把TSC的虚拟串口号找出来,在C#里按串口驱动来写,代码上只需要替换发送底层,上层TSPL字符串完全不用变。
3. C#源码拆解:一个可直接使用的TscQrLabel类
3.1 先搭通信层:用Socket把命令发给打印机
先写最底层的通信类。假设你用的是网口打印机,IP是192.168.1.200,端口默认9100。TSPL命令本质上是字符串,C#里只要有一个能发送ASCII字符串的通道就够了。我习惯把通信逻辑封装成一个TscPrinter类,后续所有打印方法都复用它。
using System.Net.Sockets; using System.Text; public class TscPrinter { private readonly string _host; private readonly int _port; private Socket _socket; public TscPrinter(string host, int port = 9100) { _host = host; _port = port; } public void Connect() { _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _socket.Connect(_host, _port); _socket.ReceiveTimeout = 3000; } public void Send(string command) { byte[] data = Encoding.ASCII.GetBytes(command); _socket.Send(data, SocketFlags.None); } public void SendWithCrLf(string command) { Send(command + "\r\n"); } public void Close() { if (_socket != null && _socket.Connected) { _socket.Close(); } } }这里有个很容易翻车的点:Socket.Send在TCP协议下有时不会一次性把完整的字节流发送出去,尤其是在上位机CPU繁忙或网络延迟波动时。命令被截断,打印机收到半条指令就会一直等待剩余字节,表现为“打印机没反应”或“偶尔打印出乱码”。稳妥做法是加一个循环发送直到把整个data数组发完,或者改用NetworkStream.Write再Flush。上面的示例为了可读性做了简化,我实际写生产代码时会这样做:
int sent = 0; while (sent < data.Length) { sent += _socket.Send(data, sent, data.Length - sent, SocketFlags.None); }发送TSPL命令还要注意行尾符。TSPL协议规定每条指令以CRLF(\r\n)结尾,不是\n也不是空格。我用SendWithCrLf方法统一拼接,避免整个代码里到处手动加\r\n导致遗漏。
3.2 用打印机原生QRCODE指令生成二维码:版本、纠错和坐标
通信层打通以后,先实现最省事的二维码打印:让打印机自己生成。TSPL指令里QRCODE的完整参数不算复杂但容易记混。我封装了一个方法,把坐标和数据传进去,它负责拼指令并发送。
public void PrintQrCode(string data, int x, int y, string ecc = "H", int cellSize = 4) { string safeData = data.Replace("\"", "\"\""); string command = $"QRCODE {x},{y},{ecc},{cellSize},0,2,\"{safeData}\"\r\n"; Send(command); }参数从左到右分别是:二维码左上角X坐标、左上角Y坐标、纠错等级、模块宽度(点)、旋转角度、QR码版本、数据。ecc用L、M、Q、H四个等级,我默认用H,因为标签打印环境常有脏污和褶皱,高纠错能扛住一部分物理损伤。cellSize是二维码每个黑色模块占用的点阵大小,203dpi打印机上4个点约等于0.5mm,大小适中。如果二维码内容特别长,比如超过200个字符,建议把cellSize调到5或6,否则打印机生成的二维码过密,扫描枪很难识别。
注意数据里的双引号必须转义。TSPL里字符串是用双引号包裹的,如果二维码内容本身含有双引号,比如JSON串里的"sku":"A-003",需要写成\",否则打印机解析命令时会把后面参数当成数据的一部分,导致整条指令报错。这个边界只在特定业务场景会踩到,但踩一次就能耗掉半小时排查。
还有方向参数。QRCODE的第四个参数如果传0就是正常方向,90是顺时针旋转,多用在竖版标签上。旋转后坐标原点还是标签左上角,只是二维码从左上角往旋转后的方向生长,设计版面时最好先出一次实物样张再决定坐标。
3.3 中文和Logo打印:TEXT指令不做中文字库,必须转成图片
有读者可能觉得,既然能发ASCII指令,那TSPL里的TEXT指令是不是也能打印中文?很遗憾,多数TSC打印机的固件里并没有GBK或UTF-8字库。TEXT指令只能打印数字、英文和部分内置符号,中文内容直接塞进命令里,要么被当成乱码,要么被丢弃。产线上最常见的商品名称、工单描述基本都是中文,所以必须走另一条路:在C#里把中文渲染成图片,再通过TSPL的BITMAP命令点阵输出。
下面是一个把Bitmap转换成TSPL位图并发送的最小实现。我把它放在同一个TscPrinter类里。
using System.Drawing; using System.Drawing.Imaging; public void PrintBitmap(Bitmap image, int x, int y, bool invert = false) { // TSC打印头是单色热敏/热转印,CMYK没有意义,这里强制转成1位索引 Bitmap mono = image.Clone(new Rectangle(0, 0, image.Width, image.Height), PixelFormat.Format1bppIndexed); int width = image.Width; int height = image.Height; int rowBytes = (width + 7) / 8; StringBuilder sb = new StringBuilder(); sb.Append($"BITMAP {x},{y},{width},{height},0,"); BitmapData bmpData = mono.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, mono.PixelFormat); int stride = Math.Abs(bmpData.Stride); byte[] pixels = new byte[stride * height]; System.Runtime.InteropServices.Marshal.Copy(bmpData.Scan0, pixels, 0, pixels.Length); mono.UnlockBits(bmpData); for (int row = 0; row < height; row++) { for (int col = 0; col < rowBytes; col++) { byte b = pixels[row * stride + col]; if (invert) b = (byte)~b; sb.Append(b.ToString("X2")); } } Send(sb.ToString()); mono.Dispose(); }这段代码的关键是,把Windows的Bitmap数据按打印机需要的“逐行字节流”拼成十六进制字符串。打印机中当像素值为1时表示黑色(或者是反色,视固件而定),所以invert参数用来调节黑白逻辑。不同型号的TSC打印机对BITMAP命令的字节对齐要求略有差异,但大多数是按“每行补到整字节”来解析,上面代码里rowBytes就是做这个补位。
打印中文时,先用Graphics绘制一行文字到一个透明位图上,再调用PrintBitmap:
public void PrintChineseText(string text, int x, int y, int fontSize) { using (Bitmap bmp = new Bitmap(400, 40)) { using (Graphics g = Graphics.FromImage(bmp)) { g.Clear(Color.White); using (Font f = new Font("微软雅黑", fontSize, FontStyle.Regular, GraphicsUnit.Pixel)) { g.DrawString(text, f, Brushes.Black, 0, 0); } } PrintBitmap(bmp, x, y); } }中文字体渲染后一定要先Clear成白色背景,否则Bitmap默认黑色透明区域在转单色时会变成大块黑斑。GraphicsUnit.Pixel用来保证字体大小和打印机点阵一致,不会出现逻辑英寸和实体像素不一致导致的尺寸偏移。
3.4 批量打印:循环发CLS、内容和PRINT,别一次塞大量数据
批量打印是产线最常见场景,比如连续打印50张资产标签,编号依次递增。核心逻辑不复杂,但有一条规矩必须遵守:每一张标签都要重新发CLS,然后发这张的内容,最后发PRINT,三部分顺序不能乱。
public void PrintSerialLabels(int startNumber, int count) { for (int i = 0; i < count; i++) { string serial = $"AT-{startNumber + i:D6}"; string qrData = $"asset:{serial};loc:WH-03"; SendWithCrLf("CLS"); // 二维码用打印机原生指令 SendWithCrLf($"QRCODE 15,15,H,4,0,2,\"{qrData}\""); // 序列号文本用TEXT,只支持英文数字,这里没问题 SendWithCrLf($"TEXT 15,115,\"3\",0,1,1,\"{serial}\""); // 打印1张 SendWithCrLf("PRINT 1,1"); } }把SIZE和GAP的初始化放在批量的第一张之前发一次即可,不要在循环里重复发,否则打印机每次都要重新校准标签纸,走纸时间会翻倍。TEXT指令里\"3\"表示使用内置字体3号,最后一个参数是放大倍数,1就是按基准大小打印。如果文字太长,可以放在二维码下方另起一行,用不同的TEXT指令调整坐标,不要试图在一条指令里塞多个位置。
C#里循环发50张的速度大约在1到2秒内完成,但实际出纸速度由打印机SPEED指令决定。如果标签很小,打印机机械结构来不及出完一张,下一张指令就可能积压在打印机缓存里。对于高速连续打印,建议每发10张加一个短暂的Thread.Sleep(50),让走纸机构喘口气。
4. 标签尺寸与坐标换算:别把毫米当成像素
4.1 DPI、标签宽高、点位换算公式
TSPL里所有坐标都是按“点”(dot)算的,而标签尺寸通常用毫米。打印机203dpi和300dpi是两种最常见的分辨率,很多C#工程师把50mm直接写成50,结果标签只打了四分之一就出纸了。正确换算是:
点数 = 毫米 × DPI ÷ 25.4203dpi打印机上,1mm约等于8个点;300dpi上是1mm约等于12个点。例如一张50mm×30mm的标签,203dpi下就应该是400点×240点。我在代码里不会手算,而是写一个换算函数:
private int MmToDots(float mm, int dpi) { return (int)Math.Round(mm * dpi / 25.4); }SIZE指令可以直接接受以mm为单位的写法,但后续所有坐标计算仍然以“点”为单位。如果坐标不从毫米换算成点,就很容易出现“二维码打在标签外面”的诡异情况。还有一个常见错觉:标签纸打印区域并不是从物理边缘算起的,打印头左边缘有2到3mm的不可印区,所以最小X坐标不要小于1mm,否则二维码会被裁掉。
4.2 用DiagTool确认打印机分辨率与固件状态
写代码前最好先用TSC官方的DiagTool检查一下打印机实际配置。这个名字在热词里也出现过,它不仅是诊断工具,还能直接发送TSPL命令、查看打印机固件版本、传感器状态和当前分辨率。第一次连接一台陌生打印机时,我会先打开DiagTool,读取它的当前分辨率,确认是203dpi还是300dpi再写代码。很多工程为了节省成本会混合购买两种分辨率的TSC机器,同一个上位机程序跑在不同机器上,如果不做配置化处理,标签上二维码的密度和位置都会不一致。
DiagTool还有一个用途:测试单条命令。当你怀疑C#发送的命令有语法问题时,可以把指令原样粘贴到DiagTool的命令行窗口,手动执行。如果能正常打印,就是上位机通信层的问题;如果同样不打印,就是TSPL指令本身写错了。这一步能帮你把“代码问题”和“打印机问题”快速二分。
4.3 标签间隙与传感器:GAP不同会走纸跑偏
标签之间要么有间隙,要么有黑标,要么是连续纸。GAP指令的第一参数是间隙长度,TSC打印机通过传感器透射或反射检测这个间隙,用于定位标签起始点。如果实际间隙是2mm,你在GAP里写了3mm,打印机每次都会多走一点,标签纸越走越偏,到最后一张二维码可能直接压在对折线中间。
热敏标签纸上如果有一道定位黑标,就不能用GAP,而要用BLINE指令。黑标传感器反射黑标和白色底之间的光学差异,实现定位。TSPL里黑标定位的基准命令是BLINE x,y,x是标间距,y是偏移量。我在一个冷链项目中遇到过,标签纸没有间隙但有很小的定位缺口,用GAP怎么调都偏,后来发现是缺口裁切不干净,反射传感器识别不稳定,只能换回间隙纸。
位置偏移不仅影响美观,还会让打印头把内容打在上一张的间隙里,导致碳带浪费和打印头不均匀磨损。所以,换任何一家标签纸供应商,都先打一张测试标签,用尺子量一下内容和标签边界的距离。如果差了1mm以上,就去校准GAP和SIZE,不要相信供应商提供的“绝对标准”参数。
5. 避开这些打印的坑:从三盒标签换来的血泪经验
5.1 现象:二维码扫不出来,手机相机能对到焦但解码失败
原因:打印机要么打印浓度太高,二维码黑块粘连成一片;要么浓度太低,浅色点阵断线。尤其是热转印打印机,碳带和质量差的标签纸组合不当时,个别点阵会印不上。
解决:先调整DENSITY指令,TSC的浓度范围是0到15。我习惯从DENSITY 6起步,打印一张灰度测试二维码,用手机扫;扫不出来就调高一档再试。如果调到8还是不行,检查打印机速度和碳带张力。速度过高时热转印加热时间不足,墨粉来不及转印,把SPEED 2降到SPEED 1再试。如果仍然不行,考虑二维码内容是否过长导致点阵过密,把cellSize从3调到4。
5.2 现象:中文字符在标签上变成“口口口”或空白
原因:TEXT指令缺少中文字库支持,命令里直接拼了中文,打印机按内置英文点阵去解析,无法映射到字符。
解决:不要用TEXT打印中文,改为在C#侧用Graphics绘制中文到Bitmap,然后用BITMAP输出。如果实在不想走图片,可以提前把中文字体刷写进打印机Flash存储器,但那需要专门的字体下载工具和对应字库授权,我建议除非有强制要求,否则直接转图片最省事。另外记得转图片时背景要白色清空,字体文件要选微软雅黑或SimHei等无衬线字体,在203dpi下更清晰。
5.3 现象:连续打印50张,前半段正常,后半段每张向上偏移2mm
原因:打印头持续发热后,标签纸受热略微伸长;更常见的是标签纸卷本身张力不稳定,打印机在连续走纸时间隙检测出现过漂移。
解决:在批量前打印几张缓冲,让走纸机构稳定。批量循环里给每10张发一次GAP校准指令。TSPL没有主动“重新校准”的指令,实际做法是把GAP原参数再发一遍,打印机会重新读取传感器状态。也可以用SET GAPLESS关闭间隙检测,但会导致打印停止位置不精确,通常不推荐。
5.4 现象:C#程序连接打印机超时,发送命令无反应,但DiagTool能连上
原因:打印机网络模块一次只允许一个Socket会话。上位机连接后没有正常释放,打印机侧的会话还处于打开状态,后面的连接被拒绝。
解决:程序每次打印完成后必须调用Close()释放Socket,不能依赖GC。如果已经出现这种情况,重开打印机电源,或者在DiagTool里清空会话。代码里还要加上重连逻辑,打印异常时关闭旧Socket再重新Connect,因为打印机在缺纸或卡纸后会自动断开TCP连接。
5.5 现象:二维码打印在标签最底边缘,被裁掉一半
原因:坐标计算没有考虑标签的高度原点。TSPL原点在标签左上角,但PRINT时还要考虑标签的高度和打印头到标签起始点的物理距离。如果你把二维码放在Y=220点的位置,标签只有240点高,二维码本身高40点,那么底部10点会被裁掉。
解决:坐标设计时给所有元素留下安全边距,底部至少留5mm。计算时要先确定标签物理高对应多少点,再在此基础上摆放内容。批量打印前打印一张“边框测试标签”,用一个矩形命令把整个标签可打印区画出来,看矩形的四条边是否都完整出现在标签上,如果底边缺失,就是Y坐标或SIZE高度不对。
6. 把打印质量做成可验收的流程:单张验证、解码测试、微调浓度
二维码标签不像普通文本,打出来肉眼看着清晰不一定能扫出来。我的习惯是建立一套“打印-扫描-微调”的流程,而不是打完一批直接送去贴。
第一步,把打印机的SIZE、GAP、DENSITY、SPEED设置好之后,不要直接发送二维码内容。先打一个测试块:用BARCODE指令打一个Code128条码,再打一个黑白网格,观察水平和垂直方向的线条是不是均匀。如果网格线有歪斜,说明标签纸走纸不正,先解决问题再继续。
第二步,正式打出一张二维码,不要只用眼睛看。用带解码功能的扫码枪或者手机扫描,确认解码出的字符串与上位机发送的数据完全一致。有些热转印打印机用的碳带是蜡基的,在光滑铜版纸上附着力不够,二维码字符可能被搓掉,肉眼又很难发现。这个流程必须做到“每次换碳带或换标签纸供应商时都执行一次”。
第三步,如果解码失败,先动DENSITY,再动SPEED,最后才动大小。因为二维码的模块尺寸是由cellSize决定的,改它会影响排版,其他一系列坐标都要跟着迁移。相对地,浓度和速度只影响印出质量,不改变二维码在标签上的位置。批量生产时如果发现同一台打印机批间颜色变浅,往往是环境温度变化导致碳带转印效率下降,此时把浓度提高一级比重启打印机更有效。
第四步,建立一个“批次首件确认”制度。每次打印任务开始后,先让程序只发一张,等这张标签出了打印机,人工确认二维码可扫、文字不偏、边距正确,再让程序继续发剩余99张。这看起来损失了10秒效率,但比起打完一整卷发现全部作废,这点检查成本完全值得。
这套流程说穿了就是把打印机当成一个需要校准的外设,而不是当成一个“黑匣子”。我早期在项目里直接批量输出,结果一整个班次打了三百张,扫出来只有一半能用,后面挨个排查才知道是碳带没安装到位、张力不均导致的点阵缺失。从那以后我再也没有跳过单张验证。换纸、换碳带、换打印机,都必须重新校准一遍,哪怕同一型号不同机器也可能有微小差异。希望这篇能让你的TSC二维码打印少走几趟弯路。
本文还有配套的精品资源,点击获取