首页 > 代码库 > DuiLib事件分析(一)——鼠标事件响应

DuiLib事件分析(一)——鼠标事件响应

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