当前位置: 首页 > 文档资料 > Windows 程序设计 >

使用打印机

优质
小牛编辑
135浏览
2023-12-01

为了处理文字和图形而使用视讯显示器时,设备无关的概念看来非常完美,但对于打印机,设备无关的概念又怎样呢?

总的说来,效果也很好。在Windows程序中,用于视讯显示器的GDI函数一样可以在印表纸上打印文字和图形,在以前讨论的与设备无关的许多问题(多数都与平面显示的尺寸、分辨率以及颜色数有关)都可以用相同的方法解决。当然,一台打印机不像使用阴极射线管的显示器那么简单,它们使用的是印表纸。它们之间有一些比较大的差异。例如,我们从来不必考虑视讯显示器没有与显示卡连结好,或者显示器出现「屏幕空间不够」的错误,但打印机offline和缺纸却是经常会遇到的问题。

我们也不必担心显示卡不能执行某些图形操作,更不用担心显示卡能否处理图形,因为,如果它不能处理图形,就根本不能使用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模块和打印驱动程序的交互作用程序。

图13-1 应用程序、GDI模块、打印驱动程序和打印队列程序的交互作用过程

Windows后台打印程序实际上是几个组件的一种组合(见表13-1)。

表13-1

打印队列程序组件

说明

打印请求队列程序

将数据流传递给打印功能提供者

本地打印功能提供者

为本地打印机建立背景文件

网络打印功能提供者

为网络打印机建立背景文件

打印处理程序

将打印队列中与设备无关的数据转换为针对目的打印机的格式

打印端口监视程序

控件连结打印机的端口

打印语言监视程序

控件可以双向通讯的打印机,设定设备设定并检测打印机状态

打印队列程序可以减轻应用程序的打印负担。Windows在启动时就加载打印队列程序,因此,当应用程序开始打印时,它已经是活动的了。当程序行印一个文件时,GDI模块会建立包含打印输出数据的文件。后台打印程序的任务是将这些文件发往打印机。GDI模块发出一个消息来通知它一个新的打印作业开始,然后它开始读文件并将文件直接传送到打印机。为了传送这些文件,打印队列程序依照打印机所连结的并列端口或串行埠使用各种不同的通信函数。在打印队列程序向打印机发送文件的操作完成后,它就将包含输出数据的临时文件删除。这个交互作用过程如图13-2所示。

图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是一个数字。

不幸的是,所使用的函数还取决于您的程序是在Windows98上执行还是在WindowsNT上执行。程序13-1展示了GetPrinterDC函数在两种操作系统上工作的用法。

程序13-1 GETPRNDC
GETPRNDC.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函数来确定程序是执行在Windows98上还是WindowsNT上。不管是什么操作系统,函数呼叫EnumPrinters两次:一次取得它所需结构的大小,一次填入结构。在Windows98上,函数使用PRINTER_INFO_5结构;在WindowsNT上,函数使用PRINTER_INFO_4结构。这些结构在EnumPrinters文件(/PlatformSDK/Graphics and Multimedia Services/GDI/Printing and PrintSpooler/Printing and Print Spooler Reference/Printing and PrintSpoolerFunctions/EnumPrinters,范例小节的前面)中有说明,它们是「容易而快速」的。

修改后的DEVCAPS程序

第五章的DEVCAPS1程序只显示了从GetDeviceCaps函数获得的关于视讯显示的基本信息。程序13-2所示的新版本显示了关于视讯显示和连结到系统之所有打印机的更多信息。

