摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P471
在 Windows 中使用打印机时,你实际上启动了一系列模块之间复杂的交互过程,包括 GDI32 库模块、打印机设备驱动程序库模块(带 .DRV 后缀的文件)、Windows 后台打印机处理程序(print spooler)和其他有关模块。在开始为打印机编程之前,让我们先来看一下这个过程是如何工作的。
13.1.1 打印和后台处理
应用程序想要开始使用打印机时,它首先调用 CreateDC 或者 PrintDlg 函数得到打印机设备环境的句柄。这使打印机设备驱动程序库模块被调入内存中(如果它还没在内存中的话),并且进行初始化。应用程序然后再调用 StartDoc 函数,这预示开始一个新文档。StartDoc 会由 GDI 模块处理。GDI 模块调用打印机驱动程序中的 Control 函数,通知设备驱动程序做好打印准备。
应用程序通过调用 StartDoc 函数开始打印文档的过程,并以调用 EndDoc 函数结束此过程。以这两个函数调用作为起始和结束的一系列标准 GDI 函数则把文字或图形显示到每个文档页面里。每一个文档页面本身由两个函数调用隔开;调用 StartPage 函数开始一页,调用 EndPage 函数结束这一页。
比如,如果应用程序想在一个页面上绘制一个椭圆,它首先调用 StartDoc 来开始打印作业,然后调用 StartPage 函数来开始新的一页。之后就像在屏幕上绘制椭圆那样调用Ellipse 函数。GDI 模块通常会把应用程序针对打印机设备环境使用的任何 GDI 函数存储到硬盘上的图元文件。这些图元文件的文件名以字符串~EMF(表示“增强型图元文件”)开头,后缀是.TMP。然而,打印机驱动程序也可能跳过这一步,下面很快就会讨论到这一点。
当应用程序完成了定义第一个页面的 GDI 函数调用以后,就调用 EndPage 函数。现在,正在的工作开始了。打印机驱动程序必须把存储在图元文件中的各个绘图命令转化成使用于打印机的输出。这些打印机输出由于定义了一整页的图形,数据量可能会非常大,在打印机没有高级页面合成语言的情况下更是如此。比如,使用长为 11 英寸,宽为 8.5 英寸打印纸时,600 点/英寸(DPI)的激光打印机可能需要超过 4 兆字节(MB)的数据来定义一页图形。
因此,打印机驱动程序通常都采用“分带”技术,将页面分成多个长方形的带。GDI 模块从打印机驱动程序得到每条带的尺寸,然后设置一个大小与这条带相等的裁剪区域,然后对图元文件中每一个绘图函数都调用一次打印机驱动程序中的 Output 函数。这个过程被称为“把图元文件送入设备驱动程序”。对于设备驱动程序定义在页面上的每一条带,GDI 模块都必须把整个图元文件送入设备驱动程序。这个过程完成后,图元文件就可以删掉了。
对于每条带,打印机驱动程序把这些绘图函数转换成相应的需要在打印机上实现的输出。这些输出的格式将根据打印机的不同而不同。对于点阵打印机,输出格式是一个控制系列集合,包括图形系列。(为了帮助构造这个输出,打印机驱动程序可以调用一些 GDI 模块中的辅助例程。)对于支持高级页面合成语言(比如 PostScript)的激光打印机,打印机的输出将会用这种语言表达。
打印机驱动程序把每一条带的打印机输出传给 GDI 模块,而 GDI 模块则把这个输出存到另一个临时文件中。这个文件以字符串 ~SPL 开头,以 .TMP 作为后缀。在整个页完成以后,GDI 模块采用进程间调用(inter-process call)告诉后台打印处理程序新的打印作业已就绪。应用程序然后可以处理下一个页面。当应用程序、GDI 模块和打印机驱动程序之间的交互。
Windows 后台打印处理程序实际上是由下面几个组件构成的:
后台打印处理程序组件 | 描 述 |
---|---|
打印请求程序 | 把数据流转发给打印提供模块 |
本地打印提供程序 | 创建用于本地打印机的后台处理文件 |
网络打印提供程序 | 创建用于网络打印机的后台处理文件 |
打印处理器 | 进行后台反向处理,也就是将进入后台的“与设备无关的”数据转换成适用于目标打印机的形式 |
端口监视程序 | 控制打印机连接的端口 |
语言监视程序 | 控制具备双向通信能力的打印机去设置配置和监视打印机状态 |
这个过程的绝大部分对于应用程序来说都是透明的。从应用程序的角度来说,“打印”只发生在 GDI 模块把所有的打印机输出存储到硬盘文件这个阶段。在那之后,或者甚至是在那之前(如果打印是另一个线程处理的话),应用程序已经可以随便做其他事情了。文档的真正打印不再是应用程序的责任,而是后台打印处理程序的责任了。用户则负责暂停打印作业、改变作业优先级或者在必要情况下取消打印。比起让应用程序实时打印并且等打印完一页再打印下一页的做法,这种安排使得程序可以更快地“打印”。
虽然我已经描述了打印的总体工作方式,但是还有其他一些变体没有提到。一种变体是 Windows 后台打印处理程序对于使用打印机的应用程序来说并不是必需的。用户可以从打印机的属性表中关闭后台打印功能。
为什么用户不想使用 Windows 后台打印处理程序呢?有可能用户已经有了比 Windows 自带的后台处理程序更快的硬件或软件后台处理程序。或者,可能是那台打印机所在的网络上已经有了自带的后台处理程序。通常来说,使用一个后台处理程序要比使用两个快,去掉 Windows 自带的后台处理程序在这些情况下可能会加快打印速度,因为打印机输出不用存储在硬盘上,而是被直接输出到打印机,或者被硬件或软件后台打印处理程序截获。
如果不使用 Windows 后台打印处理程序,GDI 模块就不会把来自设备驱动程序的打印机输出存到文件中。相反地,GDI 自己把这些输出直接传到打印机并口或串口。与由后台打印处理程序完成打印的情况不同的是,由 GDI 进行打印作业有可能会使应用程序的操作受到一定的限制(特别是当应用程序进行打印时),这种限制要到打印完成才会消失。
还有另一种变体:通常,GDI 模块把定义一页的所有函数存在图元文件里,然后针对驱动程序定义的每一条带逐一地把图元文件送入打印机驱动程序。然而,如果打印机驱动程序不要求分带,图元文件就不会被创建,GDI 只是把绘图函数直接传给驱动程序。在另一种变体中,也有可能应用程序要担负起把打印机输出分带的责任。这种做法使得应用程序的打印代码变得复杂,但也因此让 GDI 模块不用生成图元文件。同样的,GDI 模块只是把每一条带的函数传给打印机驱动程序。
现在你也许已经开始意识到,从 Windows 应用程序中打印可能比使用视频显示涉及更多的开销。有几个问题可能发生,特别是在 GDI 模块创建图元文件或者打印机输出文件时硬盘空间不够的情况下。你要么可以把这些问题你汇报给用户并且尝试着处理,要么就远离这些问题。
对于应用程序来说,打印的第一步是获得打印机设备环境。
13.1.2 打印机设备环境
正如你在视频显示器上绘图之前必须得到一个指向设备环境的句柄一样,在打印之前你也必须得到一个打印机设备环境句柄。一旦得到这个句柄,你就可以把这个打印机设备环境句柄作为第一个参数传给各个 GDI 绘图函数(必须已经调用了 StartDoc 函数宣布创建一个新文档,而且也调用了 StartPage 开始新一页),就像使用视频显示设备环境句柄那样。
很多应用程序通过调用 PrintDlg 函数来使用标准打印对话框。(我会在本章后面说明如何使用这个函数。)PrintDlg 让用户在打印之前有机会改变打印机或者指定其他作业属性,还能给应用程序一个打印机设备环境句柄。这个函数能帮应用程序减少一些工作量。但是某些应用程序(比如 Notepad)更喜欢只是得到打印机设备环境而不显示对话框。这个任务要求使用 CreateDC 函数。
在第 5 章中,我们发现可以通过如下代码得到整个视频显示器的设备环境句柄:
hdc = CreateDC(TEXT("DISPLAY"), NULL, NULL, NULL);
你可以使用同样的函数得到打印机设备环境句柄。但是,对于打印机设备环境,CreateDC 的通用代码是:
hdc = CreateDC(NULL, szDeviceName, NULL, pInitiallizationData);
参数 pInitializationData 通常设为 NULL。参数 szDeviceName 指向一个字符串,告诉 Windows 打印机设备的名称。必须找出有哪些打印机可以使用,然后才能指定打印机设备的名称。
一台计算机上可以连接多台打印机,甚至可能有某些应用程序(比如传真软件)模拟打印机。不管连接了几台打印机,只有一台打印机会被认为是“当前”或者“默认”打印机,也就是用户最近一次选用的打印机。一些 Windows 自带的小程序只用这台打印机进行打印。
获取默认打印机的设备环境的方法在这些年里发生了一些变化。现在的标准做法是使用 EnumPrinters 函数。这个函数把连接到系统上的每个打印机的信息添加到一个结构数组中。你可以根据自己想要的打印机信息的详略程序,在这个函数中使用不同的结构。这些结构名是 PRINTER_INFO_x,其中 x 是一个数字。
遗憾的是,使用哪个结构还依赖于该程序运行的平台(Windows98 或者 Windows NT)。下面显示的 GetPrinterDC 函数可以在这两种操作系统上运行。
/*------------------------------------------
GETPRNDC.C -- GetPrinterDC function
-------------------------------------------*/
#include <Windows.h>
HDC GetPrinterDC(void)
{
DWORD dwNeeded, dwReturned;
HDC hdc;
PRINTER_INFO_4 * pinfo4;
PRINTER_INFO_5 * pinfo5;
if (GetVersion() & 0x80000000) // Windows 98
{
EnumPrinters(PRINTER_ENUM_DEFAULT, NULL, 5, NULL,
0, &dwNeeded, &dwReturned);
pinfo5 = (PRINTER_INFO_5 *) malloc(dwNeeded);
EnumPrinters(PRINTER_ENUM_DEFAULT, NULL, 5, (PBYTE)pinfo5,
dwNeeded, &dwNeeded, &dwReturned);
hdc = CreateDC(NULL, pinfo5->pPrinterName, NULL, NULL);
free(pinfo5);
}
else // Windows NT
{
EnumPrinters(PRINTER_ENUM_LOCAL, NULL, 4, NULL,
0, &dwNeeded, &dwReturned);
pinfo4 = (PRINTER_INFO_4 *)malloc(dwNeeded);
EnumPrinters(PRINTER_ENUM_LOCAL, NULL, 4, (PBYTE) pinfo4,
dwNeeded, &dwNeeded, &dwReturned);
hdc = CreateDC(NULL, pinfo4->pPrinterName, NULL, NULL);
free(pinfo4);
}
return hdc;
}
这个函数使用 GetVersion 函数来判断程序是在 Windows 98 还是 Windows NT 上运行。不管是哪个操作系统,这个函数都要调用 EnumPrinters 两次,一次是为了得到所需的结构大小,然后才是真正填充该结构。在 Windows 98 上使用的是 PRINTER_INFO_5 结构,而在 Windows NT 上则是 PRINTER_INFO_4 结构。EnumPrinters 函数的文档中关于这些结构的说明特别提到它们“简单而且极其快捷”(/Platform SDK/Graphics and Multimedia Services/GDI/Printing and Print Spooler/Printing and Print Spooler Regerence/Printing and Print Spooler Functions/EnumPrinters,位于 Examples 部分之前)。
13.1.3 DEVCAPS 程序修订版
第 5 章里的 DEVCAPS1 程序显示了从 GetDeviceCaps 函数中得到的关于食品显示器的基本信息。下面的新版本则显示了关于食品显示器和所有连接到系统上的打印机的更多信息。
/*-------------------------------------------------------------------
DEVCAPS2.C -- Displays Device Capability Information (Version 2)
(c) Charles Petzold, 1998
--------------------------------------------------------------------*/
#pragma warning(disable: 4996)
#include <windows.h>
#include "resource.h"
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");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = szAppName;
wndclass.lpszClassName = szAppName;
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(szAppName, NULL,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
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 PRINTER_INFO_4 * pinfo4;
static PRINTER_INFO_5 * pinfo5;
DWORD i;
HDC hdc, hdcInfo;
HMENU hMenu;
HANDLE hPrint;
PAINTSTRUCT ps;
TEXTMETRIC tm;
switch (message)
{
case WM_CREATE:
hdc = GetDC(hwnd);
SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
GetTextMetrics(hdc, &tm);
cxChar = tm.tmAveCharWidth;
cyChar = tm.tmHeight + tm.tmExternalLeading;
ReleaseDC(hwnd, hdc);
// fall through
case WM_SETTINGCHANGE:
hMenu = GetSubMenu(GetMenu(hwnd), 0);
while (GetMenuItemCount(hMenu) > 1)
DeleteMenu(hMenu, 1, MF_BYPOSITION);
// Get a list of all local and remote printers
//
// First, find out how large an array we need; this
// call will fail, leaving the required size in dwNeeded
//
// Next, allocate space for the info array and fill it
//
// Put the printer names on the menu
if (GetVersion() & 0x80000000) // Windows 98
{
EnumPrinters(PRINTER_ENUM_DEFAULT, NULL, 5, NULL,
0, &dwNeeded, &dwReturned);
pinfo5 = (PRINTER_INFO_5 *)malloc(dwNeeded);
EnumPrinters(PRINTER_ENUM_DEFAULT, NULL, 5, (PBYTE)pinfo5,
dwNeeded, &dwNeeded, &dwReturned);
for (i = 0; i < dwReturned; ++i)
{
AppendMenu(hMenu, (i + 1) % 16 ? 0 : MF_MENUBARBREAK, i + 1,
pinfo5[i].pPrinterName);
}
free(pinfo5);
}
else // Windows NT
{
EnumPrinters(PRINTER_ENUM_LOCAL, NULL, 4, NULL,
0, &dwNeeded, &dwReturned);
pinfo4 = (PRINTER_INFO_4 *)malloc(dwNeeded);
EnumPrinters(PRINTER_ENUM_LOCAL, NULL, 4, (PBYTE)pinfo4,
dwNeeded, &dwNeeded, &dwReturned);
for (i = 0; i < dwReturned; ++i)
{
AppendMenu(hMenu, (i + 1) % 16 ? 0 : MF_MENUBARBREAK, i + 1,
pinfo4[i].pPrinterName);
}
free(pinfo4);
}
AppendMenu(hMenu, MF_SEPARATOR, 0, NULL);
AppendMenu(hMenu, 0, IDM_DEVMODE, TEXT("Properties"));
wParam = IDM_SCREEN;
// fall through
case WM_COMMAND:
hMenu = GetMenu(hwnd);
if (LOWORD(wParam) == IDM_SCREEN || // IDM_SCREEN & Printers
LOWORD(wParam) < IDM_DEVMODE)
{
CheckMenuItem(hMenu, nCurrentDevice, MF_UNCHECKED);
nCurrentDevice = LOWORD(wParam);
CheckMenuItem(hMenu, nCurrentDevice, MF_CHECKED);
}
else if (LOWORD(wParam) == IDM_DEVMODE) // Properties selection
{
GetMenuString(hMenu, nCurrentDevice, szDevice,
sizeof(szDevice) / sizeof(TCHAR), MF_BYCOMMAND);
if (OpenPrinter(szDevice, &hPrint, NULL))
{
PrinterProperties(hwnd, hPrint);
ClosePrinter(hPrint);
}
}
else // info menu items
{
CheckMenuItem(hMenu, nCurrentInfo, MF_UNCHECKED);
nCurrentInfo = LOWORD(wParam);
CheckMenuItem(hMenu, nCurrentInfo, MF_CHECKED);
}
InvalidateRect(hwnd, NULL, TRUE);
return 0;
case WM_INITMENUPOPUP:
if (lParam == 0)
EnableMenuItem(GetMenu(hwnd), IDM_DEVMODE,
nCurrentDevice == IDM_SCREEN ? MF_GRAYED : MF_ENABLED);
return 0;
case WM_PAINT:
lstrcpy(szWindowText, TEXT("Device Capabilities: "));
if (nCurrentDevice == IDM_SCREEN)
{
lstrcpy(szDevice, TEXT("DISPLAY"));
hdcInfo = CreateIC(szDevice, NULL, NULL, NULL);
}
else
{
hMenu = GetMenu(hwnd);
GetMenuString(hMenu, nCurrentDevice, szDevice,
sizeof(szDevice), MF_BYCOMMAND);
hdcInfo = CreateIC(NULL, szDevice, NULL, NULL);
}
lstrcat(szWindowText, szDevice);
SetWindowText(hwnd, szWindowText);
hdc = BeginPaint(hwnd, &ps);
SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
if (hdcInfo)
{
switch (nCurrentInfo)
{
case IDM_BASIC:
DoBasicInfo(hdc, hdcInfo, cxChar, cyChar);
break;
case IDM_OTHER:
DoOtherInfo(hdc, hdcInfo, cxChar, cyChar);
break;
case IDM_CURVE:
case IDM_LINE:
case IDM_POLY:
case IDM_TEXT:
DoBitCodedCaps(hdc, hdcInfo, cxChar, cyChar, nCurrentInfo - IDM_CURVE);
break;
}
DeleteDC(hdcInfo);
}
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
void DoBasicInfo(HDC hdc, HDC hdcInfo, int cxChar, int cyChar)
{
static struct
{
int nIndex;
TCHAR * szDesc;
}
info[] =
{
HORZSIZE, TEXT("HORZSIZE Width in millimeters:"),
VERTSIZE, TEXT("VERTSIZE Height in millimeters:"),
HORZRES, TEXT("HORZRES Width in pixels:"),
VERTRES, TEXT("VERTRES Height in raster lines:"),
BITSPIXEL, TEXT("BITSPIXEL Color bits per pixel:"),
PLANES, TEXT("PLANES Number of color planes:"),
NUMBRUSHES, TEXT("NUMBRUSHES Number of device brushes:"),
NUMPENS, TEXT("NUMPENS Number of device pens:"),
NUMMARKERS, TEXT("NUMMARKERS Number of device markers:"),
NUMFONTS, TEXT("NUMFONTS Number of device fonts:"),
NUMCOLORS, TEXT("NUMCOLORS Number of device colors:"),
PDEVICESIZE, TEXT("PDEVICESIZE Size of device structure:"),
ASPECTX, TEXT("ASPECTX Relative width of pixel:"),
ASPECTY, TEXT("ASPECTY Relative height of pixel:"),
ASPECTXY, TEXT("ASPECTXY Relative diagonal of pixel:"),
LOGPIXELSX, TEXT("LOGPIXELSX Horizontal dots per inch:"),
LOGPIXELSY, TEXT("LOGPIXELSY Vertical dots per inch:"),
SIZEPALETTE, TEXT("SIZEPALETTE Number of palette entries:"),
NUMRESERVED, TEXT("NUMRESERVED Reserved palette entries:"),
COLORRES, TEXT("COLORRES Actual color resolution:"),
PHYSICALWIDTH, TEXT("PHYSICALWIDTH Printer page pixel width:"),
PHYSICALHEIGHT, TEXT("PHYSICALHEIGHT Printer page pixel height:"),
PHYSICALOFFSETX, TEXT("PHYSICALOFFSETX Printer page x offset:"),
PHYSICALOFFSETY, TEXT("PHYSICALOFFSETY Printer page y offset:")
};
int i;
TCHAR szBuffer[80];
for (i = 0; i < sizeof(info) / sizeof(info[0]); i++)
TextOut(hdc, cxChar, (i + 1) * cyChar, szBuffer,
wsprintf(szBuffer, TEXT("%-45s%8d"), info[i].szDesc,
GetDeviceCaps(hdcInfo, info[i].nIndex)));
}
void DoOtherInfo(HDC hdc, HDC hdcInfo, int cxChar, int cyChar)
{
static BITS clip[] =
{
CP_RECTANGLE, TEXT("CP_RECTANGLE Can Clip To Rectangle:")
};
static BITS raster[] =
{
RC_BITBLT, TEXT("RC_BITBLT Capable of simple BitBlt:"),
RC_BANDING, TEXT("RC_BANDING Requires banding support:"),
RC_SCALING, TEXT("RC_SCALING Requires scaling support:"),
RC_BITMAP64, TEXT("RC_BITMAP64 Supports bitmaps >64K:"),
RC_GDI20_OUTPUT, TEXT("RC_GDI20_OUTPUT Has 2.0 output calls:"),
RC_DI_BITMAP, TEXT("RC_DI_BITMAP Supports DIB to memory:"),
RC_PALETTE, TEXT("RC_PALETTE Supports a palette:"),
RC_DIBTODEV, TEXT("RC_DIBTODEV Supports bitmap conversion:"),
RC_BIGFONT, TEXT("RC_BIGFONT Supports fonts >64K:"),
RC_STRETCHBLT, TEXT("RC_STRETCHBLT Supports StretchBlt:"),
RC_FLOODFILL, TEXT("RC_FLOODFILL Supports FloodFill:"),
RC_STRETCHDIB, TEXT("RC_STRETCHDIB Supports StretchDIBits:")
};
static TCHAR * szTech[] = { TEXT("DT_PLOTTER (Vector plotter)"),
TEXT("DT_RASDISPLAY (Raster display)"),
TEXT("DT_RASPRINTER (Raster printer)"),
TEXT("DT_RASCAMERA (Raster camera)"),
TEXT("DT_CHARSTREAM (Character stream)"),
TEXT("DT_METAFILE (Metafile)"),
TEXT("DT_DISPFILE (Display file)") };
int i;
TCHAR szBuffer[80];
TextOut(hdc, cxChar, cyChar, szBuffer,
wsprintf(szBuffer, TEXT("%-24s%04XH"), TEXT("DRIVERVERSION:"),
GetDeviceCaps(hdcInfo, DRIVERVERSION)));
TextOut(hdc, cxChar, 2 * cyChar, szBuffer,
wsprintf(szBuffer, TEXT("%-24s%-40s"), TEXT("TECHNOLOGY:"),
szTech[GetDeviceCaps(hdcInfo, TECHNOLOGY)]));
TextOut(hdc, cxChar, 4 * cyChar, szBuffer,
wsprintf(szBuffer, TEXT("CLIPCAPS (Clipping capabilities)")));
for (i = 0; i < sizeof(clip) / sizeof(clip[0]); i++)
TextOut(hdc, 9 * cxChar, (i + 6) * cyChar, szBuffer,
wsprintf(szBuffer, TEXT("%-45s %3s"), clip[i].szDesc,
GetDeviceCaps(hdcInfo, CLIPCAPS) & clip[i].iMask ?
TEXT("Yes") : TEXT("No")));
TextOut(hdc, cxChar, 8 * cyChar, szBuffer,
wsprintf(szBuffer, TEXT("RASTERCAPS (Raster capabilities)")));
for (i = 0; i < sizeof(raster) / sizeof(raster[0]); i++)
TextOut(hdc, 9 * cxChar, (i + 10) * cyChar, szBuffer,
wsprintf(szBuffer, TEXT("%-45s %3s"), raster[i].szDesc,
GetDeviceCaps(hdcInfo, RASTERCAPS) & raster[i].iMask ?
TEXT("Yes") : TEXT("No")));
}
void DoBitCodedCaps(HDC hdc, HDC hdcInfo, int cxChar, int cyChar, int iType)
{
static BITS curves[] =
{
CC_CIRCLES, TEXT("CC_CIRCLES Can do circles:"),
CC_PIE, TEXT("CC_PIE Can do pie wedges:"),
CC_CHORD, TEXT("CC_CHORD Can do chord arcs:"),
CC_ELLIPSES, TEXT("CC_ELLIPSES Can do ellipses:"),
CC_WIDE, TEXT("CC_WIDE Can do wide borders:"),
CC_STYLED, TEXT("CC_STYLED Can do styled borders:"),
CC_WIDESTYLED, TEXT("CC_WIDESTYLED Can do wide and styled borders:"),
CC_INTERIORS, TEXT("CC_INTERIORS Can do interiors:")
};
static BITS lines[] =
{
LC_POLYLINE, TEXT("LC_POLYLINE Can do polyline:"),
LC_MARKER, TEXT("LC_MARKER Can do markers:"),
LC_POLYMARKER, TEXT("LC_POLYMARKER Can do polymarkers"),
LC_WIDE, TEXT("LC_WIDE Can do wide lines:"),
LC_STYLED, TEXT("LC_STYLED Can do styled lines:"),
LC_WIDESTYLED, TEXT("LC_WIDESTYLED Can do wide and styled lines:"),
LC_INTERIORS, TEXT("LC_INTERIORS Can do interiors:")
};
static BITS poly[] =
{
PC_POLYGON, TEXT("PC_POLYGON Can do alternate fill polygon:"),
PC_RECTANGLE, TEXT("PC_RECTANGLE Can do rectangle:"),
PC_WINDPOLYGON, TEXT("PC_WINDPOLYGON Can do winding number fill polygon:"),
PC_SCANLINE, TEXT("PC_SCANLINE Can do scanlines:"),
PC_WIDE, TEXT("PC_WIDE Can do wide borders:"),
PC_STYLED, TEXT("PC_STYLED Can do styled borders:"),
PC_WIDESTYLED, TEXT("PC_WIDESTYLED Can do wide and styled borders:"),
PC_INTERIORS, TEXT("PC_INTERIORS Can do interiors:")
};
static BITS text[] =
{
TC_OP_CHARACTER,
TEXT("TC_OP_CHARACTER Can do character output precision:"),
TC_OP_STROKE,
TEXT("TC_OP_STROKE Can do stroke output precision:"),
TC_CP_STROKE,
TEXT("TC_CP_STROKE Can do stroke clip precision:"),
TC_CR_90,
TEXT("TC_CP_90 Can do 90 degree character rotation:"),
TC_CR_ANY,
TEXT("TC_CR_ANY Can do any character rotation:"),
TC_SF_X_YINDEP,
TEXT("TC_SF_X_YINDEP Can do scaling independent of X and Y:"),
TC_SA_DOUBLE,
TEXT("TC_SA_DOUBLE Can do doubled character for scaling:"),
TC_SA_INTEGER,
TEXT("TC_SA_INTEGER Can do integer multiples for scaling:"),
TC_SA_CONTIN,
TEXT("TC_SA_CONTIN Can do any multiples for exact scaling:"),
TC_EA_DOUBLE,
TEXT("TC_EA_DOUBLE Can do double weight characters:"),
TC_IA_ABLE, TEXT("TC_IA_ABLE Can do italicizing:"),
TC_UA_ABLE, TEXT("TC_UA_ABLE Can do underlining:"),
TC_SO_ABLE, TEXT("TC_SO_ABLE Can do strikeouts:"),
TC_RA_ABLE, TEXT("TC_RA_ABLE Can do raster fonts:"),
TC_VA_ABLE, TEXT("TC_VA_ABLE Can do vector fonts:")
};
static struct
{
int iIndex;
TCHAR * szTitle;
BITS(*pbits)[];
int iSize;
}
bitinfo[] =
{
CURVECAPS, TEXT("CURVCAPS (Curve Capabilities)"),
(BITS(*)[]) curves, sizeof(curves) / sizeof(curves[0]),
LINECAPS, TEXT("LINECAPS (Line Capabilities)"),
(BITS(*)[]) lines, sizeof(lines) / sizeof(lines[0]),
POLYGONALCAPS, TEXT("POLYGONALCAPS (Polygonal Capabilities)"),
(BITS(*)[]) poly, sizeof(poly) / sizeof(poly[0]),
TEXTCAPS, TEXT("TEXTCAPS (Text Capabilities)"),
(BITS(*)[]) text, sizeof(text) / sizeof(text[0])
};
static TCHAR szBuffer[80];
BITS(*pbits)[] = bitinfo[iType].pbits;
int i, iDevCaps = GetDeviceCaps(hdcInfo, bitinfo[iType].iIndex);
TextOut(hdc, cxChar, cyChar, bitinfo[iType].szTitle,
lstrlen(bitinfo[iType].szTitle));
for (i = 0; i < bitinfo[iType].iSize; i++)
TextOut(hdc, cxChar, (i + 3) * cyChar, szBuffer,
wsprintf(szBuffer, TEXT("%-55s %3s"), (*pbits)[i].szDesc,
iDevCaps & (*pbits)[i].iMask ? TEXT("Yes") : TEXT("No")));
}
DEVCPAS2.RC (excerpts)
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
/
//
// Menu
//
DEVCAPS2 MENU
BEGIN
POPUP "&Device"
BEGIN
MENUITEM "&Screen", IDM_SCREEN
END
POPUP "&Capabilities"
BEGIN
MENUITEM "&Basic Information", IDM_BASIC
MENUITEM "&Other Information", IDM_OTHER
MENUITEM "&Curve Capabilities", IDM_CURVE
MENUITEM "&Line Capabilities", IDM_LINE
MENUITEM "&Polygonal Capabilities", IDM_POLY
MENUITEM "&Text Capabilities", IDM_TEXT
END
END
RESOURCE.H (excerpts)
// Microsoft Visual C++ 生成的包含文件。
// 供 DevCaps2.rc 使用
//
#define IDM_SCREEN 40001
#define IDM_BASIC 40002
#define IDM_OTHER 40003
#define IDM_CURVE 40004
#define IDM_LINE 40005
#define IDM_POLY 40006
#define IDM_TEXT 40007
因为 DEVCAPS2 得到的只是打印机的一些信息,所以你可以从 DEVCAPS2 程序的菜单中选择任何一个打印机,哪怕它们并没有连接到任何一个真实的输出端口。也就是说,如果你要比较不同打印机的性能的话,只需要在 Printers 文件夹中安装多个打印机驱动程序就可以了。
13.1.4 PrinterProperties 函数调用
DEVCAPS2 程序的 Device 菜单中包括一个名为 Properties 的选项。要使用这个选项,首先从 Device 菜单中选择一台打印机,然后再选 Properties 选项。这会弹出一个对话框。这个对话框是从哪儿来的?它是由打印机驱动程序产生的,而且它至少会要求你选择纸张尺寸。大部分打印机驱动程序还会允许你选择“横向”或“纵向”打印模式。纵向模式(通常是默认选项)是打印纸的短边在上方,而横向模式是打印纸的长边在上方。如果你改变了打印模式,这个变化会反映在 DEVCAPS2 程序从 GetDeviceCaps 函数中得到的信息里:水平方向的尺寸和分辨率转变成垂直方向的尺寸和分辨率。彩色绘图仪的属性对话框里设置更加广泛,包括绘图仪上绘图笔的颜色和使用的纸张种类(或透明度)。
所有打印机驱动程序都有一个名为 ExtDeviceMode 的导出函数,这个函数负责产生属性对话框和保存用户输入的信息。一些驱动程序把这些信息存在注册表中属于它们自己的区域里,在下一次 Windows 会话中仍然可以访问这些信息。另一些驱动程序则不这样做。
允许用户选择打印机的 Windows 程序通常只是调用 PrintDlg 函数,我在稍后会说明如何使用这个函数。这个函数非常有用,它负责所有和用户交流的工作,处理用户为打印所做的所有改动。当用户单击 Properties 按钮时 PrintDlg 负责显示属性对话框。
Windows 程序也可以直接调用打印机驱动程序的 ExtDeviceMode 或者 ExtDeviceModePropSheet 函数来显示打印机属性对话框,不过我不建议这么做。通过调用 PrinterProperties 函数来间接生成对话框的做法(如 DEVCAPS2 所示)要好得多。
PrinterProperties 函数要求一个指向打印机对象的句柄,你可以调用 OpenPrinter 函数得到这个句柄。用户取消属性对话框时 PrinterProperties 会返回。随后你可以调用 ClosePrinter 函数关闭这个打印机句柄。DEVCAPS2 是这样做的:
DEVCAPS2 首先得到当前 Devie 菜单中选中的打印机的名称,把它存在一个名为 szDevice 的字符数组里:
GetMenuString(hMenu, nCurrentDevice, szDevice,
sizeof(szDevice) / sizeof(TCHAR), MF_BYCOMMAND)
然后它调用 OpenPrinter 函数得到这个设备的句柄。如果这个函数调用成功,程序就接着调用 PrinterProperties 函数来生成对话框,然后调用 ClosePrinter 函数删除这个设备句柄:
if (OpenPrinter(szDevice, &hPrint, NULL))
{
PrinterProperties(hwnd, hPrint);
ClosePrinter(hPrint);
}
13.1.5 检查 BitBlt 的能力
你可以用 GetDeviceCaps 函数得到页面可打印区域的尺寸和分辨率。(大部分情况下,这个区域不会正好等于打印纸的最大尺寸。)如果你想自己处理缩放,你还可以得到像素的相对宽度和高度。
打印机各种能力的信息大部分是给 GDI 使用的,而不是给应用程序使用的。很多时候如果打印机不能做某件事情,GDI 就会通过模拟的方式来实现。但是有一种打印机的能力是需要某些应用程序来检查的。
使用参数 RASTERCAPS(“光栅性能”)来调用 GetDeviceCaps 函数,得到的返回值中的 RC_BITBLT 位表示了一个特殊的打印机特性。这个位表明该打印设备是否具备位块传送能力。绝大多数点阵、激光和喷墨打印机都具备位块传送能力,但绘图仪则不具备。不能处理位块传送的设备不支持以下这些 GDI 函数:CreateCompatibleDC、CreateCompatibleBitmap、PatBlt、BitBlt、StretchBlt、GaryString、DrawIcon、SetPixel、GetPixel、FloodFill、ExtFloodFill、FillRgn、FrameRgn、InvertRgn、PaintRgn、FillRect、FrameRect 和 InvertRect。这一点是在视频显示器上使用 GDI 函数和在打印机上使用 GDI 函数之间最重要的区别。
13.1.6 最简单的打印程序
我们现在已经做好打印的准备了,让我们尽可能简单地开始打印。实际上,我们的第一个打印程序除了让打印机换页外什么都没做。FORMFEED 程序显示了打印的最低要求。
/*----------------------------------------------
FORMFEED.C -- Advance printer to next page
(c) Charles Petzold, 1998
------------------------------------------------*/
#include <windows.h>
HDC GetPrinterDC(void);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpszCmdLine, int iCmdShow)
{
static DOCINFO di = { sizeof(DOCINFO), TEXT("FormFeed") };
HDC hdcPrint = GetPrinterDC();
if (hdcPrint != NULL)
{
if (StartDoc(hdcPrint, &di) > 0)
if (StartPage(hdcPrint) > 0 && EndPage(hdcPrint) > 0)
EndDoc(hdcPrint);
DeleteDC(hdcPrint);
}
return 0;
}
这个程序需要前面讲到的 GETPRNDC.C 文件。
除了取得一个打印机设备环境(然后再删除它),这个程序只调用了本章前面讨论的四个函数。FORMFEED 首先调用 StartDoc 函数开始一个新文档,接着测试该函数的返回值,只在返回值为正数时再继续:
if (StartDoc(hdcPrint, &di) > 0)
StartDoc 函数的第二个参数是指向 DOCINFO 结构的指针。这个结构的第一个字段表名该结构的大小,第二个字段则是一个值为 “FormFeed”的字符串。在文档被打印或者正等待打印时,这个字符串将作为“文档名”出现在打印机作业队列中。通常这个标志字符串包含了打印程序和要打印的文件的名称。
如果调用 StartDoc 函数成功(返回 值为正数),FORMFEED 程序就调用 StartPage 函数,然后马上接着调用 EndPage 函数。这个调用顺序让打印机换到新的一页。同样我们也要检测返回值:
if (StartPage(hdcPrint) > 0 && EndPage(hdcPrint) > 0)
最后,如果到现在还没有出现任何错误,这个文档就结束了:
EndDoc(hdcPrint);
请注意,EndDoc 函数只在没有任何打印错误的情况下才会被调用。如果其他几个打印函数中有某一个返回了错误代码,GDI 已经自动终止了文档。如果打印机没有在打印,这个错误代码常常会导致打印机重置。检测打印函数的返回值是检查打印是否出错的最简单方法。如果你想要把某个特定的错误报告给用户,你需要调用 GetLastError 函数来得到错误代码。
如果你曾经开发过 MS-DOS 上的换页程序,你就知道 ASCII 码 12(Ctrl-L)对大部分打印机来说是个换页命令。为什么不直接用 C 库函数 open 打开打印机端口然后用 write 函数输出一个 ASCII 码 12 呢?嗯,没有人说你不能这样做。你首先要判断打印机时连接到并口还是串口,然后还要判断是否有另一个程序(比如说后台打印处理程序)正在使用打印机。(你不希望把换页命令输出到别的文档中间,对吧?)最后,你还要决定 ASCII 码 12 是不是目标打印机的换页命令。你知道,12 不是个通用值。实际上 PostScript 中的换页命令就不是 12,而是 showpage 这个词。
总而言之,不要考虑绕过 Windows。应该坚持使用 Windows 的打印函数。