程序员人生 网站导航

[置顶] 修复duilib CEditUI控件和CWebBrowserUI控件中按Tab键无法切换焦点的bug

栏目:php教程时间:2014-12-14 08:37:32

转载请说明原出处,谢谢~~:http://blog.csdn.net/zhuhongshu/article/details/41556615


        在duilib中,按tab键会让焦点在Button1类的控件中切换,但是切换焦点1直存在bug,具体的描写以下:

        1、在主窗体里弹出新的窗体,当新窗体中存在CEditUI控件并且焦点在此CEditUI控件上,那末按tab键将没法切换焦点而1直处于CEditUI中。(只在新窗体中有此bug,主创体中没有,缘由会在后面分析)

        2、CWebBrowserUI控件同CEditUI


        之间在群里就看到有人问这个问题,而且也1直没解决。


       这几天在用duilib写1个注册界面时(如图,此页面便是在主窗体上面的1个弹出窗体),上面有多个CEditUI控件,依照我们的习惯,输入完第1个edit的内容后会按tab切换到下1个edit。而由于duilib的bug致使这个焦点没法切换。我自己1般是需要甚么功能就摸索甚么功能,之前用duilib是没有遇到edit切换焦点的需求,所以就没有斟酌过这个bug,今天碰到了这个需求,就得先解决这个bug了。

       



