天天看点

DuiLib 消息机制剖析

其消息处理架构较为灵活,基本上在消息能过滤到的地方,都给出了扩展接口。

看了DuiLib入门教程后,对消息机制的处理有些模糊,为了屏蔽Esc按键,都花了大半天的时间。究其原因,是因为对DuiLib消息过滤不了解。这篇教程,可能不适合刚刚接触DuiLib没两天的人看。至少你应该看过一些代码,但可能没看懂,那么这篇文章可能会给你指点迷津。

Win32消息路由如下:

  1. 消息产生。
  2. 系统将消息排列到其应该排放的线程消息队列中。
  3. 线程中的消息循环调用GetMessage(or PeekMessage)获取消息。
  4. 传送消息TranslateMessage and DispatchMessage to 窗口过程(Windows procedure)。
  5. 在窗口过程里进行消息处理

我们看到消息经过几个步骤,DuiLib架构可以让你在某些步骤间进行消息过滤。首先,第1、2和3步骤,DuiLib并不关心。DuiLib对消息处理集中在CPaintManagerUI类中。DuiLib在发送窗口过程前后进行了消息过滤。

DuiLib的消息渠,也就是所谓的消息循环在CPaintManagerUI::MessageLoop()或者CWindowWnd::ShowModal()中实现。俩套代码的核心基本一致,以MessageLoop为例:

voidCPaintManagerUI::MessageLoop()
 {
     MSGmsg = { 0 };
     while( ::GetMessage(&msg, NULL, 0, 0) ) {
         // CPaintManagerUI::TranslateMessage进行消息过滤
        if( !CPaintManagerUI::TranslateMessage(&msg) ) {
             ::TranslateMessage(&msg);
             try{
             ::DispatchMessage(&msg);
             } catch(...) {
                 DUITRACE(_T("EXCEPTION: %s(%d)\n"), __FILET__,  __LINE__);
                 #ifdef_DEBUG
                 throw"CPaintManagerUI::MessageLoop";
                 #endif
             }
         }
     }
 }
3和4之间,DuiLib调用CPaintManagerUI::TranslateMessage做了过滤,类似MFC的PreTranlateMessage。

想象一下,如果不使用这套消息循环代码,我们如何能做到在消息发送到窗口过程前进行常规过滤(Hook等拦截技术除外)?答案肯定是做不到。因为那段循环代码你是无法控制的。CPaintManagerUI::TranslateMessage将无法被调用,所以,可以看到DuiLib中几乎所有的demo都调用了这俩个消息循环函数。下面是TranslateMessage代码:

boolCPaintManagerUI::TranslateMessage(constLPMSGpMsg)
 {
     // Pretranslate Message takes care of system-wide messages, such as
     // tabbing and shortcut key-combos. We'll look for all messages for
     // each window and any child control attached.
     UINTuStyle = GetWindowStyle(pMsg->hwnd);
     UINTuChildRes = uStyle & WS_CHILD;    
     LRESULTlRes = 0;
     if (uChildRes != 0) // 判断子窗口还是父窗口
    {
         HWNDhWndParent = ::GetParent(pMsg->hwnd);
         for(  inti = 0; i < m_aPreMessages.GetSize(); i++ ) 
         {
             CPaintManagerUI* pT =  static_cast<CPaintManagerUI*>(m_aPreMessages[i]);        
             HWNDhTempParent = hWndParent;
             while(hTempParent)
             {
                 if(pMsg->hwnd == pT->GetPaintWindow() || hTempParent ==  pT->GetPaintWindow())
                 {
                     if (pT->TranslateAccelerator(pMsg))
                         returntrue;
                     // 这里进行消息过滤
                    if(  pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes) ) 
                         returntrue;
                     returnfalse;
                 }
                 hTempParent =  GetParent(hTempParent);
             }
         }
     }
     else
     {
         for(  inti = 0; i < m_aPreMessages.GetSize(); i++ ) 
         {
             CPaintManagerUI* pT =  static_cast<CPaintManagerUI*>(m_aPreMessages[i]);
             if(pMsg->hwnd == pT->GetPaintWindow())            {                if (pT->TranslateAccelerator(pMsg))                    returntrue;                if( pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes) )                     returntrue;                returnfalse;            }        }    }    returnfalse;}
