Windows程式开发设计指南(十三)使用印表机
13. 使用印表机
为了处理文字和图形而使用视讯显示器时,装置无关的概念看来非常完美,但对於印表机,装置无关的概念又怎样呢?总的说来,效果也很好。在Windows程式中,用於视讯显示器的GDI函式一样可以在印表纸上列印文字和图形,在以前讨论的与装置无关的许多问题(多数都与平面显示的尺寸、解析度以及颜色数有关)都可以用相同的方法解决。当然,一台印表机不像使用阴极射线管的显示器那么简单,它们使用的是印表纸。它们之间有一些比较大的差异。例如,我们从来不必考虑视讯显示器没有与显示卡连结好,或者显示器出现「萤幕空间不够」的错误,但印表机off line和缺纸却是经常会遇到的问题。
我们也不必担心显示卡不能执行某些图形操作,更不用担心显示卡能否处理图形,因为,如果它不能处理图形,就根本不能使用Windows。但有些印表机不能列印图形(尽管它们能在Windows环境中使用)。绘图机尽管可以列印向量图形,却存在位元图块的传输问题。
以下是其他一些需要考虑的问题:
为了在GDI的其余部分中加入印表机支援功能,Windows提供几个只用於印表机的函式。这些限用在印表机上的函式(StartDoc、EndDoc、StartPage和EndPage)负责将印表机的输出组织列印到纸页上。而一个程式呼叫普通的GDI函式在一张纸上显示文字和图形,和在萤幕上显示的方式一样。
在第十五、十七和十八章有列印点阵图、格式化的文字以及metafile的其他资讯。
列印入门
当您在Windows下使用印表机时,实际上启动了一个包含GDI32动态连结程式库模组、列印驱动程式动态连结模组(带.DRV副档名)、Windows幕後列印程式,以及有用到的其他相关模组。在写印表机列印程式之前,让我们先看一看这个程序是如何进行的。
列印和背景处理
当应用程式要使用印表机时,它首先使用CreateDC或PrintDlg来取得指向印表机装置内容的代号,於是使得印表机装置驱动程式动态连结程式库模组被载入到记忆体(如果还没有载入记忆体的话)并自己进行初始化。然後,程式呼叫StartDoc函式,通知说一个新文件开始了。StartDoc函式是由GDI模组来处理的,GDI模组呼叫印表机装置驱动程式中的Control函式告诉装置驱动程式准备进行列印。
列印一个文件的程序以StartDoc呼叫开始,以EndDoc呼叫结束。这两个呼叫对於在文件页面上书写文字或者绘制图形的GDI命令来说,其作用就像分隔页面的书挡一样。每页本身是这样来划清界限的:呼叫StartPage来开始一页,呼叫EndPage来结束该页。
例如,如果应用程式想在一页纸上画出一个椭圆,它首先呼叫StartDoc开始列印任务,然後再呼叫StartPage通知这是新的一页,接著呼叫Ellipse,正如同在萤幕上画一个椭圆一样。GDI模组通常将程式对印表机装置内容做出的GDI呼叫储存在磁片上的metafile中,该档案名以字串~EMF(代表「增强型metafile」)开始,且以.TMP为副档名。然而,我在这里应该指出,印表机驱动程式可能会跳过这一步骤。
当绘制第一页的GDI呼叫结束时,应用程式呼叫EndPage。现在,真正的工作开始了。印表机驱动程式必须把存放在metafile中的各种绘图命令翻译成印表机输出资料。绘制一页图形所需的印表机输出资料量可能非常大,特别是当印表机没有高级页面制作语言时,更是如此。例如,一台每英寸600点且使用8.5×11英寸印表纸的雷射印表机,如果要定义一个图形页,可能需要4百万以上位元组的资料。
为此,印表机驱动程式经常使用一种称作「列印分带」的技术将一页分成若干称为「输出带」的矩形。GDI模组从印表机驱动程式取得每个输出带的大小,然後设定一个与目前要处理的输出带相等的剪裁区,并为metafile中的每个绘图函式呼叫印表机装置驱动程式的Output函式,这个程序叫做「将metafile输出到装置驱动程式」。对装置驱动程式所定义的页面上的每个输出带,GDI模组必须将整个metafile「输出到」装置驱动程式。这个程序完成以後,该metafile就可以删除了。
对每个输出带,装置驱动程式将这些绘图函式转换为在印表机上列印这些图形所需要的输出资料。这种输出资料的格式是依照印表机的特性而异的。对点阵印表机,它将是包括图形序列在内的一系列控制命令序列的集合(印表机驱动程式也能呼叫在GDI模组中的各种「helper」辅助常式,用来协助这种输出的构造)。对於带有高阶页面制作语言(如PostScript)的雷射印表机,印表机将用这种语言进行输出。
列印驱动程式将列印输出的每个输出带传送到GDI模组。随後,GDI模组将该列印输出存入另一个暂存档案中,该暂存档案名以字串~SPL开始,带有.TMP副档名。当处理好整页之後,GDI模组对幕後列印程式进行一个程序间呼叫,通知它一个新的列印页已经准备好了。然後,应用程式就转向处理下一页。当应用程式处理完所有要列印的输出页後,它就呼叫EndDoc发出一个信号,表示列印作业已经完成。图13-1显示了应用程式、GDI模组和列印驱动程式的交互作用程序。
Windows幕後列印程式实际上是几个元件的一种组合(见表13-1)。
列印伫列程式可以减轻应用程式的列印负担。 Windows在启动时就载入列印伫列程式,因此,当应用程式开始列印时,它已经是活动的了。当程式列印一个档案时,GDI模组会建立包含列印输出资料的档案。幕後列印程式的任务是将这些档案发往印表机。GDI模组发出一个讯息来通知它一个新的列印作业开始,然後它开始读档案并将档案直接传送到印表机。为了传送这些档案,列印伫列程式依照印表机所连结的并列埠或串列埠使用各种不同的通信函式。在列印伫列程式向印表机发送档案的操作完成後,它就将包含输出资料的暂存档案删除。这个交互作用过程如图13-2所示。
这个程序的大部分对应用程式来说是透明的。从应用程式的角度来看,「列印」只发生在GDI模组将所有列印输出资料储存到磁片档案中的时候,在这之後(如果列印是由第二个执行绪来操作的,甚至可以在这之前)应用程式可以自由地进行其他操作。真正的档案列印操作成了幕後列印程式的任务,而不是应用程式的任务。通过印表机档案夹,使用者可以暂停列印作业、改变作业的优先顺序或取消列印作业。这种管理方式使应用程式能更快地将列印资料以即时方式列印,况且这样必须等到列印完一页後才能处理下一页。
我们已经描述了一般的列印原理,但还有一些例外情况。其中之一是Windows程式要使用印表机时,并非一定需要幕後列印程式。使用者可以在印表机属性表格的详细资料属性页中关闭印表机的背景操作。
为什么使用者希望不使用背景操作呢?因为使用者可能使用了比Windows列印伫列程式更快的硬体或软体幕後列印程式,也可能是印表机在一个自身带有列印伫列器的网路上使用。一般的规则是,使用一个列印伫列程式比使用两个列印伫列程式更快。去掉Windows幕後列印程式可以加快列印速度,因为列印输出资料不必储存在硬碟上,而可以直接输出到印表机,并被外部的硬体列印伫列器或软体的幕後列印程式所接收。
如果没有启用Windows列印伫列程式,GDI模组就不把来自装置驱动程式的列印输出资料存入档案中,而是将这些输出资料直接输出到列印输出埠。与列印伫列程式进行的列印不同,GDI进行的列印一定会让应用程式暂停执行一段时间(特别是进行列印中的程式)直到列印完成。
还有另一个例外。通常,GDI模组将定义一页所需的所有函式存入一个增强型metafile中,然後替驱动程式定义的每个列印输出带输出一遍该metafile到列印驱动程式中。然而,如果列印驱动程式不需要列印分带的话,就不会建立这个metafile;GDI只需简单地将绘图函式直接送往驱动程式。进一步的变化是,应用程式也可能得承担起对列印输出资料进行列印分带的责任,这就使得应用程式中的列印程式码更加复杂了,但却免去了GDI模组建立metafile的麻烦。这样,GDI只需简单地为每个输出带将函式传到列印驱动程式。
或许您现在已经发现了从一个Windows应用程式进行列印操作要比使用视讯显示器的负担更大,这样可能出现一些问题-特别是,如果GDI模组在建立metafile或列印输出档案时耗尽了磁碟空间。您可以更关切这些问题,并尝试著处理这些问题并告知使用者,或者您当然也可以置之不理。
对於一个应用程式,列印文件的第一步就是如何取得印表机装置的内容。
印表机装置内容
正如在视讯显示器上绘图前需要得到装置内容代号一样,在列印之前,使用者必须取得一个印表机装置内容代号。一旦有了这个代号(并为建立一个新文件呼叫了StartDoc以及呼叫StartPage开始一页),就可以用与使用视讯显示装置内容代号相同的方法来使用印表机装置内容代号,该代号即为各种GDI呼叫的第一个参数。
大多数应用程式经由呼叫PrintDlg函式打开一个标准的列印对话方块(本章後面会展示该函式的用法)。这个函式还为使用者提供了一个在列印之前改变印表机或者指定其他特性的机会。然後,它将印表机装置内容代号交给应用程式。该函式能够省下应用程式的一些工作。然而,某些应用程式(例如Notepad)仅需要取得印表机装置内容,而不需要那个对话方块。要做到这一点,需要呼叫CreateDC函式。
在第五章中,您已知道如何通过如下的呼叫来为整个视讯显示器取得指向装置内容的代号:
hdc = CreateDC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;
您也可以使用该函式来取得印表机装置内容代号。然而,对印表机装置内容,CreateDC的一般语法为:
hdc = CreateDC (NULL, szDeviceName, NULL, pInitializationData) ;
pInitializationData参数一般被设为NULL。szDeviceName参数指向一个字串,以告诉Windows印表机设备的名称。在设定设备名称之前,您必须知道有哪些印表机可用。
一个系统可能有不只一台连结著的印表机,甚至可以有其他程式,如传真软体,将自己伪装成印表机。不论连结的印表机有多少台,都只能有一台被认为是「目前的印表机」或者「内定印表机」,这是使用者最近一次选择的印表机。许多小型的Windows程式只使用内定印表机来进行列印。
取得内定印表机装置内容的方式不断在改变。目前,标准的方法是使用EnumPrinters函式来获得。该函式填入一个包含每个连结著的印表机资讯的阵列结构。根据所需的细节层次,您还可以选择几种结构之一作为该函式的参数。这些结构的名称为PRINTER_INFO_x,x是一个数字。
不幸的是,所使用的函式还取决於您的程式是在Windows 98上执行还是在Windows NT上执行。程式13-1展示了GetPrinterDC函式在两种作业系统上工作的用法。
程式13-1 GETPRNDCGETPRNDC.C/*---------------------------------- 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 = 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 = 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 Reference/Printing and Print Spooler Functions/EnumPrinters,范例小节的前面)中有说明,它们是「容易而快速」的。
修改後的DEVCAPS程式
第五章的DEVCAPS1程式只显示了从GetDeviceCaps函式获得的关於视讯显示的基本资讯。程式13-2所示的新版本显示了关於视讯显示和连结到系统之所有印表机的更多资讯。
程式13-2 DEVCAPS2DEVCAPS2.C/*-------------------------------------- DEVCAPS2.C --Displays Device Capability Information (Version 2) (c) Charles Petzold, 1998---------------------------------------*/#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 1000int 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 menuif (GetVersion () & 0x80000000) // Windows 98 { EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 5, NULL, 0, &dwNeeded, &dwReturned) ; pinfo5 = malloc (dwNeeded) ; EnumPrinters (PRINTER_ENUM_LOCAL, 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 = 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_SCREEMF_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("PDEVICESIZESize 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_CHARACTERCan 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, EXT ("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++) extOut (hdc, cxChar, (i + 3) * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%-55s %3s"), (*pbits)[i].szDesc, iDevCaps & (*pbits)[i].iMask ? TEXT ("Yes") : TEXT ("No")));}
DEVCAPS2.RC (摘录)//Microsoft Developer Studio generated resource script.#include "resource.h"#include "afxres.h"/////////////////////////////////////////////////////////////////////////////// MenuDEVCAPS2 MENU DISCARDABLE BEGIN POPUP "&Device" BEGIN MENUITEM "&Screen",IDM_SCREEN, CHECKED 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 ENDEND
RESOURCE.H (摘录)// Microsoft Developer Studio generated include file.// Used by 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的功能表中选择所需印表机。如果使用者想比较不同印表机的功能,可以先用印表机档案夹增加各种列印驱动程式。
PrinterProperties呼叫
DEVCAPS2的「Device」功能表中上还有一个称为「Properties」的选项。要使用这个选项,首先得从 Device功能表中选择一个印表机,然後再选择 Properties,这时弹出一个对话方块。对话方块从何而来呢?它由印表机驱动程式呼叫,而且至少还让使用者选择纸的尺寸。大多数印表机驱动也可以让使用者在「直印(portrait)」或「横印(landscape)」模式中进行选择。在直印模式(一般为内定模式)下,纸的短边是顶部。在横印模式下,纸的长边是顶部。如果改变该模式,则所作的改变将在DEVCAPS2程式从GetDeviceCaps函式取得的资讯中反应出来:水平尺寸和解析度将与垂直尺寸和解析度交换。彩色绘图机的「Properties」对话方块内容十分广泛,它们要求使用者输入安装在绘图机上之画笔的颜色和使用之绘图纸(或透明胶片)的型号。
所有印表机驱动程式都包含一个称为ExtDeviceMode的输出函式,它呼叫对话方块并储存使用者输入的资讯。有些印表机驱动程式也将这些资讯储存在系统登录的自己拥有的部分中,有些则不然。那些储存资讯的印表机驱动程式在下次执行Windows时将存取该资讯。
允许使用者选择印表机的Windows程式通常只呼叫PrintDlg(本章後面我会展示用法)。这个有用的函式在准备列印时负责和使用者之间所有的通讯工作,并负责处理使用者要求的所有改变。当使用者单击「Properties」按钮时,PrintDlg还会启动属性表格对话方块。
程式还可以通过直接呼叫印表机驱动程式的ExtDeviceMode或ExtDeveModePropSheet函式,来显示印表机的属性对话方块,然而,我不鼓励您这样做。像DEVCAPS2那样,透过呼叫PrinterProperties来启动对话方块会好得多。
PrinterProperties要求印表机物件的代号,您可以通过OpenPrinter函式来得到。当使用者取消属性表格对话方块时,PrinterProperties传回,然後使用者通过呼叫ClosePrinter,释放印表机代号。DEVCAPS2就是这样做到这一点的。
程式首先取得刚刚在Device功能表中选择的印表机名称,并将其存入一个名为szDevice的字元阵列中。
GetMenuString (hMenu, nCurrentDevice, szDevice, sizeof (szDevice) / sizeof (TCHAR), MF_BYCOMMAND) ;
然後,使用OpenPrinter获得该设备的代号。如果呼叫成功,那么程式接著呼叫PrinterProperties启动对话方块,然後呼叫ClosePrinter释放设备代号:
if (OpenPrinter (szDevice, &hPrint, NULL)){ PrinterProperties (hwnd, hPrint) ; ClosePrinter (hPrint) ;}
检查BitBlt支援
您可以用GetDeviceCaps函式来取得页中可列印区的尺寸和解析度(通常,该区域不会与整张纸的大小相同)。如果使用者想自己进行缩放操作,也可以获得相对的图素宽度和高度。
印表机能力的大多数资讯是用於GDI而不是应用程式的。通常,在印表机不能做某件事时,GDI会模拟出那项功能。然而,这是应用程式应该事先检查的。
以RASTERCAPS(「位元映射支援」)参数呼叫GetDeviceCaps,它传回的RC_BITBLT位元包含了另一个重要的印表机特性,该位元标示设备是否能进行位元块传送。大多数点阵印表机、雷射印表机和喷墨印表机都能进行位元块传送,而大多数绘图机却不能。不能处理位元块传送的设备不支援下列GDI函式:CreateCompatibleDC、CreateCompatibleBitmap、PatBlt、BitBlt、StretchBlt、GrayString、DrawIcon、SetPixel、GetPixel、FloodFill、ExtFloodFill、FillRgn、FrameRgn、InvertRgn、PaintRgn、FillRect、FrameRect和InvertRect。这是在视讯显示器上使用GDI函式与在印表机上使用它们的唯一重要区别。
最简单的列印程式
现在可以开始列印了,我们尽可能简单地开始。事实上,我们的第一个程式只是让印表机送纸而已。程式13-3的FORMFEED程式,展示了列印所需的最小需求。
程式13-3 FORMFEEDFORMFEED.C/*----------------------------------- FORMFEED.C -- Advances 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 ;}
这个程式也需要前面程式13-1中的GETPRNDC.C档案。
除了取得印表机装置内容(然後再删除它)外,程式只呼叫了我们在本章前面讨论过的四个列印函式。FORMFEED首先呼叫StartDoc开始一个新的档案,它测试从StartDoc传回的值,只有传回值是正数时,才继续下去:
if (StartDoc (hdcPrint, &di) > 0)
StartDoc的第二个参数是指向DOCINFO结构的指标。该结构在第一个栏位包含了结构的大小,在第二个栏位包含了字串「FormFeed」。当档案正在被列印或者在等待列印时,这个字串将出现在印表机任务伫列中的「Document Name」列中。通常,该字串包含进行列印的应用程式名称和被列印的档案名称。
如果StartDoc成功(由一个正的传回值表示),那么FORMFEED呼叫StartPage,紧接著立即呼叫EndPage。这一程序将印表机推进到新的一页,再次对传回值进行测试:
if (StartPage (hdcPrint) > 0 && EndPage (hdcPrint) > 0)
最後,如果不出错,文件就结束:
EndDoc (hdcPrint) ;
要注意的是,只有当没出错时,才呼叫EndDoc函式。如果其他列印函式中的某一个传回错误代码,那么GDI实际上已经中断了文件的列印。如果印表机目前未列印,这种错误代码通常会使印表机重新设定。测试列印函式的传回值是检测错误的最简单方法。如果您想向使用者报告错误,就必须呼叫GetLastError来确定错误。
如果您写过MS-DOS下的简单利用印表机送纸的程式,就应该知道,对於大多数印表机,ASCII码12启动送纸。为什么不简单地使用C的程式库函式open,然後用write输出ASCII码12呢?当然,您完全可以这么做,但是必须确定印表机连结的是串列埠还是并列埠。然後您还要确定另外的程式(例如,列印伫列程式)是不是正在使用印表机。您并不希望在文件列印到一半时被别的程式把正在列印的那张纸送出印表机,对不对?最後,您还必须确定ASCII码12是不是所连结印表机的送纸字元,因为并非所有印表机的送纸字元都是12。事实上,在PostScript中的送纸命令便不是12,而是单字showpage。
简单地说,不要试图直接绕过Windows;而应该坚持在列印中使用Windows函式。
列印图形和文字
在一个Windows程式中,列印所需的额外负担通常比FORMFEED程式高得多,而且还要用GDI函式来实际列印一些东西。我们来写个列印一页文字和图形的程式,采用FORMFEED程式中的方法,并加入一些新的东西。该程式将有三个版本PRINT1、PRINT2和PRINT3。为避免程式码重复,每个程式都用前面所示的GETPRNDC.C档案和PRINT.C档案中的函式,如程式13-4所示。
程式13-4 PRINTPRINT.C/*------------------------------------ PRINT.C -- Common routines for Print1, Print2, and Print3--------------------------------------*/#include <windows.h>LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;BOOL PrintMyPage (HWND) ;extern HINSTANCE hInst ;extern TCHAR szAppName[] ;extern TCHAR szCaption[] ;int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow){ 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 = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hInst = hInstance ; hwnd = CreateWindow (szAppName, szCaption, 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 ;}void PageGDICalls (HDC hdcPrn, int cxPage, int cyPage){ static TCHAR szTextStr[] = TEXT ("Hello, Printer!") ; Rectangle (hdcPrn, 0, 0, cxPage, cyPage) ; MoveToEx (hdcPrn, 0, 0, NULL) ; LineTo (hdcPrn, cxPage, cyPage) ; MoveToEx (hdcPrn, cxPage, 0, NULL) ; LineTo (hdcPrn, 0, cyPage) ; SaveDC (hdcPrn) ; SetMapMode (hdcPrn, MM_ISOTROPIC) ; SetWindowExtEx (hdcPrn, 1000, 1000, NULL) ; SetViewportExtEx (hdcPrn, cxPage / 2, -cyPage / 2, NULL) ; SetViewportOrgEx (hdcPrn, cxPage / 2, cyPage / 2, NULL) ; Ellipse (hdcPrn, -500, 500, 500, -500) ; SetTextAlign (hdcPrn, TA_BASELINE | TA_CENTER) ; TextOut (hdcPrn, 0, 0, szTextStr, lstrlen (szTextStr)) ; RestoreDC (hdcPrn, -1) ;}LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam){ static int cxClient, cyClient ; HDC hdc ; HMENU hMenu ; PAINTSTRUCT ps ; switch (message) { case WM_CREATE: hMenu = GetSystemMenu (hwnd, FALSE) ; AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenu, 0, 1, TEXT ("&Print")) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_SYSCOMMAND: if (wParam == 1) { if (!PrintMyPage (hwnd)) MessageBox (hwnd, TEXT ("Could not print page!"), szAppName, MB_OK | MB_ICONEXCLAMATION) ; return 0 ; } break ; caseWM_PAINT : hdc = BeginPaint (hwnd, &ps) ; PageGDICalls (hdc, cxClient, cyClient) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ;}
PRINT.C包括函式WinMain、WndProc以及一个称为PageGDICalls的函式。PageGDICalls函式接收印表机装置内容代号和两个包含列印页面宽度及高度的变数。这个函式还负责画一个包围整个页面的矩形,有两条对角线,页中间有一个椭圆(其直径是印表机高度和宽度中较小的那个的一半),文字「Hello, Printer!」位於椭圆的中间。
处理WM_CREATE讯息时,WndProc将一个「Print」选项加到系统功能表上。选择该选项将呼叫PrintMyPage,此函式的功能在程式的三个版本中将不断增强。当列印成功时,PrintMyPage传回TRUE值,如果遇到错误时则传回FALSE。如果PrintMyPage传回FALSE,WndProc就会显示一个讯息方块以告知使用者发生了错误。
列印的基本程序
列印程式的第一个版本是PRINT1,见程式13-5。经编译後即可执行此程式,然後从系统功能表中选择「Print」。接著,GDI将必要的印表机输出储存在一个暂存档案中,然後列印伫列程式将它发送给印表机。
程式13-5 PRINT1PRINT1.C/*--------------------------------- PRINT1.C -- Bare Bones Printing (c) Charles Petzold, 1998----------------------------------*/#include <windows.h>HDC GetPrinterDC (void) ; // in GETPRNDC.Cvoid PageGDICalls (HDC, int, int) ; // in PRINT.CHINSTANCE hInst ;TCHAR szAppName[] = TEXT ("Print1") ;TCHAR szCaption[] = TEXT ("Print Program 1") ;BOOL PrintMyPage (HWND hwnd){ static DOCINFO di = { sizeof (DOCINFO), TEXT ("Print1: Printing") } ;BOOL bSuccess = TRUE ; HDC hdcPrn ; int xPage, yPage ; if (NULL == (hdcPrn = GetPrinterDC ())) return FALSE ; xPage = GetDeviceCaps (hdcPrn, HORZRES) ; yPage = GetDeviceCaps (hdcPrn, VERTRES) ; if (StartDoc (hdcPrn, &di) > 0) { if (StartPage (hdcPrn) > 0) { PageGDICalls (hdcPrn, xPage, yPage) ; if (EndPage (hdcPrn) > 0) EndDoc (hdcPrn) ; else bSuccess = FALSE ; } } else bSuccess = FALSE ; DeleteDC (hdcPrn) ; return bSuccess ;}
我们来看看PRINT1.C中的程式码。如果PrintMyPage不能取得印表机的装置内容代号,它就传回FALSE,并且WndProc显示讯息方块指出错误。如果函式成功取得了装置内容代号,它就通过呼叫GetDeviceCaps来确定页面的水平和垂直大小(以图素为单位)。
xPage = GetDeviceCaps (hdcPrn, HORZRES) ;yPage = GetDeviceCaps (hdcPrn, VERTRES) ;
这不是纸的全部大小,只是纸的可列印区域。呼叫後,除了PRINT1在StartPage和EndPage呼叫之间呼叫PageGDICalls,PRINT1的PrintMyPage函式中的程式码在结构上与FORMFEED中的程式码相同。仅当呼叫StartDoc、StartPage和EndPage都成功时,PRINT1才呼叫EndDoc列印函式。
使用放弃程序来取消列印
对於大型文件,程式应该提供使用者在应用程式列印期间取消列印任务的便利性。也许使用者只要列印文件中的一页,而不是列印全部的537页。应该要能在印完全部的537页之前纠正这个错误。
在一个程式内取消一个列印任务需要一种被称为「放弃程序」的技术。放弃程序在程式中只是个较小的输出函式,使用者可以使用SetAbortProc函式将该函式的位址传给Windows。然後GDI在列印时,重复呼叫该程序,不断地问:「我是否应该继续列印?」
我们看看将放弃程序加到列印处理程式中去需要些什么,然後检查一些旁枝末节。放弃程序一般命名为AbortProc,其形式为:
BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode){//其他行程式}
列印前,您必须通过呼叫SetAbortProc来登记放弃程序:
SetAbortProc (hdcPrn, AbortProc) ;
在呼叫StartDoc前呼叫上面的函式,列印完成後不必清除放弃程序。
在处理EndPage呼叫时(亦即,在将metafile放入装置驱动程式并建立临时列印档案时),GDI常常呼叫放弃程序。参数hdcPrn是印表机装置内容代号。如果一切正常,iCode参数是0,如果GDI模组在生成暂存档案时耗尽了磁碟空间,iCode就是SP_OUTOFDISK。
如果列印作业继续,那么AbortProc必须传回TRUE(非零);如果列印作业异常结束,就传回FALSE(零)。放弃程序可以被简化为如下所示的形式:
BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode){ MSG msg ; while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return TRUE ;}
这个函式看起来有点特殊,其实它看起来像是讯息回圈。使用者会注意到,这个「讯息回圈」呼叫PeekMessage而不是GetMessage。我在第五章的RANDRECT程式中讨论过PeekMessage。应该还记得,PeekMessage将会控制权返回给程式,而不管程式的讯息伫列中是否有讯息存在。
只要PeekMessage传回TRUE,那么AbortProc函式中的讯息回圈就重复呼叫PeekMessage。TRUE值表示PeekMessage已经找到一个讯息,该讯息可以通过TranslateMessage和DispatchMessage发送到程式的视窗讯息处理程式。若程式的讯息伫列中没有讯息,则PeekMessage的传回值为FALSE,因此AbortProc将控制权返回给Windows。
Windows如何使用AbortProc
当程式进行列印时,大部分工作发生在要呼叫EndPage时。呼叫EndPage前,程式每呼叫一次GDI绘图函式,GDI模组只是简单地将另一个记录加到磁片上的metafile中。当GDI得到EndPage後,对列印页中由装置驱动程式定义的每个输出带,GDI都将该metafile送入装置驱动程式中。然後,GDI将印表机驱动程式建立的列印输出储存到一个档案中。如果没有启用幕後列印,那么GDI模组必须自动将该列印输出写入印表机。
在EndPage呼叫期间,GDI模组呼叫您设定的放弃程序。通常iCode参数为0,但如果由於存在未列印的其他暂存档案,而造成GDI执行时磁碟空间不够,iCode参数就为SP_OUTOFDISK(通常您不会检查这个值,但是如果愿意,您可以进行检查)。放弃程序随後进入PeekMessage回圈从自己的讯息伫列中找寻讯息。
如果在程式的讯息伫列中没有讯息,PeekMessage会传回FALSE,然後放弃程序跳出它的讯息回圈并给GDI模组传回一个TRUE值,指示列印应该继续进行。然後GDI模组继续处理EndPage呼叫。
如果有错误发生,那么GDI将中止列印程序,这样,放弃程序的主要目的是允许使用者取消列印。为此,我们还需要一个显示「Cancel」按钮的对话方块,让我们采用两个独立的步骤。首先,我们在建立PRINT2程式时增加一个放弃程序,然後在PRINT3中增加一个带有「Cancel」按钮的对话方块,使放弃程序可用。
实作放弃程序
现在快速复习一下放弃程序的机制。可以定义一个如下所示的放弃程序:
BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode){ MSG msg ; while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return TRUE ;}
当您想列印什么时,使用下面的呼叫将指向放弃程序的指标传给Windows:
SetAbortProc (hdcPrn, AbortProc) ;
在呼叫StartDoc之前进行这个呼叫就行了。
不过,事情没有这么简单。我们忽视了AbortProc程序中PeekMessage回圈这个问题,它是个很大的问题。只有在程式处於列印程序时,AbortProc程序才会被呼叫。如果在AbortProc中找到一个讯息并把它传送给视窗讯息处理程式,就会发生一些非常令人讨厌的事情:使用者可以从功能表中再次选择「Print」,但程式已经处於列印常式之中。程式在列印前一个档案的同时,使用者也可以把一个新档案载入到程式里。使用者甚至可以退出程式!如果这种情况发生了,所有使用者程式的视窗都将被清除。当列印常式执行结束时,除了退到不再有效的视窗常式之外,您无处可去。
这种东西会把人搞得晕头转向,而我们的程式对此并未做任何准备。正是由於这个原因,当设定放弃程序时,首先应禁止程式的视窗接受输入,使它不能接受键盘和滑鼠输入。可以用以下的函式完成这项工作:
EnableWindow (hwnd, FALSE) ;
它可以禁止键盘和滑鼠的输入进入讯息伫列。因此在列印程序中,使用者不能对程式做任何工作。当列印完成时,应重新允许视窗接受输入:
EnableWindow (hwnd, TRUE) ;
您可能要问,既然没有键盘或滑鼠讯息进入讯息伫列,为什么我们还要进行AbortProc中的TranslateMessage和DispatchMessage呼叫呢?实际上并不一定非得需要TranslateMessage,但是,我们必须使用DispatchMessage,处理WM_PAINT讯息进入讯息伫列中的情况。如果WM_PAINT讯息没有得到视窗讯息处理程式中的BeginPaint和EndPaint的适当处理,由於PeekMessage不再传回FALSE,该讯息就会滞留在伫列中并且妨碍工作。
当列印期间阻止视窗处理输入讯息时,您的程式不会进行显示输出。但使用者可以切换到其他程式,并在那里进行其他工作,而幕後列印程式则能继续将输出档案送到印表机。
程式13-6所示的PRINT2程式在PRINT1中增加了一个放弃程序和必要的支援-呼叫AbortProc函式并呼叫EnableWindow两次(第一次阻止视窗接受输入讯息,第二次启用视窗)。
程式13-6 PRINT2PRINT2.C/*--------------------------------- PRINT2.C --Printing with Abort Procedure (c) Charles Petzold, 1998----------------------------------*/#include <windows.h>HDC GetPrinterDC (void) ; // in GETPRNDC.Cvoid PageGDICalls (HDC, int, int) ; // in PRINT.CHINSTANCE hInst ;TCHAR szAppName[] = TEXT ("Print2") ;TCHAR szCaption[] = TEXT ("Print Program 2 (Abort Procedure)") ;BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode){ MSG msg ; while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return TRUE ;}BOOL PrintMyPage (HWND hwnd){ static DOCINFO di = { sizeof (DOCINFO), TEXT ("Print2: Printing") } ; BOOL bSuccess = TRUE ; HDC hdcPrn ; short xPage, yPage ; if (NULL == (hdcPrn = GetPrinterDC ())) return FALSE ; xPage = GetDeviceCaps (hdcPrn, HORZRES) ; yPage = GetDeviceCaps (hdcPrn, VERTRES) ; EnableWindow (hwnd, FALSE) ; SetAbortProc (hdcPrn, AbortProc) ; if (StartDoc (hdcPrn, &di) > 0) { if (StartPage (hdcPrn) > 0) { PageGDICalls (hdcPrn, xPage, yPage) ; if (EndPage (hdcPrn) > 0) EndDoc (hdcPrn) ; else bSuccess = FALSE ; } } else bSuccess = FALSE ; EnableWindow (hwnd, TRUE) ; DeleteDC (hdcPrn) ; return bSuccess ;}
增加列印对话方块
PRINT2还不能令人十分满意。首先,这个程式没有直接指示出何时开始列印和何时结束列印。只有将滑鼠指向程式并且发现它没有反应时,才能断定它仍然在处理PrintMyPage常式。PRINT2在进行背景处理时也没有给使用者提供取消列印作业的机会。
您可能注意到,大多数Windows程式都为使用者提供了一个取消目前正在进行列印操作的机会。一个小的对话方块出现在萤幕上,它包括一些文字和「Cancel」按键。在GDI将列印输出储存到磁片档案或(如果停用列印伫列程式)印表机正在列印的整个期间,程式都显示这个对话方块。它是一个非系统模态对话方块,您必须提供对话程序。
通常称这个对话方块为「放弃对话方块」,称这种对话程序为「放弃对话程序」。为了更清楚地把它和「放弃程序」区别开来,我们称这种对话程序为「列印对话程序」。放弃程序(名为AbortProc)和列印对话程序(将命名为PrintDlgProc)是两个不同的输出函式。如果想以一种专业的Windows式列印方式进行列印工作,就必须拥有这两个函式。
这两个函式的交互作用方式如下:AbortProc中的PeekMessage回圈得被修改,以便将非系统模态对话方块的讯息发送给对话方块视窗讯息处理程式。PrintDlgProc必须处理WM_COMMAND讯息,以检查「Cancel」按钮的状态。如果「Cancel」钮被按下,就将一个叫做bUserAbort的整体变数设为TRUE。AbortProc传回的值正好和bUserAbort相反。您可能还记得,如果AbortProc传回TRUE会继续列印,传回FALSE则放弃列印。在PRINT2中,我们总是传回TRUE。现在,使用者在列印对话方块中按下「Cancel」按钮时将传回FALSE。程式13-7所示的PRINT3程式实作了这个处理方式。
程式13-7 PRINT3PRINT3.C/*----------------------------- PRINT3.C --Printing with Dialog Box (c) Charles Petzold, 1998-------------------------------*/#include <windows.h>HDC GetPrinterDC (void) ; // in GETPRNDC.CvoidPageGDICalls (HDC, int, int) ; // in PRINT.CHINSTANCE hInst ;TCHAR szAppName[] = TEXT ("Print3") ;TCHAR szCaption[] = TEXT ("Print Program 3 (Dialog Box)") ;BOOL bUserAbort ;HWND hDlgPrint ;BOOL CALLBACK PrintDlgProc (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam){ switch (message) {caseWM_INITDIALOG: SetWindowText (hDlg, szAppName) ; EnableMenuItem (GetSystemMenu (hDlg, FALSE), SC_CLOSE, MF_GRAYED) ; return TRUE ; case WM_COMMAND: bUserAbort = TRUE ; EnableWindow (GetParent (hDlg), TRUE) ; DestroyWindow (hDlg) ; hDlgPrint = NULL ; return TRUE ; } return FALSE ;}BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode){ MSG msg ; while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) { if (!hDlgPrint || !IsDialogMessage (hDlgPrint, &msg)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } } return !bUserAbort ;}BOOL PrintMyPage (HWND hwnd){ static DOCINFO di = { sizeof (DOCINFO), TEXT ("Print3: Printing") } ; BOOL bSuccess = TRUE ; HDC hdcPrn ; int xPage, yPage ; if (NULL == (hdcPrn = GetPrinterDC ())) return FALSE ; xPage = GetDeviceCaps (hdcPrn, HORZRES) ; yPage = GetDeviceCaps (hdcPrn, VERTRES) ; EnableWindow (hwnd, FALSE) ; bUserAbort = FALSE ; hDlgPrint = CreateDialog (hInst, TEXT ("PrintDlgBox"), hwnd, PrintDlgProc) ; SetAbortProc (hdcPrn, AbortProc) ; if (StartDoc (hdcPrn, &di) > 0) { if (StartPage (hdcPrn) > 0) { PageGDICalls (hdcPrn, xPage, yPage) ; if (EndPage (hdcPrn) > 0) EndDoc (hdcPrn) ; else bSuccess = FALSE ; } } else bSuccess = FALSE ; if (!bUserAbort) { EnableWindow (hwnd, TRUE) ; DestroyWindow (hDlgPrint) ; } DeleteDC (hdcPrn) ; return bSuccess && !bUserAbort ;}
PRINT.RC (摘录)//Microsoft Developer Studio generated resource script.#include "resource.h"#include "afxres.h"/////////////////////////////////////////////////////////////////////////////// DialogPRINTDLGBOX DIALOG DISCARDABLE 20, 20, 186, 63STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENUFONT 8, "MS Sans Serif"BEGIN PUSHBUTTON "Cancel",IDCANCEL,67,42,50,14 CTEXT "Cancel Printing",IDC_STATIC,7,21,172,8END
如果您使用PRINT3,那么最好临时暂停使用幕後列印;否则,只有在列印伫列程式从PRINT3中接收资料时才可见到的「Cancel」按钮可能会很快消失,让您根本没有机会去按它。如果您按「Cancel」按钮时列印并不立即终止(特别是在一个慢速印表机上),不要惊讶。印表机有一个内部缓冲区,在印表机停止之前其中的资料必须全部送出,按「Cancel」只是告诉GDI不要向印表机的缓冲区发送更多的资料而已。
PRINT3增加了两个整体变数:一个是叫做bUserAbort的布林变数,另一个是叫做hDlgPrint的对话方块视窗代号。PrintMyPage函式将bUserAbort初始化为FALSE。与PRINT2一样,程式的主视窗是不接收输入讯息的。指向AbortProc的指标用於SetAbortProc呼叫中,而指向PrintDlgProc的指标用於CreateDialog呼叫中。CreateDialog传回的视窗代号储存在hDlgPrint中。
现在,AbortProc中的讯息回圈如下:
while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)){ if (!hDlgPrint || !IsDialogMessage (hDlgPrint, &msg)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; }}return !bUserAbort ;
只有在bUserAbort为FALSE,也就是使用者还没有终止列印工作时,这段程式码才会呼叫PeekMessage。IsDialogMessage函式用来将讯息发送给非系统模态对话方块。和普通的非系统模态对话方块一样,对话方块视窗的代号在这个呼叫之前受到检查。AbortProc的传回值正好与bUserAbort相反。开始时,bUserAbort为FALSE,因此AbortProc传回TRUE,表示继续进行列印;但是bUserAbort可能在列印对话程序中被设定为TRUE。
PrintDlgProc函式是相当简单的。处理WM_INITDIALOG时,该函式将视窗标题设定为程式名称,并且停用系统功能表上的「Close」选项。如果使用者按下了「Cancel」钮,PrintDlgProc将收到WM_COMMAND讯息:
case WM_COMMAND : bUserAbort = TRUE ; EnableWindow (GetParent (hDlg), TRUE) ; DestroyWindow (hDlg) ; hDlgPrint = NULL ; return TRUE ;
将bUserAbort设定为TRUE,则说明使用者已经决定取消列印操作,主视窗被启动,而对话方块被清除(按顺序完成这两项活动是很重要的,否则,在Windows中执行其他程式之一将变成活动程式,而您的程式将消失到背景中)。与通常的情况一样,将hDlgPrint设定为NULL,防止在讯息回圈中呼叫IsDialogMessage。
只有在AbortProc用PeekMessage找到讯息,并用IsDialogMessage将它们传送给对话方块视窗讯息处理程式时,这个对话方块才接收讯息。只有在GDI模组处理EndPage函式时,才呼叫AbortProc。如果GDI发现AbortProc的传回值是FALSE,它将控制权从EndPage传回到PrintMyPage。它不传回错误码。至此,PrintMyPage认为列印页已经发完了,并呼叫EndDoc函式。但是,由於GDI模组还没有完成对EndPage呼叫的处理,所以不会列印出什么东西来。
有些清除工作尚待完成。如果使用者没在对话方块中取消列印作业,那么对话方块仍然会显示著。PrintMyPage重新启用它的主视窗并清除对话方块:
if (!bUserAbort){EnableWindow (hwnd, TRUE) ;DestroyWindow (hDlgPrint) ;}
两个变数会通知您发生了什么事:bUserAbort可以告诉您使用者是否终止了列印作业,bSuccess会告诉您是否出了故障,您可以用这些变数来完成想做的工作。PrintMyPage只简单地对它们进行逻辑上的AND运算,然後把值传回给WndProc:
return bSuccess && !bUserAbort ;
为POPPAD增加列印功能
现在准备在POPPAD程式中增加列印功能,并且宣布POPPAD己告完毕。这需要第十一章中的各个POPPAD档案,此外,还需要程式13-8中的POPPRNT.C档案。
程式13-8 POPPRNTPOPPRNT.C/*--------------------------------- POPPRNT.C -- Popup Editor Printing Functions-----------------------------------*/#include <windows.h>#include <commdlg.h>#include "resource.h"BOOL bUserAbort ;HWND hDlgPrint ;BOOL CALLBACK PrintDlgProc (HWND hDlg, UINT msg, WPARAM wParam,LPARAM lParam){ switch (msg) { case WM_INITDIALOG : EnableMenuItem (GetSystemMenu (hDlg, FALSE), SC_CLOSE, MF_GRAYED) ; return TRUE ; case WM_COMMAND : bUserAbort = TRUE ; EnableWindow (GetParent (hDlg), TRUE) ; DestroyWindow (hDlg) ; hDlgPrint = NULL ; return TRUE ; } return FALSE ;} BOOL CALLBACK AbortProc (HDC hPrinterDC, int iCode){ MSG msg ; while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) { if (!hDlgPrint || !IsDialogMessage (hDlgPrint, &msg)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } } return !bUserAbort ;}BOOL PopPrntPrintFile (HINSTANCE hInst, HWND hwnd, HWND hwndEdit, PTSTR szTitleName){ static DOCINFO di = { sizeof (DOCINFO) } ; static PRINTDLG pd ; BOOL bSuccess ; int yChar, iCharsPerLine, iLinesPerPage, iTotalLines, iTotalPages, iPage, iLine, iLineNum ; PTSTR pstrBuffer ; TCHAR szJobName [64 + MAX_PATH] ; TEXTMETRIC tm ; WORD iColCopy, iNoiColCopy ; // Invoke Print common dialog box pd.lStructSize = sizeof (PRINTDLG) ; pd.hwndOwner = hwnd ; pd.hDevMode = NULL ; pd.hDevNames = NULL ; pd.hDC = NULL ; pd.Flags = PD_ALLPAGES | PD_COLLATE | PD_RETURNDC | PD_NOSELECTION ; pd.nFromPage = 0 ; pd.nToPage = 0 ; pd.nMinPage = 0 ; pd.nMaxPage = 0 ; pd.nCopies = 1 ; pd.hInstance = NULL ; pd.lCustData = 0L ; pd.lpfnPrintHook = NULL ; pd.lpfnSetupHook = NULL ; pd.lpPrintTemplateName = NULL ; pd.lpSetupTemplateName = NULL ; pd.hPrintTemplate = NULL ; pd.hSetupTemplate = NULL ; if (!PrintDlg (&pd)) return TRUE ; if (0 == (iTotalLines = SendMessage (hwndEdit, EM_GETLINECOUNT, 0, 0))) return TRUE ; // Calculate necessary metrics for file GetTextMetrics (pd.hDC, &tm) ; yChar = tm.tmHeight + tm.tmExternalLeading ; iCharsPerLine = GetDeviceCaps (pd.hDC, HORZRES) / tm.tmAveCharWidth ; iLinesPerPage = GetDeviceCaps (pd.hDC, VERTRES) / yChar ;iTotalPages = (iTotalLines + iLinesPerPage - 1) / iLinesPerPage ; // Allocate a buffer for each line of text pstrBuffer = malloc (sizeof (TCHAR) * (iCharsPerLine + 1)) ; // Display the printing dialog box EnableWindow (hwnd, FALSE) ; bSuccess = TRUE ; bUserAbort = FALSE ; hDlgPrint = CreateDialog (hInst, TEXT ("PrintDlgBox"), hwnd, PrintDlgProc) ; SetDlgItemText (hDlgPrint, IDC_FILENAME, szTitleName) ; SetAbortProc (pd.hDC, AbortProc) ; // Start the document GetWindowText (hwnd, szJobName, sizeof (szJobName)) ; di.lpszDocName = szJobName ; if (StartDoc (pd.hDC, &di) > 0) { // Collation requires this loop and iNoiColCopy for (iColCopy = 0 ; iColCopy < ((WORD) pd.Flags & PD_COLLATE ? pd.nCopies : 1) ; iColCopy++) { for (iPage = 0 ; iPage < iTotalPages ; iPage++) { for (iNoiColCopy = 0 ; iNoiColCopy < (pd.Flags & PD_COLLATE ? 1 : pd.nCopies); iNoiColCopy++) { // Start the page if (StartPage (pd.hDC) < 0) { bSuccess = FALSE ; break ; } // For each page, print the lines for (iLine = 0 ; iLine < iLinesPerPage ; iLine++) { iLineNum = iLinesPerPage * iPage + iLine ; if (iLineNum > iTotalLines) break ; *(int *) pstrBuffer = iCharsPerLine ; TextOut(pd.hDC, 0, yChar * iLine, pstrBuffer, (int) SendMessage (hwndEdit, EM_GETLINE, (WPARAM) iLineNum, (LPARAM) pstrBuffer)); } if (EndPage (pd.hDC) < 0) { bSuccess = FALSE ; break ; } if (bUserAbort) break ; } if (!bSuccess || bUserAbort) break ; } if (!bSuccess || bUserAbort) break ; } } else bSuccess = FALSE ; if (bSuccess) EndDoc (pd.hDC) ; if (!bUserAbort) { EnableWindow (hwnd, TRUE) ; DestroyWindow (hDlgPrint) ; } free (pstrBuffer) ; DeleteDC (pd.hDC) ; return bSuccess && !bUserAbort ;}
与POPPAD尽量利用Windows高阶功能来简化程式的方针一致,POPPRNT.C档案展示了使用PrintDlg函式的方法。这个函式包含在通用对话方块程式库(common dialog box library)中,使用一个PRINTDLG型态的结构。
通常,程式的「File」功能表中有个「Print」选项。当使用者选中「Print」选项时,程式可以初始化PRINTDLG结构的栏位,并呼叫PrintDlg。
PrintDlg显示一个对话方块,它允许使用者选择列印页的范围。因此,这个对话方块特别适用於像POPPAD这样能列印多页文件的程式。这种对话方块同时也给出了一个确定副本份数的编辑区和名为「Collate(逐份列印)」的核取方块。「逐份列印」影响著多个副本页的顺序。例如,如果文件是3页,使用者要求列印三份副本,则这个程式能以两种顺序之一列印它们。选择逐份列印後的副本的页码顺序为1、2、3、1、2、3、1、2、3,未选择逐份列印的副本的页码顺序是1、1、1、2、2、2、3、3、3。程式在这里应负起的责任就是以正确的顺序列印副本。
这个对话方块也允许使用者选择非内定印表机,它包括一个标记为「Properties」的按钮,可以启动设备模式对话方块。这样,至少允许使用者选择直印或横印。
从PrintDlg函式传回後,PRINTDLG结构的栏位指明列印页的范围和是否对多个副本进行逐份列印。这个结构同时也给出了准备使用的印表机装置内容代号。
在POPPRNT.C中,PopPrntPrintFile函式(当使用者在「File」功能表里选中「Print」选项时,它由POPPAD呼叫)呼叫PrintDlg,然後开始列印档案。PopPrntPrintFile完成某些计算,以确定一行能容纳多少字元和一页能容纳多少行。这个程序涉及到呼叫GetDeviceCaps来确定页的解析度,呼叫GetTextMetrics来确定字元的大小。
这个程式通过发送一条EM_GETLINECOUNT讯息给编辑控制项来取得文件中的总行数(在变数iTotalLines中)。储存各行内容的缓冲区配置在局部记忆体中。对每一行,缓冲区的第一个字被设定为该行中字元的数目。把EM_GETLINE讯息发送给编辑控制项可以把一行复制到缓冲区中,然後用TextOut把这一行送到印表机装置内容中(POPPRNT.C还没有聪明到对超出列印宽度的文字换到下一行去处理。在第十七章我们会讨论这种文字绕行的技术)。
为了确定副本份数,应注意列印文字的处理方式包括两个for回圈。第一个for回圈使用了一个叫作iColCopy的变数,当使用者指定将副本逐份列印时,它将会起作用。第二个for回圈使用了一个叫作iNonColCopy的变数,当不对副本进行逐份列印时,它将起作用。
如果StartPage或EndPage传回一个错误,或者如果bUserAbort为TRUE,那么这个程式退出增加页号的那个for回圈。如果放弃程序的传回值是FALSE,则EndPage不传回错误。正是由於这个原因,在下一页开始之前,要直接测试bUserAbort。如果没有报告错误,则进行EndDoc呼叫:
if (!bError) EndDoc (hdcPrn) ;
您可能想通过列印多页档案来测试POPPAD。您可以从列印任务视窗中监视列印进展情况。在GDI处理完第一个EndPage呼叫之後,首先列印的档案将显示在列印任务视窗中。此时,幕後列印程式开始把档案发送到印表机。然後,如果在POPPAD中取消列印作业,那么幕後列印程式将终止列印,这也就是放弃程序传回FALSE的结果。当档案出现在列印任务视窗中,您也可以透过从「Document」功能表中选择「Cancel Printing」来取消列印作业,在这种情况下, POPPAD中的EndPage呼叫会传回一个错误。
Windows的程式设计的新手经常会抱住AbortDoc函式不放,但实际上这个函式几乎不在列印中使用。像在POPPAD中看到的那样,使用者几乎随时可以取消列印作业,或者通过POPPAD的列印对话方块及通过列印任务视窗。这两种方法都不需要程式使用AbortDoc函式。 POPPAD中允许AbortDoc的唯一时刻是在对StartDoc的呼叫和对EndPage的第一个呼叫之间,但是程式很快就会执行过去,以至不再需要AbortDoc。
图13-3显示出正确列印多页文件之列印函式的呼叫顺序。检查bUserAbort的值是否为TRUE的最佳位置是在每个EndPage函式之後。只有当对先前的列印函式的呼叫没有产生错误时,才使用EndDoc函式。实际上,如果任何一个列印函式的呼叫出现错误,那么表演就结束了,同时您也可以回家了。