Windows消息机制

 Windows的消息系统由以下3部分组成:

消息队列:Windows能够为所有的应用程序维护一个消息队列,应用程序必须从消息队列中获去消息,然后分派给某个窗体。

消息循环:通过这个循环机制,应用程序从消息队列中检索消息,再把它分派给适当的窗口,然后继续从消息队列中检索下一条消息,再分派给适当的窗口,依次进行。

窗口过程:每个窗口都有一个窗口过程,以接收Windows 传递给窗口的消息,窗口过程的任务就是获取消息并且响应它。窗口过程是一个回调函数,处理完一个消息后,通常要给Windows一个返回值。

1 消息队列

说到消息机制,可能连最初级的 Windows 程序员都会对消息队列(MessageQueue)这个名词耳熟(不过不见得能详)。对于这样一个基本概念,Windows 操作系统提供的针对消息队列的API 却少的可怜(GetQueueStatus、GetInputState、GetMessageExtraInfo、SetMessageExtraInfo),而且,这些 API 的出镜率也相当的低,甚至有不少经验丰富的程序员也从来没有使用过它们。在 Windows Mobile 上,这些 API 干脆付诸阙如,不过有一个同样极少使用的GetMessageQueueReadyTimeStamp 函数在充门面。

这一切,都归功于在 API 层极好的封装性,减少了开始接触这个平台时需要了解的概念。但是,对于我们这样既想知其然,又想知其所以然的群体,还是有必要对消息队列有充分的了解。

1.1 系统消息队列(System Message Queue) 

这是一个系统唯一的队列,输入设备驱动程序(键盘、鼠标或者其他)会把用户的操作输入转化成消息放置于系统队列中,然后系统会把此消息转到目标窗口所在线程的消息队列中等待处理。

1.2 线程消息队列(Thread-specific Message Queue)

该队列历史遗留的名称是应用程序消息队列,在 32 位(以及之后的 64 位)系统中,正确的名称应该是线程消息队列。

每一个GUI线程都会维护这样一个线程消息队列。(这个队列只有在线程调用User或者GDI函数时才会创建,默认并不创建)。然后线程消息队列中的消息会被本线程的消息循环(有时也被称为消息泵)派送到相应的窗口过程(也叫窗口回调函数WndProc)处理。

注意: 线程消息队列中WM_PAINT,WM_TIMER只有在Queue中没有其他消息的时候才会被处理,WM_PAINT消息还会被合并以提高效率(同一个窗口的多个 WM_PAINT被合并成一个 WM_PAINT 消息, 合并所有的无效区域到一个无效区域。合并WM_PAIN的目的是为了减少刷新窗口的次数)。其他所有消息以先进先出(FIFO)的方式被处理。

队列消息与非队列消息

2.1 队列消息(Queued Messages) 

     对于队列消息,最常见的是鼠标和键盘触发的消息,例如WM_MOUSERMOVE,WM_CHAR等消息,还有一些其它的消息,例如:WM_PAINT、 WM_TIMER和WM_QUIT。当鼠标、键盘事件被触发后,相应的鼠标或键盘驱动程序就会把这些事件转换成相应的消息,然后输送到系统消息队列,由 Windows系统去进行处理。Windows系统则在适当的时机,从系统消息队列中取出一个消息,根据前面我们所说的MSG消息结构确定消息是要被送往那个窗口,然后把取出的消息送往创建窗口的线程的相应队列,下面的事情就该由线程消息队列操心了,Windows开始忙自己的事情去了。线程看到自己的消息队列中有消息,就从队列中取出来,通过操作系统发送到合适的窗口过程去处理。

2.2 非队列消息(NonQueued Messages) 

     非队列消息将会绕过系统队列和消息队列,直接将消息发送到窗口过程,系统发送非队列消息通知窗口,系统发送消息通知窗口。例如,当用户激活一个窗口系统发送WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。这些消息通知窗口它被激活了。非队列消息也可以由当应用程序调用系统函数产生。例如,当程序调用SetWindowPos系统发送WM_WINDOWPOSCHANGED消息。一些函数也发送非队列消息,例如下面我们要谈到的函数。

