Windows消息机制2

注释:文章转载自如鹏网论坛,版权归原作者所有。


    正如书中所讲,Windows编程的难点”在于“Windows程序所做的一切都是响应发送给窗口过程的消息”

    学习这一章中最为重要的,我想莫过于理解消息处理机制。为此我双眼死盯着书中第61页最后一段话。猛查找了不少资料,做了不少试验,还特地绘了张Windows消息循环流程图”。下面同各位鹏友分享一下这张花了我一下午功夫的流程图。

 

消息处理小图


注:这是小图版,大图版在文章下方可以下载!

    有Charles Pezold的《Windows程序设计》这本书的鹏友可以翻开第61页,看到最后一段。只有电子版的鹏友可以在“窗口和消息”-Windows程序设计的难点”里的“别呼叫我,我会呼叫你”的最后一段找到

    下面把这段话给贴出来:


“有时候,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中的消息循环终止,然后程序结束。”
(订正:“DestroyWindow呼叫DestroyWindow”改为“DefWindowProc呼叫DestroyWindow”。)


    我的流程图便是根据文中关闭窗口这个操作绘制的。下面详谈一下我对这段话的理解。


    首先,用鼠标左键点击窗口的关闭按钮。于是我们看到窗口在桌面上消失了。仅仅停留于这层认识当然不是我们这些Windows程序设计学习者所能容许的。至少还需要了解在这个短暂的瞬间都发生了什么微妙的变化!让我们一起体验“第一次亲密接触”吧!

    在此瞬间,Windows检测到鼠标左键的点击事件。从书本第62页“3.2.2进队消息与不进队消息”第四段获悉,Windows将鼠标左键点击产生的消息(WM_LBUTTONDOWN)送入了消息队列。

    接着,应用程序的消息循环利用GetMessage函数,将msg消息结构的指针传递给Windows,让Windows对msg这个结构进行数据填充。而这个“数据填充”的过程就是所谓“从消息队列中取出消息”的过程。(是不是有点像山里用竹子做成的“自动引水”设备。想象一下,水流啊流,水车转啊转,水一瓢接着一瓢往山下送。

    接到消息后,TranslateMessage函数对这个消息进行一些处理。从书上得知进行一些键盘转换处理。咱们暂且不管它都干了些什么勾当,这不是第三章要告诉我们的。(下一篇心得会讲到我改造的HelloWin,里面有稍微涉及到这个知识。)

    TranslateMessage函数干完活回家后(返回),DispatchMessage函数又将这个msg结构传给Windows。然后就要进行一步很有意义的动作啦。Windows把这个msg结构传给适当的窗口过程进行处理。也就是传递WM_LBUTTONDOWN消息给窗口过程。而msg结构里存储着WM_LBUTTONDOWN消息及其相关参数。同咱日常生活一样,信不能乱送,得放到信封里,填上姓名、地址、邮编等等,还要贴上邮票。

    瞧瞧,Windows是不是像个勤劳尽职的杂工:负责把咱们写好的信收拾好(放入消息队列),等邮递员(GetMessage)来了,得把信纸装成一封一封的信件交给他。当邮局对信件进行审核盖章(TranslateMessage的工作)并分发(DispatchMessage也是个邮递员)到各个私人信箱后,Windows还得挨家挨户去帮人家取信件,并转交给各户的管家(窗口过程WndProc)。真是尽职尽责,佩服,在此向Windows鞠个躬。

    在流程图中并没有提到WM_LBUTTONDOWN消息,其中有两个原因:一个是第三章里没有提到这个消息的处理。HelloWin的窗口过程WndProc中没有处理这个消息的代码。(WndProc不处理这个消息,而是把它交给DefWindowProc来处理。)另外,WM_LBUTTONDOWN并不是关闭窗口的特定消息。DefWindowProc只是通过从该消息的msg结构的POINT成员结构中获取鼠标焦点,来判断鼠标是否在关闭按钮上点击。

    DefWindowProc对这一鼠标点击进行处理。在检测到我们点击了关闭按钮后,它给WndProc发送一条WM_SYSCOMMAND消息。

1

    其实DefWindowProc并没有把消息直接发送给WndProc。我们知道很多事情都是由我们勤劳的Windows来做,这边也不例外,WM_SYSCOMMAND消息是通过Windows间接发送给WndProc的。(再次鞠躬)

    同上面处理WM_LBUTTONDOWN消息大同小异。Windows将WM_SYSCOMMAND消息(流程图——消息①)放入消息队列中。(终于可以一边看文字一边参照流程图了!由于流程图中有标注,因此有些细节在后面不再详谈。

    GetMessage从消息队列中取出WM_SYSCOMMAND消息。经过TranslateMessage和DispatchMessage后,再由Windows将其发送给WndProc。WndProc不处理,又将其传给DefWindowProc。DefWindowProc接收到WM_SYSCOMMAND消息后,同样通过Windows间接发送一条WM_CLOSE消息(流程图——消息②)给WndProc。

    实际上DefWindowProc接收到WM_SYSCOMMAND消息后还需判断是否为关闭窗口。即做类似“wParam & 0xFFF0 == SC_CLOSE”这样的判断,条件成立才发送WM_CLOSE消息。(参考资料:在WM_SYSCOMMAND消息中,wParam参数的4个低位是Windows内部实用的。当应用程序检测到wParam的值时,它必须通过AND操作来将0xFFF0与wParam值相与,以便得到正确结果。)这个我们暂且不去理会,保留一些脑细胞继续往下看。我会在下一篇学习心得里涉及到一点点这个知识。

2

    WndProc还是不处理WM_CLOSE消息,再次将它传给DefWindowProc。DefWindowProc调用DestroyWindow函数来响应这条WM_CLOSE消息。在此期间,DestroyWindow对窗口进行一系列的销毁工作,并通过Windows给WndProc发送一条WM_DESTROY消息(流程图——消息③)。于是我们发现桌面上的那个窗口消失了,而在这弹指一挥间,该应用程序其实还没有结束。(下篇学习心得会演示出来。)

3、处理WM_DESTROY消息

    WndProc中终于有WM_DESTROY消息的处理代码了。WndProc调用PostQuitMessage函数,将一条WM_QUIT消息(流程图——消息④)放入消息队列中。此刻PostQuitMessage不等WM_QUIT消息是否被处理便立即返回。(嘿嘿,它忙着逃命去啦,因为我们的应用程序要完蛋了!)

4

    GetMessage从消息队列中取出WM_QUIT消息并返回0值,导致消息循环终止。紧接着整个应用程序结束运行并返回Windows。由于此前取出消息后GetMessage返回的都是非0值,故一直执行消息循环。 

    OK,汇报完毕!

    看完了汇报是不是对消息处理机制有了进一步的认识?是不是开始蠢蠢欲动,想从中做些手脚来满足一下未脱的搞怪心理?
、终止消息循环,结束程序,返回Windows
、处理WM_CLOSE消息、处理WM_SYSCOMMAND消息0、鼠标左键点击窗口关闭按钮

消息处理





整体过程分析

1. 鼠标点击,产生单击事件,鼠标设备驱动程序根据用户事件,转换成消息,并放置于WINDOWS的系统队列中

2. WINDOWS将系统队列中的消息取出,并投掷于消息对应的应用程序所属的线程队列。

3. 每个应用程序在创建时,系统都会为其创建一个消息队列,发送给应用程序的消息都存放在该消息队列中,等待被处理。 而应用程序的消息引擎

1 MSG msg;
2  
3 while(GetMessage(msg, NULL, NULL, NULL))
4 {
5       TranslateMessage(&msg);
6       DispatchMessage(&msg);
7 }

会不停的从自己的专属消息队列中获取消息,并进行消息的翻译和转发(TranslateMessage和DiapatchMessage)

 

GetMessage:从线程队列中取消息,取出后对应的消息会从队列中删除;若无消息,则阻塞

TranslateMessage:
把键盘消息转换成对应的ASCII字符内存,并重新放置于队列中,等待取出

DispatchMessage:
在注册窗口对象时,有如下代码:及设置对口的消息处理回调函数

1 WNDCLASS wc;
2 ......
3 wc.lpfnWndProc = (WNDPROC)WndProc;
4 ......

在窗口类的定义中,有如下代码: 
1 DECLARE_MESSAGE_MAP()
2 ......
3 BEGIN_MESSAGE_MAP(CMsgTestDlg, CDialog)
4     ......
5     ON_MESSAGE(WM_USER_SEND_MSG, &CMsgTestDlg::HandleSendMsg)
6     ON_MESSAGE(WM_USER_POST_MSG, &CMsgTestDlg::HandlePostMsg)
7     ......
8 END_MESSAGE_MAP()

该段代码,是的所有该类型的对话框对象共享一个消息MAP表,DispatchMessage会根据消息所属的窗口,调用回调函数WndProc,而WndProc则getMessageMap,根据消息类型,在map中进行匹配查找,找到对应的处理函数,并调用,则消息处理完毕。

 

 鼠标点击事件产生至处理的完整流程图

问题:

1. 驱动程序如何捕捉事件,转换成消息并将其放置于系统队列中

2. 系统队列如何将消息投掷于其对应的线程队列中

3. 操作系统如何为应用程序线程创建消息队列,如何体现?

 



1.窗口
   Windows程序是由一系列的窗口构成的,每个窗口都有自己的窗口过程,窗口过程就是一个拥有有固定 Signature 的 C函数,具体格式如下:

   LRESULT CALLBACK WindowProc(HWND hwnd,
       UINT uMsg,
       WPARAM wParam,
       LPARAM lParam
   );
   
   窗口类型:
   可重叠窗口(Overlapped Window),
   弹出窗口(Pop-up Window),
   子窗口(Child Window)
   
   窗口之间的关系: 父子关系,拥有关系,前后关系。
   
2.线程
    一个进程至少拥有一个线程,称为主线程,如果一个线程创建了窗口,拥有GUI资源,那么也称该线程为GUI线程,否则就为工作线程。窗口是由线程创建的,
 创建窗口的线程就拥有该窗口。这种线程拥有关系的概念对窗口有重要的意义:建立窗口的线程必须是为窗口处理所有消息的线程。为了使这个概念更加明
 确具体,可以想像一个线程建立了一个窗口,然后就结束了。在这种情况下,窗口不会收到一个WM_DESTROY或WM_NCDESTROY消息,因为线程已经结束,不可
 能被用来使窗口接收和处理这些消息。每个线程,如果它至少建立了一个窗口,都由系统对它分配一个消息队列。这个队列用于窗口消息的派送(dispatch)。
 为了使窗口接收这些消息,线程必须有它自己的消息循环,消息循环一般如下:
 
 MSG msg;
 while( GetMessage(&msg, NULL, 0, 0) )
 {
  TranslateMessage (&msg);
  DispatchMessage (&msg);
 }
 
 应用程序不断的从消息队列中获取消息,然后系统通过DispatchMessage函数分派消息到相应窗口的窗口过程,使得消息得到处理。当获取到WM_QUIT消息时,
 GetMessage返回0,循环结束。
 
3.消息
 消息,就是指Windows发出的一个通知,告诉应用程序某个事情发生了。例如,单击鼠标、改变窗口尺寸、按下键盘上的一个键都会使Windows发送一个消息
 给应用程序,它被定义为:
  typedef struct {
  HWND hwnd;    //窗口句柄, 发生在哪个窗口上
  UINT message;   //消息标识号 ( WM_MOUSEMOVE, WM_LBUTTONDOWN, ... )
  WPARAM wParam;   //消息参数1
  LPARAM lParam;   //消息参数2
  DWORD time;
  POINT pt;
 } MSG, *PMSG;
 一个消息结构体包含了该事件 所有完备信息,当应用程序收到该消息时,就可以做出相应处理了。
 
 消息分类

 <1>.队列消息和非队列消息

  从消息的发送途径上看,消息分两种:队列消息和非队列消息。
  队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。

  这里,对消息队列阐述如下:
  Windows维护一个系统消息队列(System message queue),每个GUI线程有一个线程消息队列(Thread message queue)。鼠标、键盘事件由鼠标或键盘驱动
  程序转换成输入消息并把消息放进系统消息队列,例如WM_MOUSEMOVE、WM_LBUTTONUP、WM_KEYDOWN、WM_CHAR等等。Windows每次从系统消息队列移走一个
  消息,确定它是送给哪个窗口的和这个窗口是由哪个线程创建的,然后,把它放进窗口创建线程的线程消息队列。线程消息队列接收送给该线程所创建窗口
  的消息。线程从消息队列取出消息,通过Windows把它送给适当的窗口过程来处理。
  
  除了键盘、鼠标消息以外,队列消息还有WM_PAINT、WM_TIMER和WM_QUIT。这些队列消息以外的绝大多数消息是非队列消息。


 <2>.系统消息和应用程序消息

  从消息的来源来看,可以分为:系统定义的消息和应用程序定义的消息。

  系统消息ID的范围是从0到WM_USER-1,或0X80000到0XBFFFF;应用程序消息从WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到0X7FFF范围的消息
  由应用程序自己使用;0XC000到0XFFFF范围的消息用来和其他应用程序通信,为了ID的唯一性,使用::RegisterWindowMessage来得到该范围的消息ID。
 
 <3>.窗口消息,命令消息,控件通知消息
  根据处理过程的不同,可以分为三类:窗口消息,命令消息,控件通知消息。
  
  (1).窗口消息
   一般以WM_开头,如WM_CREATE, WM_SIZE, WM_MOUSEMOVE等标准的Windows消息, 用于窗口相关的事件通知,窗口消息将由系统分配到该窗口的窗口过程处理。
  (2).命令消息 (WM_COMMAND)
   一种特殊的窗口消息,它从一个窗口发送到另一个窗口以处理来自用户的请求,通常是从子窗口发送到父窗口,例如,点击按钮时,按钮的父窗口会收到
   WM_COMMAND消息,用以通知父窗口按钮被点击,经测试:子窗口向父窗口发送WM_COMMAND消息,或者称为父窗口会收到WM_COMMAND消息,操作系统并不是
   通过将WM_COMMAND消息放入到父窗口的消息队列中去,而是直接调用了父窗口的窗口过程,以 WM_COMMAND 为消息标识参数(UINT uMsg),实现这个功能的
   API函数正是: LRESULT DispatchMessage(const MSG *lpmsg);
  (3).控件通知消息
   WM_NOTIFY消息,当用户与控件交互(Edit, Button...)时,通知消息会从控件窗口发送到父窗口,这种消息的目的不是为了处理用户命令,而是为了让父窗
   口能够适时的改变控件。
  

4.测试
 <1>.测试代码:
 
  消息循环中,将从消息队列中取出的消息逐一打印出来,

while (GetMessage( & msg, NULL,  0 0 ))
  
{
   
char buf[1024];
   sprintf_s(buf, 
1024"hWnd:%d uMsg: %d WParam: %d  LParam: %d\n",
    msg.hwnd, msg.message, msg.wParam, msg.lParam);
   std::cout
<<buf;
   TranslateMessage(
&msg);
   DispatchMessage(
&msg);
  }


  在窗口过程中,如果收到 WM_COMMAND 消息,就在窗口上输入来。

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
  
{
   
switch (message)
   
{
   
case WM_COMMAND:
    HDhdc 
= GetDC (hwnd) ;
    SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
    TextOut(hdc, 
24 * cxChar, cyChar * (rect.bottom / cyChar - 1),
      szBuffer,
      wsprintf(szBuffer, szFormat,
      TEXT (
"WM_COMMAND"),
      HIWORD(wParam), LOWORD(wParam),
      HIWORD(lParam), LOWORD(lParam))
      );

    ReleaseDC (hwnd, hdc) ;
    ValidateRect (hwnd, 
&rect) ;
    
break ;
   
   
   
   }

   

   
return DefWindowProc (hwnd, message, wParam, lParam) ;
 }


 

      <2>.运行结果
    一个窗口,窗口上有一个按钮子窗口,然后还有一个控制台,输出消息循环中的每条消息,当父窗口收到 WM_COMMAND 消息时,
    会在屏幕上输出。





      (1). 当鼠标在父窗口上移动时:
   



            可见父窗口 hWnd: 461982,uMsg : 521 ( WM_MOUSEMOVE ),从线程消息队列中取出的。



      (2). 当鼠标在按钮子窗口上移动时:



            可见按钮 hWnd: 462118,uMsg : 521 ( WM_MOUSEMOVE),从线程消息队列中取出的。



      (3). 当鼠标在父窗口上点击时:




         父窗口收到了 513(WM_LBUTTONDOWN), 514(WM_LBUTTONUP) 消息,从线程消息队列中取出的。



      (4). 当鼠标在按钮窗口上点击时:




         按钮窗口从线程的消息队列中取出了 513(WM_LBUTTONDOWN), 514(WM_LBUTTONUP) 消息,父窗口收到了WM_COMMAND
         消息,TextOut 绘制出 WM_COMMAND 文本。
         

         我在 WM_COMMAND 的消息处理语句处打有断点,看下图:



         
         可见,窗口过程是被系统调用的,调用时系统传入的参数值为:
                        hwnd:          0x00070c9e,十进制就是461982,父窗口句柄;
                        message:      273 (WM_COMMAND)
                        wParam:      ...
                         lParam:       ...
          具体是WinMain中的哪一个函数中最后调用了 窗口过程 WndProc 呢,见下图:







            原来是在 DispatchMessage 函数中,再看看参数的值:
            msg.hwnd:             0x00070d26,十进制是462118,是按钮窗口的句柄;
            msg.message:         514 (  WM_LBUTTONUP  )
            哦~~~~,原来是操作系统在从该线程的消息队列中取出按钮的 WM_LBUTTONUP (鼠标左键释放) 消息后,调用
            DispatchMessage 分派消息,DispatchMessage 会先将 WM_LBUTTONUP 消息分派到按钮的窗口过程(系统默认有),
            这里的分派到按钮的窗口过程就是调用俺就的窗口过程,然后又以 按钮的父窗口的句柄为 窗口过程的第一个
            参数, WM_COMMAND 为窗口过程的第二个参数 调用了 父窗口的窗口过程,也就是将 WM_COMMAND
            消息分发到了父窗口,从而使父窗口得到了通知。这些,都是 Windows 来完成的,应用程序只需要在相应的窗口
            过程中处理相应的消息。

            从上面,我们还可以看出,WM_COMMAND 是非队列消息,直接分派到目的窗口过程,而不是放入到消息队列中,
            让消息循环去取。



 总结:

           简而言之, 标准Windows消息发送到产生窗口,通知消息(WM_COMMAND, WM_NOTIFY)发送到父窗口,这是Windows
           的标准消息处理过程,MFC对 Window API 进行了封装,有自己的一套消息处理流程, 消息顺着一条路径流动,需要
           处理的对象可以添加消息响应函数处理之,对于命令消息,它有 CView , CDocument,  CMainFram ,  CWinApp 一系列处理
          节点,对于通知消息,MFC还加入一种很好的机制:消息反射,就是父窗口收到子窗口发出的通知消息后,会将此消息
           发送给子窗口,先让子窗口处理,如果子窗口不处理,父窗口再处理之,这样有利于将所有消息处理代码都集成了子窗口
           中,有利于控件的开发。MFC的消息处理,我不予详细讨论了,有兴趣的可以参考侯捷的<<深入浅出MFC>>。





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值