分析进程1:


        很明显可以看出来,这个bug只存在于CEditUI和CWebBrowserUI控件中,而这两个控件与其他控件的区分就在于他们都是用了原生的wini32控件,我这里就只分析CEditUI控件了。


        在CEditUI控件的源码里可以很容易看到,当他的DoEvent函数里收到获得焦点的UIEVENT_SETFOCUS消息或鼠标按下的UIEVENT_BUTTONDOWN消息后,他就会创建1个子窗体并且保护这个子窗体的相干数据。而这个子窗体会自动通过CreateWindowEx函数创建1个原生的win32的edit控件,当子窗体失去焦点时自动烧毁本身,这也就是CEditUI控件的实现原理。


       焦点切换的处理是由CPaintManager类管理的,当我们在界面中按下Tab键打算切换焦点后,CPaintManager会拦截键盘消息然后去管理焦点切换,那末我修复出发点就从焦点管理函数开始。焦点管理的函数是PreMessageHandler,原型以下:


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); if( bHandled ) { return true; } } switch( uMsg ) { case WM_KEYDOWN: { // Tabbing between controls if( wParam == VK_TAB ) { if( m_pFocus && m_pFocus->IsVisible() && m_pFocus->IsEnabled() && _tcsstr(m_pFocus->GetClass(), _T("RichEditUI")) != NULL ) { if( static_cast<CRichEditUI*>(m_pFocus)->IsWantTab() ) return false; } SetNextTabControl(::GetKeyState(VK_SHIFT) >= 0); return true; } } break; //....省略无用代码 }


        可以看到函数里接活VK_TAB按键后,会去调用SetNtextTabControl函数去设置下1个控件获得焦点,然后返回true。而SetNtextTabControl函数的原型以下:


bool CPaintManagerUI::SetNextTabControl(bool bForward) { // If we're in the process of restructuring the layout we can delay the // focus calulation until the next repaint. if( m_bUpdateNeeded && bForward ) { m_bFocusNeeded = true; ::InvalidateRect(m_hWndPaint, NULL, FALSE); return true; } // Find next/previous tabbable control FINDTABINFO info1 = { 0 }; info1.pFocus = m_pFocus; info1.bForward = bForward; CControlUI* pControl = m_pRoot->FindControl(__FindControlFromTab, &info1, UIFIND_VISIBLE | UIFIND_ENABLED | UIFIND_ME_FIRST); if( pControl == NULL ) { if( bForward ) { // Wrap around FINDTABINFO info2 = { 0 }; info2.pFocus = bForward ? NULL : info1.pLast; info2.bForward = bForward; pControl = m_pRoot->FindControl(__FindControlFromTab, &info2, UIFIND_VISIBLE | UIFIND_ENABLED | UIFIND_ME_FIRST); } else { pControl = info1.pLast; } } if( pControl != NULL ) SetFocus(pControl); m_bFocusNeeded = false; return true; }

       函数里调用FindControl函数,根据__FindControlFromTab函数和bForward参数来决定搜索下1个焦点的控件,__FindControlFromTab函数的代码我就不分析了,当找到了下1个应当获得焦点的控件后,调用CPaintManager的SetFocus函数让新控件获得焦点。而SetFocus函数里,首先对旧的获得焦点的控件发送UIEVENT_KILLFOCUS消息让他失去焦点,然后将新的获得焦点的控件指针赋值给m_pFocus变量(CPaintManager中保存当前获得焦点的控件指针的成员变量),并且给新的获得焦点的控件发送UIEVENT_SETFOCUS消息让他获得焦点。


      从代码中看,理论上没有甚么问题,我就针对CEditUI来进行修改。在CEditUI的内嵌子窗体类CEditWnd中的HandleMessage函数里加入以下代码,让CEditWnd收到Tab消息后来主动调用CPaintManager的SetNextTabControl函数来切换焦点:


else if( uMsg == WM_CHAR ){ if(TCHAR(wParam) == VK_TAB) { m_pOwner->GetManager()->SetNextTabControl(::GetKeyState(VK_SHIFT) >= 0); } else bHandled = FALSE; }


      这样修改后还不起作用,缘由是PreMessageHandler函数中处理WM_KEYDOWN消息后直接reutrn true致使了消息的截断,从而没法传递到CEditWnd,所以再把return true语句注释掉,这时候会欣喜的发现,可以切换焦点了!


分析进程2:


      这样莫名其妙的修复了bug,并且测试正常。但是我心里很疑惑为何这样在CEditWnd里面调用SetNextTabControl可以切换焦点但是在CpaintManager的PreMessageHandler里面调用SetNextTabControl函数却失效。而且这也没法解释为何这个bug只存在于弹出窗体而不是主窗体中,后来才意想到问题的缘由根本不在于CEditWnd和PreMessageHandler!


      接着分析进程1以后,我1直调试SetNextTabControl函数和SetFocus函数,下了很多条件断点和数据断点,试图找到在CPaintManager的PreMessageHandler里面调用SetNextTabControl函数失效的缘由。最后发现履行PreMessageHandler的CpaintManager类根本不是弹出窗体的CPaintManager,而是主窗体的CPaintManager!主窗体的CPaintManager调用了SetNextTabControl,他是给主窗体的控件切换了焦点!而弹出的子窗体的CPaintManager根本没有履行PreMessageHandler函数,所以他的SetNextTabControl失效了,而我莫名其妙的在CEditWnd里面调用了SetNextTabControl歪打正着的调用了弹出窗体的SetNextTabControl。这就解析了分析进程1中为何看上去修复了bug。


      那末现在就要分析1下为何明明在弹出窗体中按了Tab键,最后调用的却是主窗体的PreMessageHandler函数。


      这要从duilib的最底层消息处理函数说起,他是所以duilib程序消息的出发点。duilib的最底层消息处理函数有两个,1个是CWindowWnd类的ShowModal函数,1个是CPaintManager类的MessageLoop函数,这两个函数有1个共同点,共同的代码以下:


while( ::IsWindow(m_hWnd) && ::GetMessage(&msg, NULL, 0, 0) ) {         if( msg.message == WM_CLOSE && msg.hwnd == m_hWnd ) {             nRet = msg.wParam;             ::EnableWindow(hWndParent, TRUE);             ::SetFocus(hWndParent);         }         if( !CPaintManagerUI::TranslateMessage(&msg) ) {             ::TranslateMessage(&msg);             ::DispatchMessage(&msg);         }         if( msg.message == WM_QUIT ) break;     }

      大家都知道win32程序的消息需要先调用GetMessage,然后调用win32的TranslateMessage和DispatchMessage函数来分派消息。而duililb在win32的TranslateMessage之前先调用了CPaintManager中的1个名为TranslateMessage的静态函数来过滤消息。而这个TranslateMessage才是bug的出处!他的代码以下:


<pre name="code" class="cpp">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++ ) { int size = m_aPreMessages.GetSize(); 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; }



       我来分析1下致使bug的缘由。首先说1下当窗体中没有CEditUI或CWebBrowserUI控件的情况。函数进入后调用者两行代码判断发送消息的窗体是否是子窗体

UINT uStyle = GetWindowStyle(pMsg->hwnd); UINT uChildRes = uStyle & WS_CHILD;

      如果没有CEditUI或CWebBrowserUI控件,通常情况下就不会有子窗体,那末TranslateMessage往下履行后if (uChildRes != 0)判断就不会成功,也就是会调用else里面的代码。在else里面,会遍历m_aPreMessages数组中的元素(m_aPreMessages是全局变量,里面保存了所有窗体的CPaintManager对象的指针),然后调用每一个元素的PreMessageHandler函数,直到消息被处理。


      而如果包括CEditUI或CWebBrowserUI控件,那末他们内部就会创建win32原生的控件(也就是子窗体),那末if (uChildRes != 0)判断就会成功,任然是顺次遍历m_aPreMessages数组的元素,但是代码有些不同


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); }


         其中的hTempParent句柄会在while循环中被GetParent函数修改。问题就在这里了!当遍历到m_aPreMessages的的元素,也就是主窗体的CPaintManager时


