Duilib是个轻量级的Windows界面库,可以让使用者在最快地时间构建出自己的项目,而且依赖最少。这里之所以突出它的“轻量级”,是为了强调作者在构建该界面库时的良苦用心。但是,物极必反,有些东西做得足够“傻瓜”后,就会变的不灵活。Duilib的控件的Caputre是个硬伤啊(希望作者不要骂我啊,我真是不好意思拆这个台,但是我今天真的遇到一个不可忽视的bug)。这个bug咱们暂且不说,先介绍一下什么是Capture,我想大家如果经常做界面的话,这个东西应该并不陌生,当我们鼠标按下一个Slider时,只要左键不放,鼠标无论滑到哪里,MouseMove事件都会一直在这Slider上面响应,这就是Capture的一个典型应用。在windows API中有SetCapture和ReleaseCapture这两个函数,SetCapture就是把当前窗口“Capture”(捕获),一旦Capture后,所有的鼠标事件都会在这个窗口响应,无论你的鼠标指针在什么位置。但是这两个函数是窗口级别的,windows的标准控件都是基于窗口,在Duilib中我们的控件都是绘图产生的,不受windows的管理,所以我们要自己为控件模拟一个类似于Windows标准控件的capture事件。
其实,这个Capture事件的模拟,Duilib的原作者已经为我们做了,只是没有把capture事件暴露出来,而是傻瓜式的鼠标按下时就强制给窗口SetCapture,鼠标弹起时去ReleaseCapture,单击事件产生后,Capture事件强制产生,就用m_pEventClick把这个被点击的控件存下来,在窗口的MouseMove事件到来时,让m_pEventClick去响应。这样Capture事件就不用让控件层干涉了,PaintManager就全搞定了,如果这样做没有问题的话,那这个办法还相当不错,但是最终还是出现问题了。比如现在的Slider,当你按下鼠标左键不放,然后拖动到Slider区域外的位置然后点击右键然后松开,就会发现,Slider的鼠标左键好像一直没有弹起来,鼠标一移动上去,滑块就有反应了。再举个例子,水平布局的分隔线,在拖动时,同样按下右键,会立即出现问题,其实这都是一个原因导致的,就是这个Capture事件导致的。
CSliderUI这样使用:
其实,这个Capture事件的模拟,Duilib的原作者已经为我们做了,只是没有把capture事件暴露出来,而是傻瓜式的鼠标按下时就强制给窗口SetCapture,鼠标弹起时去ReleaseCapture,单击事件产生后,Capture事件强制产生,就用m_pEventClick把这个被点击的控件存下来,在窗口的MouseMove事件到来时,让m_pEventClick去响应。这样Capture事件就不用让控件层干涉了,PaintManager就全搞定了,如果这样做没有问题的话,那这个办法还相当不错,但是最终还是出现问题了。比如现在的Slider,当你按下鼠标左键不放,然后拖动到Slider区域外的位置然后点击右键然后松开,就会发现,Slider的鼠标左键好像一直没有弹起来,鼠标一移动上去,滑块就有反应了。再举个例子,水平布局的分隔线,在拖动时,同样按下右键,会立即出现问题,其实这都是一个原因导致的,就是这个Capture事件导致的。
这些bug不再一一列举了,反正只要和Capture相关的拖动事件,都会有这个问题。直接说说我的的解决方案吧,说先找到CPaintManagerUI里面的m_pEventClick变量,直接把它删掉,然后把所有和m_pEventClick相关的语句全部删掉,这样做就是为了不再依赖作者这种方式,破釜沉舟,完全自己来解决这个Capture的问题。找到CPaintManagerUI里面的一对成员函数 SetCapture ,ReleaseCaputure,我们给SetCapture 加一个CControlUI* pControl参数。给CPaintManagerUI加一个新的成员变量CControlUI* m_pEventCapture(记得在构造时初始化为空),把SetCapture传入的pControl赋值给 m_pEventCapture,在ReleaseCaputure里面把m_pEventCapture置空,修改MessageHandler里面下面几个消息的处理,代码如下:
case WM_MOUSEMOVE:
{
// Start tracking this entire window again...
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 && m_pEventCapture == NULL ) break;
TEventUI event = { 0 };
event.ptMouse = pt;
event.dwTimestamp = ::GetTickCount();
if (m_pEventCapture==NULL)
{
if( pNewHover != m_pEventHover && m_pEventHover != NULL ) {
event.Type = UIEVENT_MOUSELEAVE;
event.pSender = m_pEventHover;
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_pEventCapture != NULL ) {
event.Type = UIEVENT_MOUSEMOVE;
event.pSender = m_pEventCapture;
m_pEventCapture->Event(event);
}
else if( pNewHover != NULL ) {
event.Type = UIEVENT_MOUSEMOVE;
event.pSender = pNewHover;
pNewHover->Event(event);
}
}
break;
case WM_LBUTTONDOWN:
{
// We alway set focus back to our app (this helps
// when Win32 child windows are placed on the dialog
// and we need to remove them on focus change).
if (!m_bUnfocusPaintWindow)
{
::SetFocus(m_hWndPaint);
}
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
m_ptLastMousePos = pt;
CControlUI* pControl = FindControl(pt);
if( (pControl == NULL||pControl->GetManager() != this)&&m_pEventCapture!=NULL) break;
if (m_pEventCapture!=NULL)
{
pControl = m_pEventCapture;
}
pControl->SetFocus();
TEventUI event = { 0 };
event.Type = UIEVENT_BUTTONDOWN;
event.pSender = pControl;
event.wParam = wParam;
event.lParam = lParam;
event.ptMouse = pt;
event.wKeyState = (WORD)wParam;
event.dwTimestamp = ::GetTickCount();
pControl->Event(event);
}
break;
case WM_LBUTTONDBLCLK:
{
if (!m_bUnfocusPaintWindow)
{
::SetFocus(m_hWndPaint);
}
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
m_ptLastMousePos = pt;
CControlUI* pControl = FindControl(pt);
if( (pControl == NULL|| pControl->GetManager() != this)&&m_pEventCapture!=NULL) break;
if (m_pEventCapture!=NULL)
{
pControl = m_pEventCapture;
}
TEventUI event = { 0 };
event.Type = UIEVENT_DBLCLICK;
event.pSender = pControl;
event.ptMouse = pt;
event.wKeyState = (WORD)wParam;
event.dwTimestamp = ::GetTickCount();
pControl->Event(event);
}
break;
case WM_LBUTTONUP:
{
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
m_ptLastMousePos = pt;
CControlUI* pControl = FindControl(pt);
if( (pControl == NULL|| pControl->GetManager() != this)&&m_pEventCapture!=NULL) break;
if (m_pEventCapture!=NULL)
{
pControl = m_pEventCapture;
}
TEventUI event = { 0 };
event.Type = UIEVENT_BUTTONUP;
event.pSender = pControl;
event.wParam = wParam;
event.lParam = lParam;
event.ptMouse = pt;
event.wKeyState = (WORD)wParam;
event.dwTimestamp = ::GetTickCount();
pControl->Event(event);
}
break;
case WM_RBUTTONDOWN:
{
if (!m_bUnfocusPaintWindow)
{
::SetFocus(m_hWndPaint);
}
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
m_ptLastMousePos = pt;
CControlUI* pControl = FindControl(pt);
if( (pControl == NULL||pControl->GetManager() != this)&&m_pEventCapture!=NULL) break;
if (m_pEventCapture!=NULL)
{
pControl = m_pEventCapture;
}
pControl->SetFocus();
TEventUI event = { 0 };
event.Type = UIEVENT_RBUTTONDOWN;
event.pSender = pControl;
event.wParam = wParam;
event.lParam = lParam;
event.ptMouse = pt;
event.wKeyState = (WORD)wParam;
event.dwTimestamp = ::GetTickCount();
pControl->Event(event);
}
break;
case WM_CONTEXTMENU:
{
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
::ScreenToClient(m_hWndPaint, &pt);
m_ptLastMousePos = pt;
CControlUI* pControl = FindControl(pt);
if( (pControl == NULL||pControl->GetManager() != this)&&m_pEventCapture!=NULL) break;
if (m_pEventCapture!=NULL)
{
pControl = m_pEventCapture;
}
// SetCapture();
TEventUI event = { 0 };
event.Type = UIEVENT_CONTEXTMENU;
event.pSender = pControl;
event.wParam = wParam;
event.lParam = lParam;
event.ptMouse = pt;
event.wKeyState = (WORD)wParam;
event.dwTimestamp = ::GetTickCount();
pControl->Event(event);
}
break;
完事后给CControlUI类加两个成员函数SetCapture,ReleaseCapture和一个成员变量bool m_bCapture(构造时初始化为假),关于SetCapture,ReleaseCapture的实现如下:
void CControlUI::SetCapture()
{
if (m_pManager)
{
m_pManager->SetCapture(this);
m_bCapture = true;
}
}
void CControlUI::ReleaseCapture()
{
if (m_pManager)
{
m_pManager->ReleaseCapture();
m_bCapture = false;
}
}
以上,我们只是简单地把Capture事件在控件上模拟了一下(因为Duilib本身的轻量级,就不整太复杂了,干净利索就好),下面就要修改控件层了。为了不影响任何一个控件的使用,我们把所有控件的UIEVENT_BUTTONDOWN和UIEVENT_RBUTTONDOWN的处理的开头处,执行控件的SetCapture,在UIEVENT_BUTTONUP和UIEVENT_CONTEXTMENU的处理的开头处,执行控件的ReleaseCapture。比如CButtonUI中应该这样使用:
if( event.Type == UIEVENT_BUTTONDOWN || event.Type == UIEVENT_DBLCLICK)
{
SetCapture();
if( ::PtInRect(&m_rcItem, event.ptMouse) && IsEnabled() ) {
m_uButtonState |= UISTATE_PUSHED | UISTATE_CAPTURED;
Invalidate();
}
return;
}
if( event.Type == UIEVENT_MOUSEMOVE )
{
if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) {
if( ::PtInRect(&m_rcItem, event.ptMouse) ) m_uButtonState |= UISTATE_PUSHED;
else m_uButtonState &= ~UISTATE_PUSHED;
Invalidate();
}
return;
}
if( event.Type == UIEVENT_BUTTONUP )
{
ReleaseCapture();
if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) {
if( ::PtInRect(&m_rcItem, event.ptMouse) ) Activate();
m_uButtonState &= ~(UISTATE_PUSHED | UISTATE_CAPTURED);
Invalidate();
}
return;
}
if( event.Type == UIEVENT_CONTEXTMENU )
{
if( IsContextMenuUsed() ) {
m_pManager->SendNotify(this, DUI_MSGTYPE_MENU, event.wParam, event.lParam);
}
return;
}
CSliderUI这样使用:
if( event.Type == UIEVENT_BUTTONDOWN || event.Type == UIEVENT_DBLCLICK )
{
SetCapture();
if( IsEnabled() ) {//2014.7.28 redrain 注释掉原来的代码,加上这些代码后可以让Slider不是在鼠标弹起时才改变滑块的位置
m_uButtonState |= UISTATE_CAPTURED;
int nValue;
if( m_bHorizontal ) {
if( event.ptMouse.x >= m_rcItem.right - m_szThumb.cx / 2 ) nValue = m_nMax;
else if( event.ptMouse.x <= m_rcItem.left + m_szThumb.cx / 2 ) nValue = m_nMin;
else nValue = m_nMin + (m_nMax - m_nMin) * (event.ptMouse.x - m_rcItem.left - m_szThumb.cx / 2 ) / (m_rcItem.right - m_rcItem.left - m_szThumb.cx);
}
else {
if( event.ptMouse.y >= m_rcItem.bottom - m_szThumb.cy / 2 ) nValue = m_nMin;
else if( event.ptMouse.y <= m_rcItem.top + m_szThumb.cy / 2 ) nValue = m_nMax;
else nValue = m_nMin + (m_nMax - m_nMin) * (m_rcItem.bottom - event.ptMouse.y - m_szThumb.cy / 2 ) / (m_rcItem.bottom - m_rcItem.top - m_szThumb.cy);
}
if(m_nValue !=nValue && nValue>=m_nMin && nValue<=m_nMax)
{
m_nValue =nValue;
Invalidate();
}
}
return;
}
// if( event.Type == UIEVENT_BUTTONDOWN || event.Type == UIEVENT_DBLCLICK )
// {
// if( IsEnabled() ) {
// RECT rcThumb = GetThumbRect();
// if( ::PtInRect(&rcThumb, event.ptMouse) ) {
// m_uButtonState |= UISTATE_CAPTURED;
// }
// }
// return;
// }
if( event.Type == UIEVENT_BUTTONUP)
{
ReleaseCapture();
int nValue;
if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) {
m_uButtonState &= ~UISTATE_CAPTURED;
}
if( m_bHorizontal ) {
if( event.ptMouse.x >= m_rcItem.right - m_szThumb.cx / 2 ) nValue = m_nMax;
else if( event.ptMouse.x <= m_rcItem.left + m_szThumb.cx / 2 ) nValue = m_nMin;
else nValue = m_nMin + (m_nMax - m_nMin) * (event.ptMouse.x - m_rcItem.left - m_szThumb.cx / 2 ) / (m_rcItem.right - m_rcItem.left - m_szThumb.cx);
}
else {
if( event.ptMouse.y >= m_rcItem.bottom - m_szThumb.cy / 2 ) nValue = m_nMin;
else if( event.ptMouse.y <= m_rcItem.top + m_szThumb.cy / 2 ) nValue = m_nMax;
else nValue = m_nMin + (m_nMax - m_nMin) * (m_rcItem.bottom - event.ptMouse.y - m_szThumb.cy / 2 ) / (m_rcItem.bottom - m_rcItem.top - m_szThumb.cy);
}
if(/*m_nValue !=nValue && 2014.7.28 redrain 这个注释很关键,是他导致了鼠标拖动滑块无法发出DUI_MSGTYPE_VALUECHANGED消息*/nValue>=m_nMin && nValue<=m_nMax)
{
m_nValue =nValue;
m_pManager->SendNotify(this, DUI_MSGTYPE_VALUECHANGED);
Invalidate();
}
return;
}
使用方法很简单,还有CScrollBarUI还有水平和垂直布局的分隔线也需要这样去使用,因为比较简单,就不过多解释了,照着步骤改就行了。如果发现bug或者其他问题请联系我,QQ:848861075(Skilla)