Windows服务中使用Microsoft XPS Document Writer打印后无法二次打印的解决方法
我之前碰到过几乎一模一样的问题,这事儿主要是Windows服务的会话隔离机制,加上Microsoft XPS Document Writer(MXDW)的特殊行为搞出来的麻烦。下面给你拆解问题根源,再一步步说怎么解决:
问题根源分析
- Windows服务默认跑在会话0里,而MXDW在会话0中完成首次打印后,很可能因为资源没清理干净,或者会话隔离导致的权限/资源访问问题,让打印队列里的首个任务残留并标记为错误,直接把后续所有打印作业都堵死了。
- 你用
CreateDC这类GDI打印API时,如果没严格遵循调用顺序、遗漏了资源释放步骤(比如没正确结束文档或释放设备上下文),MXDW会一直认为作业还在进行,最终超时后标记为错误,彻底阻塞队列。
正确实现步骤
1. 先把资源释放的坑填上
用GDI打印API的时候,必须严格按顺序收尾,一步都不能少。比如你的代码里,打印完成后一定要确保调用这些API:
// 打印内容完成后,先结束文档 if (!EndDoc(hdcMXDW)) { // 这里要加错误处理,比如GetLastError()排查问题 } // 最后必须释放设备上下文 DeleteDC(hdcMXDW);
要是漏了EndDoc,MXDW会一直挂着这个作业,最后直接报错阻塞队列。
2. 换用更适合服务环境的打印API
GDI的CreateDC系列API更适合桌面程序,服务里用容易踩会话隔离的坑。推荐改用打印后台处理程序API(比如OpenPrinter、StartDocPrinter、WritePrinter),这些API直接和打印后台服务交互,不受会话限制,稳定性高得多。给你个示例代码片段:
void Print(const std::wstring& path) { HANDLE hPrinter = NULL; PRINTER_DEFAULTS pd = {0}; pd.DesiredAccess = PRINTER_ALL_ACCESS; // 打开打印机 if (!OpenPrinter(printerName.c_str(), &hPrinter, &pd)) { // 处理错误,比如输出GetLastError() return; } // 初始化打印文档信息 DOC_INFO_1 di = {0}; di.pDocName = L"XPS Print Job from Service"; di.pDatatype = L"RAW"; // MXDW支持RAW格式的XPS数据 // 启动打印作业 DWORD jobId = StartDocPrinter(hPrinter, 1, (LPBYTE)&di); if (jobId == 0) { ClosePrinter(hPrinter); return; } // 启动打印页 if (!StartPagePrinter(hPrinter)) { EndDocPrinter(hPrinter); ClosePrinter(hPrinter); return; } // 读取XPS文件内容并写入打印机 HANDLE hFile = CreateFile(path.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile != INVALID_HANDLE_VALUE) { CHAR buffer[4096]; DWORD bytesRead, bytesWritten; while (ReadFile(hFile, buffer, sizeof(buffer), &bytesRead, NULL) && bytesRead > 0) { WritePrinter(hPrinter, buffer, bytesRead, &bytesWritten); } CloseHandle(hFile); } // 收尾:结束页、结束文档、关闭打印机 EndPagePrinter(hPrinter); EndDocPrinter(hPrinter); ClosePrinter(hPrinter); }
3. 主动清理打印队列的错误任务
可以在服务启动时,或者每次打印前,主动检查并清理队列里的错误任务,避免它们堵着后续作业。用EnumJobs枚举任务,再用SetJob删除错误任务:
void ClearErrorPrintJobs(const std::wstring& printerName) { HANDLE hPrinter = NULL; if (!OpenPrinter(const_cast<wchar_t*>(printerName.c_str()), &hPrinter, NULL)) { return; } DWORD jobCount = 0; DWORD bytesNeeded = 0; // 先获取需要的缓冲区大小 EnumJobs(hPrinter, 0, 100, JOB_INFO_1, NULL, 0, &bytesNeeded, &jobCount); if (bytesNeeded > 0) { std::vector<BYTE> buffer(bytesNeeded); JOB_INFO_1* pJobs = reinterpret_cast<JOB_INFO_1*>(buffer.data()); if (EnumJobs(hPrinter, 0, 100, JOB_INFO_1, buffer.data(), bytesNeeded, &bytesNeeded, &jobCount)) { for (DWORD i = 0; i < jobCount; ++i) { // 找到状态为错误的任务并删除 if (pJobs[i].Status & JOB_STATUS_ERROR) { SetJob(hPrinter, pJobs[i].JobId, 0, NULL, JOB_CONTROL_DELETE); } } } } ClosePrinter(hPrinter); }
4. 给服务账户加足够的权限
服务运行的账户得有打印权限,比如把它加入「Print Operators」组,或者直接在打印机的权限设置里,给这个账户授予「打印」和「管理文档」的权限,避免因为权限不足导致打印失败。
总结
最核心的两个解决点:一是严格按API调用顺序清理所有打印资源,二是换掉GDI的CreateDC方式,改用打印后台处理程序API——后者能从根本上避开会话隔离的坑,在服务环境里更稳定。
内容的提问来源于stack exchange,提问作者Fobos
相关产品推荐
相关产品推荐

