在一个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 ;
}