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

资讯详情

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

VC++通过USB向GT800打印机发送ZPL指令实现条码标签自动打印

VC++通过USB向GT800打印机发送ZPL指令实现条码标签自动打印 简介一套基于VCMFC框架通过USB接口调用winspool驱动、向斑马GT800打印机发送ZPL指令打印条形码的示例工程面向需要实现底层打印控制的C桌面程序开发者适用于物流、仓储、零售等标签打印场景。压缩包共24个文件约5.73MB包含C源码.cpp/.h、Visual Studio工程配置.sln/.vcxproj/.filters、编译中间文件.tlog/.db/.log/.ipch等及ReadMe说明结构清晰便于直接打开编译参考。已有674人学习下载。从内容预览和描述看工程内不仅包含条形码打印的核心ZPL命令示例还涉及USB设备枚举、设备接口打开、句柄管理等关键API调用并给出一个可复用的封装思路读者可据此快速搭建自己的斑马打印机通信模块也可作为理解winspool底层打印机制的入门参照。1. GT800和USB打印ZPL到底在解决什么问题产线工位上最烦的一种活是打印内容不固定、但格式必须统一的标签。今天这批货要打300张Code128每张条码数字来自数据库明天换了批次条码数量、产品型号全变。操作工不可能打开标签编辑软件手动改那太慢也容易出错。这时候就需要写一个VC程序拿着数据库里的数据拼成一段ZPL指令直接从USB口发给GT800打印机让它即时出标签。ZPL是斑马打印机的原生指令语言GT800是斑马最经典的200dpi桌面条码打印机USB连接是Windows工位上最常见的接法。这套方案的实质是让你的程序绕过标签编辑软件用代码直接控制打印机把「人改模板再点打印」变成「程序发指令自动出纸」。这件事适合谁做MES、WMS、固定资产管理、样品追溯这类Windows桌面应用的人。你在C里写业务逻辑最后需要往条码打印机送数据这篇就把从零到能跑的路径讲完。这篇文档涉及斑马专有技术实际应用请以官方ZPL手册为准。2. 为什么是USB加GDI打印管线而不是直接写USB端点接触过嵌入式的人第一反应可能是「USB连上以后直接写端点不就行了」。这个思路在打印场景里是弯路。GT800的USB口装完驱动以后在Windows眼里已经被抽象成一个打印队列不再是一个裸的USB设备。你的程序通过打印驱动访问它就像通过文件系统访问硬盘一样不需要也不需要直接操作USB端点、批量传输和协议包。直接写USB端点等于自己重写一遍打印驱动还要处理双向通信、状态查询、驱动签名这些东西投入产出比太差。2.1 先搞清GT800的USB口在Windows里是什么GT800插上USB装好斑马自带的驱动后你在控制面板的「设备和打印机」里能看到一个名为「ZDesigner GT800」的打印机图标。这个图标背后是三层结构最低层是USB硬件中间是斑马打印驱动最上层就是打印队列。你的VC程序面对的是最上层。你调用的打印API会把数据交给驱动驱动再负责跟USB硬件通信。这个结构决定了你要用什么技术栈Windows的GDI打印API就够用了。核心是四个函数。OpenPrinter打开打印机句柄StartDocPrinter开始一个打印文档WritePrinter把原始字节写进去EndDocPrinter结束文档。这套流程不经过画布、不经过页面渲染直接把数据倒给驱动驱动再把数据原样推给打印机。因为ZPL本身就是文本指令打印机会逐条解释执行所以要用打印驱动提供的「RAW」数据类型让驱动放弃所有解析纯透明转发。2.2 用WritePrinter发ZPL的最小C代码// 发送ZPL到GT800打印机的核心流程 #include windows.h #include winspool.h #pragma comment(lib, winspool.lib) BOOL SendZplToPrinter(const char* szPrinterName, const char* szZplData, int nDataLen) { HANDLE hPrinter NULL; // 1. 打开打印机参数一是打印机名参数二出句柄 if (!OpenPrinter((LPSTR)szPrinterName, hPrinter, NULL)) return FALSE; // 2. 开始一个打印文档 DOC_INFO_1 docInfo { 0 }; docInfo.pDocName (LPSTR)ZPLDoc; // 文档名打印队列里显示的名字 docInfo.pDatatype (LPSTR)RAW; // RAW表示驱动不解析内容纯转发 if (!StartDocPrinter(hPrinter, 1, (LPBYTE)docInfo)) { ClosePrinter(hPrinter); return FALSE; } // 3. 开始打印页RAW模式下这一页就是一条完整的ZPL指令 if (!StartPagePrinter(hPrinter)) { EndDocPrinter(hPrinter); ClosePrinter(hPrinter); return FALSE; } // 4. 写入数据第二参数字节数第三参数数据指针 DWORD dwWritten 0; BOOL bOK WritePrinter(hPrinter, (LPBYTE)szZplData, nDataLen, dwWritten); // 5. 按顺序收尾结束页、结束文档、关句柄 EndPagePrinter(hPrinter); EndDocPrinter(hPrinter); ClosePrinter(hPrinter); return (bOK (dwWritten (DWORD)nDataLen)); }逻辑说明这段代码把WritePrinter当成「把整段ZPL文本塞给打印机」的通道。关键在于pDatatype必须设为RAW。如果设成TEXT驱动会按文本流处理把ZPL里的换行符和转义序列改掉打印机收到的指令就坏了。另外一个细节是dwWritten不一定会等于你传入的nDataLen一定要校验这个返回值它代表驱动实际接收的字节数小了说明数据没发全。参数说明szPrinterName是打印机名也就是控制面板里看到的那个名字比如ZDesigner GT800。szZplData指向待发送的ZPL字符串nDataLen是它的字节长度注意是字节不是字符因为ZPL里可能有中文字符的UTF-8多字节编码。OpenPrinter的第一个参数类型是LPSTR如果你的工程是Unicode字符集编译会报错需要转成(char*)或用USES_CONVERSION宏转一下这个坑第4章专门讲。2.3 每次WritePrinter发多大分包发送与缓冲区GT800的输入缓冲区大概几十KB但Windows打印驱动一次WritePrinter能吞下的长度并不保证等于你传入的长度。最常见的情况是发几百字节的标签没问题一旦标签塞进图片、字体ZPL膨胀到几十KB一次WritePrinter就会丢尾。我一般会做一个循环发送把大ZPL按4KB一段切分循环写到写完为止。// 大ZPL分包发送防止一次WritePrinter写入不完整 BOOL SendZplByChunks(HANDLE hPrinter, const char* szData, int nLen) { const int CHUNK 4096; int nOffset 0; while (nOffset nLen) { int nSend min(CHUNK, nLen - nOffset); DWORD dwWritten 0; if (!WritePrinter(hPrinter, (LPBYTE)(szData nOffset), nSend, dwWritten)) return FALSE; if ((int)dwWritten ! nSend) // 如果没写完重试当前段 { // 简单重试一次仍失败则返回 WritePrinter(hPrinter, (LPBYTE)(szData nOffset), nSend, dwWritten); } nOffset nSend; } return TRUE; }分段的最小单位是4KB这是工程上比较稳的经验值。有些老驱动在缓冲区满时会进入忙状态连续写太快可能丢数据所以循环里可以加一个20毫秒的Sleep给打印机留出消化时间。批量打印几十张时这点延迟影响不大但能明显降低丢数据的概率。3. 从ZPL到可见条码模板、条码参数与中文的编码问题代码通道打通了接下来要考虑「发什么」。ZPL2是斑马打印机的指令语言它由一个个控制字段组成^XA开头^XZ结尾中间是定位、条码、文字和图形的定义。打印机收到一段完整ZPL后逐条执行把标签渲染出来后走纸。理解ZPL的基本语法是发好标签的前提。3.1 最小ZPL模板逐段拆解下面这一段是一个可以稳定出Code128条码的最小模板。先把它原样发给打印机试试能出来再改参数^XA ^FO20,20 ^BCN,100,Y,N,N ^FD1234567890^FS ^XZ这段指令的每一行都有具体含义。^XA和^XZ是格式的开始和结束标记缺一不可。缺少^XZ会导致打印机一直等待当前标签不输出下一张标签内容跟它合在一起。^FO是字段起点坐标参数是X轴和Y轴的坐标点单位是打印点GT800是203dpi1毫米大约8个点所以^FO20,20表示从距离标签左上角约2.5毫米的位置开始打印。^BCN表示这是一个Code128条码字段N代表普通模式。100是条码高度单位是点100点大约12.5毫米。第一个Y表示要打印可读字符也就是条码下方那排人眼可读的数字第二个N表示不附加UCC校验位第三个N表示不强制使用Code128的特定子集让打印机自动选最优编码。^FD后面是条码实际的数据^FS表示字段内容到此结束。逻辑说明^FD里跟的就是你要进条码的内容。它不能包含空格不能包含^FS这两个字符否则会被当成字段结束符提前截断。数据里有空格时打印机能生成的条码在扫码时会不稳定很多扫码枪对中间带空格的Code128识别率极低。我的习惯是进条码之前先过滤空格、换行和制表符。3.2 条码参数怎么调密度、高度与可读性上面模板里的^BCN,100这几个参数是最常调的。第一个位置是打印模式可选N普通、UUCC模式、A自动模式工位上打一维码用N就行。第二个是符号高度建议不要低于100点。很多翻车现场是条码太矮扫描枪稍微歪一点就扫不到。第三个是可读字符开关Y或N仓储内部标签可以关掉省空间但涉及外销或追溯建议开着人眼能直接核对数字。第四个表示是否在UCC模式下发数据普通模式全程用N。条码的密度由^BY字段控制放在^BC之前^XA ^BY2,3,150 ^FO20,100 ^BCN,100,Y,N,N ^FDABC123456^FS ^XZ^BY的第一个参数2是模块宽度也叫窄条宽度单位是点。值越大条码越粗可扫距离越远但标签上能容纳的字符数越少。第二个参数3是宽窄比宽条和窄条的比值Code128推荐用2.5到3。第三个参数150是条码总体高度如果这行写了150^BC里那个100就失效了^BY优先。踩过的一次坑模块宽度设成1理论上最省空间但GT800在200dpi下打印细条容易断线尤其新换的碳带还没磨合好时扫不出是家常便饭。我一般直接^BY2起步条码宽度不够再换密度更高的Code128子集而不是一味把窄条改细。3.3 中文内容ZPL指令本身是ASCII中文得想别的办法GT800出厂固件默认不支持中文字库你直接把中文放到^FD后面打印机打印出来是一堆方块或者干脆什么都不出。这是一个高频翻车点。方案有三个按推荐程度排序。第一种是标签设计层面避开中文。序列号、批次号、内部编码用字母数字表达人眼可读字符那行就显示英文缩写。很多MES系统就是这么干的不是不能显示中文而是没必要承担中文字库的兼容风险。第二种是图形化把中文画成位图插到ZPL里。在VC程序里先用GDI在内存DC上把中文文字画成单色位图再把位图转成ZPL的^GF图形字段发给打印机。位图转ZPL的^GF有个好用的格式^FO20,20 ^GFB,2048,2048,8,十六进制数据 ^FS^GF的参数B表示二进制数据第二个是总字节数第三个是整行像素数第四个是每行的字节数十六进制数据是图片原始像素按行排列后的HEX字符串。这个方法最稳打印机识别的是图片跟字库无关缺点是ZPL体积会膨胀标签下载会稍慢。第三种是给打印机插中文字库卡部分型号支持但GT800这种入门级机型没有扩展卡槽基本不用考虑。我实际项目里全部采用第二种程序里封装一个TextToZplGraphic函数CreateCompatibleDC、SelectObject放到内存画布再用GetDIBits取像素最后拼成^GF。这套流程写一次以后所有标签都能复用。4. VC工程里的关键配置字符集、打印机枚举与驱动版本代码本身不难但放进VC工程里立刻会撞上几个「windows.h相关」的老问题。最常见的是字符集不匹配其次是打印机名写死以后换USB口就找不到。这两个问题不解决前面写的SendZplToPrinter根本编译不过去或者跑不起来。4.1 多字节与UnicodeWritePrinter按字节写不按字符写VC工程默认有两种字符集多字节字符集MBCS和Unicode字符集。如果你是新建的MFC工程默认可能是Unicode这意味着你写的字符串字面量是宽字符比如L^XA^FS^XZ占用的内存是每字符2字节。WritePrinter要求传入字节数和字节指针直接把宽字符传进去会打印出夹着大量0x00的乱码打印机收到的指令就是破碎的。解决办法是在发送前把宽字符串转成UTF-8或者GBK窄字符串。对ZPL来说指令本身全是ASCII字符转成UTF-8后ASCII部分不变中文部分变成多字节序列再配合第3章讲的^GF方案使用就不会出乱码。// 宽字符ZPL转UTF-8窄字符串再发送 std::string WideToUtf8(const std::wstring wstr) { int nLen WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL); std::string strOut(nLen - 1, 0); WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, strOut[0], nLen, NULL, NULL); return strOut; }逻辑说明WideCharToMultiByte第一次调用传入NULL输出缓冲区返回目标缓冲区字节数第二次正式转换。代码里nLen - 1是因为第一次返回值包含了结尾的空字符去掉后string里不会有多余的0。OpenPrinter和WritePrinter都用转换后的窄字符串。参数说明如果工程是多字节字符集直接用字符串字面量没有转换烦恼但ZPL本身不含中文所以大部分实际项目里用多字节字符集反而省事。问题是现在很多第三方库要求Unicode这时WideToUtf8就是必要的过渡。4.2 枚举打印机不写死打印机名扛住USB口变更GT800换一个USB口插Windows会把它识别成一个新的打印机实例名字可能变成ZDesigner GT800 (副本 2)代码里写死的名字就找不到了。更隐蔽的情况是同一台电脑装过旧驱动又装新驱动队列里同时存在两个名字。用EnumPrinters枚举所有已安装的打印机按名称模糊匹配GT800是更稳妥的做法。// 枚举本机打印机找到匹配GT800的打印机全名 std::string FindGt800Printer() { DWORD dwNeeded 0, dwReturned 0; EnumPrinters(PRINTER_ENUM_LOCAL, NULL, 2, NULL, 0, dwNeeded, dwReturned); if (dwNeeded 0) return ; std::vectorBYTE buffer(dwNeeded); PRINTER_INFO_2* pInfo (PRINTER_INFO_2*)buffer.data(); if (!EnumPrinters(PRINTER_ENUM_LOCAL, NULL, 2, (LPBYTE)pInfo, dwNeeded, dwNeeded, dwReturned)) return ; for (DWORD i 0; i dwReturned; i) { std::string strName (char*)pInfo[i].pPrinterName; // 匹配GT800兼容ZDesigner GT800和GT800 (副本 2) if (strName.find(GT800) ! std::string::npos) return strName; } return ; }逻辑说明EnumPrinters第一次调用只返回缓冲区大小第二次真正取数据。PRINTER_INFO_2结构里有pPrinterName字段拿回来跟GT800做子串匹配。这样即使驱动版本升级、USB口变了只要打印机队列里还挂着GT800程序就能找到它。参数说明PRINTER_ENUM_LOCAL代表本机安装的打印机够用了不需要去枚举网络打印机。level2返回的信息包含了打印机名、端口、驱动名、状态等字段后面检查打印机是否联机也能用这些字段。4.3 驱动版本差异新驱动RAW行为变化斑马驱动升级到新版后个别系统上RAW数据发送会出现「前面自动加了一段初始化指令」的现象。这是因为新版驱动在启动文档时会自动下发一些配置。解决方案是在ZPL开头显式声明^XA结尾^XZ驱动自动加的内容在^XA^XZ之外打印机会忽略格式外的指令。不需要改代码只要严格保证自己的ZPL以^XA开头、^XZ结尾就行。这也是为什么从第一节开始就一直强调这两个标记不能省。5. GT800打印ZPL的4个高频踩坑与排查代码写完了接上打印机总会遇到一些「玄学」现象。这里把最常见的四个问题按现象、原因、解决写清楚。遇到问题按这个顺序排查通常十分钟内能定位。5.1 打印空白页或打印机滴滴响现象打印机走纸但标签是白的或者打印机亮红灯滴滴报警不走纸。原因最常见的是ZPL数据没有真正到打印机。打开Windows打印队列如果队列里显示文档正常消失说明数据进入驱动了问题出在驱动和打印机之间如果队列里文档卡住不动那就是驱动出问题了。第二个常见原因是发送了两遍ZPL第一遍进去了第二遍是空数据打印机收到空任务直接走一张白纸。第三个原因是Windows假脱机缓存里残留了上一次失败的损坏任务它会在你正常发送前先被执行。解决先确认数据来源。用斑马的驱动自带的打印工具发同一个ZPL能打印说明打印机和驱动都没问题问题出在你的程序。检查程序里是不是把dwWritten当成字符数用了比如发送^XA^FS^XZ共9字节但传了18字节会把中间的空字符也发进去打印机就会把空任务当作完成标记产生空白页。最简单验证是让程序只发一条^XA^XZ如果出空白标签说明链路通问题在数据内容。5.2 条码能出来但扫码枪扫不到现象标签打印正常条码也看得见但扫码枪死活扫不出来或者需要凑得很近、角度很正才能偶尔扫出。原因条码高度不够是第一位很多标签设计把条码高度压到50点以下扫描枪的光线覆盖不了足够长的纵向区域。第二位是模块宽度太小^BY1或者^BC里的密度太高细条在200dpi下打印出来不连续。第三位是条码内容里带了空格或者特殊字符扫码枪解析时不符合Code128的规则。解决先把^BY改成^BY2,3,150高度提到至少100点。这两条改完还扫不出检查^FD里的数据把空格、换行全部替换掉。还不行就换打印机模式把^BCN的N改成U用UCC模式强制走Code128的A/B/C子集自动切换。我在现场见到的翻车案例九成是模块宽度和空格这两个原因。5.3 连续打印时标签内容逐渐偏现象同一张模板发100张前几张位置正常打到第20张时内容开始往右偏越打越偏最后标签内容跑到标签纸外面。原因标签纸的检测和ZPL里声明的标签长度不一致。GT800有两种走纸模式——连续纸和间隙纸。如果打印机设置成连续纸模式它会认为每一张标签的长度等于你ZPL里^LL声明的长度。你声明一个长度但实际标签纸的间距略大一点点累积误差就会出现位置漂移。解决ZPL里显式声明标签尺寸。在^XA后面加^LL和^PW^PW600表示标签宽度600点75毫米^LL400表示标签长度400点50毫米。同时确认打印机面板的介质类型设置为「有间隙的标签纸」让打印机用传感器检测间隙来定位每张标签的起点。改完之后再做一次自动校准按住打印机面板上的暂停键不放直到指示灯闪烁打印机会自动测纸。这个动作能消除累积误差重新对齐标签起点。5.4 中文打成方块或乱码现象标签上中文位置打出来是一排小方块或者一串毫不相干的乱码。原因GT800标准配置没有中文字库。你在^FD后面直接放中文打印机找不到对应的点阵字模只能打方块。如果显示乱码则是Unicode字符被按GBK或ASCII方式错误发送字节序列变了。解决标签内容里不放中文改用拼音或者英文。一定要显示中文的用第3.3节的^GF图形方案把文字画成位图再插入。这里有一个更快速的替代方法——先用Windows的GDI画好整张标签的全部图像然后整张图转成^GF发送。这样ZPL部分只需要^XA、^FO、^GF、^XZ其他一切内容都是图形完全绕开字符集和字库问题。缺点是单张标签的数据量会到几十KB但GT800的缓冲区足够容纳发送速度慢零点几秒现场完全能接受。6. 批量打印多张标签用打印机状态检查避免任务堆积前面解决的是「单张标签能不能打对」批量打印时要防的就是「任务积压」。如果你在循环里连续调用SendZplToPrinter每张标签一个打印任务Windows假脱机会把几十个任务堆在队列里。一旦中间有一张标签数据错了整个队列卡住打印机后面全是排队等待的文档你都不知道哪张出错。我现在的做法是循环里先检查打印机状态再发下一张。用GetPrinter拿PRINTER_INFO_2看Status字段是否是PRINTER_STATUS_OFFLINE或者PRINTER_STATUS_ERROR。是就不发弹错误提示而不是硬往队列里塞任务。// 批量发送前检查打印机是否就绪 BOOL IsPrinterReady(const std::string strPrinterName) { HANDLE hPrinter NULL; if (!OpenPrinter((LPSTR)strPrinterName.c_str(), hPrinter, NULL)) return FALSE; DWORD dwNeeded 0; GetPrinter(hPrinter, 2, NULL, 0, dwNeeded); std::vectorBYTE buffer(dwNeeded); if (!GetPrinter(hPrinter, 2, buffer.data(), dwNeeded, dwNeeded)) { ClosePrinter(hPrinter); return FALSE; } PRINTER_INFO_2* pInfo (PRINTER_INFO_2*)buffer.data(); DWORD dwStatus pInfo-Status; ClosePrinter(hPrinter); // 脱机、错误、暂停中的打印机都不发任务 if (dwStatus (PRINTER_STATUS_OFFLINE | PRINTER_STATUS_ERROR | PRINTER_STATUS_PAUSED)) return FALSE; return TRUE; }批量循环里每发一张检查一次发现打印机卡纸或者缺纸时立即停止而不是继续发下去这样操作工处理完打印机后程序还能接着从断点处继续打。标签数量大时一张标签一个打印任务确实有点慢可以改用把多张标签拼成一段ZPL一次发送中间用^XZ^XA分隔这样一次WritePrinter出多张标签速度会快很多。这个方案我从第一版翻车到稳定运行最大的教训就是别图省事跳过状态检查。打印机缺纸或者碳带用完时程序还在闷头发送看起来像是打印机坏了其实是自己的代码没长眼睛。处理完这些问题后这套「VC拼ZPL、USB连GT800、WritePrinter发送」的方案在产线上连续跑几年都不用动。希望帮到你。本文还有配套的精品资源点击获取
返回列表