win32程序是靠消息驱动,消息映射是win32编程的一大主题,作为win32平台上程序员,绝对有必要深入了解消息在程序中的流动。
消息映射在MFC的实现里,是用宏定义出一颗庞大的消息树,当有消息到来的时候,遍历这棵消息树去寻找对应的处理函数,但在流动的时候,有一个优先级的问题(关于这问题在《深入浅出MFC》里有阐述),这优先级会常常使人陷入迷惑当中。
而在delphi里,消息映射直接由编译器支持,这给delphi带来了很大的简洁性,但同时也缺乏一种灵活。
delphi处理消息的方式比MFC友好得多,比如一个Button,在Clicked消息到的时候,必定会触发OnClick函数,但是若深究为什么会触发OnClick函数就没那么容易了。
本文提出一个与MFC和Delphi都不同的实现方式,比MFC友好,比delphi的灵活。但是要注意,友好和灵活都是相对,有一利必有一弊,很多时候都是利弊之间的权衡。
本文要求的读者是C++的中级程序员,或有志于了解win32消息的读者。
在学习程序语言的书籍中,一开始总有个hello world程序,本文也不例外,若读者对win32 sdk的hello world程序很熟悉,可以略过不看(本不想写这hello world程序,但为了本文的完整性,最终还是加上)。
先看看Windows的消息结构,定义如下(摘自windows.h):
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
#ifdef _MAC
DWORD lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
其中,常用的只有四个字段:hwnd, message, wParam, lParam,根据message的不同,wParam和lParam有不同的含义。比如,若message为WM_CREATE消息,lParam指向一个类型为CREATESTRUCT的变量地址。但若message为WM_DESTROY消息,wParam和lParam的值则没有任何意义。
1.基本的SDK程序(摘自VC模板生成)
TCHAR szWindowClass[]= _T("Hello");
TCHAR szTitle[]= _T("Hello");
ATOM MyRegisterClass(HINSTANCE hInstance)
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
MyRegisterClass(hInstance);
HWND hWnd = ::CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
::ShowWindow(hWnd, nCmdShow);
::UpdateWindow(hWnd);
// Main message loop:
MSG msg;
while ( ::GetMessage(&msg, NULL, 0, 0))
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
return int(msg.wParam);
}
首先,要向windows注册一个类(class),这个类名为szWindowClass(也就是"Hello"),其回调函数为WndProc:
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
// 初始化wcex的其它成员...
wcex.lpszClassName = szWindowClass;
wcex.lpfnWndProc = (WNDPROC)WndProc;
return ::RegisterClassEx(&wcex);
}
接着,创建一个主窗口:
HWND hWnd = ::CreateWindow(szWindowClass, ...);
我们指定这个窗口的类名为szWindowClass(也就是"Hello"),在这里我们要明白,为什么要指定类名?指定类名最重要的意义是为这个窗口指定一个回调函数(也就是注册"Hello"类时指定的WndProc)。为什么要指定回调函数?它的意义在于,当窗口有消息到时,windows会通过这个回调函数来告诉窗口:“嘿,你有消息到了!”。(若这个类名没有被注册,创建窗口肯定是失败,这时候CreateWindow会返回0。)
紧接着,就进入消息处理循环:
while ( ::GetMessage(...) )
{
// ....
}
粗粗一看,这个消息处理循环和我们刚才创建的窗口一点关系都没有,但实际上,由于每个注册的类,都有一个回调函数,当这些类的实例(也就是窗口)有消息到时,DispatchMessage会自动找到这个实例的回调函数来告知。
那什么时候退出这个消息处理循环?当GetMessage检索到WM_QUIT消息的时候。那GetMessage什么检索到WM_QUIT消息?这就要看回调函数的实现。
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps;
HDC hdc;
switch (message)
{
case WM_PAINT:
hdc = ::BeginPaint(hWnd, &ps);
::TextOut( hdc, 0, 0, _T("Hello"), 5 );
::EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
::PostQuitMessage(0);
break;
default:
return ::DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
上面已经说过,当窗口有消息时,windows会自动调用WndProc函数,窗口被关闭时,windows会自动往窗口发送WM_DESTROY消息,在这里,窗口关闭了,程序自然也要退出,所以,我们一旦收到WM_DESTROY,就调用PostQuitMessage往消息队列投递WM_QUIT消息,从而强制GetMessage返回FALSE,退出消息处理循环。
PostQuitMessage有一个参数,用来指示程序退出的返回值,这个值会被被存放到与WM_QUIT对应的msg.wParam里。另外,要注意到这个函数的名字有一个Post,不是Send,这说明WM_QUIT消息是添加到消息队列中,不会立即被触发。这样,就带来另外一个话题,若检索消息的时候,消息队列除了WM_QUIT消息之外,还有其它消息,那么先检索哪个消息?关于这个话题,就参阅《Windows 2000核心编程》。
在WndProc里,hWnd代表的是注册类的实例,若多个窗口都用同一个类名创建,只能通过hWnd区分,至于wParam和lParam,不同的message对应着不同的含义,这里不可能一一列举,需要的时候请查阅MSDN。
从上面的代码我们可以看出,一个SDK程序基本的框架为:
1. 程序入口为WinMain;
2. 注册一个class;
3. 创建窗口并显示;
4. 进入消息循环,直到遇到WM_QUIT消息。
从上面的代码上看,各个环节的实现都是很简单,真正能变得极为复杂的是那个窗口回调函数(WndProc)。当前只处理WM_PAINT和WM_DESTROY两个消息,代码看起来还是很清爽,但若处理n个消息呢?必然会有一个庞大的switch case列表。
另外一个的麻烦之处是,每个窗口几乎都有自己的私有数据,而在回调函数里,只有hWnd参数指明是哪个窗口,那如何通过hWnd找到对应的私有数据?
这些迹象表明,我们需要对SDK进行封装,才能以更合乎常规的方式来编写程序。
请点击这里下载'wabc'库的最终源码。