postMessage发送的消息是队列消息,它会把消息Post到消息队列中; SendMessage发送的消息是非队列消息, 被直接送到窗口过程处理。

​​​​​​​3 消息的生命周期

      事件驱动围绕着消息的产生与处理展开,一条消息是关于发生的事件的消息。事件驱动是靠消息循环机制来实现的。也可以理解为消息是一种报告有关事件发生的通知,所以消息循环往往是一个Windows 应用程序的核心部分。

 

 

          Windows 操作系统为每个线程维持一个消息队列,当事件产生时,操作系统感知这一事件的发生,并包装成消息发送到消息队列,应用程序通过GetMessage()函数取得消息并存于一个消息结构体中,然后通过一个TranslateMessage()和DispatchMessage()解释和分发消息。

3.1 消息的产生

        消息产生的源头有两个,一个是系统,一个是应用程序。系统产生的消息又可以大致分为两类,一类是由输入设备导致的,例如 WM_MOUSEMOVE,一类是User部分(或者是系统内的其他部分通过User部分)为了实现自身的正常行为或者管理功能而主动生成的,如 WM_WINDOWPOSCHANGED。

        产生的方式也有两种,一种称为发送(Send),另一种称为投递(Post),对应于大家极为熟悉的两个 API,SendMessage 和 PostMessage。

        系统产生的消息,虽然我们看不到代码,不过我们还是可以粗略地划拨一下,基本上所有的输入类消息,都是以投递的方式抵达应用的,而其他的消息,则大部分是采取了发送方式。

至于应用程序,可以随意选用适合自己的消息产生方式。

3.2 消息的处理

        在绝大部分情况下,消息总是有一个目标窗口的,因此,消息也绝大部分是被某个窗口所处理的。处理消息的地方,就是这个窗口的回调函数。

        窗口的回调函数,之所以被称作“回调”,就是因为这个函数一般并不是由用户(程序员)主动调用它的,而是系统认为在恰当的时候对它进行调用。那么,这个“恰当的时候”是什么时候呢?根据消息产生的方式,“恰当的时候”也有两个时机。第一个时机是,DispatchMessage 函数被调用时,另一个时机是SendMessage 函数被调用时。

        另外还需要提一下窗口句柄:说到消息就不能不说窗口句柄,系统通过窗口句柄来在整个系统中唯一标识一个窗口,发送一个消息时必须指定一个窗口句柄表明该消息由那个窗口接收。而每个窗口都会有自己的窗口过程,所以用户的输入就会被正确的处理。例如有两个窗口共用一个窗口过程代码,你在窗口一上按下鼠标时消息就会通过窗口一的句柄被发送到窗口一而不是窗口二。

        取得的消息将交由窗口处理函数进行处理,对于每个窗口类Windows 为我们预备了一个默认的窗口过程处理函数DefWindowProc(),这样做的好处是,我们可以着眼于我们感兴趣的消息,把其他不感兴趣的消息传递给默认窗口过程函数进行处理。每一个窗口类都有一个窗口过程函数,此函数是一个回调函数,它是由Windows 操作系统负责调用的,而应用程序本身不能调用它。以switch 语句开始,对于每条感兴趣的消息都以一个case 引出。

LRESULT CALLBACK WndProc(HWND hwnd,UINT message,WPARAM wParam,
LPARAM lParam)
{

   switch(uMsgId)
   {

   case WM_TIMER://对WM_TIMER 定时器消息的处理过程
         return 0;
    case WM_LBUTTONDOWN://对鼠标左键单击消息的处理过程
         reurn 0;
       . …
     default:

     //其他消息交给由系统提供的缺省处理函数
     return DefWindowProc(hwnd,uMsgId,wParam,lParam);
   }
}

        对于每条已经处理过的消息都必须返回0,否则消息将不停的重试下去;对于不感兴趣的消息,交给DefWindowProc()函数进行处理,并需要返回其处理值。

首先我们来看一下VC中的消息泵:

