最近在处理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是无法预知,也不应该知道用户层面想要的特效的。所以最终要解决这个问题,还是应该放到用户消息循环,通过比较复杂的逻辑去实现。