我是荔园微风,作为一名在IT界整整25年的老兵,今天说说Windows程序的运行机制。经常被问到MFC到底是一个什么技术,为了解释这个我之前还写过帖子,但是很多人还是不理解。其实这没什么,我在学生时代也被这个问题困绕过。而且那个时间学习资料没有那么丰富,网上也没有什么资料,周围也没有懂的人,那个时候理解MFC更困难。甚至在我看来,理解这个比理解人工神经网络更难。
我认为造成这种现象的根本原因就是没有搞清楚Windows程序的运行机制,因为不理解Windows程序的运行机制,所以给理解MFC带来了很大的困难。我决定带所有微软开发技术的初学者一起攻破这个问题,但是一篇文章肯定是讲不清楚的,我们要分好几章来说。需要你有足够的耐心,一起来吧。我们这次来搞清楚什么是Windows程序的窗口机制。
下面我们来创建一个窗口,并在该窗口中响应键盘及鼠标消息,程序实现的步骤我们分三个部分,分别用三篇文章来讲清楚。
上一次我重点讲了第一部分,现在我们来说第二部分,就是消息循环。
在完成创建窗口、显示窗口、更新窗口的工作后,需要编写一个消息循环,不断地从消息队列中取出消息,并进行响应。要从消息队列中取出消息,我们需要调用GetMessage()函数,该函数的原型声明如下:
BOOL GetMessage(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax
);
第一个参数lpMsg。指向一个消息(MSG)结构体,GetMessage从线程的消息队列中取出的消息信息将保存在该结构体对象中。
第二个参数hWnd。指定接收属于哪一个窗口的消息。通常我们将其设置为NULL,用于接收属于调用线程的所有窗口的窗口消息。
第三个参数 wMsgFilterMin。指定要获取的消息的最小值,通常设置为0。
第四个参数 wMsgFilterMax。指定要获取的消息的最大值。
如果 wMsgFilterMin 和wMsgFilterMax都设置为0,则接收所有消息。GetMessage 函数接收到 WM_QUIT以外的消息均返回非零值。对于 WM_QUIT 消息,该函数返回0。如果出现了错误,该函数返回-1,例如,当参数 hWnd是无效的窗口句柄或lpMsg是无效的指针时,该函数返回-1。
通常我们编写的消息循环代码如下:
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
那作为消息处理的机制,只有有消息来,我们才要处理,没有消息了,就不处理,那这样就可以通过上面这个函数来完成这个工作。我们已经说了GetMessage i函数只有在接收到 WM_QUIT消息时,才返回0。此时while语句判断的条件为假,循环退出,程序才有可能结束运行。在没有接收到WM_QUIT消息时, Windows应用程序就通过这个while循环来保证程序始终处于运行状态。
TranslateMessage 函数用于将虚拟键消息转换为字符消息。字符消息被投递到调用线程的消息队列中,当下一次调用 GetMessage 函数时被取出。我们现在来具体说一下这个过程,当我们敲击键盘上的某个字符键时,系统将产生 WM_KEYDOWN 和 WM_KEYUP 消息。这两个消息的附加参数,也就是wParam和IParam,包含的是虚拟键代码和扫描码等信息,而我们在程序中往往需要得到某个字符的 ASCII 码,TranslateMessage 这个函数就可以将 WM_KEYDOWN 和 WMKEYUP消息的组合转换为一条 WM_CHAR消息,该消息的 wParam 附加参数包含了字符的ASCII码,并将转换后的新消息投递到调用线程的消息队列中。TranslateMessage函数并不会修改原有的消息,它只产生新的消息并投递到消息队列中。
DispatchMessage函数分派一个消息到窗口过程,由窗口过程函数对消息进行处理。DispachMessage 实际上是将消息回传给操作系统,由操作系统调用窗口过程函数对消息进行处理,也就是响应。
Windows应用程序的消息处理机制可以画出一个图来看,画法很多,目前各大论坛中,我认为这张图画得最好,如下。
我们把上图这个Windows应用程序的消息处理过程再详细说说,上图1是指操作系统接收到应用程序的窗口消息,将消息投递到该应用程序的消息队列中。上图2应用程序在消息循环中调用GetMessage函数从消息队列中取出一条一条的消息。在取出消息后,应用程序可以对消息进行一些预处理,例如,放弃对某些消息的响应,或者调用TranslateMessage产生新的消息。上图3应用程序调用DispatchMessage,将总回传给操作系统。消息是由MSG结构体对象来表示的,其中就包含了接收消息的窗口的句柄。因此,DispatchMessage函数总能进行正确的传递。上图4系统利用 WNDCLASS结构体的 lpfn WndProe 成员保存的窗口过程函数的指针调用窗口过程,对消息进行处理(即“系统给应用程序发送了消息”)。
大家不要把前面的内容忘记了,我来回顾一下:
消息队列:消息队列有两种,分为系统消息队列和应用程序消息队列。产生的消息首先由Windows系统捕获,放在系统消息队列,再拷贝到对应的应用程序消息队列。32/64位系统为每一个应用程序维护一个消息队列。
消息循环:系统为每个应用程序维护一个消息循环,消息循环会不断检索自身的消息队列。每有一个消息,就用GetMessage()取出消息。
GetMessage具有阻塞机制。当消息队列中没有消息时,程序非忙等,而是让权等待。当收到WM_QUIT时,GetMessage返回false,循环停止,同时应用程序终止。
消息处理:DispatchMessage()把取出来的消息分配给相应的窗口或线程,由窗口过程处理函数DefWindowProc()处理。
Windows的应用程序靠消息驱动来实现功能。而消息驱动靠消息机制来处理。消息机制就是由消息队列,消息循环,消息处理构成的。
那么,消息机制是如何运作的呢?
当用户运行一个应用程序,通过对鼠标的点击或键盘按键,产生一些特定事件。由于Windows一直监控着I/O设备,该事件首先会被翻译成消息,由系统捕获,存放于系统消息队列。
经分析,Windows知道该消息应由那个应用程序处理,则拷贝到相应的应用程序消息队列。由于消息循环不断检索自身的消息队列,当发现应用程序消息队列里有消息,就用GetMessage()取出消息,封装成Msg()结构。如果该消息是由键盘按键产生的,用TranslateMessage()翻译为WM_CHAR消息,否则,用DisPatchMessage()将取出的消息分发到相应的应用程序窗口,交由窗口处理程序处理。
Windows为每个窗体预留了过程窗口函数,该函数是一个回调函数,由系统调用,应用程序不能调用。程序员可以通过重载该函数处理我们想要去处理的消息。对于不想处理的消息,则由系统默认的窗口过程处理程序做出处理。
目前各论坛里画的最好的一张图是这个,我上次也向大家推荐过:
上图很好地解释了消息机制的运行原理,是我见过的画的最好的一幅。
当运行程序->事件操作引发消息->消息先存在系统消息队列->再存入到应用程序消息队列->用消息循环提取消息->处理消息->再返回消息队列。
当然,从消息队列中获取消息还可以调用PeekMessage 函数,该函数的原型声明如下所示:
BOOL PeekMessage(
LPNSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax,
UINT wRemoveMsg
);
前4个参数和GetMessage函数的4个参数的作用相同。最后1个参数指定消息获取的方式,如果设为PM_NOREMOVE,那么消息将不会从消息队列中被移除;如果设为PM_REMOVE,那么消息将从消息队列中被移除(与GetMessage 函数的行为一致)。
发送消息可以使用 SendMessage 和 PostMessage函数。 SendMessage将消息直接发送给窗口,并调用该窗口的窗口过程进行处理。在窗口过程对消息处理完毕后,该函数才返回(SendMessage 发送的消息为不进队消息)。PostMessage函数将消息放入与创建窗口的线程相关联的消息队列后立即返回。除了这两个函数外,还有一个 PostThreadMessage 函数,用于向线程发送消息,对于线程消息,MSG结构体中的 hwnd成员为NULL。
现在我们来理一下,windows消息循环的详细过程:
第一步,我们创建完win32应用程序,当用户通过对鼠标,键盘操作应用程序时,由于Windows一直监控着I/O设备,该事件首先会被转化成消息,由windows系统捕获,存放于系统消息队列。Windows系统知道该消息应由哪个应用程序处理,然后拷贝到相应的应用程序消息队列。同时将该消息从系统消息队列中删除。
第二步,应用程序的消息循环不断在执行,此时,调用GetMessage()从消息队列中查找消息,发现了该消息,GetMessage()将返回一个正值,并获取到了该消息Msg;如果消息队列为空,程序将停止执行并等待(程序阻塞)。
第三步, 然后取出消息(Msg)并将其传递给TranslateMessage()函数,这个函数做一些额外的处理:将虚拟键值信息转换为字符信息。这一步是可选的。
第四步,上面的步骤执行完后,将消息MSG传递给DispatchMessage()函数。DispatchMessage()函数将消息再给windows系统,由windows系统找到目标窗口并分发给该窗口,调用消息对应的窗口过程函数,既窗口的WinPro函数,让WinPro函数处理。WinPro函数可以允许我们对不同的消息做特定的处理,若不处理的话,则会调用DefWindowProc函数,做默认处理,所以为什么默认代码中WinPro的类型是CallBack(回调),因为不是我们主动调用的,是系统回调的。
第五步, 一旦一个消息处理完成,窗口过程WinPro函数返回,DispatchMessage()函数返回,应用程序的消息循环继续while循环,Window系统继续监控各类消息,重复上述步骤。
各位小伙伴,我们就说到这里,下次我们再深入研究windows程序的运行机制。
作者简介:荔园微风,1981年生,高级工程师,浙大工学硕士,软件工程项目主管,做过程序员、软件设计师、系统架构师,早期的Windows程序员,Visual Studio忠实用户,C/C++使用者,是一位在计算机界学习、拼搏、奋斗了25年的老将,经历了UNIX时代、桌面WIN32时代、Web应用时代、云计算时代、手机安卓时代、大数据时代、ICT时代、AI深度学习时代、智能机器时代,我不知道未来还会有什么时代,只记得这一路走来,充满着艰辛与收获,愿同大家一起走下去,充满希望的走下去。