while(GetMessage(&msg, NULL, 0, 0)) 

       if(!TranslateAccelerator(msg.hWnd, hAccelTable, &msg)) 
 {  
            TranslateMessage(&msg); 
            DispatchMessage(&msg); 
       } 

}

 示例说明:

        我们正常情况下以系统处理对一个顶级窗口的关闭按钮的鼠标左键点击事件为例来说明:

这个点击事件完成的标志性消息是 WM_NCLBUTTONUP,表示在一个窗口的非客户区的鼠标左键释放动作,另外,这个鼠标消息的其他数据中会表明,发生这个动作的位置是在关闭按钮上(HTCLOSE)。这是一个鼠标输入事件,从前文可以知道,它会被系统投递到消息队列中。

        于是,在消息循环中GetMessage 的某次执行结束后,这个消息被取到了 MSG 结构里。从文章开头的消息循环代码可知,这个消息接下来会被 TranslateMessage 函数做必要的(事实上是“可能的”)翻译,然后交给 DispatchMessage 来全权处理。

DispatchMessage 拿到了 MSG 结构,开始自己的一套办事流程。

        首先,检查消息指定的目标窗口句柄。看系统内(实际上是本线程内)是不是确实存在这样一个窗口,如果没有,那说明这个消息已经不会有需要对它负责的人选了,那么这个消息就会被丢弃。如果有,它就会直接调用目标窗口的回调函数。终于看到,我们写的回调函数出场了,这就是“恰当的时机”之一。当然,为了叙述清晰,此处省略了系统做的一些其他处理。

这样,对于系统来说,一条投递消息就处理完成,转而继续 GetMessage。

不过对于我们上面的例子,事情还没有完。

        我们都清楚,对于 WM_NCLBUTTONUP 这样一条消息,通常我们是无暇去做额外处理的(正事还忙不过来呢……)。所以,我们一般都会把它扔到那个著名的垃圾堆里,没错,DefWindowProc。尽管如此,我们还是可以看出,DefWindowProc其实已经成了我们的回调函数的一个组成部分,唯一的差别在,这个函数不是我们自己写的而已。

        DefWindowProc 对这个消息的处理也是相当轻松,它基本上没有做什么实质性的事情,而是生成了另外一个消息,WM_SYSCOMMAND,同时在 wParam 里指定为 SC_CLOSE。这一次,消息没有被投递到消息队列里,而是直接 Send 出来的。

于是,SendMessage 的艰难历程开始。

第一步,SendMessage 的方向和DispatchMessage 几乎一模一样,检查句柄。

第二步,事情就来了,它需要检查目标窗口和自己在不在一个线程内。如果在,那就比较好办,按照 DispatchMessage 趟出来的老路走:调用目标窗口的回调函数。这,就是“恰当的时机”之二。

可是要是不在一个线程内,那就麻烦了。道理很简单,别的线程有自己的运行轨迹,没有办法去让它立即就来处理这个消息。

现在,SendMessage该怎么处理手里的这个烫手山芋呢?

        微软的架构师做了个非常聪明的选择:不干涉其他线程的内政。我不会生拉硬拽让你来处理我的消息,我会把消息投递给你(这个投递是内部操作,从外面看,这条消息应该一直被认为是发送过去的),然后—— 我等着。

        这下,球踢到了目标线程那边。目标线程一点也不含糊,既然消息来到了我的队列里,那我的 GetMessage 会按照既定的流程走,不过,和上文WM_NCLBUTTONUP 的经历有所不同。鉴于这条消息是外来客,而且是Send 方式,于是它以优先于线程内部的其他消息进行处理(毕竟友邦在等着啊),处理完毕之后,把结果返回给消息的源线程。可以参见下文中对 GetMessage 函数的叙述。

        在我们的现在讨论的这个例子里,由于SendMessage(WM_SYSCOMMAND) 是属于本线程内的,所以就会递归调用回窗口的回调函数里。此后的处理,还是另外的几个消息被衍生出来,如 WM_CLOSE 和 WM_DESTROY。这就是系统内所有消息的处理方式。这个例子仅仅出于概念性的展示,而不是完全精确可靠的。

        另外一个需要稍做注解的问题是消息的返回值问题,这个问题有些微妙。对于大多数的消息,返回值都没有什么意义。对于另外的一些消息,返回值意义重大。我相信有很多人对 WM_ERASEBKGND 消息的返回值会有印象,该消息的返回值直接影响到系统是不是要进行缺省的绘制窗口背景操作。所以,处理完一条消息究竟应该返回什么,查一下文档会更稳妥一些。本文最后的补充章节会对这一内容再做一下整理。