boolCPaintManagerUI::PreMessageHandler(UINTuMsg, WPARAMwParam, LPARAMlParam, LRESULT&  /*lRes*/)
 {
     for(  inti = 0; i < m_aPreMessageFilters.GetSize(); i++ ) 
     {
         boolbHandled = false;
         LRESULTlResult = static_cast<IMessageFilterUI*>(m_aPreMessageFilters[i])->MessageHandler(uMsg, wParam,  lParam, bHandled); // 这里调用接口 IMessageFilterUI::MessageHandler 来进行消息过滤
        if(  bHandled ) {
             returntrue;
         }
 }
…… ……
returnfalse;
}
在发送到窗口过程前,有一个过滤接口:IMessageFilterUI,此接口只有一个成员:MessageHandler,我们的窗口类要提前过滤消息,只要实现这个IMessageFilterUI,调用CPaintManagerUI:: AddPreMessageFilter,将我们的窗口类实例指针添加到m_aPreMessageFilters array中。当消息到达窗口过程之前,就会会先调用我们的窗口类的成员函数:MessageHandler。

下面是AddPreMessageFilter代码:

boolCPaintManagerUI::AddPreMessageFilter(IMessageFilterUI* pFilter)
 {
     // 将实现好的接口实例,保存到数组 m_aPreMessageFilters 中。
    ASSERT(m_aPreMessageFilters.Find(pFilter)<0);
     returnm_aPreMessageFilters.Add(pFilter);
 }
我们从函数CPaintManagerUI::TranslateMessage代码中能够看到,这个过滤是在大循环:

for( inti = 0; i < m_aPreMessages.GetSize(); i++ )

中被调用的。如果m_aPreMessages.GetSize()为0,也就不会调用过滤函数。从代码中追溯其定义:

staticCStdPtrArraym_aPreMessages;    

是个静态变量,MessageLoop,TranslateMessage等也都是静态函数。其值在CPaintManagerUI::Init中被初始化:

voidCPaintManagerUI::Init(HWNDhWnd)
 {
     ASSERT(::IsWindow(hWnd));
     // Remember the window context we came from
     m_hWndPaint =  hWnd;
     m_hDcPaint = ::GetDC(hWnd);
     // We'll want to filter messages globally too
     m_aPreMessages.Add(this);
 }      

看来,m_aPreMessages存储的类型为CPaintManagerUI* ,也就说,这个静态成员数组里,存储了当前进程中所有的CPaintManagerUI实例指针,所以,如果有多个CPaintManagerUI实例,也不会存在过滤问题,互不干扰,都能各自过滤。当然m_aPreMessages不止用在消息循环中,也有别的用处。我觉得这个名字起得有点诡异。

然后再说,消息抵达窗口过程后,如何处理。首先,要清楚,窗口过程在哪儿?使用DuiLib开发,我们的窗口类无外呼,继承俩个基类:一个是功能简陋一点的:CWindowWnd,一个是功能健全一点的:WindowImplBase(继承于CWindowWnd)。然后,我们创建窗口先实例化窗口,然后带调用这俩个基类的Create函数,创建窗口,其内部注册了窗口过程:

LRESULTCALLBACKCWindowWnd::__WndProc(HWNDhWnd, UINTuMsg, WPARAMwParam, LPARAMlParam)
 {
     CWindowWnd*  pThis = NULL;
     if(  uMsg == WM_NCCREATE ) {
         LPCREATESTRUCTlpcs = reinterpret_cast<LPCREATESTRUCT>(lParam);
         pThis =  static_cast<CWindowWnd*>(lpcs->lpCreateParams);
         pThis->m_hWnd = hWnd;
         ::SetWindowLongPtr(hWnd, GWLP_USERDATA,  reinterpret_cast<LPARAM>(pThis));
     } 
     else {
         pThis =  reinterpret_cast<CWindowWnd*>(::GetWindowLongPtr(hWnd, GWLP_USERDATA));
         if(  uMsg == WM_NCDESTROY && pThis != NULL ) {
             LRESULTlRes = ::CallWindowProc(pThis->m_OldWndProc, hWnd,  uMsg, wParam, lParam);
             ::SetWindowLongPtr(pThis->m_hWnd, GWLP_USERDATA, 0L);
             if(  pThis->m_bSubclassed ) pThis->Unsubclass();
             pThis->m_hWnd = NULL;
             pThis->OnFinalMessage(hWnd);
             returnlRes;
         }
     }
     if(  pThis != NULL ) {
         returnpThis->HandleMessage(uMsg, wParam,  lParam);
     } 
     else {
         return ::DefWindowProc(hWnd, uMsg,  wParam, lParam);
     }
 }      

里面,主要做了一些转换,细节自行研究,最终,他会调用pThis->HandleMessage(uMsg, wParam, lParam);。也即是说,HandleMessage相当于一个窗口过程(虽然它不是,但功能类似)。他是CWindowWnd的虚函数:virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);所以,如果我们的窗口类实现了HandleMessage,就相当于再次过滤了窗口过程,HandleMessage代码框架:

