本來想自己寫寫duilib的消息機制來幫助duilib的新手朋友,不過今天發現已經有人寫過了,而且寫得很不錯,把duilib的主干消息機制都說明了,我就直接轉載過來了,原地址:http://blog.163.com/hao_dsliu/blog/static/1315789082014101913351223/
duilib官方github地址: https://github.com/duilib/duilib
DuiLib核心的大體結構圖如下:
分為幾個大部分:
- 控件
- 容器(本質也是控件)
- UI構建解析器(XML解析)
- 窗體管理器(消息循環,消息映射,消息處理,窗口管理等)
- 渲染引擎
DuiLib 消息循環剖析
DuiLib的消息循環非常靈活,但不熟悉的可能會覺得非常混亂,不知道該如何下手。所以,我總結了下DuiLib的各種消息響應的方式,幫助大家理解DuiLib和加快開發速度。
其消息處理架構較為靈活,基本上在消息能過濾到的地方,都給出了擴展接口。
看了DuiLib入門教程后,對消息機制的處理有些模糊,為了屏蔽Esc按鍵,都花了大半天的時間。究其原因,是因為對DuiLib消息過濾不了解。
你至少應該看過上面提及的那篇入門教程,看過一些DuiLib的代碼,但可能沒看懂,那么這篇文章會給你指點迷津。
Win32消息路由如下:
- 消息產生。
- 系統將消息排列到其應該排放的線程消息隊列中。
- 線程中的消息循環調用GetMessage(or PeekMessage)獲取消息。
- 傳送消息TranslateMessage and DispatchMessage to 窗口過程(Windows procedure)。
- 在窗口過程里進行消息處理
我們看到消息經過幾個步驟,DuiLib架構可以讓你在某些步驟間進行消息過濾。首先,第1、2和3步驟,DuiLib並不關心。DuiLib對消息處理集中在CPaintManagerUI類中(也就是上面提到的窗體管理器)。DuiLib在發送到窗口過程的前和后都進行了消息過濾。
DuiLib的消息渠,也就是所謂的消息循環在CPaintManagerUI::MessageLoop()或者CWindowWnd::ShowModal()中實現。倆套代碼的核心基本一致,以MessageLoop為例:
void CPaintManagerUI::MessageLoop()
{
MSG msg = { 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代碼:
bool CPaintManagerUI::TranslateMessage(const LPMSG pMsg)
{
// 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.
UINT uStyle = GetWindowStyle(pMsg->hwnd);
UINT uChildRes = uStyle & WS_CHILD;
LRESULT lRes = 0;
if (uChildRes != 0) // 判斷子窗口還是父窗口
{
HWND hWndParent = ::GetParent(pMsg->hwnd);
for( int i = 0; i < m_aPreMessages.GetSize(); i++ )
{
CPaintManagerUI* pT = static_cast<CPaintManagerUI*>(m_aPreMessages[i]);
HWND hTempParent = hWndParent;
while(hTempParent)
{
if(pMsg->hwnd == pT->GetPaintWindow() || hTempParent == pT->GetPaintWindow())
{
if (pT->TranslateAccelerator(pMsg))
return true;
// 這里進行消息過濾
if( pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes) )
return true;
return false;
}
hTempParent = GetParent(hTempParent);
}
}
}
else
{
for( int i = 0; i < m_aPreMessages.GetSize(); i++ )
{
CPaintManagerUI* pT = static_cast<CPaintManagerUI*>(m_aPreMessages[i]);
if(pMsg->hwnd == pT->GetPaintWindow())
{
if (pT->TranslateAccelerator(pMsg))
return true;
if( pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes) )
return true;
return false;
}
}
}
return false;
}
bool CPaintManagerUI::PreMessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& /*lRes*/)
{
for( int i = 0; i < m_aPreMessageFilters.GetSize(); i++ )
{
bool bHandled = false;
LRESULT lResult = static_cast<IMessageFilterUI*>(m_aPreMessageFilters[i])->MessageHandler(uMsg, wParam, lParam, bHandled); // 這里調用接口 IMessageFilterUI::MessageHandler 來進行消息過濾
if( bHandled ) {
return true;
}
}
…… ……
return false;
}
在發送到窗口過程前,有一個過濾接口:IMessageFilterUI,此接口只有一個成員:MessageHandler,我們的窗口類要提前過濾消息,只要實現這個IMessageFilterUI,調用CPaintManagerUI::AddPreMessageFilter,將我們的窗口類實例指針添加到CPaintManagerUI::m_aPreMessageFilters 數組中。當消息到達窗口過程之前,就會會先調用我們的窗口類的成員函數:MessageHandler。
下面是AddPreMessageFilter代碼:
bool CPaintManagerUI::AddPreMessageFilter(IMessageFilterUI* pFilter)
{
// 將實現好的接口實例,保存到數組 m_aPreMessageFilters 中。
ASSERT(m_aPreMessageFilters.Find(pFilter)<0);
return m_aPreMessageFilters.Add(pFilter);
}
我們從函數CPaintManagerUI::TranslateMessage代碼中能夠看到,這個過濾是在大循環:
for( int i = 0; i < m_aPreMessages.GetSize(); i++ )
中被調用的。如果m_aPreMessages.GetSize()為0,也就不會調用過濾函數。從代碼中追溯其定義:
static CStdPtrArray m_aPreMessages;
是個靜態變量,MessageLoop,TranslateMessage等也都是靜態函數。其值在CPaintManagerUI::Init中被初始化:
void CPaintManagerUI::Init(HWND hWnd)
{
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函數,創建窗口,其內部注冊了窗口過程:
LRESULT CALLBACK CWindowWnd::__WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CWindowWnd* pThis = NULL;
if( uMsg == WM_NCCREATE ) {
LPCREATESTRUCT lpcs = 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 ) {
LRESULT lRes = ::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);
return lRes;
}
}
if( pThis != NULL ) {
return pThis->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代碼框架如下:
LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if( uMsg == WM_XXX ) {
… …
return 0;
}
else if( uMsg == WM_XXX) {
… …
return 1;
}
LRESULT lRes = 0;
if( m_pm.MessageHandler(uMsg, wParam, lParam, lRes) ) //CPaintManagerUI::MessageHandler
return lRes;
return CWindowWnd::HandleMessage(uMsg, wParam, lParam); // 調用父類HandleMessage
}
在注意:CPaintManagerUI::MessageHandler,名稱為MessageHandler,而不是HandleMessage。
沒有特殊需求,一定要調用此函數,此函數處理了絕大部分常用的消息響應。而且如果你要響應Notify事件,不調用此函數將無法響應,后面會介紹。
好現在我們已經知道,倆個地方可以截獲消息:
- 實現IMessageFilterUI接口,調用CPaintManagerUI:: AddPreMessageFilter,進行消息發送到窗口過程前的過濾。
- 重載HandleMessage函數,當消息發送到窗口過程中時,最先進行過濾。
下面繼續看看void Notify(TNotifyUI& msg)是如何響應的。我們的窗口繼承於INotifyUI接口,就必須實現此函數:
class INotifyUI
{
public:
virtual void Notify(TNotifyUI& msg) = 0;
};
上面我說了,在我們的HandleMessage要調用CPaintManagerUI::MessageHandler來進行后續處理。下面是一個代碼片段:
bool CPaintManagerUI::MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, 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( int j = 0; j < m_aNotifiers.GetSize(); j++ ) {
static_cast<INotifyUI*>(m_aNotifiers[j])->Notify(*pMsg);
}
delete pMsg;
}
// Cycle through listeners
for( int i = 0; i < m_aMessageFilters.GetSize(); i++ )
{
bool bHandled = false;
LRESULT lResult = static_cast<IMessageFilterUI*>(m_aMessageFilters[i])->MessageHandler(uMsg, wParam, lParam, bHandled);
if( bHandled ) {
lRes = lResult;
return true;
}
}
… …
}
定義為CStdPtrArray m_aNotifiers;數組,目前還看不出其指向的實際類型。看看,什么時候給該數組添加成員:
bool CPaintManagerUI::AddNotifier(INotifyUI* pNotifier)
{
ASSERT(m_aNotifiers.Find(pNotifier)<0);
return m_aNotifiers.Add(pNotifier);
}
不錯,正是AddNotifier,類型也有了:INotifyUI。所以,入門教程里會在響應WM_CREATE消息的時候,調用 AddNotifier(this),將自身加入數組中,然后在CPaintManagerUI::MessageHandler就能枚舉調用。由於 AddNotifer的參數為INotifyUI*,所以,我們要實現此接口。
所以,當HandleMessage函數被調用后,緊接着會調用我們的Notify函數。如果你沒有對消息過濾的特殊需求,實現INotifyUI即可,在Notify函數中處理消息響應。
上面的Notify調用,是響應系統產生的消息。程序本身也能手動產生,其函數為:
void CPaintManagerUI::SendNotify(TNotifyUI& Msg, bool bAsync /*= false*/)
DuiLib將發送的Notify消息分為了同步和異步消息。同步就是立即調用(類似SendMessage),異步就是先放到隊列中,下次再處理。(類似PostMessage)。
void CPaintManagerUI::SendNotify(TNotifyUI& Msg, bool bAsync /*= false*/)
{
… …
if( !bAsync ) {
// Send to all listeners
// 同步調用OnNotify,注意不是Notify
if( Msg.pSender != NULL ) {
if( Msg.pSender->OnNotify ) Msg.pSender->OnNotify(&Msg);
}
// 還會再次通知所有注冊了INotifyUI的窗口。
for( int i = 0; i < m_aNotifiers.GetSize(); i++ ) {
static_cast<INotifyUI*>(m_aNotifiers[i])->Notify(Msg);
}
}
else {
// 異步調用,添加到m_aAsyncNotify array中
TNotifyUI *pMsg = new TNotifyUI;
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是響應消息的另外一種方式。它的定義為:
CEventSource OnNotify;
屬於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);
類似的代碼,調用它,我們就會得到通知。
現在,又多了兩種消息處理的方式:
- 實現INotifyUI,調用CPaintManagerUI::AddNotifier,將自身加入Notifier隊列。
- 添加消息代理(其實就是將成員函數最為回到函數加入),MakeDelegate(this, &CFrameWindowWnd::OnAlphaChanged);,當程序某個地方調用了 CPaintManagerUI::SendNotify,並且Msg.pSender正好是注冊的this,我們的類成員回調函數將被調用。
搜尋CPaintManagerUI代碼,我們發現還有一些消息過濾再里面:
bool CPaintManagerUI::AddMessageFilter(IMessageFilterUI* pFilter)
{
ASSERT(m_aMessageFilters.Find(pFilter)<0);
return m_aMessageFilters.Add(pFilter);
}
m_aMessageFilters也是IMessageFilterUI array,和m_aPreMessageFilters類似。
上面我們介紹的是CPaintManagerUI::AddPreMessageFilter,那這個又是在哪兒做的過濾?
還是CPaintManagerUI::MessageHandler中:
……
// Cycle through listeners
for( int i = 0; i < m_aMessageFilters.GetSize(); i++ )
{
bool bHandled = false;
LRESULT lResult = static_cast<IMessageFilterUI*>(m_aMessageFilters[i])->MessageHandler(uMsg, wParam, lParam, bHandled);
if( bHandled ) {
lRes = lResult;
return true;
}
}
… …
這個片段是在,異步OnNotify和Nofity消息響應,被調用后。才被調用的,優先級也就是最低。但它始終會被調用,因為異步OnNotify和 Nofity消息響應沒有返回值,不會因為消息已經被處理,而直接退出。DuiLib再次給用戶一個處理消息的機會。用戶可以選擇將bHandled設置 為True,從而終止消息繼續傳遞。我覺得,這個通常是為了彌補OnNotify和Nofity沒有返回值的問題,在m_aMessageFilters 做集中處理。
處理完所有的消息響應后,如果消息沒有被截斷,CPaintManagerUI::MessageHandler繼續處理大多數默認的消息,它會處理在其管理范圍中的所有控件的大多數消息和事件等。
然后,消息機制還沒有完,這只是CPaintManagerUI::MessageHandler中的消息機制,如果繼承的是 WindowImplBase, WindowImplBase實現了DuiLib窗口的大部分功能。WindowImplBase繼承了CWindowWnd,重載了 HandleMessage,也就是說,消息發送的窗口過程后,第一個調用的是WindowImplBase::HandleMessage:
LRESULT WindowImplBase::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
LRESULT lRes = 0;
BOOL bHandled = TRUE;
switch (uMsg)
{
case WM_CREATE: lRes = OnCreate(uMsg, wParam, lParam, bHandled); break;
case WM_CLOSE: lRes = OnClose(uMsg, wParam, lParam, bHandled); break;
case WM_DESTROY: lRes = OnDestroy(uMsg, wParam, lParam, bHandled); break;
#if defined(WIN32) && !defined(UNDER_CE)
case WM_NCACTIVATE: lRes = OnNcActivate(uMsg, wParam, lParam, bHandled); break;
case WM_NCCALCSIZE: lRes = OnNcCalcSize(uMsg, wParam, lParam, bHandled); break;
case WM_NCPAINT: lRes = OnNcPaint(uMsg, wParam, lParam, bHandled); break;
case WM_NCHITTEST: lRes = OnNcHitTest(uMsg, wParam, lParam, bHandled); break;
case WM_GETMINMAXINFO: lRes = OnGetMinMaxInfo(uMsg, wParam, lParam, bHandled); break;
case WM_MOUSEWHEEL: lRes = OnMouseWheel(uMsg, wParam, lParam, bHandled); break;
#endif
case WM_SIZE: lRes = OnSize(uMsg, wParam, lParam, bHandled); break;
case WM_CHAR: lRes = OnChar(uMsg, wParam, lParam, bHandled); break;
case WM_SYSCOMMAND: lRes = OnSysCommand(uMsg, wParam, lParam, bHandled); break;
case WM_KEYDOWN: lRes = OnKeyDown(uMsg, wParam, lParam, bHandled); break;
case WM_KILLFOCUS: lRes = OnKillFocus(uMsg, wParam, lParam, bHandled); break;
case WM_SETFOCUS: lRes = OnSetFocus(uMsg, wParam, lParam, bHandled); break;
case WM_LBUTTONUP: lRes = OnLButtonUp(uMsg, wParam, lParam, bHandled); break;
case WM_LBUTTONDOWN: lRes = OnLButtonDown(uMsg, wParam, lParam, bHandled); break;
case WM_MOUSEMOVE: lRes = OnMouseMove(uMsg, wParam, lParam, bHandled); break;
case WM_MOUSEHOVER: lRes = OnMouseHover(uMsg, wParam, lParam, bHandled); break;
default: bHandled = FALSE; break;
}
if (bHandled) return lRes;
lRes = HandleCustomMessage(uMsg, wParam, lParam, bHandled);
if (bHandled) return lRes;
if (m_PaintManager.MessageHandler(uMsg, wParam, lParam, lRes))
return lRes;
return CWindowWnd::HandleMessage(uMsg, wParam, lParam);
}
WindowImplBase處理一些消息,使用成員函數On***來處理消息,所以,可以重載這些函數達到消息過濾的目的。 然后,我們看到,有一個函數:WindowImplBase::HandleCustomMessage,它是虛函數,我們可以重載此函數,進行消息過濾,由於還沒有調用m_PaintManager.MessageHandler,所以在收到Notify消息之前進行的過濾。
有多了兩種方式:
- 重載父類:WindowImplBase的虛函數
- 重載父類:WindowImplBase::HandleCustomMessage函數
最后,繼承於WindowImplBase,還有一種過濾消息的方式,和Notify消息平級,實現方式是仿造的MFC消息映射機制: WindowImplBase實現了INotifyUI接口,並且AddNotify了自身,所以,它會收到Notify消息:
void WindowImplBase::Notify(TNotifyUI& msg)
{
return CNotifyPump::NotifyPump(msg);
}
NotifyPump 調用 LoopDispatch,代碼片段如下:
... ...
const DUI_MSGMAP_ENTRY* lpEntry = NULL;
const DUI_MSGMAP* pMessageMap = NULL;
#ifndef UILIB_STATIC
for(pMessageMap = GetMessageMap(); pMessageMap!=NULL; pMessageMap = (*pMessageMap->pfnGetBaseMap)())
#else
for(pMessageMap = GetMessageMap(); pMessageMap!=NULL; pMessageMap = pMessageMap->pBaseMap)
#endif
{
#ifndef UILIB_STATIC
ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)());
#else
ASSERT(pMessageMap != pMessageMap->pBaseMap);
#endif
if ((lpEntry = DuiFindMessageEntry(pMessageMap->lpEntries,msg)) != NULL)
{
goto LDispatch;
}
}
... ...
代碼量過多,這里進行原理說明,和MFC一樣,提供了一些消息宏
DUI_DECLARE_MESSAGE_MAP()
DUI_BEGIN_MESSAGE_MAP(CKeyBoardDlg, CNotifyPump)
DUI_ON_MSGTYPE(DUI_MSGTYPE_CLICK, OnClick)
DUI_ON_MSGTYPE(DUI_MSGTYPE_WINDOWINIT, OnInitWindow)
DUI_END_MESSAGE_MAP()
和MFC原理一樣,聲明一些靜態變量,存儲類的信息,插入一些成員函數,最為回調,最后生成一張靜態表。當WindowImplBase::Notify有消息時,遍歷表格,進行消息通知。
總結,DuiLib消息響應方式:
- 實現IMessageFilterUI接口,調用CPaintManagerUI::AddPreMessageFilter,進行消息發送到窗口過程前的過濾。
- 重載HandleMessage函數,當消息發送到窗口過程中時,最先進行過濾。
- 實現INotifyUI,調用CPaintManagerUI::AddNotifier,將自身加入Notifier隊列。
- 添加消息代理(其實就是將成員函數最為回到函數加入),MakeDelegate(this, &CFrameWindowWnd::OnAlphaChanged);,當程序某個地方調用了 CPaintManagerUI::SendNotify,並且Msg.pSender正好是this,我們的類成員回調函數將被調用。
- 重載父類:WindowImplBase的虛函數
- 重載父類:WindowImplBase::HandleCustomMessage函數
- 使用類似MFC的消息映射