13. 使用印表機

Post date: 2012/3/23 上午 05:39:54

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模組和列印驅動程式的交互作用程序。

圖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函式。

第五章 中,您已知道如何通過如下的呼叫來為整個視訊顯示器取得指向裝置內容的代號:

您也可以使用該函式來取得印表機裝置內容代號。然而,對印表機裝置內容,CreateDC的一般語法為:

pInitializationData參數一般被設為NULL。szDeviceName參數指向一個字串,以告訴Windows印表機設備的名稱。在設定設備名稱之前,您必須知道有哪些印表機可用。

一個系統可能有不只一臺連結著的印表機,甚至可以有其他程式,如傳真軟體,將自己偽裝成印表機。不論連結的印表機有多少臺,都只能有一臺被認為是「目前的印表機」或者「內定印表機」,這是使用者最近一次選擇的印表機。許多小型的Windows程式只使用內定印表機來進行列印。

取得內定印表機裝置內容的方式不斷在改變。目前,標準的方法是使用EnumPrinters函式來獲得。該函式填入一個包含每個連結著的印表機資訊的陣列結構。根據所需的細節層次,您還可以選擇幾種結構之一作為該函式的參數。這些結構的名稱為PRINTER_INFO_x,x是一個數字。

不幸的是,所使用的函式還取決於您的程式是在Windows 98上執行還是在Windows NT上執行。程式13-1展示了GetPrinterDC函式在兩種作業系統上工作的用法。

hdc = CreateDC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;

hdc = CreateDC (NULL, szDeviceName, NULL, pInitializationData) ;

程式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函式來確定程式是執行在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 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 ; 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_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("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, 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" ///////////////////////////////////////////////////////////////////////////// // 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」。當檔案正在被列印或者在等待列印時,這個字串將出現在印表機任務佇列中的「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 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 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 ; 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 void PageGDICalls (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 ; 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 PRINT3 PRINT3.C /*----------------------------------------------------------------- PRINT3.C -- Printing with Dialog Box (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 ("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訊息:

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 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 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呼叫:

您可能想通過列印多頁檔案來測試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函式。實際上,如果任何一個列印函式的呼叫出現錯誤,那麼表演就結束了,同時您也可以回家了。

if (!bError) EndDoc (hdcPrn) ;

圖13-3 列印一個文件時的函式呼叫順序