LRESULTHandleMessage(UINTuMsg, WPARAMwParam, LPARAMlParam)
 {
if( uMsg == WM_XXX ) {
     … … 
        return 0;
     }
     elseif( uMsg == WM_XXX) {
     … … 
        return 1;
 }
     LRESULTlRes = 0;
if( m_pm.MessageHandler(uMsg, wParam,  lParam, lRes) ) //CPaintManagerUI::MessageHandler
returnlRes;
     returnCWindowWnd::HandleMessage(uMsg, wParam,  lParam); // 调用父类HandleMessage
 }      

在注意:CPaintManagerUI::MessageHandler,他们的名称为MessageHandler,而不是HandleMessage。没有特殊需求,一定要调用此函数,此函数处理了绝大部分常用的消息响应。而且如果你要响应Notify事件,不调用此函数将无法响应,后面会介绍。好现在我们已经知道,俩个地方可以截获消息:实现IMessageFilterUI接口,调用CPaintManagerUI:: AddPreMessageFilter,进行消息发送到窗口过程前的过滤。重载HandleMessage函数:

boolCPaintManagerUI::MessageHandler(UINTuMsg, WPARAMwParam, LPARAMlParam, LRESULT&  lRes)
 {
     … …
    TNotifyUI*  pMsg = NULL;
     while(  pMsg = static_cast<TNotifyUI*>(m_aAsyncNotify.GetAt(0)) ) {
         m_aAsyncNotify.Remove(0);
         if(  pMsg->pSender != NULL ) {
             if(  pMsg->pSender->OnNotify ) pMsg->pSender->OnNotify(pMsg);
         }
         // 先看这里,其它代码先忽略;我们看到一个转换操作static_cast<INotifyUI*>
         for(  intj = 0; j < m_aNotifiers.GetSize(); j++ ) {
             static_cast<INotifyUI*>(m_aNotifiers[j])->Notify(*pMsg);
         }
         deletepMsg;
     }
     
     // Cycle through listeners
     for(  inti = 0; i < m_aMessageFilters.GetSize(); i++ ) 
     {
         boolbHandled = false;
         LRESULTlResult = static_cast<IMessageFilterUI*>(m_aMessageFilters[i])->MessageHandler(uMsg, wParam,  lParam, bHandled);
         if(  bHandled ) {
             lRes =  lResult;
             returntrue;
         }
 }
… …
}      

定义为CStdPtrArraym_aNotifiers;数组,目前还看不出其指向的实际类型。看看,什么时候给该数组添加成员: boolCPaintManagerUI::AddNotifier(INotifyUI* pNotifier){    ASSERT(m_aNotifiers.Find(pNotifier)<0);    returnm_aNotifiers.Add(pNotifier);}不错,正是AddNotifier,类型也有了:INotifyUI。所以,入门教程里会在响应WM_CREATE消息的时候,调用AddNotifier(this),将自身加入数组中,然后在CPaintManagerUI::MessageHandler就能枚举调用。由于AddNotifer的参数为INotifyUI*,所以,我们要实现此接口。

所以,当HandleMessage函数被调用后,紧接着会调用我们的Notify函数。如果你没有对消息过滤的特殊需求,实现INotifyUI即可,在Notify函数中处理消息响应。

上面的Notify调用,是响应系统产生的消息。程序本身也能手动产生,其函数为:

voidCPaintManagerUI::SendNotify(TNotifyUI&Msg, boolbAsync/*= false*/)

DuiLib将发送的Notify消息分为了同步和异步消息。同步就是立即调用,异步就是先放到队列中,下次再处理。(类似PostMessage)。

