Programming windows 5th Edition Chapter 3 窗口和消息
1. 本章讲述了一个最简单的带窗口的windows程序HelloWin。
2. 窗口类别。在创建窗口(调用CreateWindow)之前,需要先注册窗口类别(RegisterClass)。所谓窗口类别表示的是窗口大体应该遵循 的共性,比如按钮窗口,他们的类别是一样的,比如光标,背景,最关键的是消息处理函数,这些东西都应该是一样的,至于每个按钮窗口不同的地方,比如大小, 位置等,放在CreateWindow中由我们来定义。所以窗口类别就是抽象了窗口的一些共性的东西,多个窗口在CreateWindow的时候,可以共 享一个窗口类别,也就是只需执行一次RegisterClass。窗口类别中最重要的就是定义了消息CALLBACK函数了,也就是消息处理函数了。看到 这里有人会担心了,既然窗口类别中定义了消息处理函数,那么,如果一堆窗口创建的时候都是用的一个窗口类别的话,那这些窗口的消息处理函数就都是一样的 喽?--没错,OK,那既然一样,我怎么对这些窗口的同一个消息做不同的处理呢?--很简单,在消息处理函数中,有一个参数hWnd,用来标识了窗口的句 柄(也就是CreateWindow返回的东西),用这个就可以区分不同的窗口了。
3. 来看本章HelloWin的代码:
-
Code:
Select all
-
/*------------------------------------------------------------
HELLOWIN.C -- Displays "Hello, Windows!" in client area
Eric Zhang 2007
------------------------------------------------------------*/
#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 ;
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 (BLACK_BRUSH) ;
wndclass.lpszMenuName = 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!"), -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) ;
}
4. HelloWin很简单,就是建立了一个窗口,在中央位置写上文字,另外在窗口显示的时候会播放一个wav文件。注:为了播放这个wav文件,代码中调用了PlaySound函数,要使用这个函数在Project中记得将WinMM.lib文件链接进去。
5. 现在来看代码。首先代码中有很多常量,比如CS_HREDRAW, WM_PAINT等,这些常量不用记忆,需要的时候查手册即可,这些常量都有特定的意义和一些约定俗成的书写格式:
附件1
除此以外,代码中还有一些其他的数据类型,如下:
附件2 附件3
附 件3中列的都是句柄,只是指向不同东西的句柄而已。在windows中,句柄使用非常普遍,就好像是Linux编程中的文件描述符一样。windows中 的句柄就是一个32位的整数,用来代表一个对象而已。很多windows函数都需要句柄,这样windows函数才知道我们操作的对象是谁。
6. 匈牙利标记法。代码中的变量名称基本上都使用了匈牙利标记法,其实第一章中的程序开始就已经开始使用了,这里做了一个总结,以后写代码可以参考:
附件4
7. 现在来看代码。首先定义了一个函数原型,也就是我们的消息处理函数的原型。LRESULT就是long型;CALLBACK和WINAPI一样,就是 __stdcall,加上CALLBACK,windows将来调用我们的消息处理函数的时候,免得发生call convention不一致的问题,所以一般都要加这个CALLBACK;WndProc的四个参数中,第一个参数是窗口句柄,第二个参数是消息代码,第 三个参数和第四个参数是两个parameter,wParam在32位windows下,就是UINT -- unsigned int,lParam就是long型,在16位windows下,wParam原来是WORD -- unsigned short类型。所以,按照匈牙利表示法,wParam应该写成uiParam,但由于原来16位windows下是wParam,所以就没有修改保留了 下来。
8. 然后的代码就是注册窗口类别了。注册窗口类别调用RegisterClass,这个函数只需要一个参数,就是窗口类别structure WNDCLASS。由于这个WNDCLASS中含有两个字符串成员变量,所以回想起第二章的内容,自然这个WNDCLASS就有WNDCLASSA和 WNDCLASSW两个版本了。而这个WNDCLASS中的字符串自然就是 T 类型的字符串了。OK,下面来看这个Structure中的内容:
style -- 指出窗口的风格。代码中写的是CS_HREDRAW|CS_VREDRAW,这表示窗口在横向或纵向发生变化的时候,重绘窗口。后面会看到,这就是为什么 窗口大小改变后,文字依然会显示在窗口的正中,就是因为这里的设定,窗口重绘,触发WM_PAINT消息。在WINUSER.H中可以找到全部的CS打头 的(表示窗口类别样式)的常量。注意观察这些常量的数值,其实他们每个数值都将二进制数中的某一位置成了1,所以,他们之间可以互相用 或 符号连接起来。
lpfnWndProc -- 这是最重要的了,指定消息处理函数,这是一个函数指针。
cbClsExtra, cbWndExtra -- 预留的空间,如果需要,程序可以自定义这部分内容。
hInstance -- 就是我们这个程序的实例句柄,WinMain的第一个参数
hIcon -- 指定窗口图标,该图标出现在窗口标题栏的最左边和任务栏中
hCursor -- 指定鼠标的光标。这里,LoadIcon和LoadCursor,如果第一个参数是NULL,就表示使用windows预定义好的那些图标和鼠标光标。具 体看这两个函数的MSDN;如果我们要使用自己的图标或光标,那么,第一个参数要设置成hInstance,也就是我们这个程序的实例句柄。
hbrBackground -- 窗口的背景。hbr表示handle to a brush。windows中的brush表示用来填充一个区域的着色样式。Windows有几个标准的brush,他们称为Stock Brush(库存画刷)。所以我们用GetStockObject得到了一个白色画刷。
lpszMenuName -- 指定应用程序菜单
lpszClassName -- 窗口类别名称。这里的内容要和CreateWindow中的第一个参数一样,如果我们创建的这个window想使用这个窗口类别的话。
9. OK,一切具备,调用RegisterClass了,这里代码做了一个出错处理判断,这是应该的,因为RegisterClass也有A/W两个版本,如 果我们定义了UNICODE条件编译变量,那么将来被调用的将是RegisterClassW,而win98虽然有这个函数,但是一调这个函数win98 会立刻返回错误(win9x只有很小一部分支持Unicode,比如MessageBoxW是可以用的)。
BTW: 对于出错的处理,很多时候我们会调用GetLastError函数,这个函数能返回上次操作失败的错误码,这些错误码对应的含义可以在MSDN的 /Platform SDK/Windows Base Services/Debugging and Error Handling/Error Codes/System Errors - Numerical Order中找到
10. 然后就看到CreateWindow函数了。代码中有注释,就不一项一项说了。只有第二个参数,CW_OVERLAPPEDWINDOW,这个常量是一批常量的合集(定义在WINUSER.H中):
-
Code:
Select all
-
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | \
WS_CAPTION | \
WS_SYSMENU | \
WS_THICKFRAME | \
WS_MINIMIZEBOX | \
WS_MAXIMIZEBOX)
用来表示窗口的表现形态。
11. 然后代码调用了ShowWindow,用来显示窗口,然后又调了UpdateWindow,这个函数会触发WM_PAINT消息,用来初始化窗口中的图形和文字。
其 实我自己试了一下,发现不调用UpdateWindow也照样有WM_PAINT消息的产生,窗口也照样能正常显示。原来我以为这里调用 UpdateWindow只是为了规范,可是后来通过下一章的学习,发现这里调用UpdateWindow是有道理的。道理就在于 UpdateWindow是产生WM_PAINT消息,但是不同的是,这次产生的WM_PAINT消息是非入队消息,也就是说,调用这个函数 后,windows会立马调用我们的消息处理函数,直到这个消息处理完毕,UpdateWindow才返回。如果不使用这个函数,一般情况下产生的 WM_PAINT消息只会排在消息队列中,而且windows约定WM_PAINT消息是一个优先级低的消息,所以,这样会导致界面更新不及时--简言 之,当我们需要窗口界面立即更新的时候,请使用UpdateWindow,在很多地方都可以使用。
12. 然后就进入消息循环了,用GetMessage从消息队列中取出消息,注意:windows中不是什么消息都会进消息队列的,有些消息是不进消息队列的, 此时windows会直接调用消息处理函数,这些消息叫非入队消息。不过我们不需要关心这些复杂的问题,我们只需要知道--任何消息都会在我们的消息处理 函数中被处理。GetMessage中MSG的结构是这样的:
-
Code:
Select all
-
typedef struct tagMSG
{
HWND hwnd ;
UINT message ;
WPARAM wParam ;
LPARAM lParam ;
DWORD time ;
POINT pt ;
} MSG, * PMSG ;
GetMessage函数的参数含义可以看MSDN,第二个参数为NULL表示接受本进程创建的所有窗口的消息;第三个参数和第四个参数表示过滤 消息的min和max值,也就是说,消息编号在这个区间内的才会被GetMessage抓下来,如果这两个值都是0,那就是没有消息过滤。 GetMessage函数会一直返回非0的数,除非取到了WM_QUIT消息。
MSG结构中,hwnd是消息发生窗口句柄,message是消息编号,wParam,lParam是消息参数,可以在这里取到消息的详细内容,time是发生时间,PT是发生该消息的时候鼠标所在的坐标。
13. TranslateMessage是把msg传给windows,进行一些键盘转换。这一点会在第六章深入讨论;DispatchMessage是把 msg传回给windows,然后windows就会调用我们的消息处理函数(WndProc),所以对于入队消息,是由我们程序手动 GetMessage,然后再派发的,非入队消息不通过GetMessage,直接由windows调用消息处理函数。
14. 对于消息循环和消息处理。有几点需要重视:
1. DispatchMessage将消息派发出去后,会一直等到这个消息被处理完(消息处理函数返回了),本函数才会返回,GetMessage才会被执行去取下一条消息
2. 在消息处理函数中,也是一样,如果在消息处理过程中触发了其他的消息,那么触发消息的代码也会block,一直到被触发出来的新消息被处理完为止。
3. 消息处理函数必须return 0,除非是return DefWindowProc。
15. 下面的代码就是消息处理函数了,有这么一些要点:
WM_CREATE消息会在CreateWindow执行过程中产生
对于WM_PAINT消息的处理,基本上都是从BeginPaint开始,以EndPaint结束。因为BeginPaint会返回一个hdc, 有了这个hdc,我们才能在窗口上写字,绘图。而且BeginPaint会填充一个PAINTSTRUCTURE,能告诉我们哪些地方需要重绘 (invalid)。在BeginPaint中,如果显示区域背景没有被删除,则由windows来删除,windows根据注册窗口类别中的背景来擦除 显示区域。如果我们的消息处理函数不处理WM_PAINT,那么DefWindowProc只是简单的调用BeginPaint和EndPaint来使显 示区域重新生效。
WM_DESTROY消息相应中我们调用了PostQuitMessage函数,该函数 会发出WM_QUIT消息,GetMessage碰到这个消息会返回0,从而WinMain中的消息循环结束。参数0会被设置到该消息的wParam,所 以我们在WinMain中return了msg.wParam
有时候,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中的消息循环终止,然后程序结 束。