1.2 Windows消息机制的处理
SendMessage()将指定的消息发送给指定的窗口,窗口接收到消息也有相应的行为发生。那么窗口接收到消息后的一系列行为是如何发生的?下面通过熟悉 Windows 的消息机制来理解消息处理背后的秘密。
1.2.1 DOS程序与Windows程序执行流程对比
Windows下的窗口应用程序都是基于消息机制的,操作系统与应用程序之间、应用程序与应用程序之间,大部分都是通过消息机制进行通信、交互的。要实际掌握Windows应用程序内部对消息的处理,必须分析实际的源代码。在编写一个基于消息的Windows应用程序前,先来比较DOS程序和Windows程序在执行时的流程。
1.DOS程序执行流程
在DOS下将编写完的程序进行执行,在执行时有明显的流程。比如用C语言编写程序后,程序执行时的大致流程如图1-6所示。
图1-6 传统DOS程序执行流程
在图1-6中可以看出,DOS程序的流程是按照代码的顺序和流程依次执行。大致步骤为:DOS程序从main()主函数开始执行(其实程序真正的入口并不是main函数);执行的过程中按照代码编写流程依次调用各个子程序;在执行的过程中会等待用户的输入等操作;当各个子程序执行完成后,最终会返回main()主函数,执行主函数的return语句后,程序退出(其实程序真正的出口也并不是main函数的return语句)。
2.Windows程序执行流程
DOS程序的执行流程比较简单,但是Windows应用程序的执行流程就比较复杂了。DOS是单任务的操作系统。在DOS中,通过输入命令,DOS操作系统会将控制权由Command.com转交给DOS程序从而执行。而Windows是多任务的操作系统,在Windows下同时会运行若干个应用程序,那么Windows就无法把控制权完全交给一个应用程序。Windows下的应用程序是如何工作的?首先看一下Windows应用程序内部的大致结构图,如图1-7所示。
图1-7可能看起来比较复杂,其实Windows应用程序的内部结构比该示意图更复杂。在实际开发Windows应用程序时,需要关注的部分主要是“主程序”和“窗口过程”两部分。但是从图1-7来看,主程序和窗口过程没有直接的调用关系,而在主程序和窗口过程之间有一个“系统程序模块”。“主程序”的功能是用来注册窗口类、获取消息和分发消息。而“窗口过程”中定义了需要处理的消息,会根据不同的消息执行不同的动作,而不需要程序处理的消息则会交给默认的系统过程进行处理。
在“主程序”中,RegisterClassEx()函数会注册一个窗口类,窗口类中的字段中包含了“窗口过程”的地址信息,也就是把“窗口类”的信息(包括“窗口过程的地址信息”)告诉操作系统。然后“主程序”不断通过调用 GetMessage()函数获取消息,再交由 DispatchMessge()函数来分发消息。消息分发后并没有直接调用“窗口过程”让其处理消息,而是由系统模块查找该窗口指定的窗口类,通过窗口类再找到窗口过程的地址,最后将消息送给该窗口过程,由窗口过程处理消息。
图1-7 Windows 应用程序执行原理图
1.2.2 一个简单的Windows应用程序
相对一个简单的DOS程序来说一个简单的Windows应用程序,要很长。下面的例子中只实现一个特别简单的程序,这个程序在桌面上显示一个简单的窗口,它没有菜单栏、工具栏、状态栏,只是在窗口中输出一段简单的字符串。虽然程序如此简单,但是也要编写 100行左右的代码。考虑到初学的读者,这里将一部分一部分地逐步介绍代码中的细节,以减少代码的长度,从而方便初学者的学习。
1.Windows窗口应用程序的主函数——WinMain()
在DOS时代,在Windows下的命令行的程序,要使用C语言编写代码的时候都是从main()函数开始的。而在Windows下编写有窗口的程序时,要用C语言编写窗口程序就不再从main()函数开始了,取而代之的是WinMain()函数。
既然Windows应用程序的主函数是WinMain(),那么就从了解WinMain()函数的定义开始学习Windows应用程序的开发。WinMain()函数的定义如下:
int WINAPI WinMain(
HINSTANCE hInstance, // handle to current instance
HINSTANCE hPrevInstance, // handle to previous instance
LPSTR lpCmdLine, // command line
int nCmdShow // show state
);
该函数的定义取自MSDN中,在看到WinMain()函数的定义后,很直观地会发现WinMain函数的参数比main()函数的参数变多了。从参数个数上来说,WinMain()函数接收的信息更多了。下面来看每个参数的含义。
hInstance是应用程序的实例句柄。保存在磁盘上的程序文件是静态的,当被加载到内存中时,被分配了CPU、内存等进程所需的资源。这样,一个静态的程序被实例化为一个有各种执行资源的进程了。句柄的概念随上下文的不同而不同,句柄是操作某个资源的“把手”。当需要对某个实例化进程操作时,需要借助该实例句柄进行操作。这里的实例句柄是程序装入内存后的起始地址。实例句柄的值也可以通过GetModuleHandle()参数来获得(注意系统中没有GetInstanceHandle()函数,不要误以为是hInstance就会有GetInstanceXXX()类的函数)。
hPrevInstance是同一个文件创建的上一个实例的实例句柄。这个参数是Win16平台下的遗留物,在Win32下已经不再使用了。
lpCmdLine 是主函数的参数,用于在程序启动时给进程传递参数。比如在“开始”菜单的“运行”中输入“notepad c:\boot.ini”,这样就通过记事本打开了C 盘下的boot.ini 文件。C:\Boot.ini文件是通过lpCmdLine传递给notepad.exe程序的。
nCmdShow是进程显示的方式,可以是最大化显示、最小化显示,或者是隐藏等显示方式(如果是启动木马程序的话,启动方式当然要由自己进行控制)。
主函数的参数都介绍完了。编写Windows的窗口程序,需要主函数中应该完成哪些操作是下面要讨论的内容。
2.WinMain()函数中的流程
编写Windows下的窗口程序,在WinMain()主函数中主要完成的任务是注册一个窗口类,创建一个窗口并显示创建的窗口,然后不停地获取属于自己的消息并分发给自己的窗口过程,直到收到WM_QUIT消息后退出消息循环结束进程。这是主函数中程序的执行脉络,程序中将注册窗口类、创建窗口的操作封装为自定义函数。
代码如下:
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG Msg;
BOOL bRet;
//注册窗口类
MyRegisterClass(hInstance);
//创建窗口并显示窗口
if ( !InitInstance(hInstance, SW_SHOWNORMAL) )
{
return FALSE;
}
//消息循环
//获取属于自己的消息并进行分发
while( (bRet = GetMessage(&Msg, NULL, 0, 0)) != 0 )
{
if ( bRet==1 )
{
//handle the error and possibly exit
break;
}
else
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
}
return Msg.wParam;
}
在代码中,MyRegisterClass()和InitInstance()是两个自定义的函数,分别用来注册窗口类,创建窗口并显示更新创建的窗口。后面的消息循环部分用来获得消息并进行消息分发。它的流程如图1-7所示的“主程序”部分。
代码中主要是3个函数,分别是GetMessage()、TranslateMessage()和DispatchMessage()。这3个函数是Windows提供的API函数,它们的定义如下。
BOOL GetMessage(
LPMSG lpMsg, // message information
HWND hWnd, // handle to window
UINT wMsgFilterMin, // first message
UINT wMsgFilterMax // last message
);
该函数用来获取属于自己的消息,并填充 MSG 结构体。有一个类似于 GetMessage()的函数是 PeekMessage(),它可以判断消息队列中是否有消息,如果没有消息,可以主动让出CPU时间给其他进程。关于PeekMessage()函数的使用,请参考MSDN。
BOOL TranslateMessage(CONST MSG *lpMsg);
该函数是用来处理键盘消息的。它将虚拟码消息转换为字符消息,也就是将WM_KEYDOWN消息和WM_KEYUP消息转换为WM_CHAR消息,将WM_SYSKEYDOWN消息和WM_SYSKEYUP消息转换为WM_SYSCHAR消息。
LRESULT DispatchMessage(CONST MSG *lpmsg);
该函数是将消息分发到窗口过程中。
3.注册窗口类的自定义函数
在 WinMain()函数中,首先调用了 MyRegisterClass()这个自定义函数,需要传递进程的实例句柄 hInstance 作为参数。该函数完成窗口类的注册,分为两步:第一步是填充WNDCLASSEX 结构体,第二步是调用 RegisterClassEx()函数进行注册。该函数相对简单,稍微复杂的是WNDCLASSEX结构体的成员较多。
代码如下:
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX WndCls;
//填充结构体为0
ZeroMemory(&WndCls, sizeof(WNDCLASSEX));
//cbSize是结构体大小
WndCls.cbSize = sizeof(WNDCLASSEX);
//lpfnWndProc 是窗口过程地址
WndCls.lpfnWndProc = WindowProc;
//hInstance 是实例句柄
WndCls.hInstance = hInstance;
//lpszClassName 是窗口类类名
WndCls.lpszClassName = CLASSNAME;
//style是窗口类风格
WndCls.style = CS_HREDRAW | CS_VREDRAW;
//hbrBackground 是窗口类背景色
WndCls.hbrBackground = (HBRUSH)COLOR_WINDOWFRAME + 1;
//hCursor 是鼠标句柄
WndCls.hCursor = LoadCursor(NULL, IDC_ARROW);
//hIcon是图标句柄
WndCls.hIcon = LoadIcon(NULL, IDI_QUESTION);
//其他
WndCls.cbClsExtra = 0;
WndCls.cbWndExtra = 0;
return RegisterClassEx(&WndCls);
}
在代码中,WNDCLASSEX结构体的成员都介绍了。WNDCLASSEX中最重要的字段是lpfnWndProc,它将保存的是窗口过程的地址。窗口过程是对各种消息进程处理的“汇集地”,也是编写Windows应用程序的重点部分。代码中的函数都比较简单,主要涉及LoadCursor()、LoadIcon()和RegisterClassEx()3个函数。由于这3个函数使用简单,通过代码就可以进行理解,这里不做过多介绍。
注册窗口类的重点是在后面的代码中可以根据该窗口类创建该种类型的窗口。代码中,在定义窗口类时指定了背景色、鼠标指针、窗口图标等,那么使用该窗口类创建的窗口都具有相同的窗口类型。
4.创建主窗口并显示更新
注册窗口类后,根据该窗口类创建具体的主窗口并显示和更新窗口。
代码如下:
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd=NULL;
//创建窗口
hWnd = CreateWindowEx(WS_EX_CLIENTEDGE,
CLASSNAME,
"MyFirstWindow",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
if ( NULL == hWnd )
{
return FALSE;
}
//显示窗口
ShowWindow(hWnd, nCmdShow);
//更新窗口
UpdateWindow(hWnd);
return TRUE;
}
在调用该函数时,需要给该函数传递实例句柄和窗口显示方式两个参数。这两个参数的第1个参数通过WinMain()函数的参数hInstance指定,第2个参数可以通过WinMain()函数的第3个参数指定,也可以进行自定义指定。程序中的调用代码如下:
InitInstance(hInstance, SW_SHOWNORMAL);
在创建主窗口时调用了CreateWindowEx()函数,先来看看它的函数原型:
HWND CreateWindowEx(
DWORD dwExStyle, // extended window style
LPCTSTR lpClassName, // registered class name
LPCTSTR lpWindowName, // window name
DWORD dwStyle, // window style
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height
HWND hWndParent, // handle to parent or owner window
HMENU hMenu, // menu handle or child identifier
HINSTANCE hInstance, // handle to application instance
LPVOID lpParam // window-creation data
);
CreateWindowEx()中的第2个参数是lpClassName,由注释可以知道是已经注册的类名。这个已经注册的类名就是WNDCLASSEX结构体的lpszClassName字段。
5.处理消息的窗口过程
按照如图1-7所示的流程,主函数的部分已经都实现完成了。接下来看程序中关键的部分——窗口过程。从主函数中看出,在主函数中没有任何地方直接调用窗口过程,只是在注册窗口类时指定了窗口过程的地址。那么窗口类是由谁进行调用的呢?答案是由操作系统进行调用的。原因有二,首先窗口过程的地址是由系统维护的,注册窗口类时是向系统进行注册。其次是除了应用程序本身会调用自己的窗口过程外,其他应用程序也会调用自己的窗口过程,比如前面的例子中调用SendMessage()函数发送消息后,需要系统调用目标程序的窗口过程来完成相应的动作。如果窗口过程由自己调用,那么窗口就要自己维护窗口类的信息,进程间消息的通信会非常繁琐,也会无形中增加系统的开销。
窗口过程的代码如下:
LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam)
{
PAINTSTRUCT ps;
HDC hDC;
RECT rt;
char *pszDrawText = "Hello Windows Program.";
switch (uMsg)
{
case WM_PAINT:
{
hDC=BeginPaint(hwnd,&ps);
GetClientRect(hwnd, &rt);
DrawTextA(hDC,
pszDrawText, strlen(pszDrawText),&rt,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
EndPaint(hwnd, &ps);
break;
}
case WM_CLOSE:
{
if ( IDYES==MessageBox(hwnd,
"是否退出程序", "MyFirstWin", MB_YESNO) )
{
DestroyWindow(hwnd);
PostQuitMessage(0);
}
break;
}
default:
{
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
return 0;
}
在WinMain()函数中,通过调用RegisterClassEx()函数进行了窗口类的注册,通过调用CreateWindowEx()函数创建了窗口,并且GetMessage()函数不停地获取消息,但是在主函数中没有对被创建的窗口做任何处理。那是因为真正对窗口行为的处理全部放在了窗口过程中。当WinMain()函数中的消息循环得到消息以后,通过调用DispatchMessage()函数将消息派发(不是由 DispatchMessage()函数直接派发)给了窗口过程,从而由窗口过程对消息进行处理。
窗口过程的定义是按照MSDN上给出的形式进行定义的,MSDN上的定义形式如下:
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
WindowProc是窗口过程的函数名,这个函数名可以随意改变,但是该窗口过程的函数名必须与WNDCLASSEX结构体中lpfnWndProc的成员变量的值一致。函数的第1个参数hwnd是窗口的句柄,第2个参数uMsg是消息值,第3个和第4个参数是对于消息值的附加参数。这四个参数的类型与SendMessage()函数的参数相对应。
上面 WindowProc()窗口过程中只对两个消息进行了处理,分别是 WM_PAINT 和WM_CLOSE。这里为了演示因此简单处理了两个消息。Windows 中有上千种消息,那么多的消息不可能全部都由程序员自己去处理,程序员只处理一些程序中需要的消息,其余的消息就交给了DefWindowProc()函数进行处理。DefWindowProc()函数实际上是将消息传递给了操作系统,由操作系统来处理程序中没有处理的消息。比如,在调用CreateWindow()函数时,系统会发送消息 WM_CREATE 给窗口过程,但是这个消息可能对程序的功能并不需要进行特殊的处理,因此直接交由DefWindowProc()函数让系统进行处理。
DefWindowProc()函数的定义如下:
LRESULT DefWindowProc(
HWND hWnd, // handle to window
UINT Msg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
该函数的 4 个参数跟窗口过程的参数相同,只要将窗口过程的参数依次传递给DefWindowProc()函数就可以完成该函数的调用。在switch分支结构中的default位置直接调用DefWindowProc()函数就可以了。
WM_CLOSE 消息是关闭窗口时发出的消息,在这个消息中需要调用 DestoryWindow()函数来销毁窗口,并且调用PostQuitMessage()来退出消息循环,使程序退出。对于WM_PAINT消息,这里不进行介绍,涉及的几个API函数可以参考MSDN进行了解。
有的资料在介绍消息循环时会给出一个建议,就是把需要经常处理的消息放到程序靠上的位置,而将不经常处理的消息放到程序靠下的位置,从而提高程序的效率。其实,在窗口过程中往往会使用 switch 结构对消息进行判断,而 switch 结构在编译器进行编译后会进行优化处理,从而提高程序的运行效率。关于 switch 结构的优化,将在其他章节进行介绍。