二、Windows自己的窗口—注册窗口类别
窗口依照某一窗口类别建立,窗口类别用以标识处理窗口消息的窗口消息处理程序。
不同窗口可以依照同一种窗口类别建立。例如,Windows中的所有按钮窗口-包括按键、复选框,以及单选按钮-都是依据同一种窗口类别建立的。窗口类别定义了窗口消息处理程序和依据此类别建立的窗口的其它特征。在建立窗口时,要定义一些该窗口所独有的特征。
在为程序建立窗口之前,必须首先呼叫RegisterClass注册一个窗口类别。该函数只需要一个参数,即一个指向型态为WNDCLASS的结构指针。此结构包括两个指向字符串的字段,因此结构在WINUSER.H表头文件中定义了两种不同的方式,第一个是ASCII版的WNDCLASSA:
typedef struct tagWNDCLASSA { UINT style ; WNDPROC lpfnWndProc ; int cbClsExtra ; int cbWndExtra ; HINSTANCE hInstance ; HICON hIcon ; HCURSOR hCursor ; HBRUSH hbrBackground ; LPCSTR lpszMenuName ; LPCSTR lpszClassName ; } WNDCLASSA, * PWNDCLASSA, NEAR * NPWNDCLASSA, FAR * LPWNDCLASSA ;
在这里提示一下数据型态和匈牙利表示法:其中的lpfn前缀代表「指向函数的长指标」。(在Win32 API中,长指标和短指标(或者近程指标)没有区别。这只是16位Windows的遗物。)cb前缀代表「字节数」而且通常作为一个常数来表示一个字节的大小。h前缀是一个句柄,而hbr前缀代表「一个画刷的代号」。lpsz前缀代表「指向以0结尾字符串的指针」。
Unicode版的结构定义如下:
typedef struct tagWNDCLASSW { UINT style ; WNDPROC lpfnWndProc ; int cbClsExtra ; int cbWndExtra ; HINSTANCE hInstance ; HICON hIcon ; HCURSOR hCursor ; HBRUSH hbrBackground ; LPCWSTR lpszMenuName ; LPCWSTR lpszClassName ; } WNDCLASSW, * PWNDCLASSW, NEAR * NPWNDCLASSW, FAR * LPWNDCLASSW ;
与前者唯一的区别在于最后两个字段定义为指向宽字符串常数,而不是指向ASCII字符串常数。
WINUSER.H定义了WNDCLASSA和WNDCLASSW结构(以及指向结构的指针)以后,表头文件依据对UNICODE标识符的解释,定义了WNDCLASS和指向WNDCLASS的指标(包括一些向后兼容的程序代码):
#ifdef UNICODE typedef WNDCLASSW WNDCLASS ; typedef PWNDCLASSW PWNDCLASS ; typedef NPWNDCLASSW NPWNDCLASS ; typedef LPWNDCLASSW LPWNDCLASS ; #else typedef WNDCLASSA WNDCLASS ; typedef PWNDCLASSA PWNDCLASS ; typedef NPWNDCLASSA NPWNDCLASS ; typedef LPWNDCLASSA LPWNDCLASS ; #endif
本书后面列出结构时,将只列出功用相同的结构定义,对WNDCLASS就像这样:
typedef struct { UINT style ; WNDPROC lpfnWndProc ; int cbClsExtra ; int cbWndExtra ; HINSTANCE hInstance ; HICON hIcon ; HCURSOR hCursor ; HBRUSH hbrBackground ; LPCTSTR lpszMenuName ; LPCTSTR lpszClassName ; } WNDCLASS, * PWNDCLASS ;
我也不再着重说明指标的定义。一个程序写作者的程序不应该因为使用以LP或NP为前缀的不同指针型态而被搅乱。
在WinMain中为WNDCLASS定义一个结构,通常像这样:
WNDCLASS wndclass ;
然后,你就可以初始化该结构的10个字段,并呼叫RegisterClass。
在WNDCLASS结构中最重要的两个字段是第二个和最后一个,第二个字段(lpfnWndProc) 是依据这个类别来建立的所有窗口所使用的窗口消息处理程序的地址。在HELLOWIN.C中,这个是WndProc函数。最后一个字段是窗口类别的文字名称。程序写作者可以随意定义其名称。在只建立一个窗口的程序中,窗口类别名称通常设定为程序名称。
其它字段依照下面的方法描述了窗口类别的一些特征。让我们依次看看WNDCLASS结构中的每个字段。
叙述
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
使用C的位「或」运算子结合了两个「窗口类别样式」标识符。在表头文件WINUSER.H中,已定义了一整组以CS为前缀的标识符:
#define CS_VREDRAW 0x0001 #define CS_HREDRAW 0x0002 #define CS_KEYCVTWINDOW 0x0004 #define CS_DBLCLKS 0x0008 #define CS_OWNDC 0x0020 #define CS_CLASSDC 0x0040 #define CS_PARENTDC 0x0080 #define CS_NOKEYCVT 0x0100 #define CS_NOCLOSE 0x0200 #define CS_SAVEBITS 0x0800 #define CS_BYTEALIGNCLIENT 0x1000 #define CS_BYTEALIGNWINDOW 0x2000 #define CS_GLOBALCLASS 0x4000 #define CS_IME 0x00010000
由于每个标识符都可以在一个复合值中设置一个位的值,所以按这种方式定义的标识符通常称为「位旗标」。通常我们只使用少数的窗口类别样式。HELLOWIN中用到的这两个标识符表示,所有依据此类别建立的窗口,每当窗口的水平方向大小(CS_HREDRAW)或者垂直方向大小(CS_VREDRAW)改变之后,窗口要完全重画。改变HELLOWIN的窗口大小,可以看到字符串仍然显示在窗口的中央,这两个标识符确保了这一点。不久我们就将看到窗口消息处理程序是如何得知这种窗口大小的变化的。
WNDCLASS结构的第二个字段由以下叙述进行初始化:
wndclass.lpfnWndProc = WndProc ;
这条叙述将这个窗口类别的窗口消息处理程序设定为WndProc,即HELLOWIN.C中的第二个函数。这个过程将处理依据这个窗口类别建立的所有窗口的全部消息。在C语言中,像这样在结构中使用函数名时,真正提供的是指向函数的指标。
下面两个字段用于在窗口类别结构和Windows内部保存的窗口结构中预留一些额外空间:
wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ;
程序可以根据需要来使用预留的空间。HELLOWIN没有使用它们,所以设定值为0。否则,和匈牙利表示法所指示的一样,这个字段将被当成「预留的字节数」。(在第七章的程序CHECKER3将使用cbWndExtra字段。)
下一个字段就是程序的执行实体句柄(它也是WinMain的参数之一):
wndclass.hInstance = hInstance ;
叙述
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
为所有依据这个窗口类别建立的窗口设置一个图标。图标是一个小的位图图像,它对使用者代表程序,将出现在Windows工作列中和窗口的标题列的左端。在本书的后面,您将学习如何为您的Windows程序自订图标。现在,为了方便起见,我们将使用预先定义的图示。
要取得预先定义图示的句柄,可以将第一个参数设定为NULL来呼叫LoadIcon。在加载程序写作者自订的图标时(图标应该存放在磁盘上的.EXE程序文件中),这个参数应该被设定为程序的执行实体句柄hInstance。第二个参数代表图示。对于预先定义图示,此参数是以IDI开始的标识符(「ID代表图示」),标识符在WINUSER.H中定义。IDI_APPLICATION图标是一个简单的窗口小图形。LoadIcon函数传回该图示的句柄。我们并不关心这个句柄的实际值,它只用于设置hIcon字段元的值。该字段在WNDCLASS结构中定义为HICON型态,此型态名的含义为「handle to an icon(图示句柄)」。
叙述
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
与前一条叙述非常相似。LoadCursor函数加载一个预先定义的鼠标光标(命名为IDC_ARROW),并传回该游标的句柄。该句柄被设定给WNDCLASS结构的hCursor字段。当鼠标光标在依据这个类别建立的窗口的显示区域上出现时,它变成一个小箭头。
下一个字段指定依据这个类别建立的窗口背景颜色。hbrBackground字段名称中的hbr前缀代表「handle to a brush(画刷句柄)」。画刷是个绘图词汇,指用来填充一个区域的着色样式。Windows有几个标准画刷,也称为「备用(stock)」画刷。这里所示的GetStockObject呼叫将传回一个白色画刷的句柄:
wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
这意味着窗口显示区域的背景完全为白色,这是一种极其普遍的做法。
下一个字段指定窗口类别菜单。HElLOWIN没有应用程序菜单,所以该字段被设定为NULL:
wndclass.lpszMenuName = NULL ;
最后,必须给出一个类别名称。对于小程序,类别名称可以与程序名相同,即存放在szAppName变量中的「HelloWin」字符串。
wndclass.lpszClassName = szAppName ;
至于该字符串由ASCII字符组成或由Unicode字符组成,取决于是否定义了UNICODE标识符。
在初始化该结构的10个字段后,HELLOWIN呼叫RegisterClass来注册这个窗口类别。该函数只有一个参数,即指向WNDCLASS结构的指针。实际上,RegisterClassA函数将获得一个指向WNDCLASSA结构的指针,而RegisterClassW函数将获得一个指向WNDCLASSW结构的指针。程序要使用哪个函数来注册窗口类别,取决于发送给窗口的消息包含ASCII文字还是Unicode文字。
现在有一个问题:如果用定义的UNICODE标识符编译了程序,程序将呼叫RegisterClassW。该程序可以在Microsoft Windows NT中执行良好。但如果此程序在Windows 98上执行,RegisterClassW函数并未真地被执行到。函数有一个进入点,但函数呼叫后只传回0,表明错误。对于在Windows 98下执行的Unicode程序来说,这是一个通知使用者有问题并终止执行的好机会。这是本书中多数程序处理RegisterClass函数呼叫的方法:
if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; }
由于MessageBoxW是可在Windows 98环境下执行的几个Unicode函数之一,所以其执行正常。
当然,这段程序假定RegisterClass不会因为其它原因而呼叫失败,诸如WNDCLASS结构中lpfnWndProc字段被设定成NULL之类的错误。GetLastError函数会帮助您确定在这样的情况下产生错误的原因。GetLastError是Windows中常用的函数,它可以在函数呼叫失败时获得更多错误信息。不同函数的文件将指出您是否能够用GetLastError来获得这些信息。在Windows 98中呼叫RegisterClassW时,GetLastError将传回120。在WINERROR.H中您可以看到,值120与标识符ERROR_CALL_NOT_IMPLEMENTED相等。您也可以在/Platform SDK/Windows Base Services/Debugging and Error Handling/Error Codes/System Errors - Numerical Order查看错误。
一些Windows程序写作者喜欢检查所有可能发生错误的函数呼叫的传回值。这么做确实有点道理,相信您也非常习惯在配置内存后检查错误。而许多Windows函数需要配置内存。例如,RegisterClass需要配置内存,以保存窗口类别的信息。如此一来,您就应该要检查这个函数的执行结果。另一方面说来,如果由于RegisterClass不能得到所需要的内存,它会声明呼叫失败,而Windows大概也快当掉了。
在本书的范例程序中,我做了最少的错误检查。这不是因为我认为错误检查不是一个好方法,而是因为这会让我们在程序举例中分心。
最后,一个老经验是:在一些Windows范例程序中,您可能在WinMain中看到以下程序代码:
if (!hPrevInstance) { wndclass.cbStyle = CS_HREDRAW | CS_VREDRAW ; 初始化其它 wndclass RegisterClass (&wndclass) ; }
这是出于「旧习难改」的原因。在16位的Windows中,如果您启动正在执行的程序的一个新执行实体,WinMain的hPrevInstance参数将是前一个执行实体的执行实体句柄。为节省内存,两个或多个执行实体就可能会共享相同的窗口类别。这样,窗口类别就只在hPrevInstance是NULL的时候才注册,这表明程序没有其它执行实体。
在32位的Windows中,hPrevInstance总是NULL。此程序代码会正常执行,而实际上也没必要检查hPrevInstance。