天天看點

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。還是得繼續完善。

沒有堅守就沒有事業,沒有執着就沒有未來!