程序13-2 DEVCAPS2
DEVCAPS2.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 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 ;   HDChdc, hdcInfo ;   HMENU hMenu ;   HANDLE hPrint ;   PAINTSTRUCTps ;   TEXTMETRICtm ;  
  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_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 ("VERTRESHeight 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   ("NUMFONTSNumber 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 ;  
   TCHARszBuffer[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_CIRCLESCan do circles:"),  CC_PIE,TEXT ("CC_PIECan 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_SCANLINECan do scanlines:"),  PC_WIDE,  TEXT ("PC_WIDECan 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,EXT ("TC_SA_DOUBLECan 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"  
/////////////////////////////////////////////////////////////////////////////  
// Menu  
DEVCAPS2 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  
   END  
END  
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 FORMFEED
FORMFEED.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」。当文件正在被打印或者在等待打印时,这个字符串将出现在打印机任务队列中的「DocumentName」列中。通常,该字符串包含进行打印的应用程序名称和被打印的文件名称。

如果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 PRINT
PRINT.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 intcxClient, 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 ;  case   WM_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 PRINT1
PRINT1.C  
/*---------------------------------------------------------------------  
  PRINT1.C -- Bare Bones Printing (c) Charles Petzold, 1998  
----------------------------------------------------------------------*/  
#include <windows.h>  
HDC GetPrinterDC (void) ;// in GETPRNDC.C  
voidPageGDICalls (HDC, int, int) ;   // in PRINT.C  

HINSTANCE 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 PRINT2
PRINT2.C  
/*---------------------------------------------------------------------  
  PRINT2.C --   Printing with Abort Procedure (c) Charles Petzold, 1998  
----------------------------------------------------------------------*/  
#include <windows.h>  
HDC GetPrinterDC (void) ;// in GETPRNDC.C  
void PageGDICalls (HDC, int, int) ;   // in PRINT.C  

HINSTANCE 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 ;   shortxPage, 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 PRINT3
PRINT3.C  
/*-----------------------------------------------------------------  
  PRINT3.C --   Printing with Dialog Box (c) Charles Petzold, 1998  
-------------------------------------------------------------------*/  
#include <windows.h>  
HDC GetPrinterDC (void) ;// in GETPRNDC.C  
voidPageGDICalls (HDC, int, int) ;   // in PRINT.C  

HINSTANCE 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)   {  
case   WM_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"  
/////////////////////////////////////////////////////////////////////////////  
// Dialog  
PRINTDLGBOX DIALOG DISCARDABLE  20, 20, 186, 63  
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU  
FONT 8, "MS Sans Serif"  
BEGIN  
   PUSHBUTTON   "Cancel",IDCANCEL,67,42,50,14  
   CTEXT"Cancel Printing",IDC_STATIC,7,21,172,8  
END  

如果您使用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消息:

caseWM_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 POPPRNT
POPPRNT.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 DOCINFOdi = { sizeof (DOCINFO) } ;   static PRINTDLG   pd ;   BOOL  bSuccess ;   int   yChar, iCharsPerLine, iLinesPerPage, iTotalLines,  iTotalPages, iPage, iLine, iLineNum ;   PTSTR pstrBuffer ;   TCHAR szJobName [64 + MAX_PATH] ;   TEXTMETRICtm ;   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函数的方法。这个函数包含在通用对话框链接库(commondialog 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」菜单中选择「CancelPrinting」来取消打印作业,在这种情况下,POPPAD中的EndPage呼叫会传回一个错误。

Windows的程序设计的新手经常会抱住AbortDoc函数不放,但实际上这个函数几乎不在打印中使用。像在POPPAD中看到的那样,使用者几乎随时可以取消打印作业,或者通过POPPAD的打印对话框及通过打印任务窗口。这两种方法都不需要程序使用AbortDoc函数。POPPAD中允许AbortDoc的唯一时刻是在对StartDoc的呼叫和对EndPage的第一个呼叫之间,但是程序很快就会执行过去,以至不再需要AbortDoc。

图13-3显示出正确打印多页文件之打印函数的呼叫顺序。检查bUserAbort的值是否为TRUE的最佳位置是在每个EndPage函数之后。只有当对先前的打印函数的呼叫没有产生错误时,才使用EndDoc函数。实际上,如果任何一个打印函数的呼叫出现错误,那么表演就结束了,同时您也可以回家了。

图13-3 打印一个文件时的函数呼叫顺序