(基础篇) 第三章 窗口和消息

3. 窗口和消息

在前两章,程序使用了同一个函数MessageBox来向使用者输出文字。MessageBox函数会建立一个「窗口」。在Windows中,「窗口」一词有确切的含义。一个窗口就是屏幕上的一个矩形区域,它接收使用者的输入并以文字或图形的格式显示输出内容。

MessageBox函数建立一个窗口,但这只是一个功能有限的特殊窗口。消息窗口有一个带关闭按钮的标题列、一个选项图标、一行或多行文字,以及最多四个按钮。当然,必须选择Windows提供给您的图标与按钮。

MessageBox函数非常有用,但下面不会过多地使用它。我们不能在消息方块中显示图形,而且也不能在消息方块中添加菜单。要添加这些对象,就需要建立自己的窗口,现在就开始。

自己的窗口
 

建立窗口很简单,只需调用CreateWindow函数即可。

好啦,虽然建立窗口的函数的确名为CreateWindow,而且您也能在/Platform SDK/User Interface Services/Windowing/Windows/Window Reference/Window Functions找到此文件,但您将发现CreateWindow的第一个参数就是所谓的「窗口类别名称」,并且该窗口类别连接所谓的「窗口消息处理程序」。在我们调用CreateWindow之前,有一点背景知识会对您大有帮助。

总体结构
 

进行Windows程序设计,实际上是在进行一种面向对象的程序设计(OOP)。这一点在Windows中使用得最多的对象上表现最为明显。这种对象正是Windows之所以命名为「Windows」的原因,它具有人格化的特徵,甚至可能会在您的梦中出现,这就是那个叫做「窗口」的东西。

桌面上最明显的窗口就是应用程序窗口。这些窗口含有显示程序名称的标题列、菜单甚至可能还有工具列和卷动列。另一类窗口是对话框,它可以有标题列也可以没有标题列。

装饰对话框表面的还有各式各样的按键、单选按钮、复选框、列表框、卷动列和文字输入区域。其中每一个小的视觉对象都是一个窗口。更确切地说,这些都称为「子窗口」或「控件窗口」或「子窗口控件」。

作为对象,使用者会在屏幕上看到这些窗口,并通过键盘和鼠标直接与它们进行交互操作。更有趣的是,程序写作者的观点与使用者的观点极其类似。窗口以「消息」的形式接收窗口的输入,窗口也用消息与其它窗口通讯。对消息的理解将是学习如何写作Windows程序所必须越过的障碍之一。

这有一个Windows的消息示例:我们知道,大多数的Windows程序都有大小合适的应用程序窗口。也就是说,您能够通过鼠标拖动窗口的边框来改变窗口的大小。通常,程序将通过改变窗口中的内容来回应这种大小的变化。您可能会猜测(并且您也是正确的),是Windows本身而不是应用程序在处理与使用者重新调整窗口大小相关的全部杂乱程序。由于应用程序能改变其显示的样子,所以它也「知道」窗口大小改变了。

应用程序是如何知道使用者改变了窗口的大小的呢?由于程序写作者习惯了往常的文字模式程序,操作系统没有设置将此类消息通知给使用者的机制。问题的关键在于理解Windows所使用的架构。当使用者改变窗口的大小时,Window给程序发送一个消息指出新窗口的大小。然后程序就可以调整窗口中的内容,以回应大小的变化。

「Windows给程序发送消息。」我们希望读者不要对这句话视而不见。它到底表达了什么意思呢?我们在这里讨论的是程序码,而不是一个电子邮件系统。操作系统怎么给程序发送消息呢?

其实,所谓「Windows给程序发送消息」,是指Windows调用程序中的一个函数,该函数的参数描述了这个特定消息。这种位于Windows程序中的函数称为「窗口消息处理程序」。

无疑,读者对程序调用操作系统的做法是很熟悉的。例如,程序在打开磁片文件时就要使用有关的系统调用。读者所不习惯的,可能是操作系统调用程序,而这正是Windows面向对象架构的基础。

程序建立的每一个窗口都有相关的窗口消息处理程序。这个窗口消息处理程序是一个函数,既可以在程序中,也可以在动态连接库中。Windows通过调用窗口消息处理程序来给窗口发送消息。窗口消息处理程序根据此消息进行处理,然后将控制传回给Windows。

更确切地说,窗口通常是在「窗口类别」的基础上建立的。窗口类别标识了处理窗口消息的窗口消息处理程序。使用窗口类别使多个窗口能够属于同一个窗口类别,并使用同一个窗口消息处理程序。例如,所有Windows程序中的所有按钮均依据同一个窗口类别。这个窗口类别与一个处理所有按钮消息的窗口消息处理程序(位于Windows的动态连接库中)联结。

在面向对象的程序设计中,对象是程序与资料的组合。窗口是一种对象,其程序是窗口消息处理程序。资料是窗口消息处理程序保存的信息和Windows为每个窗口以及系统中那个窗口类别保存的信息。

窗口消息处理程序处理给窗口发送消息。这些消息经常是告知窗口,使用者正使用键盘或者鼠标进行输入。这正是按键窗口知道它被「按下」的奥妙所在。在窗口大小改变,或者窗口表面需要重画时,由其它消息通知窗口。

Windows程序开始执行后,Windows为该程序建立一个「消息伫列」。这个消息伫列用来存放该程序可能建立的各种不同窗口的消息。程序中有一小段程序码,叫做「消息循环」,用来从伫列中取出消息,并且将它们发送给相应的窗口消息处理程序。有些消息直接发送给窗口消息处理程序,不用放入消息伫列中。

如果您对这段Windows架构过于简略的描述将信将疑,就让我们去看看在实际的程序中,窗口、窗口类别、窗口消息处理程序、消息伫列、消息循环和窗口消息是如何相互配合的。这或许会对您有些帮助。

HELLOWIN程序
 

建立一个窗口首先需要注册一个窗口类别,那需要一个窗口消息处理程序来处理窗口消息。处理窗口消息对每个Windows程序都带来了些开销。程序3-1所示的HELLOWIN程序中整个做的事情差不多就是料理这些事情。

 程序3-1  HELLOWIN