if(pMsg->hwnd == pT->GetPaintWindow() || hTempParent == pT->GetPaintWindow())
           

         这句代码的hTempParent == pT->GetPaintWindow()会被判断为成功,由于win32原生控件句柄屡次GetParent后就会得到主窗体的句柄,这时候hTempParent的值就和m_aPreMessages的第1个元素,也就是pT->GetPaintWindow()的结构相同。

     

         判断成功后,会调用pT->PreMessageHandler,履行主窗体的PreMessageHandler函数,然后通过PreMessageHandler的代码可以知道,主窗体设置了自己的Tab焦点后,履行了return true。而PreMessageHandler返回true,在这个TranslateMessage里面也就返回了true,这时候TranslateMessage就结束了。明显看到,这类情况下,弹出窗体的CPaintManager根本没法履行PreMessageHandler函数,这就解析了为何子窗体的CEditUI和CWebBrowserUI没法切换焦点而主窗体可以。


          这下子找到了本源,分析进程1的修复代码就是没必要的,这里这样修改代码后,bug就修复了。(注意,终究的bug修复代码只需要修改这1个函数就好了,之前分析进程1的不需要修改了!)


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; pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes); // if( pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes) ) // return true; // // return false; //code by redrain } hTempParent = GetParent(hTempParent); } } } else { for( int i = 0; i < m_aPreMessages.GetSize(); i++ ) { int size = m_aPreMessages.GetSize(); 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; }

        修复代码很简单,不让他return,而是继续把消息传递下去。附效果图:



        几经波折,前后我分析和调试了4个多小时duilib,终究只要修改3行代码,bug就修复了。


总结:

       实际的修复进程其实不是文章描写的这么顺利,期间修改过量次CEditUI的控件代码也实现了焦点切换,还该多其他地方的很多代码,我就不在文章中描写了。而在后续的调试进程中才发现了原来问题的根本在于CPaintManager中的TranslateMessage消息处理。几次周转总算修复了bug。但是我还没有对这个修复的代码进行完全的测试,不知道他会不会引发甚么新的问题。所以如果有打算修复这个bug的朋友建议你多做1些测试,如果发现有甚么问题,请在博客中留言或QQ上告知我1下,谢谢~~


Redrain   2014.11.28


QQ:491646717

      

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