|
|
為了處理文字和圖形而使用視訊顯示器時,設備無關的概念看來非常完美,但對於打印機,設備無關的概念又怎樣呢?
總的說來,效果也很好。在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函數。
在第五章中,您已知道如何通過如下的呼叫來為整個視訊顯示器取得指向設備內容的句柄:
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函數在兩種操作系統上工作的用法。
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所示的新版本顯示了關於視訊顯示和連結到系統之所有打印機的更多信息。
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("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_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"))); }
//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
// 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程序,展示了打印所需的最小需求。
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所示。
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將必要的打印機輸出儲存在一個臨時文件中,然后打印隊列程序將它發送給打印機。
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兩次(第一次阻止窗口接受輸入消息,第二次啟用窗口)。
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程序實作了這個處理方式。
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消息:
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文件。
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呼叫:
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函數。實際上,如果任何一個打印函數的呼叫出現錯誤,那么表演就結束了,同時您也可以回家了。
圖13-3 打印一個文件時的函數呼叫順序
|