HELLOWIN.C
/*------------------------------------------------------------------------
   	HELLOWIN.C -- Displays "Hello, Windows 98!" in client area
		      (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 ("HelloWin") ;
	HWND	hwnd ;
	MSG	msg ;
	WNDCLAS	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.lpszMenuNam	= NULL ;
	wndclass.lpszClassName	= szAppName ;

	if (!RegisterClass (&wndclass))
     {
		MessageBox (	NULL, TEXT ("This program requires Windows NT!"), 
          				szAppName, MB_ICONERROR) ;
		return 0 ;
     }
	hwnd = CreateWindow( szAppName,	// window class name
			TEXT ("The Hello Program"),	// window caption
			WS_OVERLAPPEDWINDOW,	// window style
			CW_USEDEFAULT,	// initial x position
			CW_USEDEFAULT,	// initial y position
			CW_USEDEFAULT,	// initial x size
			CW_USEDEFAULT,	// initial y size
			NULL,			// parent window handle
		    NULL,	        // window menu handle
		    hInstance,	    // program instance handle
		    NULL) ; 	    // creation parameters
     
	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)
{
	HDC			hdc ;
	PAINTSTRUCT ps ;
	RECT		rect ;
     
	switch (message)
     {
	case WM_CREATE:
	PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ;
		return 0 ;

	case 	WM_PAINT:
		hdc = BeginPaint (hwnd, &ps) ;
          
		GetClientRect (hwnd, &rect) ;
          
		DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect,
			DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
  			EndPaint (hwnd, &ps) ;
			return 0 ;
          
	case	WM_DESTROY:
		PostQuitMessage (0) ;
		return 0 ;
     }
   return DefWindowProc (hwnd, message, wParam, lParam) ;
}

程序建立一个普通的应用程序窗口,如图3-1所示。在窗口显示区域的中央显示「Hello, Windows 98!」。如果安装了音效卡,那么您还可以听到相应的朗读声音。


 

 

图3-1 HELLOWIN窗口

提醒您注意:如果您使用Microsoft Visual C++ 为此程序建立新项目,那么您得加上连结程序所需的程序库文件。从 Project 菜单选择 Setting 选项,然后选取 Link 页面标签。从 Category 列表框中选择 General ,然后在 Object/Library Modules 文字方块添加 WINMM.LIB  Windows multimedia  Windows多媒体 )。您这样做是因为HELLOWIN将使用多媒体功能调用,而内定的项目中又不包括多媒体程序库文件。不然连结程序报告了错误信息,表明PlaySound函数不可用。

HELLOWIN将访问文件HELLOWIN.WAV,该文件在本书所附光碟的HELLOWIN目录中。执行HELLOWIN.EXE时,内定的目录必须是HELLOWIN。在Visual C++中执行此程序时,虽然执行档会产生在HELLOWIN的RELEASE或DEBUG子目录中,但执进程序的目录还是必须在HELLOWIN中。

通盘考量
 

实际上,每一个Windows程序码中都包括HELLOWIN.C程序的大部分。没人能真正记住此程序的全部写法;通常,Windows程序写作者在开始写一个新程序时总是会复制一个现有的程序,然后再做相应的修改。您可以按此习惯自由使用本书附带光碟中的程序。

上面提到,HELLOWIN将在其窗口的中央显示字符串。这种说法不是完全正确的。文字实际显示在程序显示区域的中央,它在图3-1中是标题列和边界范围内的大片白色区域。这区别对我们来说很重要;显示区域就是程序自由绘图并且向使用者显示输出结果的窗口区域。

如果您认真思考一下,将会发现虽然只有80进程序码,这个窗口却令人惊讶地具有许多功能。您可以用鼠标按住标题列,在屏幕上移动窗口;可以按住大小边框,改变窗口的大小。在窗口大小改变时,程序自动地将「Hello, Windows 98!」字符串重新定位在显示区域的中央。您可以按最大化按钮,放大HELLOWIN以充满整个屏幕;也可以按最小化按钮,将程序缩小成一个图标。您可以在系统菜单中执行所有选项(就是按下在标题列最左端的小图标);也可以从系统菜单中选择 Close 选项,或者单击标题列最右端的关闭按钮,或者双击标题列最左端的图标,来关闭窗口以终止程序的执行。

我们将在本章的余下部分对此程序作一详细的检查。当然,我们首先要从整体上看一下。

与前两章中的示例程序一样,HELLOWIN.C也有一个WinMain函数,但它还有另外一个函数,名为WndProc。这就是窗口消息处理程序。注意,在HELLOWIN.C中没有调用WndProc的程序码。当然,在WinMain中有对WndProc的参考,而这就是该函数要在程序开头附近声明的原因。

Windows函数调用
 

HELLOWIN至少调用了18个Windows函数。下面以它们在HELLOWIN中出现的次序列出这些函数以及各自的简明描述:

  • LoadIcon 载入图标供程序使用。
     
  • LoadCursor 载入鼠标游标供程序使用。
     
  • GetStockObject 取得一个图形对象(在这个例子中,是取得绘制窗口背景的画刷对象)。
     
  • RegisterClass 为程序窗口注册窗口类别。
     
  • MessageBox 显示消息方块。
     
  • CreateWindow 根据窗口类别建立一个窗口。
     
  • ShowWindow 在屏幕上显示窗口。
     
  • UpdateWindow 指示窗口自我更新。
     
  • GetMessage 从消息伫列中取得消息。
     
  • TranslateMessage 转译某些键盘消息。
     
  • DispatchMessage 将消息发送给窗口消息处理程序。
     
  • PlaySound 播放一个音效文件。
     
  • BeginPaint 开始绘制窗口。
     
  • GetClientRect 取得窗口显示区域的大小。
     
  • DrawText 显示字符串。
     
  • EndPaint 结束绘制窗口。
     
  • PostQuitMessage 在消息伫列中插入一个「退出程序」消息。
     
  • DefWindowProc 执内联定的消息处理。
     

这些函数均在Platform SDK文件中说明,并在不同的表头文件中声明,其中绝大多数声明在WINUSER.H中。

大写字母识别字
 

读者可能注意到,HELLOWIN.C中有几个大写的识别字,这些识别字是在Windows表头文件中定义的。有些识别字含有两个字母或者三个字母的字首,这些字首后头接著一个底线:

 

CS_HREDRAWDT_VCENTERSND_FILENAME
CS_VREDRAWIDC_ARROWWM_CREATE
CW_USEDEFAULTIDI_APPLICATIONWM_DESTROY
DT_CENTERMB_ICONERRORWM_PAINT
DT_SINGLELINESND_ASYNCWS_OVERLAPPEDWINDOW

这些是简单的数值常数。字首指示该常数所属的类别,如表3-1所示。

表3-1
字首 类别
CS窗口类别样式
CW建立窗口
DT绘制文字
IDI图标ID
IDC游标ID
MB消息方块
SND声音
WM窗口消息
WS窗口样式

奉劝程序写作者不要费力气去记忆Windows程序设计中的数值常数。实际上,Windows中使用的每个数值常数在表头文件中均有相应的识别字定义。

新的资料型态
 

HELLOWIN.C中的其它识别字是新的资料型态,也在Windows表头文件中使用typedef叙述或者#define叙述加以定义了。最初是为了便于将Windows程序从原来的16位系统上移植到未来的使用32位(或者其它)技术的操作系统上。这种作法并不如当时每个人想像的那样顺利,但是这种概念基本上是正确的。

有时这些新的资料型态只是为了方便缩写。例如,用于WndProc的第二个参数的UINT资料型态只是一个unsigned int (无正负号整数),在Windows 98中,这是一个32位的值。用于WinMain的第三个参数的PSTR资料型态是指向一个字符串的指针,即是一个char *。

其它资料型态的含义不太明显。例如,WndProc的第三和第四个参数分别被定义为WPARAM和LPARAM,这些名字的来源有点历史背景:当Windows还是16位系统时,WndProc的第三个参数被定义为一个WORD,这是一个16位的 无正负号短 (unsigned short)整数,而第四个参数被定义为一个LONG,这是一个32位有正负号长整数,从而导致了文字「PARAM」前面加上了前置字首「W」和「L」。当然,在32位的Windows中,WPARAM被定义为一个UINT,而LPARAM被定义为一个LONG(这就是C中的long整数型态),因此窗口消息处理程序的这两个参数都是32位的值。这也许有点奇怪,因为WORD资料型态在Windows98中仍然被定义为一种16位的 无正负号 整数,因此「PARAM」前的「W」就有点误用了。

WndProc函数传回一个型态为LRESULT的值,该值简单地被定义为一个LONG。WinMain函数被指定了一个WINAPI型态(在表头文件中定义的所有Windows函数都被指定这种型态),而WndProc函数被指定一个CALLBACK型态。这两个识别字都被定义为_stdcall,表示在Windows本身和使用者的应用程序之间发生的函数调用的调用参数传递方式。

HELLOWIN还使用了Windows表头文件中定义的四种资料结构(我们将在本章稍后加以讨论)。这些资料结构如表3-2所示。

表3-2
结构 含义
MSG消息结构
WNDCLASS窗口类别结构
PAINTSTRUCT绘图结构
RECT矩形结构

前面两个资料结构在WinMain中使用,分别定义了两个名为msg和wndclass的结构,后面两个资料结构在WndProc中使用,分别定义了ps和rect结构。

句柄简介
 

最后,还有三个大写识别字(见表3-3),用于不同型态的「句柄」:

表3-3
识别字 含义
HINSTANCE执行实体(程序自身)句柄
HWND窗口句柄
HDC设备上下文句柄

句柄在Windows中使用非常频繁。在本章结束之前,我们将遇到HICON(图标句柄)、HCURSOR(鼠标游标句柄)和HBRUSH(画刷句柄)。

句柄是一个(通常为32位的)整数,它代表一个对象。Windows中的句柄类似传统C或者MS-DOS程序设计中使用的文件句柄。程序几乎总是通过调用Windows函数取得句柄。程序在其它Windows函数中使用这个句柄,以使用它代表的对象。句柄的实际值对程序来说是无关紧要的。但是,向您的程序提供句柄的Windows模组知道如何利用它来使用相对应的对象。

匈牙利表示法
 

读者可能注意到,HELLOWIN.C中有一些变数的名字显得很古怪。如szCmdLine,它是传递给WinMain的参数。

许多Windows程序写作者使用一种叫做「匈牙利表示法」的变数命名通则。这是为了纪念传奇性的Microsoft程序写作者Charles Simonyi。非常简单,变数名以一个或者多个小写字母开始,这些字母表示变数的资料型态。例如,szCmdLine中的sz代表「以0结尾的字符串」。在hInstance和hPrevInstance中的h字首表示「句柄」;在iCmdShow中的i字首表示「整数」。 WndProc的后两个参数也使用匈牙利表示法。正如我在前面已经解释过的,尽管wParam应该更适当地被命名为uiParam(代表「无正负号整数」),但是因为这两个参数是使用资料型态WPARAM和LPARAM定义的,因此保留它们传统的名字。

在命名结构变数时,可以用结构名(或者结构名的一种缩写)的小写作为变数名的字首,或者用作整个变数名。例如,在HELLOWIN. C的WinMain函数中,msg变数是MSG型态的结构;wndclass是WNDCLASSEX型态的一个结构。在WndPmc函数中,ps是一个PAINTSTRUCT结构,rect是一个RECT结构。

匈牙利表示法能够帮助程序写作者及早发现并避免程序中的错误。由于变数名既描述了变数的作用,又描述了其资料型态,就比较容易避免产生资料型态不合的错误。

表3-4列出了在本书中经常用到的变数字首。

表3-4
字首 资料型态
cchar或WCHAR或TCHAR
byBYTE (无正负号字符)
nshort
iint
x, yint分别用作x座标和y座标
cx, cyint分别用作x长度和y长度;C代表「计数器」
b或fBOOL (int);f代表「旗标」
wWORD (无正负号短整数)
lLONG (长整数)
dwDWORD (无正负号长整数)
fnfunction(函数)
sstring(字符串)
sz以字节值0结尾的字符串
h句柄
p指针

注册窗口类别
 

窗口依照某一窗口类别建立,窗口类别用以标识处理窗口消息的窗口消息处理程序。

不同窗口可以依照同一种窗口类别建立。例如,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。

建立窗口
 

窗口类定义了窗口的一般特徵,因此可以使用同一窗口类别建立许多不同的窗口。实际调用CreateWindow建立窗口时,可能指定有关窗口的更详细的信息。

Windows程序设计新手有时会混淆窗口类别和窗口之间的区别,以及为什么一个窗口的所有特徵不能被一次设定好。实际上,以这种方式分开这些样式信息是非常方便的。例如,所有的按钮窗口都可以依据同样的窗口类别来建立,与这个窗口类别相关的窗口消息处理程序位于Windows内部。由窗口类别来负责处理按钮的键盘和鼠标输入,并定义按钮在屏幕上的外观形象。从这一点看来,所有的按钮都是以同样的方式工作的。但是并非所有的按钮都是一样的。它们可以有不同的大小,不同的屏幕位置,以及不同的字符串。后面的这样一些特徵是窗口定义的一部分,而不是窗口类定义的。

传递给RegisterClass函数的信息会在一个资料结构中设定好,而传递给CreateWindow函数的信息会在函数单独的参数中设定好。下面是HELLOWIN.C中的CreateWindows调用,每一个栏位都做了完整的说明:

hwnd = CreateWindow (szAppName, 	// window class name
	TEXT (	"The Hello Program"), 	// window caption
		WS_OVERLAPPEDWINDOW,	    // window style
      	CW_USEDEFAULT,	            // initial x position
       	CW_USEDEFAULT,	            // initial y position
		CW_USEDEFAULT, 	            // initial x size
  		CW_USEDEFAULT,	            // initial y size
		NULL,    	                // parent window handle
 		NULL,	                    // window menu handle
    	hInstance, 	                // program instance handle
    	NULL) ;  	                // creation parameters

在这里,我不想提实际上有CreateWindowA函数和CreateWindowW函数,两个函数分别将前两个参数当成ASCII或者Unicode字符串来处理。

标记为「window class name」的参数是szAppName,它含有字符串「HelloWin」-这是程序注册的窗口类别名称。这就是我们建立的窗口联结窗口类别的方式。

此程序建立的窗口是一个普通的重叠式窗口。它含有一个标题列,标题列左边有一个系统菜单按钮,标题列右边有缩小、放大和关闭图标,四周还有一个表示窗口大小的边框。这是标准样式的窗口,名为WS_OVERLAPPEDWINDOW,出现在CreateWindow的「窗口样式」参数中。如果看一下WINUSER.H,您将会发现此样式是几种位旗标的组合:

#define 	WS_OVERLAPPEDWINDOW (WS_OVERLAPPED	| /
      		WS_CAPTION 		                    | /
     		WS_SYSMENU 		                    | /
      		WS_THICKFRAME		                | /
     		WS_MINIMIZEBOX 		                | /
     		WS_MAXIMIZEBOX)

「窗口标题」是显示在标题列中的文字。

注释著「initial x position」和「initial y position」的参数指定了窗口左上角相对于屏幕左上角的初始位置。由于这些参数使用CW_USEDEFAULT识别字,指示Windows使用重叠窗口的内定位置。(CW_USEDEFAULT定义为0x80000000。)内定情况下,Windows依次对新建立的窗口定位,使各窗口左上角的垂直和水平距离在屏幕上按一定的大小递增。与此类似,注释著「initial x size」和「initial y size」的参数分别指定窗口的宽度和高度。同样使用了CW_USEDEFAULT识别字,表明希望Windows使用内定尺寸。

在建立一个「最上层」窗口,如应用程序窗口时,注释为「父窗口句柄」的参数设定为NULL。通常,如果窗口之间存在有父子关系,则子窗口总是出现在父窗口的上面。应用程序窗口出现在桌面窗口的上面,但不必为调用CreateWindow而找出桌面窗口的句柄。

因为窗口没有菜单,所以「窗口菜单句柄」也设定为NULL。「程序执行实体句柄」设定为执行实体句柄,它是作为WinMain的参数传递给这个程序的。最后,「建立参数」指针设定为NULL,可以用这个参数访问稍后程序中可能引用到的资料。

CreateWindow传回被建立的窗口的句柄,该句柄存放在变数hwnd中,后者被定义为HWND型态(「窗口句柄型态」)。Windows中的每个窗口都有一个句柄,程序用句柄来使用窗口。许多Windows函数需要使用hwnd作为参数,这样,Windows才能知道函数是针对哪个窗口的。如果一个程序建立了许多窗口,则每个窗口均有一个句柄。窗口句柄是Windows程序所处理最重要的句柄之一。

显示窗口
 

在CreateWindow调用传回之后,Windows内部已经建立了这个窗口。这就是说,Windows已经配置了一块内存,用来保存在CreateWindow调用中指定窗口的全部信息跟一些其它信息,而Windows稍后就是依据窗口句柄找到这些信息的。

然而,光是这样子,窗口并不会出现在显示器上。您还需要两个函数调用,一个是:

ShowWindow (hwnd, iCmdShow) ;

第一个参数是刚刚用CreateWindow建立的窗口句柄。第二个参数是作为参数传给WinMain的iCmdShow。它确定最初如何在屏幕上显示窗口,是一般大小、最小化还是最大化。在开始菜单中安装程序时,使用者可能做出最佳选择。如果窗口按一般大小显示,那么WinMain接收到后传递给ShowWindow的就是SW_SHOWNORMAL;如果窗口是最大化显示的,则为SW_SHOWMAXIMIZED。而如果窗口只显示在工作列上,则是SW_SHOWMINNOACTIVE。

ShowWindow函数在显示器上显示窗口。如果ShowWindow的第二个参数是SW_SHOWNORMAL,则窗口的显示区域就会被窗口类别中定义的背景画刷所覆盖。函数调用

UpdateWindow (hwnd) ;

会重画显示区域。它经由发送给窗口消息处理程序(即HELLOWIN.C中的WndProc函数)一个WM_PAINT消息做到这一点。后面,我们将说明WndProc如何处理这个消息。

消息循环
 

调用UpdateWindow之后,窗口就出现在显示器上。程序现在必须准备读入使用者用键盘和鼠标输入的资料。Windows为当前执行的每个Windows程序维护一个「消息伫列」。在发生输入事件之后,Windows将事件转换为一个「消息」并将消息放入程序的消息伫列中。

程序通过执行一块称之为「消息循环」的程序码从消息伫列中取出消息:

while	(GetMessage (&msg, NULL, 0, 0))
{
	TranslateMessage (&msg) ;
	DispatchMessage (&msg) ;
}

msg变数是型态为MSG的结构,型态MSG在WINUSER.H中定义如下:

typedef struct tagMSG 
{
	HWND	hwnd ;
	UINT	message ;
	WPARAM 	wParam ;
	LPARAM 	lParam ;
	DWORD  	time ;
	POINT  	pt ;
}
MSG, * PMSG ;

POINT资料型态也是一个结构,它在WINDEF.H中定义如下:

typedef struct tagPOINT
{
	LONG  x ;
	LONG  y ;
}
POINT, * PPOINT;

消息循环以GetMessage调用开始,它从消息伫列中取出一个消息:

GetMessage (&msg, NULL, 0, 0)

这一调用传给Windows一个指针,指向名为msg的MSG结构。第二、第三和第四个参数设定为NULL或者0,表示程序接收它自己建立的所有窗口的所有消息。Windows用从消息伫列中取出的下一个消息来填充消息结构的各个栏位,结构的各个栏位包括:

  • hwnd 接收消息的窗口句柄。在HELLOWIN程序中,这一参数与CreateWindow传回的hwnd值相同,因为这是该程序拥有的唯一窗口。
     
  • message 消息识别字。这是一个数值,用以标识消息。对于每个消息,均有一个对应的识别字,这些识别字定义于Windows表头文件(其中大多数在WINUSER.H中),以字首WM(「window message」,窗口消息)开头。例如,使用者将鼠标游标放在HELLOWIN显示区域之内,并按下鼠标左按钮,Windows就在消息伫列中放入一个消息,该消息的message栏位等于WM_LBUTTONDOWN。这是一个常数,其值为0x0201。
     
  • wParam 一个32位的「message parameter(消息参数)」,其含义和数值根据消息的不同而不同。
     
  • lParam 一个32位的消息参数,其值与消息有关。
     
  • time 消息放入消息伫列中的时间。
     
  • pt 消息放入消息伫列时的鼠标座标。
     

只要从消息伫列中取出消息的message栏位不为WM_QUIT(其值为0x0012),GetMessage就传回一个非零值。WM_QUIT消息将导致GetMessage传回0。

叙述

TranslateMessage (&msg) ;

将msg结构传给Windows,进行一些键盘转换。(关于这一点,我们将在第六章中深入讨论。)

叙述

DispatchMessage (&msg) ;

又将msg结构回传给Windows。然后,Windows将该消息发送给适当的窗口消息处理程序,让它进行处理。这也就是说,Windows将调用窗口消息处理程序。在HELLOWIN中,这个窗口消息处理程序就是WndProe函数。处理完消息之后,WndProc传回到Windows。此时,Windows还停留在DispatchMessage调用中。在结束DispatchMessage调用的处理之后,Windows回到HELLOWIN,并且接著从下一个GetMessage调用开始消息循环。

窗口消息处理程序
 

以上我们所讨论的都是必要的开销:注册窗口类别,建立窗口,然后在屏幕上显示窗口,程序进入消息循环,然后不断从消息伫列中取出消息来处理。

实际的动作发生在窗口消息处理程序中。窗口消息处理程序确定了在窗口的显示区域中显示些什么以及窗口怎样回应使用者输入。

在HELLOWIN中,窗口消息处理程序是命名为WndProc的函数。窗口消息处理程序可任意命名(只要求不和其它名字发生冲突)。一个Windows程序可以包含多个窗口消息处理程序。一个窗口消息处理程序总是与调用RegisterClass注册的特定窗口类别相关联。CreateWindow函数根据特定窗口类别建立一个窗口。但依据一个窗口类别,可以建立多个窗口。

窗口消息处理程序总是定义为如下形式:

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

注意,窗口消息处理程序的四个参数与MSG结构的前四个栏位是相同的。第一个参数hwnd是接收消息的窗口的句柄,它与CreateWindow函数的传回值相同。对于与HELLOWIN相似的程序(只建立一个窗口),这个参数是程序所知道的唯一窗口句柄。如果程序是依据同一窗口类别(同时也是同一窗口消息处理程序)建立多个窗口,则hwnd标识接收消息的特定窗口。

第二个参数与MSG结构中的message栏位相同,它是标识消息的数值。最后两个参数都是32位的消息参数,提供关于消息的更多信息。这些参数包含每个消息型态的详细信息。有时消息参数是两个存放在一起的16位值,而有时消息参数又是一个指向字符串或资料结构的指针。

程序通常不直接调用窗口消息处理程序,窗口消息处理程序通常由Windows本身调用。通过调用SendMessage函数,程序能够直接调用它自己的窗口消息处理程序。我们将在后面的章节讨论SendMessage函数。

处理消息
 

窗口消息处理程序所接受的每个消息均是用一个数值来标识的,也就是传给窗口消息处理程序的message参数。Windows表头文件WINUSER.H为每个消息参数定义以「WM」(窗口消息)为字首开头的识别字。

一般来说,Windows程序写作者使用switch和case结构来确定窗口消息处理程序接收的是什么消息,以及如何适当地处理它。窗口消息处理程序在处理消息时,必须传回0。窗口消息处理程序不予处理的所有消息应该被传给名为DefWindowProc的Windows函数。从DefWindowProc传回的值必须由窗口消息处理程序传回。

在HELLOWIN中,WndProc只选择处理三种消息:WM_CREATE、WM_PAINT和WM_DESTROY。窗口消息处理程序的结构如下:

switch (iMsg)
{
case	WM_CREATE :
  	处理WM_CREATE消息
	return 0 ;
          
case	WM_PAINT :
 	处理WM_PAINT消息
	return 0 ;
          
case	WM_DESTROY :
	处理WM_DESTROY消息
	return 0 ;
}
return DefWindowProc (hwnd, iMsg, wParam, lParam) ;

调用DefWindowProc来为窗口消息处理程序不予处理的所有消息提供内定处理,这是很重要的。不然一般动作,如终止程序,将不会正常执行。

播放音效文件
 

窗口消息处理程序接收的第一个消息-也是WndProc选择处理的第一个消息-是WM_CREATE。当Windows在WinMain中处理CreateWindow函数时,WndProc接收这个消息。就是说,在HELLOWIN调用CreateWindow时,Windows将做一些它必须做的工作。在这些工作中,Windows调用WndProc,将第一个参数设定为窗口句柄,第二个参数设定为WM_CREATE。WndProc处理WM_CREATE消息并将控制传回给Windows。 Windows然后可以从CreateWindow调用中传回到HELLOWIN中,继续在WinMain中进行下一步的处理。

通常,窗口消息处理程序在WM_CREATE处理期间进行一次窗口初始化。HELLOWIN对这个消息的处理中播放一个名为HELLOWIN.WAV的音效文件。它使用简单的PlaySound函数来做到这一点。该函数说明在/Platform SDK/Graphics and Multimedia Services/Multimedia Audio/Waveform Audio中,而文件在/Platform SDK/Graphics and Multimedia Services/Multimedia Reference/Multimedia Functions中。

PlaySound的第一个参数是音效文件的名称(它也可能是在Control Panel的Sounds中定义的一种声音的别名,或者是一个程序资源)。第二个参数只有当音效文件是一种资源时才被使用。第三个参数指定一些选项。在这个例子中,我指定第一个参数是一个文件名,并且非同步地播放声音,即PlaySound函数调用在音效文件开始播放时立即传回,而不会等待它的完成。在这种方法下,程序能够继续初始化。

WndProc通过从窗口消息处理程序中传回0,结束了整个WM_CREATE的处理。

WM_PAINT消息
 

WndProc处理的第二个消息为WM_PAINT。这个消息在Windows程序设计中是很重要的。当窗口显示区域的一部分显示内容或者全部变为「无效」,以致于必须「更新画面」时,将由这个消息通知程序。

显示区域的显示内容怎么会变得无效呢?在最初建立窗口的时候,整个显示区域都是无效的,因为程序还没有在窗口上画什么东西。第一条WM_PAINT消息(通常发生在WinMain中调用UpdateWindow时)指示窗口消息处理程序在显示区域上画一些东西。

在使用者改变HELLOWIN窗口的大小后,显示区域的显示内容重新变得无效。读者应该还记得,HELLOWIN中wndclass结构的style栏位设定为标志CS_HREDRAW和CS_VREDRAW,这样的格式设定指示Windows,在窗口大小改变后,就把整个窗口显示内容当成无效。然后,窗口消息处理程序将收到一条WM_PAINT消息。

当使用者将HELLOWIN最小化,然后再次将窗口恢复为以前的大小时,Windows将不会保存显示区域的内容。在图形环境下,窗口显示区域涉及的资料量很大。因此,Windows令窗口无效,窗口消息处理程序接收一条WM_PAINT消息,并自动恢复其窗口的内容。

在移动窗口以致其相互重叠时,Windows不保存一个窗口中被另一个窗口所遮盖的内容。在这一部分不再被遮盖之后,它就被标志为无效。窗口消息处理程序接收到一条WM_PAINT消息,以更新窗口的内容。

对WM_PAINT的处理几乎总是从一个BeginPaint调用开始:

hdc = BeginPaint (hwnd, &ps) ;

而以一个EndPaint调用结束:

EndPaint (hwnd, &ps) ;

在这两个调用中,第一个参数都是程序的窗口句柄,第二个参数是指向型态为PAINTSTRUCT的结构指针。PAINTSTRUCT结构中包含一些窗口消息处理程序,可以用来更新显示区域的内容。我们将在下一章中讨论该结构的各个栏位。现在我们只在BeginPaint和EndPaint函数中用到它。

在BeginPaint调用中,如果显示区域的背景还未被删除,则由Windows来删除。它使用注册窗口类别的WNDCLASS结构的hbrBackground栏位中指定的画刷来删除背景。在HELLOWIN中, 这是一个白色备用画刷。这意味著,Windows将通过把窗口背景设定为白色来删除窗口背景。BeginPaint调用令整个显示区域有效,并传回一个「设备上下文句柄」。设备上下文是指实体输出设备(如显示器)及其设备驱动程序。在窗口的显示区域显示文字和图形需要设备上下文句柄。但是从BeginPaint传回的设备上下文句柄不能在显示区域之外绘图,读者可以试一试。EndPaint释放设备上下文句柄,使之不再有效。

如果窗口消息处理程序不处理WM_PAINT消息(这是很罕见的),它们必须被传送给DefWindowProc。DefWindowProc只是依次调用BeginPaint和EndPaint,以使显示区域有效。

调用完BeginPaint之后,WndProc接著调用GetClientRect:

GetClientRect (hwnd, &rect) ;

第一个参数是程序窗口的句柄。第二个参数是一个指针,指向一个RECT型态的rectangle结构。该结构有四个LONG栏位,分别为left、top、right和bottom。GetClientRect将这四个栏位设定为窗口显示区域的尺寸。left和top栏位通常设定为0,right和bottom栏位设定为显示区域的宽度和高度(图元点数)。

WndProc除了将该RECT结构指针作为DrawText的第四个参数传递外,不再对它做其它处理:

DrawText (	hdc, TEXT ("Hello, Windows 98!"), -1, &rect,
      		DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;

DrawText可以输出文字(正如其名字所表明的一样)。由于该函数要输出文字,第一个参数是从BeginPaint传回的设备上下文句柄,第二个参数是要输出的文字,第三个参数是 -1,指示字符串是以字节0终结的。

DrawText最后一个参数是一系列位旗标,它们均在WINUSER.H中定义(虽然由于其显示输出的效果,使得DrawText像一个GDI函数调用,但它确实因为相当高级的画图功能而成为User模组的一部分。此函数在/Platform SDK/Graphics and Multimedia Services/GDI/Fonts and Text中说明)。旗标指示了文字必须显示在一行上,水平方向和垂直方向都位于第四个参数指定的矩形中央。因此,这个函数调用将让字符串「Hello, Windows 98!」显示在显示区域的中央。

一旦显示区域变得无效(正如在改变大小时所发生的情况一样),WndProc就接收到一个新的WM_PAINT消息。WndProc通过调用GetClientRect取得变化后的窗口大小,并在新窗口的中央显示文字。

WM_DESTROY消息
 

WM_DESTROY消息是另一个重要消息。这一个消息指示,Windows正在根据使用者的指示关闭窗口。该消息是使用者单击Close按钮或者在程序的系统菜单上选择 Close时发生的(在本章的后面,我们将详细讨论WM_DESTROY消息是如何生效的)。

HELLOWIN通过调用PostQuitMessage以标准方式回应WM_DESTROY消息:

PostQuitMessage (0) ;

该函数在程序的消息伫列中插入一个WM_QUIT消息。前面提到过,GetMessage对于除了WM_QUIT之外的从消息伫列中取出的所有消息都传回非0值。而当GetMessage得到一个WM_QUIT消息时,它传回0。这将导致WinMain退出消息循环,并终止程序。然后程序执行下面的叙述:

return msg.wParam ;

结构的wParam栏位是传递给PostQuitMessage函数的值(通常是0)。然后return叙述将退出WinMain并终止程序。

WINDOWS程序设计的难点
 

即使有了对HELLOWIN的说明,读者对程序的结构和原理可能仍然觉得神秘。在为传统环境编写简单的C程序时,整个程序可能包含在main函数中。而在HELLOWIN中,WinMain只包含了注册窗口类别,建立窗口,从消息伫列中取出消息和发送消息所必须的程序码。

程序的所有实际动作均在窗口消息处理程序中发生。在HELLOWIN中,这些动作不多,WndProc只是简单地播放了一个音效文件并在窗口中显示一个字符串。但是在后面的章节中,读者将发现,Windows程序所作的一切,都是回应发送给窗口消息处理程序的消息。这是概念上的主要难点之一,在开始写作Windows程序之前,必须先搞清楚。

别调用我,我会调用您
 

前面我们提到过,程序写作者已经熟悉了使用操作系统调用的做法。例如,C程序写作者使用fopen函数打开文件。fopen函数最终通过调用操作系统来打开文件,这一点问题也没有。

但是Windows不同,尽管Windows有1000个以上的函数可供程序调用,但Windows也调用使用者程序,比如前面定义的窗口消息处理程序WndProc。窗口消息处理程序与窗口类别相关,窗口类别是程序调用RegisterClass注册的。依据该类别建立的窗口使用这个窗口消息处理程序来处理窗口的所有消息。Windows通过调用窗口消息处理程序对窗口发送消息。

在第一次建立窗口时,Windows调用WndProc。在窗口关闭时,Windows也调用WndProc。窗口改变大小、移动或者变成图标时,从菜单中选择某一项目、挪动卷动列、按下鼠标按钮或者从键盘输入字符时,以及窗口显示区域必须被更新时,Windows都要调用WndProc。

所有这些WndProc调用都以消息的形式进行。在大多数Windows程序中,程序的主要部分都用来处理消息。Windows可以发送给窗口消息处理程序的消息通常都以WM开头的名字标识,并且都在WINUSER.H表头文件中定义。

实际上,从程序外调用程序内的常式这一种做法,在传统的程序设计中并非前所未闻。C中的signal函数可以拦截Ctrl-C中断或操作系统的其它中断。为MS-DOS编写的老程序中经常有拦截硬件中断的程序码。

但在Windows中,这种概念扩展为包括一切事件。窗口中发生的一切都以消息的形式传给窗口消息处理程序。然后,窗口消息处理程序以某种方式回应这个消息,或者将消息传给DefWindowProc,进内联定处理。

在HELLOWIN中,窗口消息处理程序的wParam和lParam参数除了作为传递给DefWindowProc的参数外,不再有其它用处。这些参数给出了关于消息的其它信息,参数的含义与具体消息相关。

让我们来看一个例子。一旦窗口的显示区域大小发生了改变,Windows就调用窗口的窗口消息处理程序。窗口消息处理程序的hwnd参数是改变大小的窗口的句柄(请记住,一个窗口消息处理程序能处理依据同一个窗口类别建立的多个窗口的消息。参数hwnd让窗口消息处理程序知道是哪个窗口在接收消息)。参数message是WM_SIZE。消息WM_SIZE的参数wParam的值是SIZE_RESTORED、SIZE_MINIMIZED、SIZE_MAXIMIZED、SIZE_MAXSHOW或SIZE_MAXHIDE (在WINUSER.H表头文件中分别定义为数字0到4)。也就是说,参数wParam表明窗口是非最小化还是非最大化,是最小化、最大化,还是隐藏。

lParam参数包含了新窗口的大小,新宽度和新高度均为16位值,合在一起成为32位的lParam。WINDEF.H中提供了帮助程序写作者从lParam中取出这两个值的宏,我们将在下一章说明这个宏。

有时候,DefWindowProc处理完消息后会产生其它的消息。例如,假设使用者执行HELLOWIN,并且使用者最终单击了 Close 按钮,或者假设用键盘或鼠标从系统菜单中选择了 Close , DefWindowProc处理这一键盘或者鼠标输入,在检测到使用者选择了 Close 选项之后,它给窗口消息处理程序发送一条WM_SYSCOMMAND消息。WndProc将这个消息传给DefWindowProc。DefWindowProc给窗口消息处理程序发送一条WM_CLOSE消息来回应之。WndProc再次将它传给DefWindowProc。DestroyWindow调用DestroyWindow来回应这条WM_CLOSE消息。DestroyWindow导致Windows给窗口消息处理程序发送一条WM_DESTROY消息。WndProc再调用PostQuitMessage,将一条WM_QUIT消息放入消息伫列中,以此来回应此消息。这个消息导致WinMain中的消息循环终止,然后程序结束。

伫列化消息与非伫列化消息
 

我们已经谈到过,Windows给窗口发送消息,这意味著Windows调用窗口消息处理程序。但是,Windows程序也有一个消息循环,它调用GetMessage从消息伫列中取出消息,并且调用DispatchMessage将消息发送给窗口消息处理程序。

那么,Windows程序是依次等待消息(类似于普通程序中相同的键盘输入),然后将消息送到某地方去的吗?或者,它是直接从程序外面接收消息的吗?实际上,两种情况都存在。

消息能够被分为「伫列化的」和「非伫列化的」。伫列化的消息是由Windows放入程序消息伫列中的。在程序的消息循环中,重新传回并分配给窗口消息处理程序。非伫列化的消息在Windows调用窗口时直接送给窗口消息处理程序。也就是说,伫列化的消息被「发送」给消息伫列,而非伫列化的消息则「发送」给窗口消息处理程序。任何情况下,窗口消息处理程序都将获得窗口所有的消息--包括伫列化的和非伫列化的。窗口消息处理程序是窗口的「消息中心」。

伫列化消息基本上是使用者输入的结果,以击键(如WM_KEYDOWN和WM_KEYUP消息)、击键产生的字符(WM_CHAR)、鼠标移动(WM_MOUSEMOVE)和鼠标按钮(WM_LBUTTONDOWN)的形式给出。伫列化消息还包含时钟消息(WM_TIMER)、更新消息(WM_PAINT)和退出消息(WM_QUIT)。

非伫列化消息则是其它消息。在许多情况下,非伫列化消息来自调用特定的Windows函数。例如,当WinMain调用CreateWindow时,Windows将建立窗口并在处理中给窗口消息处理程序发送一个WM_CREATE消息。当WinMain调用ShowWindow时,Windows将给窗口消息处理程序发送WM_SIZE和WM_SHOWWINDOW消息。当WinMain调用UpdateWindow时,Windows将给窗口消息处理程序发送WM_PAINT消息。键盘或鼠标输入时发出的伫列化消息信号,也能在非伫列化消息中出现。例如,用键盘或鼠标选择了一个菜单项时,键盘或鼠标消息就是伫列化的,而说明菜单项已选中的WM_COMMAND消息则可能就是非伫列化的。

这一过程显然很复杂,但幸运的是,其中的大部分是由Windows解决的,不关我们的程序的事。从窗口消息处理程序的角度来看,这些消息是以一种有序的、同步的方式进出的。窗口消息处理程序可以处理它们,也可以不处理。

当我说消息是以一种有序的同步的方式进出时,我是说首先消息与硬件的中断不同。在一个窗口消息处理程序中处理消息时,程序不会被其它消息突然中断。

虽然Windows程序可以多线程执行,但每个线程的消息伫列只为窗口消息处理程序在该线程中执行的窗口处理消息。换句话说,消息循环和窗口消息处理程序不是并发执行的。当一个消息循环从其消息伫列中接收一个消息,然后调用DispatchMessage将消息发送给窗口消息处理程序时,直到窗口消息处理程序将控制传回给Windows,DispatchMessage才能结束执行。

当然,窗口消息处理程序能调用给窗口消息处理程序发送另一个消息的函数。这时,窗口消息处理程序必须在函数调用传回之前完成对第二个消息的处理。那时窗口消息处理程序将处理最初的消息。例如,当窗口程序调用UpdateWindow时,Windows将调用窗口消息处理程序来处理WM_PAINT消息。窗口消息处理程序处理WM_PAINT消息结束以后,UpdateWindow调用将把控制传回给窗口消息处理程序。

这也就是说窗口消息处理程序必须是可重入。在大多数情况下,这不会带来问题,但是程序写作者应该意识到这一点。例如,假设您在窗口消息处理程序中处理一个消息时设置了一个静态变数,然后调用了一个Windows函数。在这个函数传回时,您还能保证那个变数的值还是原来那个吗?难说--很可能您调用的Windows函数产生了另外一个消息,并且窗口消息处理程序在处理这个消息时改变了该变数的值。这也是在编译Windows程序时,有些编译最佳化选项必须关闭的原因之一。

在许多情况下,窗口消息处理程序必须保存它从消息中取得的信息,并在处理另一个消息时使用这些信息。这些信息可以储存在窗口的静态(static)变数或整体变数中。

当然,读者将在下面几章对此有一个更清楚的了解,因为窗口消息处理程序将处理更多的消息。

行动迅速
 

Windows 98和Windows NT都是优先权式的多任务环境。这意味著当一个程序在进行一项长时间工作时,Windows可以允许使用者将控制切换到另一个程序中。这是一件好事,也是现在的Windows优越于以前16位Windows的地方。

然而,由于Windows设计的方式,这种优先权式多任务并不总是以您希望的样子工作。例如,假设您的程序花费一分钟左右来处理某一个消息。是的,使用者可以将控制切换到另一个程序,但是却无法对您的程序进行任何动作。使用者无法移动您的程序窗口、缩放它、最小化、关闭它、什么都不能做。这是因为您的窗口消息处理程序正忙于进行一项长时间的作业。表面上并不是窗口消息处理程序在执行它自己的移动和缩放操作,但实际上确实是它在做。这就是DefWindowProc部分的工作,它必须被考虑为您的窗口消息处理程序的一部分。

如果您的程序在处理某些消息时需要长时间的作业的话,可以选择我在第二十章里描述的那些方法来做得更有优雅一些。即使是在优先权式多任务环境中,也不应该让您的程序呆在屏幕上一动不动。这会让使用者讨厌的,他们会认为您的程序中有bug、不标准的动作,说明文件没写好。最好让使用者觉得程序只停了一下子就把全部消息中快速料理完了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值