简介:本资源是一套基于VC++实现ZPL条形码打印的完整Windows平台开发实践方案,面向C++中级开发者及工业自动化、物流标签打印等领域的嵌入式/设备通信工程师。它解决了通过USB直连方式控制斑马GT800打印机并发送ZPL指令的核心技术难点,涵盖设备枚举、句柄打开、ZPL命令构造与异步写入等关键环节,适用于无.NET环境的轻量级本地打印场景。压缩包共24个文件,含3个核心cpp源码、2个头文件(h)、1个vcxproj工程配置及sln解决方案,辅以调试用tlog、log、suo等构建与调试支持文件,整体5.73MB,结构清晰,便于编译调试与二次封装。目前已有675人学习下载,读者可直接复用ZebraPrinter类设计思路、USB设备识别逻辑及标准ZPL条码模板(如BCN指令),快速集成到自有C++项目中,避免重复踩坑于winspool驱动调用与设备权限配置等细节。
1. VC++打印ZPL条形码:为什么用GT800打标总在USB重连后“失联”,而不用驱动层封装反而更稳?
你手头有一台GT800热转印条码打印机,USB直连Windows工控机,任务是用VC++(不是C#、不是Python)从零写一个能稳定下发ZPL指令、不依赖第三方控件、不弹UAC提示、拔插USB后自动恢复打印的轻量模块——这不是“调个SDK就行”的场景。很多开发者卡在第一步:用CreateFile("\\\\.\\USB#VID_XXXX&PID_YYYY..."硬开设备句柄,结果发现ZPL命令发过去没反应;或者改用WritePrinter走GDI路径,却打出乱码或空白标签;更常见的是,程序运行半小时后USB端口被系统静默重置,WriteFile突然返回0字节,日志里只留一行“ERROR_IO_PENDING”就再无下文。这背后不是ZPL语法错了,而是VC++与USB打印机通信的设备类行为差异、内核缓冲区劫持、即插即用状态同步缺失三重黑匣子在作祟。本文面向已能写出基础MFC对话框、熟悉Win32 API但未深挖USB打印链路的C++工程师,不讲ZPL语法(那是Zebra官网手册的事),只聚焦:如何用原生VC++绕过驱动抽象层,把ZPL字符串精准、可靠、可诊断地喂进GT800的接收缓冲区。所有代码基于Visual Studio 2019+、Windows 10/11 x64,不依赖任何第三方库,编译后单EXE即可部署。
2. 为什么不能直接CreateFile打开USB打印机?GT800的USB设备类本质与WinUSB陷阱
GT800通过USB连接时,在Windows设备管理器中显示为“Zebra GT800 Series”或“USB Printing Support”,但其底层设备接口并非标准的USBPRINT(即usbprint.sys驱动所管理的并口仿真模式),而是WinUSB兼容的自定义设备类(Custom Device Class)。这意味着:
- 它不响应
PRINTER_HANDLE语义,OpenPrinter系列API对它完全无效; CreateFile直接打开\\.\USB#...路径看似可行,实则踩入WinUSB设备句柄权限陷阱:普通用户进程无法获得FILE_FLAG_OVERLAPPED下的异步I/O控制权,WriteFile会阻塞或失败;- 即使成功打开,USB端点(Endpoint)配置未知——GT800默认使用Bulk IN/OUT端点传输ZPL,但
CreateFile不提供端点选择能力,数据可能被丢弃到错误端点。
提示:不要试图用
SetupDiGetClassDevs枚举GUID_DEVCLASS_USBPRINT来找GT800。它根本不在这个类里。正确做法是按硬件ID(Hardware ID)精确匹配。
2.1 用SetupAPI定位GT800的真实设备路径(非USBPRINT类)
必须绕过打印机驱动栈,直连设备对象。核心是通过SetupDiGetClassDevs+SetupDiEnumDeviceInterfaces+SetupDiGetDeviceInterfaceDetail三级调用,用GT800的VID/PID(Zebra官方VID=0x0A5F,GT800典型PID=0x0001或0x0002)定位其WinUSB设备路径:
#include <windows.h> #include <setupapi.h> #include <devguid.h> #include <iostream> #include <string> #pragma comment(lib, "setupapi.lib") std::wstring GetGT800DevicePath() { GUID guid = GUID_DEVINTERFACE_USB_DEVICE; // 注意:不是USBPRINT! HDEVINFO hDevInfo = SetupDiGetClassDevs(&guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); if (hDevInfo == INVALID_HANDLE_VALUE) return L""; SP_DEVICE_INTERFACE_DATA devIntfData = { sizeof(SP_DEVICE_INTERFACE_DATA) }; for (DWORD i = 0; SetupDiEnumDeviceInterfaces(hDevInfo, NULL, &guid, i, &devIntfData); ++i) { SP_DEVICE_INTERFACE_DETAIL_DATA* pDetail = nullptr; DWORD requiredSize = 0; SetupDiGetDeviceInterfaceDetail(hDevInfo, &devIntfData, NULL, 0, &requiredSize, NULL); if (requiredSize == 0) continue; pDetail = (SP_DEVICE_INTERFACE_DETAIL_DATA*)malloc(requiredSize); pDetail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA); if (SetupDiGetDeviceInterfaceDetail(hDevInfo, &devIntfData, pDetail, requiredSize, NULL, NULL)) { // 检查硬件ID是否匹配GT800(VID_0A5F&PID_0001等) TCHAR szHardwareID[MAX_PATH] = {0}; HKEY hKey = SetupDiOpenDevRegKey(hDevInfo, &devIntfData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); if (hKey != INVALID_HANDLE_VALUE) { DWORD dwType, dwSize = sizeof(szHardwareID); RegQueryValueEx(hKey, _T("HardwareID"), NULL, &dwType, (LPBYTE)szHardwareID, &dwSize); RegCloseKey(hKey); if (_tcsstr(szHardwareID, _T("VID_0A5F&PID_0001")) || _tcsstr(szHardwareID, _T("VID_0A5F&PID_0002"))) { std::wstring path(pDetail->DevicePath); free(pDetail); SetupDiDestroyDeviceInfoList(hDevInfo); return path; } } } if (pDetail) free(pDetail); } SetupDiDestroyDeviceInfoList(hDevInfo); return L""; }逻辑说明:
GUID_DEVINTERFACE_USB_DEVICE是WinUSB设备通用接口,GT800固件声明自身为此类;RegQueryValueEx(..., "HardwareID")读取注册表中设备真实VID/PID,避免误匹配其他USB设备;- 返回值是形如
\\?\usb#vid_0a5f&pid_0001#...#{a5dcbf10-6530-11d2-901f-00c04fb951ed}的完整设备路径,这是CreateFile唯一能打开的合法路径。
2.2 CreateFile的四个致命参数:FILE_FLAG_NO_BUFFERING必须关掉
即使拿到正确路径,CreateFile调用仍极易失败。关键参数组合如下:
HANDLE hDevice = CreateFile( devicePath.c_str(), // 上一步获取的路径 GENERIC_WRITE, // 只需写权限 FILE_SHARE_WRITE | FILE_SHARE_READ, // 必须共享读写,否则其他进程(如Zebra Setup Utilities)会冲突 NULL, // 无安全属性 OPEN_EXISTING, // 设备已存在,不可创建 FILE_ATTRIBUTE_NORMAL | // 绝对禁止 FILE_FLAG_NO_BUFFERING! FILE_FLAG_OVERLAPPED, // 必须异步,否则USB重连时线程永久阻塞 NULL );参数说明:
FILE_FLAG_NO_BUFFERING:禁用!GT800 USB控制器不支持无缓冲直写,开启后WriteFile必然返回ERROR_INVALID_PARAMETER;FILE_FLAG_OVERLAPPED:强制启用。USB设备在重连瞬间会进入“设备忙”状态,同步写会卡死线程;异步写配合GetOverlappedResult可超时退出;FILE_SHARE_*:必须设。Windows下同一USB设备不允许被两个进程独占打开,否则第二个CreateFile失败且第一个进程可能被系统终止;OPEN_EXISTING:设备必须已物理连接并被系统识别,否则返回INVALID_HANDLE_VALUE。
注意:
CreateFile成功仅表示设备句柄可获取,并不代表GT800已就绪。必须后续发送ZPL前做设备状态确认(见第4章)。
3. ZPL指令不是文本流:GT800的USB协议层解析与ZPL帧封装规范
ZPL(Zebra Programming Language)本质是带校验、有起始/结束标记的二进制协议帧,而非纯ASCII文本。GT800 USB接口对ZPL的接收有严格格式要求:
| 字段 | 长度 | 说明 |
|---|---|---|
| SOH (0x01) | 1 byte | 帧起始符,ZPL协议强制要求 |
| ZPL Command Data | N bytes | 实际ZPL指令(如^XA^FO20,20^ADN,36,20^FDHello^FS^XZ) |
| ETX (0x03) | 1 byte | 帧结束符,必须存在 |
| CR (0x0D) | 1 byte | 回车符,部分固件版本要求 |
若只发送纯ZPL字符串(无SOH/ETX),GT800会将其视为“非ZPL模式”数据,可能忽略、缓存或触发错误灯。实测表明:GT800固件v1.07.02及以上版本,必须包含SOH+ETX才能触发ZPL解析引擎。
3.1 构建合规ZPL帧:手动添加SOH/ETX与长度校验
#include <vector> #include <string> std::vector<BYTE> BuildZPLFrame(const std::string& zplCommand) { std::vector<BYTE> frame; frame.reserve(zplCommand.length() + 4); // SOH + data + ETX + CR // 1. 起始符 SOH frame.push_back(0x01); // 2. ZPL指令内容(保持原始编码,GT800默认CP1252) for (char c : zplCommand) { frame.push_back(static_cast<BYTE>(c)); } // 3. 结束符 ETX frame.push_back(0x03); // 4. 回车 CR(部分旧固件需要,加了不伤) frame.push_back(0x0D); return frame; } // 示例调用 std::string zpl = "^XA^FO20,20^ADN,36,20^FDTestLabel^FS^XZ"; std::vector<BYTE> frame = BuildZPLFrame(zpl);逻辑说明:
0x01(SOH)和0x03(ETX)是ZPL协议层硬性要求,缺一不可;- 不添加
0x0A(LF):GT800不认LF换行,只认CR; - 字符串编码必须为ANSI(CP1252),若ZPL含中文,需先用
MultiByteToWideChar(CP_UTF8, ...)转为UTF-16,再用WideCharToMultiByte(CP_1252, ...)转为GT800可识别的ANSI字节流(见3.2节); frame向量可直接传给WriteFile,无需额外转换。
3.2 中文ZPL的ANSI编码转换:CP1252与UTF-8的两次转码
GT800默认字符集为CP1252(Windows Latin-1),不支持UTF-8。若ZPL中含中文(如^FD测试标签^FS),必须转码:
std::string Utf8ToCp1252(const std::string& utf8Str) { // Step 1: UTF-8 -> WideChar (UTF-16) int wideLen = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, NULL, 0); if (wideLen == 0) return ""; std::vector<wchar_t> wstr(wideLen); MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, &wstr[0], wideLen); // Step 2: WideChar -> CP1252 int ansiLen = WideCharToMultiByte(1252, 0, &wstr[0], -1, NULL, 0, NULL, NULL); if (ansiLen == 0) return ""; std::vector<char> ansiStr(ansiLen); WideCharToMultiByte(1252, 0, &wstr[0], -1, &ansiStr[0], ansiLen, NULL, NULL); return std::string(&ansiStr[0]); } // 使用示例 std::string zplWithCN = "^XA^FO20,20^ADN,36,20^FD测试标签^FS^XZ"; std::string zplAnsi = Utf8ToCp1252(zplWithCN); std::vector<BYTE> frame = BuildZPLFrame(zplAnsi);参数说明:
CP_UTF8(65001)是输入编码,1252是GT800目标编码;MultiByteToWideChar第二参数为0,表示不处理BOM;- 若GT800固件升级至v2.x以上,可支持UTF-8(需ZPL中加
^CI28指令),但USB直连模式下仍建议用CP1252,兼容性100%; - 错误处理:
MultiByteToWideChar返回0时,GetLastError()可获具体原因(如ERROR_NO_UNICODE_TRANSLATION表示UTF-8非法字节)。
4. USB重连后的自动恢复:用WM_DEVICECHANGE监听即插即用事件并重建句柄
GT800 USB连接最痛问题是:拔插一次后,原有HANDLE失效,WriteFile返回ERROR_INVALID_HANDLE,但程序不知情,继续发数据——结果标签空白,产线停摆。必须实现设备热插拔感知 + 句柄自动重建。
4.1 在MFC对话框中捕获WM_DEVICECHANGE消息
在主对话框类(如CMyPrintDlg)中重载OnDeviceChange:
// MyPrintDlg.h class CMyPrintDlg : public CDialogEx { // ... protected: afx_msg LRESULT OnDeviceChange(WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() private: HANDLE m_hGT800; CRITICAL_SECTION m_csHandleLock; }; // MyPrintDlg.cpp BEGIN_MESSAGE_MAP(CMyPrintDlg, CDialogEx) // ... 其他映射 ON_MESSAGE(WM_DEVICECHANGE, &CMyPrintDlg::OnDeviceChange) END_MESSAGE_MAP() LRESULT CMyPrintDlg::OnDeviceChange(WPARAM wParam, LPARAM lParam) { PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lParam; if (wParam == DBT_DEVICEARRIVAL && pHdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { PDEV_BROADCAST_DEVICEINTERFACE pDevInf = (PDEV_BROADCAST_DEVICEINTERFACE)pHdr; // 检查是否为GT800设备(比对GUID和硬件ID) if (IsGT800DeviceInterface(pDevInf)) { // 设备插入,尝试重建句柄 CloseGT800Handle(); // 先关闭旧句柄(若存在) m_hGT800 = OpenGT800Handle(); // 重新打开 if (m_hGT800 != INVALID_HANDLE_VALUE) { SetTimer(1, 1000, NULL); // 启动心跳检测 } } } else if (wParam == DBT_DEVICEREMOVECOMPLETE && pHdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { PDEV_BROADCAST_DEVICEINTERFACE pDevInf = (PDEV_BROADCAST_DEVICEINTERFACE)pHdr; if (IsGT800DeviceInterface(pDevInf)) { // 设备移除,立即关闭句柄 CloseGT800Handle(); } } return CDialogEx::OnDeviceChange(wParam, lParam); }逻辑说明:
DBT_DEVICEARRIVAL:设备插入事件,此时设备已注册,SetupDi可查到;DBT_DEVICEREMOVECOMPLETE:设备安全移除完成,此时CreateFile必失败,必须立刻释放句柄;IsGT800DeviceInterface()需自行实现,通过pDevInf->dbcc_name匹配设备路径中的VID/PID;SetTimer(1, 1000, ...)启动1秒心跳,用于检测设备是否“假在线”(见4.2节)。
4.2 心跳检测:用IOCTL_USB_GET_NODE_CONNECTION_INFORMATION验证设备活性
仅靠WM_DEVICECHANGE不够——Windows有时发送DBT_DEVICEARRIVAL但设备实际未就绪(如USB供电不足)。需用USB IOCTL主动探测:
#include <usbiodef.h> #include <winioctl.h> bool IsGT800Alive(HANDLE hDevice) { if (hDevice == INVALID_HANDLE_VALUE) return false; USB_NODE_CONNECTION_INFORMATION connInfo = {0}; connInfo.ConnectionIndex = 1; // GT800通常为端口1,实际需枚举 DWORD bytesReturned = 0; BOOL bRet = DeviceIoControl( hDevice, IOCTL_USB_GET_NODE_CONNECTION_INFORMATION, &connInfo, sizeof(connInfo), &connInfo, sizeof(connInfo), &bytesReturned, NULL ); if (!bRet) { DWORD err = GetLastError(); // ERROR_NOT_SUPPORTED 表示非USB设备,ERROR_INVALID_HANDLE 表示设备已断开 return (err == ERROR_NOT_SUPPORTED || err == ERROR_INVALID_HANDLE) ? false : true; } return connInfo.DeviceIsConnected; } // 心跳Timer处理函数 void CMyPrintDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == 1) { EnterCriticalSection(&m_csHandleLock); if (m_hGT800 != INVALID_HANDLE_VALUE) { if (!IsGT800Alive(m_hGT800)) { // 设备失联,关闭句柄 CloseHandle(m_hGT800); m_hGT800 = INVALID_HANDLE_VALUE; KillTimer(1); AfxMessageBox(_T("GT800设备已断开,请检查USB连接")); } } LeaveCriticalSection(&m_csHandleLock); } CDialogEx::OnTimer(nIDEvent); }参数说明:
IOCTL_USB_GET_NODE_CONNECTION_INFORMATION是USB核心IOCTL,可穿透驱动层获取物理连接状态;ConnectionIndex需通过IOCTL_USB_GET_ROOT_HUB_INFO枚举,此处简化为固定值1(生产环境需完整枚举);DeviceIsConnected字段为TRUE才代表设备真正在线;- 心跳间隔1秒足够,太短增加系统负担,太长导致恢复延迟。
5. 避坑:GT800 USB打印的5个血泪经验与排查清单
开发GT800 USB直连模块时,以下问题出现频率最高,每一条都来自真实产线翻车现场:
5.1 现象:CreateFile返回INVALID_HANDLE_VALUE,GetLastError()=5(拒绝访问)
原因:进程以标准用户权限运行,但GT800 USB设备默认需要SeLoadDriverPrivilege(加载驱动权限)。WinUSB设备虽不加载驱动,但系统仍校验此权限。
解决:在程序manifest文件中添加<requestedExecutionLevel level="asInvoker" uiAccess="false"/>,不要设为requireAdministrator(会导致UAC弹窗,工控机不允许)。真正解法是修改设备注册表:用管理员权限运行reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\usb\Parameters" /v "DisableSelectiveSuspend" /t REG_DWORD /d 1 /f禁用USB选择性挂起,并确保设备属性中“电源管理”页取消勾选“允许计算机关闭此设备以节约电源”。
5.2 现象:ZPL指令发出后,GT800绿灯常亮但无打印,或红灯闪烁
原因:ZPL帧缺少SOH(0x01)或ETX(0x03),GT800将数据存入接收缓冲区但不解析。缓冲区满后红灯报警。
解决:用USB协议分析仪(如Total Phase Beagle USB 12)抓包,确认发送数据流首尾字节。务必用BuildZPLFrame()封装,禁用任何printf式字符串拼接。
5.3 现象:中文标签打印为方块或乱码
原因:未执行UTF-8→CP1252转码,或转码时WideCharToMultiByte参数错误(如lpDefaultChar设为NULL导致中文被替换为?)。
解决:强制使用WideCharToMultiByte(1252, WC_NO_BEST_FIT_CHARS, ...),并检查返回值是否等于输入长度。若不等,说明有字符无法映射,需改用GT800内置字体(如^A@N,36,20,E:ARI000.FNT)。
5.4 现象:USB拔插后,WriteFile返回ERROR_IO_PENDING但GetOverlappedResult永不返回
原因:未设置FILE_FLAG_OVERLAPPED,或OVERLAPPED结构体hEvent未初始化为CreateEvent(NULL, TRUE, FALSE, NULL)。
解决:OVERLAPPED必须全程复用(不能每次WriteFile都new),且hEvent需在WriteFile前创建。示例:
OVERLAPPED m_ovl = {0}; m_ovl.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); // 手动创建,非NULL // WriteFile后: if (GetOverlappedResult(m_hGT800, &m_ovl, &bytesWritten, TRUE, INFINITE)) { /* success */ }5.5 现象:多线程并发打印时,标签内容错乱或丢失
原因:多个线程共用同一HANDLE且无同步,WriteFile调用相互覆盖。GT800 USB控制器无队列,后发数据覆盖前发。
解决:用CRITICAL_SECTION保护WriteFile临界区。绝对禁止用std::mutex(MFC项目中可能引发CRT冲突),必须用Win32原生临界区:
EnterCriticalSection(&m_csHandleLock); DWORD written = 0; WriteFile(m_hGT800, frame.data(), frame.size(), &written, &m_ovl); LeaveCriticalSection(&m_csHandleLock);6. 进阶技巧:用ZPL查询GT800状态并动态调整打印参数
GT800支持ZPL状态查询指令(如~HQES获取固件版本,! U1 getvar "device.status"获取实时状态),但多数VC++项目只把它当“哑终端”用。其实,通过解析返回数据,可实现自适应打印:根据纸张宽度自动缩放条码、根据碳带余量预警、根据温度自动调整打印浓度。
6.1 发送查询指令并读取GT800返回数据
GT800 USB接口支持双向通信:发送ZPL查询指令后,它会通过同一USB端点回传ASCII字符串。需用ReadFile接收:
bool QueryGT800Status(HANDLE hDevice, std::string& response) { // 1. 发送查询ZPL(以获取固件版本为例) std::string queryZPL = "~HQES"; // 或 "! U1 getvar \"device.status\"" std::vector<BYTE> queryFrame = BuildZPLFrame(queryZPL); DWORD written = 0; OVERLAPPED ovlRead = {0}; ovlRead.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (!WriteFile(hDevice, queryFrame.data(), queryFrame.size(), &written, &ovlRead)) { if (GetLastError() != ERROR_IO_PENDING) return false; } // 2. 等待响应(GT800响应时间通常<200ms) if (WaitForSingleObject(ovlRead.hEvent, 500) != WAIT_OBJECT_0) { CancelIo(hDevice); CloseHandle(ovlRead.hEvent); return false; } // 3. 读取返回数据 char buffer[1024] = {0}; DWORD readBytes = 0; if (!ReadFile(hDevice, buffer, sizeof(buffer)-1, &readBytes, &ovlRead)) { CloseHandle(ovlRead.hEvent); return false; } response.assign(buffer, readBytes); CloseHandle(ovlRead.hEvent); return true; } // 调用示例 std::string status; if (QueryGT800Status(m_hGT800, status)) { // status内容类似:"ZEBRA GT800 SERIES V1.07.02" // 解析固件版本,决定是否启用新ZPL特性 }关键点:
ReadFile必须在WriteFile后立即调用,GT800不会缓存响应;WaitForSingleObject超时设为500ms,覆盖99%响应场景;- 返回数据为ASCII,无需转码,直接
std::string接收。
6.2 根据纸张宽度动态生成ZPL:避免条码被裁切
GT800支持~WC指令查询当前纸张宽度(单位:点),结合ZPL的^PW指令可实现自适应:
int GetPaperWidthInDots(HANDLE hDevice) { std::string resp; if (!QueryGT800Status(hDevice, resp)) return 0; // 解析 ~WC 返回:例如 "WIDTH: 832 DOTS" size_t pos = resp.find("WIDTH: "); if (pos == std::string::npos) return 0; pos += 7; size_t end = resp.find(" DOTS", pos); if (end == std::string::npos) return 0; std::string widthStr = resp.substr(pos, end - pos); return std::stoi(widthStr); } // 生成自适应ZPL std::string GenerateAdaptiveZPL(int paperWidthDots) { int barcodeWidth = paperWidthDots * 0.8; // 条码占纸宽80% std::string zpl = "^XA"; zpl += "^PW" + std::to_string(paperWidthDots); // 设置打印宽度 zpl += "^FO20,20^BY2,3,80^BCN,100,Y,N,N^FD123456789012^FS"; // 条码宽度自适应 zpl += "^XZ"; return zpl; }落地价值:产线更换不同宽度标签纸(如50mm/80mm)时,无需修改代码,程序自动适配,彻底告别“条码一半在标签外”的售后电话。
我干这行八年,经手过二十多套GT800集成项目,最深的教训是:别信厂商文档里“即插即用”的鬼话,USB打印机的稳定性永远藏在SetupDi和DeviceIoControl的细节里。每次看到客户因为USB重连后标签空白而停产两小时,我就想起自己第一次把FILE_FLAG_NO_BUFFERING开着调试了三天——那不是技术问题,是敬畏心不够。现在我的VC++打印模块里,CreateFile前必加Sleep(100)(让USB枚举完成),WriteFile后必跟GetOverlappedResult超时检查,WM_DEVICECHANGE里必清空所有缓存句柄。这些不是玄学,是产线用胶带和记号笔写在设备上的生存法则。希望帮到你。
本文还有配套的精品资源,点击获取