这才算是功德圆满了。

3.3 消息的优先级

上一节中其实已经暗示了这一点,来自于其他线程的发送的消息优先级会高一点点。

        不过还需要注意,还有那么几个优先级比正常的消息低一点点的。它们是:WM_PAINT、WM_TIMER、WM_QUIT。只有在队列中没有其他消息的时候,这几个消息才会被处理,多个 WM_PAINT 消息还会被合并以提高效率(内幕揭示:WM_PAINT 其实也是一个标志位,所以看上去是被“合并了”)。

其他所有消息则以先进先出(FIFO)的方式被处理。

3.4 未处理的消息

        有人会问出这个问题的。事实上,这差不多就是一个伪命题,基本不存在没有处理的消息。从 4.2.2 节的叙述也可以看出,消息总会流到某一个处理分支里去。

        微软为窗口编写了默认的窗口过程,这个窗口过程将负责处理那些你不处理消息。正因为有了这个默认窗口过程我们才可以利用Windows的窗口进行开发而不必过多关注窗口各种消息的处理。例如窗口在被拖动时会有很多消息发送,而我们都可以不予理睬让系统自己去处理。

那么:如果窗口回调函数没有处理某个消息,那这个消息最终怎么样了?其实这还是取决于回调函数实现者的意志。如果你只是简单地返回,那事实上也是进行了处理,只不过,处理的方式是“什么都没做”而已;如果你把消息传递给 DefWindowProc,那么它会处理自己感兴趣的若干消息,对于别的消息,它也一概不管,直接返回。

3.5 消息死锁

假设有线程A和B, 现在有以下步骤:

1) 线程A SendMessage 给线程B,A 等待消息在线程B 中处理后返回

2) 线程 B 收到了线程A 发来的消息,并进行处理,在处理过程中,B 也向线程 A SendMessage,然后等待从A 返回。

此时线程A正等待从线程B返回,无法处理B发来的消息,从而导致了线程A 和B相互等待,形成死锁。

以此类推,多个线程也可以形成环形死锁。

可以使用 SendNotifyMessage 或 SendMessageTimeout来避免出现此类死锁。

(本节的内容尚需写程序进行验证)

3.6 模态

        Windows 中的模态有好几个场景,比较典型的有:显示了一个对话框、显示出一个菜单、操作滚动条、移动窗口、改变窗口大小…。

把我的体会归纳起来,那就是:如果进入了一个模态场景,那么,除了这个模态本身的明确目标,其余操作被一概禁止。概念上可以理解为,模态,是一种独占模式、一种强制模式,一种霸道模式。

        在 Windows 里,模态的实现其实很简单,只不过就是包含了自己的消息循环而已,说穿了毫无悬念可言,但是如果不明白这个内幕的话,就会觉得很神秘。那么,根据此结论,我们就可以做一些有趣(或者有意义)的事情了,看一下以下代码,预测一下 TestModal 的执行结果:

void CALLBACK RequestQuit(HWNDhwnd, UINT uMsg, UINT idEvent, DWORD dwTime);  

void TestModal()  

{  

         UINT uTimerId =SetTimer(NULL, 66, 1000, RequestQuit);  

         MessageBox(NULL, NULL, NULL,MB_OK);  

             KillTimer(NULL, uTimerId);  

}  

  

void CALLBACK RequestQuit(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime)  

