二、Windows非模态对话框—HEXCALC:窗口还是对话框?
HEXCALC程序可能是写程序偷懒的经典之作,如程序11-5所示。这个程序完全不呼叫CreateWindow,也不处理WM_PAINT消息,不取得设备内容,也不处理鼠标消息。但是它只用了不到150行的原始码,就构成了一个具有完整键盘和鼠标接口以及10种运算的十六进制计算器。计算器如图11-5所示。
HEXCALC.C /*------------------------------------------------------------------------ HEXCALC.C -- Hexadecimal Calculator (c) Charles Petzold, 1998 -------------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("HexCalc") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = DLGWINDOWEXTRA ; // Note! wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateDialog (hInstance, szAppName, 0, NULL) ; ShowWindow (hwnd, iCmdShow) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void ShowNumber (HWND hwnd, UINT iNumber) { TCHAR szBuffer[20] ; wsprintf (szBuffer, TEXT ("%X"), iNumber) ; SetDlgItemText (hwnd, VK_ESCAPE, szBuffer) ; } DWORD CalcIt (UINT iFirstNum, int iOperation, UINT iNum) { switch (iOperation) { case '=': return iNum ; case '+': return iFirstNum + iNum ; case '-': return iFirstNum - iNum ; case '*': return iFirstNum * iNum ; case '&': return iFirstNum & iNum ; case '|': return iFirstNum | iNum ; case '^': return iFirstNum ^ iNum ; case '<': return iFirstNum << iNum ; case '>': return iFirstNum >> iNum ; case '/': return iNum ? iFirstNum / iNum: MAXDWORD ; case '%': return iNum ? iFirstNum % iNum: MAXDWORD ; default : return 0 ; } } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static BOOL bNewNumber = TRUE ; static int iOperation = '=' ; static UINT iNumber, iFirstNum ; HWND hButton ; switch (message) { case WM_KEYDOWN: // left arrow --> backspace if (wParam != VK_LEFT) break ; wParam = VK_BACK ; // fall through case WM_CHAR: if ((wParam = (WPARAM) CharUpper ((TCHAR *) wParam)) == VK_RETURN) wParam = '=' ; if (hButton = GetDlgItem (hwnd, wParam)) { SendMessage (hButton, BM_SETSTATE, 1, 0) ; Sleep (100) ; SendMessage (hButton, BM_SETSTATE, 0, 0) ; } else { MessageBeep (0) ; break ; } // fall through case WM_COMMAND: SetFocus (hwnd) ; if (LOWORD (wParam) == VK_BACK) //backspace ShowNumber (hwnd, iNumber /= 16) ; else if (LOWORD (wParam) == VK_ESCAPE) // escape ShowNumber (hwnd, iNumber = 0) ; else if (isxdigit (LOWORD (wParam))) // hex digit { if (bNewNumber) { iFirstNum = iNumber ; iNumber = 0 ; } bNewNumber = FALSE ; if (iNumber <= MAXDWORD >> 4) ShowNumber (hwnd, iNumber = 16 * iNumber + wParam - (isdigit (wParam) ? '0': 'A' - 10)) ; else MessageBeep (0) ; } else // operation { if (!bNewNumber) ShowNumber (hwnd, iNumber = CalcIt (iFirstNum, iOperation, iNumber)) ; bNewNumber = TRUE ; iOperation = LOWORD (wParam) ; } return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
//Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Icon HEXCALC ICON DISCARDABLE "HexCalc.ico" ///////////////////////////////////////////////////////////////////////////// #include "hexcalc.dlg"
HEXCALC.DLG /*-------------------------------- HEXCALC.DLG dialog script ----------------------------------*/ HexCalc DIALOG -1, -1, 102, 122 STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX CLASS "HexCalc" CAPTION "Hex Calculator" { PUSHBUTTON "D", 68, 8, 24, 14, 14 PUSHBUTTON "A", 65, 8, 40, 14, 14 PUSHBUTTON "7", 55, 8, 56, 14, 14 PUSHBUTTON "4", 52, 8, 72, 14, 14 PUSHBUTTON "1", 49, 8, 88, 14, 14 PUSHBUTTON "0", 48, 8, 104,14, 14 PUSHBUTTON "0", 27, 26, 4, 50, 14 PUSHBUTTON "E", 69, 26, 24, 14, 14 PUSHBUTTON "B", 66, 26, 40, 14, 14 PUSHBUTTON "8", 56, 26, 56, 14, 14 PUSHBUTTON "5", 53, 26, 72, 14, 14 PUSHBUTTON "2", 50, 26, 88, 14, 14 PUSHBUTTON "Back", 8, 26, 104,32, 14 PUSHBUTTON "C", 67, 44, 40, 14, 14 PUSHBUTTON "F", 70, 44, 24, 14, 14 PUSHBUTTON "9", 57, 44, 56, 14, 14 PUSHBUTTON "6", 54, 44, 72, 14, 14 PUSHBUTTON "3", 51, 44, 88, 14, 14 PUSHBUTTON "+", 43, 62, 24, 14, 14 PUSHBUTTON "-", 45, 62, 40, 14, 14 PUSHBUTTON "*", 42, 62, 56, 14, 14 PUSHBUTTON "/", 47, 62, 72, 14, 14 PUSHBUTTON "%", 37, 62, 88, 14, 14 PUSHBUTTON "Equals", 61, 62, 104,32, 14 PUSHBUTTON "&&",38, 80, 24, 14, 14 PUSHBUTTON "|", 124, 80, 40, 14, 14 PUSHBUTTON "^", 94, 80, 56, 14, 14 PUSHBUTTON "<", 60, 80, 72, 14, 14 PUSHBUTTON ">", 62, 80, 88, 14, 14 }
HEXCALC.ICO |
HEXCALC是一个普通的中序表达式计算器,使用C语言的符号表示方式进行计算。它对无正负号32位整数作加、减、乘、除和取余数运算,位AND, OR, exclusive-OR运算,还有左右位移运算。被0除将导致结果被设定为FFFFFFFF。
在HEXCALC中既可以使用鼠标又可以使用键盘。您从按键点入」或者输入第一个数(最多8位十六进制数字)开始,然后输入运算子,然后是第二个数。接着,您可以透过单击「Equals」按钮或者按下等号键或Enter键便可以显示运算结果。为了更正输入,您可以使用「Back」按钮、Backspace或者左箭头键。单击「display」方块或者按下Esc键即可清除目前的输入。
HEXCALC比较奇怪的一点是,屏幕上显示的窗口似乎是普通的重迭式窗口与非模态对话框的混合体。一方面,HEXCALC的所有消息都在函数的WndProc中处理,这个函数与通常的窗口消息处理程序相似,该函数传回一个长整数,它处理WM_DESTROY消息,呼叫DefWindowProc,就像普通的窗口消息处理程序一样。另一方面,窗口是在WinMain中呼叫CreateDialog并使用HEXCALC.DLG中的对话框模板建立的。那么,HEXCALC到底是一个普通的可重迭窗口,还是一个非模态对话框呢?
简单的回答是,对话框就是窗口。通常,Windows使用它自己内部的窗口消息处理程序处理对话框窗口的消息,然后,Windows将这些消息传送给建立对话框的程序内的对话框程序。在HEXCALC中,我们让Windows使用对话框模板建立一个窗口,但是自己写程序处理这个窗口的消息。
不幸的是,在Developer Studio的Dialog Editor中,对话框模板需要一些我们不能添加的东西。因此,对话框模板包含在HEXCALC.DLG文件中,而且需要手工输入。依照下面的方法,您可以将一个文本文件添加到任何项目中:从「 File」菜单选择「New」,再选择「 Files」页面卷标,然后从文件型态列表中选择「Text File」。像这样的文件-包含附加资源定义-需要包含在资源描述中。从「 View」菜单选择「Resource Includes」。这显示一个对话框。在「Compile-time Directives」编辑栏输入
#include "hexcalc.dlg"
这一行将插入到HEXCALC.RC资源描述中,像上面所显示的一样。
仔细看一下HEXCALC.DLG文件中的对话框模板,您将发现HEXCALC如何为对话框使用它自己的窗口消息处理程序。对话框模板的上方如下:
HexCalc DIALOG -1, -1, 102, 122 STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX CLASS "HexCalc" CAPTION "Hex Calculator"
注意诸如WS_OVERLAPPED和WS_MINIMIZEBOX等标识符,我们可以将它们用在CreateWindow呼叫中以建立普通的窗口。CLASS叙述是这个对话框与曾经建立过的对话框之间最重要的区别(而且它也是Developer Studio中的Dialog Editor不允许我们指定的)。当对话框模板省略了这个叙述时,Windows为对话框注册一个窗口类别,并使用它自己的窗口消息处理程序处理对话框消息。这里,包含CLASS叙述就告诉Windows将消息发送到其它的地方-具体的说,就是发送到在HexCalc窗口类别中指定的窗口消息处理程序。
HexCalc窗口类别是在HEXCALC的WinMain函数中注册的,就像普通窗口的窗口类别一样。但是,请注意有个十分重要的区别:WNDCLASS结构的cbWndExtra字段设定为DLGWINDOWEXTRA。对于您自己注册的对话框程序,这是必需的。
在注册窗口类别之后,WinMain呼叫CreateDialog:
hwnd = CreateDialog (hInstance, szAppName, 0, NULL) ;
第二个参数(字符串「HexCaEc」)是对话框模板的名字。第三个参数通常是父窗口的窗口句柄,这里设定为0,因为窗口没有父窗口。最后一个参数,通常是对话框程序的地址,这里不需要。因为Windows不会处理这些消息,因而也不会将消息发送给对话框程序。
这个CreateDialog呼叫与对话框模板一起,被Windows有效地转换为一个CreateWindow呼叫。该CreateWindow呼叫的功能与下面的呼叫相同:
hwnd = CreateWindow (TEXT ("HexCalc"), TEXT ("Hex Calculator"), WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 102 * 4 / cxChar, 122 * 8 / cyChar, NULL, NULL, hInstance, NULL) ;
其中,cxChar和cyChar变量分别是系统字体字符的宽度和高度。
我们通过让Windows来进行CreateWindow呼叫而收获甚丰:Windows不会在建立弹出式窗口1后就停止,它还会为对话框模板中定义的其它29个子窗口按键控件呼叫CreateWindow。所有这些控件都给父窗口的窗口消息处理程序发送WM_COMMAND消息,该程序正是WndProc。对于建立一个包含许多子窗口的窗口来说,这是一个很好的技巧。
下面是使HEXCALC的程序代码量下降到最少的另一种方法:或许您会注意到HEXCALC没有表头文件,表头文件中通常包含对话框模板中,需要为所有子窗口控件定义的标识符。我们之所以可以不要这个文件,是因为每个按键控件的ID设定为出现在控件上的文字的ASCII码。这意味着,WndProc可以完全相同地对待WM_COMMAND消息和WM_CHAR消息。在每种情况下,wParam的低字组都是按钮的ASCII码。
当然,对键盘消息进行一些处理是必要的。WndProc拦截WM_KEYDOWN消息,将左箭头键转换为Backspace键。在处理WM_CHAR消息时,WndProc将字符代码转换为大写,Enter键转换为等号键的ASCII码。
WM_CHAR消息的有效性是通过呼叫GetDlgItem来检验的。如果GetDlgItem函数传回0,那么键盘字符不是对话框模板中定义的ID之一。如果字符是ID之一,则通过给相应的按钮发送一对BM_SETSTATE消息,来使之闪烁:
if (hButton = GetDlgItem (hwnd, wParam)) { SendMessage (hButton, BM_SETSTATE, 1, 0) ; Sleep (100) ; SendMessage (hButton, BM_SETSTATE, 0, 0) ; }
这样做,用最小的代价,却为HEXCALC的键盘接口增色不少。Sleep函数将程序暂停100毫秒。这会防止按钮被按得太快而让人注意不到。
当WndProc处理WM_COMMAND消息时,它总是将输入焦点设定给父窗口:
case WM_COMMAND : SetFocus (hwnd) ;
否则,一旦使用鼠标单击某按钮,输入焦点就会切换到该按钮上。