持之以恒
分类:
2009-05-18 10:40:52
本文内容来自于<Windows 程序设计>一书
作者:Charles Petzold
译 者:余孟学
校 阅 者:林昭仁
注意文章内容和书中原文并不完全一样,要的地方加入了自己的想法,所以不一定正确!
3. 窗口和消息
一个窗口就是屏幕上的一个矩形区域,它接收使用者的输入并以文字或图形的格式显示输出内容。
建立窗口很简单,只需调用CreateWindow函数即可。
好啦,虽然建立窗口的函数的确名为CreateWindow,但您将发现CreateWindow的第一个参数就是所谓的「窗口类别名称」,并且该窗口类别连接所谓的「窗口消息处理程序」。在我们调用CreateWindow之前,有一点背景知识会对您大有帮助。
总体结构
窗口以「消息」的形式接收窗口的输入,窗口也用消息与其它窗口通讯。对消息的理解将是学习如何写作Windows程序所必须越过的障碍之一。
所谓Windows给程序发送消息,是指Windows调用程序中的一个函数,该函数的参数描述了这个特定消息。这种位于Windows程序中的函数称为「窗口消息处理程序」。
无疑,读者对程序调用操作系统的做法是很熟悉的。读者所不习惯的,可能是操作系统调用程序,而这正是Windows面向对象架构的基础。
程序建立的每一个窗口都有相关的窗口消息处理程序。这个窗口消息处理程序是一个函数,既可以在程序中,也可以在动态连接库中。Windows通过调用窗口消息处理程序来给窗口发送消息。窗口消息处理程序根据此消息进行处理,然后将控制传回给Windows。
更确切地说,窗口通常是在「窗口类别」的基础上建立的。窗口类别标识了处理窗口消息的窗口消息处理程序。使用窗口类别使多个窗口能够属于同一个窗口类别,并使用同一个窗口消息处理程序。例如,所有Windows程序中的所有按钮均依据同一个窗口类别。这个窗口类别与一个处理所有按钮消息的窗口消息处理程序(位于Windows的动态连接库中)联结。
在面向对象的程序设计中,对象是程序与资料的组合(或者可以叫做是方法和数据)。窗口是一种对象,其程序是窗口消息处理程序。资料是窗口消息处理程序保存的信息和Windows为每个窗口以及系统中那个窗口类别保存的信息。
窗口消息处理程序处理给窗口发送的消息。这些消息经常是告知窗口,使用者正使用键盘或者鼠标进行输入。这正是按键窗口知道它被「按下」的奥妙所在。在窗口大小改变,或者窗口表面需要重画时,由其它消息通知窗口。
Windows程序开始执行后,Windows为该程序建立一个「消息队列」。这个消息队列用来存放该程序可能建立的各种不同窗口的消息。程序中有一小段程序码,叫做「消息循环」,用来从队列中取出消息,并且将它们发送给相应的窗口消息处理程序。有些消息直接发送给窗口消息处理程序,不用放入消息队列中。
如果您对这段Windows架构过于简略的描述将信将疑,就让我们去看看在实际的程序中,窗口、窗口类别、窗口消息处理程序、消息队列、消息循环和窗口消息是如何相互配合的。这或许会对您有些帮助。
建立一个窗口首先需要注册一个窗口类别,那需要一个窗口消息处理程序来处理窗口消息。处理窗口消息对每个Windows程序都带来了些开销。程序3-1所示的HELLOWIN程序中整个做的事情差不多就是料理这些事情。
程序3-1
HELLOWIN
HELLOWIN.C
/*------------------------------------------------------------------------
HELLOWIN.C -- Displays "Hello,
Windows 98!" in client area
(c) Charles Petzold, 1998
-----------------------------------------------------------------------*/
#include
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:
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) ;
}
通盘考量
我们首先要从整体上看一下。
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 将消息发送给窗口消息处理程序。
·
BeginPaint 开始绘制窗口。
·
GetClientRect 取得窗口显示区域的大小。
·
DrawText 显示字符串。
·
EndPaint 结束绘制窗口。
·
PostQuitMessage 在消息队列中插入一个「退出程序」消息。
·
DefWindowProc 执内联定的消息处理。
这些函数均在Platform SDK文件中说明,并在不同的表头文件中声明,其中绝大多数声明在WINUSER.H中。
大写字母识别字
表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位的值。
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 |
设备上下文句柄 |
句柄是一个(通常为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 |
字首 |
资料型态 |
c |
char或WCHAR或TCHAR |
by |
BYTE (无正负号字符) |
n |
short |
i |
int |
x, y |
int分别用作x座标和y座标 |
cx, cy |
int分别用作x长度和y长度;C代表「计数器」 |
b或f |
BOOL (int);f代表「旗标」 |
w |
WORD (无正负号短整数) |
l |
LONG (长整数) |
dw |
DWORD (无正负号长整数) |
fn |
function(函数) |
s |
string(字符串) |
sz |
以字节值0结尾的字符串 |
h |
句柄 |
p |
指针 |
注册窗口类别
窗口依照某一窗口类别建立,窗口类别用以标识处理窗口消息的窗口消息处理程序。
不同窗口可以依照同一种窗口类别建立。例如,Windows中的所有按钮窗口-包括按键、复选框,以及单选按钮-都是依据同一种窗口类别建立的。窗口类定义了窗口消息处理程序和依据此类别建立的窗口的其它特徵。在建立窗口时,要定义一些该窗口所独有的特征。
在为程序建立窗口之前,必须首先调用RegisterClass注册一个窗口类别。该函数只需要一个参数,即一个指向型态为WNDCLASS的结构指针。此结构包括两个指向字符串的栏位,因此结构在WINUSER.H表头文件中定义
对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来注册这个窗口类别。