{  

               PostMessage(NULL, WM_QUIT,0, 0);  

 

窗口过程

        Windows是基于事件机制的,任何窗口都能发送和处理消息,每一个窗口都对应着自己的消息处理函数,即通常所说的窗口过程(WindowProc)。窗口过程是一个函数,它带有四个参数,分别为:窗口句柄(Window Handle),消息ID(Message ID),和两个消息参数(wParam,lParam),当窗口收到消息时系统就会调用此窗口过程来处理消息。(所以叫回调函数),用来接收和处理所有发送到该窗口的消息,每个窗口类都有一个窗口过程,同一窗口类所创建的窗口共用同一个窗口过程来响应消息。 

        窗口过程一般不会忽略—条消息,如果它不处理某条消息,它就必须把这条消息传回系统进行 默认处理,窗口过程是调用函数DefWindowProc 来完成的,由它完成一个默认的操作并返回消息结果。绝大多数 窗口过程只处理几种类型的消息,其它的则通过调用DefWindowProc 传给了系统。因为窗口过程是由所有属于同类窗口共享的,所以它能处理 几个不同窗口的消息,要识别受消息影响的某个窗口,窗口过程可以检查消息所带的窗口句柄。

​​​​​​​5 钩子

很多人都或多或少地听说过或者接触过钩子。钩子在处理事务的正常流程之外,额外给予了我们一种监听或者控制的方式。(这里暂时不展开详细研究)

​​​​​​​6 消息反射

        在介绍消息类型时,提到了MFC引入了消息反射机制,这是由于Windows SDK 的 API 是以 C 的风格暴露给使用者的,与 C++ 语言的主要用类编程的风格有一些需要啮合的地方。

        举例来说,一个Button,在 SDK 中是一个已经定型的控件,基本上实现了自包容,要扩展它的功能的话(例如,绘制不同的外观),系统把接口(广义上的接口,即一种交互上的契约)制定为发给 Button 的属主(通常就是父窗口)的两条消息(WM_MEASUREITEM 和 WM_DRAWITEM)。其道理在于,使用 Button 控件的父窗口,往往是用户自己实现的,处理起来更方便,而不需要对 Button 自身做什么手脚。

        但是,这种交互方式在 C++ 的世界里是相当忌讳的。C++ 的自包容单位是对象,那么一个 Button 对象的封装类,假定是CButton,不能自己处理自己的绘制问题,这是不太符合法则的。

        为了消除这一不和谐音,就有人提出了反射机制。其核心就在于,对于本该子控件自己处理的事件所对应的消息(如前面的 WM_DRAWITEM),父窗口即使收到,也不进行直接处理,而是把这个消息重新发回给子控件本身。

        这样带来一个问题,当 Button 收到一个 WM_DRAWITEM消息时,弄不清楚究竟是自己的子窗口发来的(虽说往 Button 上建立子窗口不常见,但不是不可以),还是父窗口把原本是自己的消息反射回来了。所以,最后微软给出一个解决办法,就是反射消息的时候,把消息的值上加一个固定的附加值,这个值就是 OCM__BASE。尽管最初只是微软自己在这样做,这个值也完全可以各取各的,但是后来别的类/类库的编制者几乎都无一例外地和微软保持了一致。

当控件收到消息之后,先把这个附加值减掉,就可以知道是哪一条消息被反射回来了,然后再作相应的处理。 

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Windows消息机制是指在Windows操作系统中,用于实现应用程序之间的通信和事件处理的机制。每个消息都由一个消息标识符和一些相关的参数组成。当系统中发生某个事件时,Windows会将这个事件转化为一个消息,并将其放入消息队列中。应用程序通过接收消息并将其传递给适当的窗口过程来处理这些消息。 在Windows消息机制中,每个线程都有自己的消息队列。GUI线程通常拥有一个消息循环,负责接收和处理消息消息循环会不断从消息队列中获取消息,并将其翻译和分发给对应的窗口过程进行处理。 除了通过消息队列派发消息到窗口过程外,有些消息也可以直接发送到窗口过程进行处理,绕过消息队列和线程消息队列。例如,当用户激活一个新的应用程序窗口时,系统会直接发送一系列消息到窗口,包括WM_ACTIVATE、WM_SETFOCUS和WM_SETCURSOR等消息,用于通知窗口被激活、键盘输入被定向到窗口以及鼠标光标移到窗口的边界内。 因此,Windows消息机制是通过将事件转化为消息并通过消息队列进行传递,以实现应用程序之间的通信和事件处理的机制。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Windows消息机制](https://blog.csdn.net/King_weng/article/details/100072633)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值