在Windows中使用打印机时,在调用一系列与打印相关的GDI绘图函数的背后,实际上启动了一系列模块之间复杂的交互过程,包括 GDI32库模块、打印机设备驱动程序库模块(带.DRV后缀的文件)、Windows后台打印处理程序(print spooler)和其他有关模块。在开始为打印机编程之前,让我们先来看一下这个过程是如何工作的。
本节必须掌握的知识点:
打印和后台处理
打印机设备环境
第79练:获取显示器及打印机设备信息
第80练:最简单的打印程序
13.1.1打印和后台处理
■打印的一般处理过程
●应用程序首先调用CreateDC或者PrintDIg函数得到打印机设备环境的句柄。加载打印机设备驱动程序,并且进行初始化。
●然后再调用StartDoc函数,开始一个新文档。StartDoc 会由GDI模块处理。GDI模块调用打印机驱动程序中的Control函数,通知设备驱动程序做好打印准备。
●调用StartPage函数开始一页。GDI函数则把文字或图形显示到每个文档页面里。GDI模块会把应用程序针对打印机设备环境使用的任何GDI函数存储到硬盘上的图元文件(打印程序生成的临时文件)。这些图元文件的文件名以字符串“EMF”(表示“增强型图元文件”)开头,后缀是.TMP。我们将在第十七章详细讲述图元文件。
●完成一个页面后,调用EndPage函数结束这一页。
●打印机驱动程序必须把存储在图元文件中的各个绘图命令转化成适用于打印机的输出。打印机驱动程序把每一条带(页码扫描块)的打印机输出传给GDI模块,而GDI模块则把这个输出存到另一个临时文件中。GDI模块采用进程间调用告诉后台打印处理程序新的打印作业已就绪。
●是否继续处理下一页?否,打印输出。当应用程序完成了定义第一个页面的GDI函数调用以后,调用EndPage函数。然后再次循环处理下一页。
●打印完毕,调用EndDoc函数结束此过程。
■图元文件的打印输出的过程
●打印机驱动程序通常都采用“分带”技术,将页面分成多个长方形的带。GDI 模块从打印机驱动程序得到每条带的尺寸,然后设置一个大小与这条带相等的剪裁区域;
●对图元文件中每一个绘图函数都调用一次打印机驱动程序中的Output函数。这个过程被称为“把图元文件送入设备驱动程序”。对于设备驱动程序定义在页面上的每一条带, GDI模块都必须把整个图元文件送入设备驱动程序。这个过程完成后,图元文件就可以删掉了。
●对于每条带,打印机驱动程序把这些绘图函数转换成相应的需要在打印机上实现的输 出。这些输出的格式将根据打印机的不同而不同。对于点阵打印机,输出格式是一个控制系列集合,包括图形系列。(为了帮助构造这个输出,打印机驱动程序可以调用一些GDI 模块中的辅助例程。)对于支持高级页面合成语言(比如PostScript)的激光打印机,打印机的输出将会用这种语言表达。由GDI模块负责与不同打印机驱动之间的兼容,应用程序只需调用相关的GDI函数就可以了。
■打印驱动程序
打印机驱动程序把每一条带的打印机输出传给GDI模块,而GDI模块则把这个输出存到另一个临时文件中。这个文件以字符串“SPL”开头,以“.TMP”作为后缀。在整个页完成以后,GDI模块采用进程间调用(inter-process call)告诉后台打印处理程序新的打印作业已就绪。应用程序然后可以处理下一个页面。当应用程序处理完所有要打印的页面以后,要调用EndDoc函数来表示打印作业己经完成。图13-1显示了应用程序、GDI模块和打印机驱动程序之间的交互。
图13-1 应用程序、GDI模块、打印驱动程序与后台处理程序之间的交互
■打印后台处理程序
Windows后台打印处理程序实际上是由下面几个组件构成的:
后台打印处理程序组件 |
描述 |
打印请求程序 |
把数据流转发给打印提供程序 |
本地打印提供程序 |
创建用于本地打印机的后台处理程序 |
网络打印提供程序 |
创建用于网络打印机的后台处理程序 |
打印处理器 |
进行后台反向处理,也就是将进入后台的“与设备无关的”数据转换成适用于目标打印机的形式 |
端口监视程序 |
控制打印机连接的端口 |
语言监视程序 |
控制具备双向通信能力的打印机去设置设备配置和监视打印机状态 |
后台处理程序将应用程序从一些涉及打印的工作中解脱出来。Windows在启动时加载后台打印处理程序,所以应用程序开始打印的时候后台打印处理程序就已经在运行中。当程序打印文档时,GDI模块创建包含打印机输出的文件。后台打印处理程序的工作就是把这些文件发给打印机。它从GDI模块那儿得知有新的打印作业,于是开始读入文件,并把文件直接传给打印机。为了传输这些文件,后台处理程序使用了各种通信函数与连接打印机的并口、串口、USB端口或网络端口进行通信。后台处理程序把文件传输给打印机以后,就会删除输出时产生的临时文件。这个过程如图13-2所示。
图13-2 后台打印处理程序的操作
这个过程的绝大部分对于应用程序来说都是透明的。从应用程序的角度来说,“打印” 只发生在GDI模块把所有的打印机输出存储到硬盘文件这个阶段。在那之后,或者甚至是在那之前(如果打印是由另一个线程处理的话应用程序已经可以随便做其他事情了。文档的真正打印不再是应用程序的责任,而是后台打印处理程序的责任了。用户则负责暂停打印作业、
改变作业优先级或者在必要情况下取消打印。比起让应用程序实时打印并且等打印完一页再打印下一页的做法,这种安排使得程序可以更快地“打印”。
13.1.2打印机设备环境
在 Windows 操作系统中,打印机设备环境(Printer Device Context)用于绘制和输出图形到打印机的设备。
打印机设备环境是通过使用 GDI 函数 CreateDC 或 CreateDCW 来创建的,其中指定了打印机的驱动程序、打印机名称和其他相关参数。创建打印机设备环境后,可以使用 GDI 函数对其进行绘图操作,如绘制图形、文本和图像,实现打印输出。
■以下是一些常用的打印机设备环境相关的函数:
CreateDC 或 CreateDCW:用于创建打印机设备环境的函数,需要指定打印机驱动程序和打印机名称。
●CreateDC 函数或其变体函数CreateDCW,可以用于创建显示器设备上下文或打印机设备上下文,具体取决于传递给函数的参数。
以下是 CreateDC 函数的声明:
HDC CreateDC(
LPCTSTR lpszDriver, // 驱动程序名称或驱动程序文件路径
LPCTSTR lpszDevice, // 设备名称或设备文件路径
LPCTSTR lpszOutput, // 输出设备名称
const DEVMODE *lpInitData // 设备模式信息
);
返回值:
如果函数调用成功,将返回创建的设备上下文的句柄(HDC)。如果函数调用失败,将返回 NULL。
示例用法:
以下示例展示了如何使用 CreateDCW 函数创建打印机设备上下文:
#include <windows.h>
void PrintTextToPrinter(const wchar_t* printerName, const wchar_t* text) {
HDC hdcPrinter = CreateDCW(L"WINSPOOL", printerName, NULL, NULL);
if (hdcPrinter != NULL) {
// 在打印机设备上下文中绘制文本或进行其他打印操作
TextOutW(hdcPrinter, 100, 100, text, lstrlenW(text));
// 释放打印机设备上下文
DeleteDC(hdcPrinter);
}
}
int main() {
const wchar_t* printerName = L"Printer Name";
const wchar_t* text = L"Hello, Printer!";
PrintTextToPrinter(printerName, text);
return 0;
}
在上述示例中,我们使用 CreateDCW 函数创建了一个打印机设备上下文(hdcPrinter),然后使用 TextOutW 函数在打印机上绘制文本。最后,我们使用 DeleteDC 函数释放了打印机设备上下文。
【注意】示例中的 printerName 应该替换为实际的打印机名称。
●StartDoc:开始一个打印文档,标识一个打印作业的开始。
StartDoc 函数用于告诉打印机驱动程序开始打印一个文档,并指定文档的名称。
以下是 StartDoc 函数的声明:
int StartDoc(
HDC hdc, //设备上下文(DC)的句柄
const DOCINFO *lpdi //指向一个 DOCINFO 结构体的指针
);
返回值:
如果函数调用成功,返回值为大于零的唯一标识符,表示打印作业的标识。如果函数调用失败,返回值为小于零的错误代码。
DOCINFO 结构体的定义如下:
typedef struct _DOCINFO {
int cbSize; // 结构体的大小,用于指定结构体的字节数
LPCTSTR lpszDocName; // 文档的名称,将在打印队列中显示
LPCTSTR lpszOutput; // 输出设备的名称,可以为 NULL
LPCTSTR lpszDatatype; // 打印数据的类型,可以为 NULL
DWORD fwType; // 保留字段,应该设置为 0
} DOCINFO, *LPDOCINFO;
示例用法:
以下示例展示了如何使用 StartDoc 函数开始打印作业:
#include <windows.h>
void PrintTextToPrinter(const wchar_t* printerName, const wchar_t* docName, const wchar_t* text) {
HDC hdcPrinter = CreateDCW(L"WINSPOOL", printerName, NULL, NULL);
if (hdcPrinter != NULL) {
DOCINFO di = { sizeof(DOCINFO), docName, NULL, NULL, 0 };
int jobId = StartDoc(hdcPrinter, &di);
if (jobId > 0) {
// 在打印作业中绘制文本或进行其他打印操作
TextOutW(hdcPrinter, 100, 100, text, lstrlenW(text));
// 结束打印作业
EndDoc(hdcPrinter);
}
// 释放打印机设备上下文
DeleteDC(hdcPrinter);
}
}
int main() {
const wchar_t* printerName = L"Printer Name";
const wchar_t* docName = L"My Document";
const wchar_t* text = L"Hello, Printer!";
PrintTextToPrinter(printerName, docName, text);
return 0;
}
在上述示例中,我们使用 CreateDCW 函数创建了一个打印机设备上下文(hdcPrinter),然后使用 StartDoc 函数开始一个打印作业,并指定了文档名称。在打印作业中,我们使用 TextOutW 函数在打印机上绘制文本。最后,我们使用 EndDoc 函数结束打印作业,并使用 DeleteDC 函数释放打印机设备上下文。
●StartPage:开始打印作业的一个页面,用于指定页面的设置和准备。
StartPage 函数用于告诉打印机驱动程序开始打印一个页面。
以下是 StartPage 函数的声明:
int StartPage(
HDC hdc //设备上下文(DC)的句柄
);
函数说明:
StartPage 函数用于开始打印作业中的一个页面。
返回值:
如果函数调用成功,返回值为大于零的值。如果函数调用失败,返回值为小于零的错误代码。
●EndPage:结束当前打印作业的页面,完成页面的输出。EndPage 函数用于告诉打印机驱动程序当前页面的打印操作已经完成。
以下是 EndPage 函数的声明:
int EndPage(
HDC hdc //设备上下文(DC)的句柄
);
返回值:
如果函数调用成功,返回值为大于零的值。如果函数调用失败,返回值为小于零的错误代码。
●EndDoc:结束打印文档,标识打印作业的结束。EndDoc 函数用于告诉打印机驱动程序当前打印作业的打印操作已经完成。
以下是 EndDoc 函数的声明:
int EndDoc(
HDC hdc //设备上下文(DC)的句柄
);
函数说明:
EndDoc 函数用于结束当前打印作业的打印操作。
返回值:
如果函数调用成功,返回值为大于零的值。如果函数调用失败,返回值为小于零的错误代码。
●DeleteDC:删除打印机设备环境,释放相关资源。DeleteDC 函数用于释放由 CreateDC 或 CreateCompatibleDC 函数创建的设备上下文的资源。
以下是 DeleteDC 函数的声明:
BOOL DeleteDC(
HDC hdc //要删除的设备上下文的句柄
);
函数说明:
DeleteDC 函数用于删除设备上下文(DC)并释放与之关联的资源。
返回值:
如果函数调用成功,返回值为非零值(TRUE)。如果函数调用失败,返回值为零(FALSE)。
●Escape:用于发送特定的打印机命令和查询打印机信息。通过调用 Escape 函数,可以向设备发送特定的命令或请求,并获得相应的信息或效果。
以下是 Escape 函数的声明:
int Escape(
HDC hdc, //设备上下文(DC)的句柄
int nEscape, //指定要执行的操作的命令代码
int cbInput, //指定输入数据的字节数
LPCSTR lpszInData,// 指向输入数据的指针
LPVOID lpOutData//指向输出数据的缓冲区
);
返回值:
如果函数调用成功,返回值为大于或等于零的值,具体取决于所执行操作的命令代码。如果函数调用失败,返回值为小于零的错误代码。
示例用法:
以下示例展示了如何使用 Escape 函数查询打印机的状态:
#include <windows.h>
void QueryPrinterStatus(const wchar_t* printerName) {
HDC hdcPrinter = CreateDCW(L"WINSPOOL", printerName, NULL, NULL);
if (hdcPrinter != NULL) {
// 查询打印机状态
int status = Escape(hdcPrinter, QUERYESCSUPPORT,
sizeof(DWORD), NULL, NULL);
if (status > 0) {
// 打印机支持查询操作
if (status & PRINTER_STATUS_BUSY) {
// 打印机忙碌
printf("Printer is busy.\n");
} else {
// 打印机空闲
printf("Printer is idle.\n");
}
} else {
// 查询操作不受支持或出错
DWORD error = GetLastError();
printf("Failed to query printer status. Error code: %lu\n", error);
}
// 释放打印机设备上下文
DeleteDC(hdcPrinter);
}
}
int main() {
const wchar_t* printerName = L"Printer Name";
QueryPrinterStatus(printerName);
return 0;
}
在上述示例中,我们使用 CreateDCW 函数创建了一个打印机设备上下文(hdcPrinter),然后使用 Escape 函数执行 QUERYESCSUPPORT 命令来查询打印机的状态。如果返回的状态中包含 PRINTER_STATUS_BUSY 标志,则表示打印机正在忙碌中,否则表示打印机处于空闲状态。如果查询操作不受支持或出错,我们使用 GetLastError 函数获取错误代码并进行相应的处理。最后,我们使用 DeleteDC 函数释放打印机设备上下文。
使用打印机设备环境时,可以调用 GDI 函数(例如 CreatePen、CreateBrush、TextOut 等)来设置绘图对象、绘制文本和图形,然后使用 StartPage 和 EndPage 控制打印页面的输出,最后使用 EndDoc 结束打印作业。
需要注意的是,打印机设备环境与显示器设备环境(屏幕上的绘图环境)在功能和使用方式上有一些区别。打印机设备环境主要用于输出到打印机设备,而显示器设备环境主要用于在屏幕上显示图形。因此,在编写打印机输出代码时,需要考虑和处理一些特定于打印机的设置和操作,如页面设置、纸张大小、分辨率等。
13.1.3 第79练:获取显示器及打印机设备信息
/*------------------------------------------------------------------------
079 WIN32 API 每日一练
第79个例子DEVCAPS2.C:获取显示器及打印机设备信息
WM_SETTINGCHANGE消息
CheckMenuItem 函数
EnumPrinters 函数
AppendMenu 函数
OpenPrinter 函数
PrinterProperties 函数
GetMenuString 函数
CreateIC函数
(c) www.bcdaren.com 编程达人
-----------------------------------------------------------------------*/
#include <windows.h>
#include "resource.h"
#include <VersionHelpers.h>
#pragma warning(disable:4996)
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
void DoBasicInfo (HDC, HDC, int, int) ; //基本信息
void DoOtherInfo (HDC, HDC, int, int) ; //其他信息
void DoBitCodedCaps (HDC, HDC, int, int, int) ; //位图信息
//设备其他信息
typedef struct
{
int iMask;//标识符
TCHAR * szDesc;//描述信息
}
BITS ;
#define IDM_DEVMODE 1000 //预定义值
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("DevCaps2");
…(略)
return msg.wParam;
}
LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
static TCHAR szDevice[32], szWindowText[64] ;
static int cxChar, cyChar, nCurrentDevice = IDM_SCREEN,nCurrentInfo = IDM_BASIC ; //基础信息菜单
static DWORD dwNeeded, dwReturned ;
static