voidCPaintManagerUI::SendNotify(TNotifyUI&Msg, boolbAsync/*= false*/) {     … …     if( !bAsync ) {         // Send to all listeners         // 同步调用OnNotify,注意不是Notify         if( Msg.pSender != NULL ) {             if( Msg.pSender->OnNotify ) Msg.pSender->OnNotify(&Msg);         }         // 还会再次通知所有注册了INotifyUI的窗口。         for( inti = 0; i < m_aNotifiers.GetSize(); i++ ) {             static_cast<INotifyUI*>(m_aNotifiers[i])->Notify(Msg);         }     } else {         // 异步调用,添加到m_aAsyncNotify array中         TNotifyUI *pMsg = newTNotifyUI;         pMsg->pSender = Msg.pSender;         pMsg->sType = Msg.sType;         pMsg->wParam = Msg.wParam;         pMsg->lParam = Msg.lParam;         pMsg->ptMouse = Msg.ptMouse;         pMsg->dwTimestamp = Msg.dwTimestamp;         m_aAsyncNotify.Add(pMsg);     } } 我们CPaintManagerUI::MessageHandler在开始处发现一些代码:

TNotifyUI* pMsg = NULL; while( pMsg = static_cast<TNotifyUI*>(m_aAsyncNotify.GetAt(0)) ) {     m_aAsyncNotify.Remove(0);     if( pMsg->pSender != NULL ) {         if( pMsg->pSender->OnNotify ) pMsg->pSender->OnNotify(pMsg); } 可以看到MessageHandler首先从异步队列中一个消息并调用OnNotify。OnNotify和上面的Notify不一样哦。

OnNotify是响应消息的另外一种方式。它的定义为:

CEventSourceOnNotify;

属于CControlUI类。重载了一些运算符,如 operator();要让控件响应手动发送(SendNotify)的消息,就要给控件的OnNotify,添加消息代理。在DuiLib的TestApp1中的OnPrepare函数里,有:

CSliderUI* pSilder = static_cast<CSliderUI*>(m_pm.FindControl(_T("alpha_controlor"))); if( pSilder ) pSilder->OnNotify += MakeDelegate(this, &CFrameWindowWnd::OnAlphaChanged); 至于消息代码的代码,我就不展示了,这里简单说明,就是将类成员函数,作为回调函数,加入到OnNotify中,然后调用pMsg->pSender->OnNotify(pMsg)的时候,循环调用所有的类函数,实现通知的效果。

这段代理代码处理的很巧妙,结婚多态和模板,能将任何类成员函数作为回调函数。

查阅CSliderUI代码,发现他在自身的DoEvent函数内调用了诸如:

m_pManager->SendNotify(this, DUI_MSGTYPE_VALUECHANGED);

类似的代码,调用它,我们就会得到通知。

现在,又多了两种消息处理的方式:

  1. 实现INotifyUI,调用CPaintManagerUI::AddNotifier,将自身加入Notifier队列。
  2. 添加消息代理(其实就是将成员函数最为回到函数加入),MakeDelegate(this, &CFrameWindowWnd::OnAlphaChanged);,当程序某个地方调用了CPaintManagerUI::SendNotify,并且Msg.pSender正好是this,我们的类成员回调函数将被调用。

搜寻CPaintManagerUI代码,我们发现还有一些消息过滤再里面:

boolCPaintManagerUI::AddMessageFilter(IMessageFilterUI* pFilter) {     ASSERT(m_aMessageFilters.Find(pFilter)<0);     returnm_aMessageFilters.Add(pFilter); } m_aMessageFilters也是IMessageFilterUI array,和m_aPreMessageFilters类似。

上面我们介绍的是CPaintManagerUI::AddPreMessageFilter,那这个又是在哪儿做的过滤?

还是CPaintManagerUI::MessageHandler中:

……     // Cycle through listeners     for( inti = 0; i < m_aMessageFilters.GetSize(); i++ )     {         boolbHandled = false;         LRESULTlResult = static_cast<IMessageFilterUI*>(m_aMessageFilters[i])->MessageHandler(uMsg, wParam, lParam, bHandled);         if( bHandled ) {             lRes = lResult;             returntrue;         } } … … 这个片段是在,异步OnNotify和Nofity消息响应,被调用后。才被调用的,优先级也就是最低。但它始终会被调用,因为异步OnNotify和Nofity消息响应没有返回值,不会因为消息已经被处理,而直接退出。DuiLib再次给用户一个处理消息的机会。用户可以选择将bHandled设置为True,从而终止消息继续传递。我觉得,这个通常是为了弥补OnNotify和Nofity没有返回值的问题,在m_aMessageFilters做集中处理。

处理完所有的消息响应后,如果消息没有被截断,CPaintManagerUI::MessageHandler继续处理大多数默认的消息,它会处理在其管理范围中的所有控件的大多数消息和事件等。

总结,DuiLib消息响应方式:

  1. 实现IMessageFilterUI接口,调用CPaintManagerUI:: AddPreMessageFilter,进行消息发送到窗口过程前的过滤。
  2. 重载HandleMessage函数,当消息发送到窗口过程中时,最先进行过滤。

话说DuiLib做界面,真是太方便了。我花了三天时间,从零开始学习,搞出了一个软键盘,发现逻辑上的代码量大大简化了。同样用MFC写,我的同事花了一周的时间,来调整布局和代码逻辑,里面写死了一堆要定位的坐标相关的值,如读天书一般,而且还很丑陋,再看看DuiLib做的软键盘,完全没有可比性。不过DuiLib的工具类如CDuiString等,会有一些bug。还是得继续完善。

没有坚守就没有事业,没有执着就没有未来!