最近在處理DuiLib中自定義列表行元素事件,因為處理方案得不到較好的效果,於是只好一層一層的去剝離DuiLib事件是怎么來的,看能否在某一層截取消息,自己重寫。
我這里使用CListContainerElementUI行元素,元素中有插入button,平時行元素不顯示,鼠標移動上去顯示出來,鼠標移走就隱藏button。Duilib自己是不帶這個功能的,它有一個鼠標移動上去的熱點事件,按理說重寫熱點事件就好了。但是當時比較急沒找到怎么觸發的,之后一直沒繼續走這條思路。后來找到源碼事件里面有
void CListContainerElementUI::DoEvent(TEventUI& event) if( event.Type == UIEVENT_MOUSEENTER ) { if( IsEnabled() ) { m_uButtonState |= UISTATE_HOT; Invalidate();
//我自己添加的回調函數
pCallBackFunc(this);
} return; }
於是就在UIEVENT_MOUSEENTER中加入了自己寫的一個回調函數,只要行元素觸發鼠標移動上去的事件,就去調用回調,回調再去根據業務邏輯操作UI。想法很好,但是遇到一個問題,當鼠標移動到行元素的button上時,行元素會觸發
if( event.Type == UIEVENT_MOUSELEAVE ) { if( (m_uButtonState & UISTATE_HOT) != 0 ) { m_uButtonState &= ~UISTATE_HOT; Invalidate(); } return; }
所以,如果在UIEVENT_MOUSEENTER時去設置為移動上行元素效果,在UIEVENT_MOUSELEAVE時設置為離開行元素效果,行不通。
當時設置了一個偷懶的方式,當鼠標移動到另外一個行元素上時,重置其他行元素的UI為鼠標離開。看似這樣就解決問題了,但是當鼠標不移動到其他行元素上,直接離開列表時,則會導致鼠標最后停留的那個行元素不能被置為鼠標離開狀態。
通過對相關源碼的閱讀,得到以下關系圖:
而CControlUI並不繼承自任何類。
class UILIB_API CControlUI { public: CControlUI(); virtual ~CControlUI(); ...... }
同時,在CControlUI中,DoEvent是來自於以下調用
void CControlUI::Event(TEventUI& event) { if( OnEvent(&event) ) DoEvent(event); }
查看Event引用得到下圖:
前面提到CControlUI不繼承自任何類,顯然Event不是重構了一個系統函數,調用必然不會來自於上圖的后面4條,而1,2條是定義和實現。這條線索算是中斷了。
於是,現在想從行元素鼠標離開事件本身着手,看能否從堆棧跟蹤里面得到答案。在UIEVENT_MOUSELEAVE處斷點,跟蹤堆棧:
這樣我們就看到了是來自於以下調用
> dclient.exe!DuiLib::CPaintManagerUI::MessageHandler(unsigned int uMsg, unsigned int wParam, long lParam, long & lRes) 行 843 + 0x21 字節 C++
我們再來看看MessangeHandler的代碼
bool CPaintManagerUI::MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lRes) { ...... switch( uMsg ) { ..... case WM_CLOSE:... case WM_SIZE:... case WM_MOUSEHOVER:... case WM_MOUSELEAVE:... case WM_MOUSEMOVE:... ...... }
也就是說,消息會在這里被默認處理,而它的再上一層調用是用戶自己定義的消息處理機制。我們此時再來看自己寫的調用:
//消息循環 LRESULT MyFrameWnd::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(uMsg) { ...... case WM_CLOSE:... ...... default: break; } if( bHandled ) return lRes; return WindowImplBase::HandleMessage(uMsg, wParam, lParam); }
這里的return就是把消息返回給了WindowImplBase
官方示例文檔Duilib入門是返回給了
return CWindowWnd::HandleMessage(uMsg, wParam, lParam);
這兩個有些差異,但是並不影響我們繼續分析,我們的WindowImpIBase的調用就是來自於
LRESULT CALLBACK CWindowWnd::__WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWindowWnd* pThis = NULL; ...... pThis = reinterpret_cast<CWindowWnd*>(::GetWindowLongPtr(hWnd, GWLP_USERDATA)) ..... if( pThis != NULL ) { return pThis->HandleMessage(uMsg, wParam, lParam); } else { return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } }
可見所有事件是來自於CWindowWnd窗口,窗口直接把事件給了開發者,當開發者不攔截相應事件的時候,就return給了DuiLib處理。DuiLib通過層層機制,反饋給相應的控件去處理事件。
所有,要實現自定義消息處理,不用動源碼,只要直接在HandleMessage中攔截消息即可。
OK,看似思路清晰了,但是還面臨一個問題,給開發者調用的事件里面,沒有參數表明是哪個元素觸發的,想從這個層面攔截,似乎只能攔截一些通用事件,比如關閉,比如按鈕點擊。
我們來看看UIManager中,MessageHandler在處理鼠標移動時的處理方法 case WM_MOUSEMOVE:
if( !m_bMouseTracking ) { TRACKMOUSEEVENT tme = { 0 }; tme.cbSize = sizeof(TRACKMOUSEEVENT); tme.dwFlags = TME_HOVER | TME_LEAVE; tme.hwndTrack = m_hWndPaint; tme.dwHoverTime = m_hwndTooltip == NULL ? 400UL : (DWORD) ::SendMessage(m_hwndTooltip, TTM_GETDELAYTIME, TTDT_INITIAL, 0L); _TrackMouseEvent(&tme); m_bMouseTracking = true; } // Generate the appropriate mouse messages
//獲取鼠標當前位置 POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; m_ptLastMousePos = pt;
//根據位置獲取鼠標在某個元素上 CControlUI* pNewHover = FindControl(pt); if( pNewHover != NULL && pNewHover->GetManager() != this ) break; TEventUI event = { 0 }; event.ptMouse = pt; event.dwTimestamp = ::GetTickCount();
//判斷是否鼠標離開某個元素 if( pNewHover != m_pEventHover && m_pEventHover != NULL ) { event.Type = UIEVENT_MOUSELEAVE; event.pSender = m_pEventHover;
//這里的m_pEventHover經過后面一些代碼運作,就會被賦值為pNewHover,也就是鼠標移動上去的那個元素 m_pEventHover->Event(event); m_pEventHover = NULL; if( m_hwndTooltip != NULL ) ::SendMessage(m_hwndTooltip, TTM_TRACKACTIVATE, FALSE, (LPARAM) &m_ToolTip); }
//判斷鼠標是否進入某個元素 if( pNewHover != m_pEventHover && pNewHover != NULL ) { event.Type = UIEVENT_MOUSEENTER; event.pSender = pNewHover; pNewHover->Event(event);
//設置熱點元素為當前元素 m_pEventHover = pNewHover; } if( m_pEventClick != NULL ) { event.Type = UIEVENT_MOUSEMOVE; event.pSender = m_pEventClick; m_pEventClick->Event(event); } else if( pNewHover != NULL ) { event.Type = UIEVENT_MOUSEMOVE;
event.pSender = pNewHover; pNewHover->Event(event); } }
先通過鼠標位置判斷是否在某個元素上,在元素上則標記m_pEventHover為當前元素,再去配置相關消息,調用元素的Event。也就是說,在DuiLib處理鼠標移動事件時,根據鼠標位置獲取了相應元素,並通觸發該元素行為。由此可見,自定義列表行元素,當鼠標停留在行元素上時,會觸發UIEVENT_MOUSEENTER,而當鼠標移動到行元素的button上時,會修改m_pEventHover,它會認為這是移動到另外一個元素上了,去調用這個新元素的Event。
到我們可以整理出DuiLib鼠標事件響應的大概流程:
1.窗體觸發事件
2.用戶消息循環,把處理不掉的信息反饋給DuiLib的基類處理,基類通過CPaintManagerUI的MessageHandler來處理一些基礎消息
3.MessageHandler把消息分類處理
根據以上分析,如果要實現我前面提到的鼠標移動上行元素,再改變行元素狀態,要通過修改DuiLib源碼來實現就非常困難了。MessageHandler是無法預知,也不應該知道用戶層面想要的特效的。所以最終要解決這個問題,還是應該放到用戶消息循環,通過比較復雜的邏輯去實現。