[置頂] 修復(fù)duilib CEditUI控件和CWebBrowserUI控件中按Tab鍵無法切換焦點的bug
來源:程序員人生 發(fā)布時間:2014-12-14 08:37:32 閱讀次數(shù):8561次
轉(zhuǎn)載請說明原出處,謝謝~~:http://blog.csdn.net/zhuhongshu/article/details/41556615
在duilib中,按tab鍵會讓焦點在Button1類的控件中切換,但是切換焦點1直存在bug,具體的描寫以下:
1、在主窗體里彈出新的窗體,當新窗體中存在CEditUI控件并且焦點在此CEditUI控件上,那末按tab鍵將沒法切換焦點而1直處于CEditUI中。(只在新窗體中有此bug,主創(chuàng)體中沒有,緣由會在后面分析)
2、CWebBrowserUI控件同CEditUI
之間在群里就看到有人問這個問題,而且也1直沒解決。
這幾天在用duilib寫1個注冊界面時(如圖,此頁面便是在主窗體上面的1個彈出窗體),上面有多個CEditUI控件,依照我們的習慣,輸入完第1個edit的內(nèi)容后會按tab切換到下1個edit。而由于duilib的bug致使這個焦點沒法切換。我自己1般是需要甚么功能就摸索甚么功能,之前用duilib是沒有遇到edit切換焦點的需求,所以就沒有斟酌過這個bug,今天碰到了這個需求,就得先解決這個bug了。

分析進程1:
很明顯可以看出來,這個bug只存在于CEditUI和CWebBrowserUI控件中,而這兩個控件與其他控件的區(qū)分就在于他們都是用了原生的wini32控件,我這里就只分析CEditUI控件了。
在CEditUI控件的源碼里可以很容易看到,當他的DoEvent函數(shù)里收到獲得焦點的UIEVENT_SETFOCUS消息或鼠標按下的UIEVENT_BUTTONDOWN消息后,他就會創(chuàng)建1個子窗體并且保護這個子窗體的相干數(shù)據(jù)。而這個子窗體會自動通過CreateWindowEx函數(shù)創(chuàng)建1個原生的win32的edit控件,當子窗體失去焦點時自動燒毀本身,這也就是CEditUI控件的實現(xiàn)原理。
焦點切換的處理是由CPaintManager類管理的,當我們在界面中按下Tab鍵打算切換焦點后,CPaintManager會攔截鍵盤消息然后去管理焦點切換,那末我修復(fù)出發(fā)點就從焦點管理函數(shù)開始。焦點管理的函數(shù)是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;
//....省略無用代碼
}
可以看到函數(shù)里接活VK_TAB按鍵后,會去調(diào)用SetNtextTabControl函數(shù)去設(shè)置下1個控件獲得焦點,然后返回true。而SetNtextTabControl函數(shù)的原型以下:
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;
}
函數(shù)里調(diào)用FindControl函數(shù),根據(jù)__FindControlFromTab函數(shù)和bForward參數(shù)來決定搜索下1個焦點的控件,__FindControlFromTab函數(shù)的代碼我就不分析了,當找到了下1個應(yīng)當獲得焦點的控件后,調(diào)用CPaintManager的SetFocus函數(shù)讓新控件獲得焦點。而SetFocus函數(shù)里,首先對舊的獲得焦點的控件發(fā)送UIEVENT_KILLFOCUS消息讓他失去焦點,然后將新的獲得焦點的控件指針賦值給m_pFocus變量(CPaintManager中保存當前獲得焦點的控件指針的成員變量),并且給新的獲得焦點的控件發(fā)送UIEVENT_SETFOCUS消息讓他獲得焦點。
從代碼中看,理論上沒有甚么問題,我就針對CEditUI來進行修改。在CEditUI的內(nèi)嵌子窗體類CEditWnd中的HandleMessage函數(shù)里加入以下代碼,讓CEditWnd收到Tab消息后來主動調(diào)用CPaintManager的SetNextTabControl函數(shù)來切換焦點:
else if( uMsg == WM_CHAR ){
if(TCHAR(wParam) == VK_TAB)
{
m_pOwner->GetManager()->SetNextTabControl(::GetKeyState(VK_SHIFT) >= 0);
}
else
bHandled = FALSE;
}
這樣修改后還不起作用,緣由是PreMessageHandler函數(shù)中處理WM_KEYDOWN消息后直接reutrn true致使了消息的截斷,從而沒法傳遞到CEditWnd,所以再把return true語句注釋掉,這時候會欣喜的發(fā)現(xiàn),可以切換焦點了!
分析進程2:
這樣莫名其妙的修復(fù)了bug,并且測試正常。但是我心里很疑惑為何這樣在CEditWnd里面調(diào)用SetNextTabControl可以切換焦點但是在CpaintManager的PreMessageHandler里面調(diào)用SetNextTabControl函數(shù)卻失效。而且這也沒法解釋為何這個bug只存在于彈出窗體而不是主窗體中,后來才意想到問題的緣由根本不在于CEditWnd和PreMessageHandler!
接著分析進程1以后,我1直調(diào)試SetNextTabControl函數(shù)和SetFocus函數(shù),下了很多條件斷點和數(shù)據(jù)斷點,試圖找到在CPaintManager的PreMessageHandler里面調(diào)用SetNextTabControl函數(shù)失效的緣由。最后發(fā)現(xiàn)履行PreMessageHandler的CpaintManager類根本不是彈出窗體的CPaintManager,而是主窗體的CPaintManager!主窗體的CPaintManager調(diào)用了SetNextTabControl,他是給主窗體的控件切換了焦點!而彈出的子窗體的CPaintManager根本沒有履行PreMessageHandler函數(shù),所以他的SetNextTabControl失效了,而我莫名其妙的在CEditWnd里面調(diào)用了SetNextTabControl歪打正著的調(diào)用了彈出窗體的SetNextTabControl。這就解析了分析進程1中為何看上去修復(fù)了bug。
那末現(xiàn)在就要分析1下為何明明在彈出窗體中按了Tab鍵,最后調(diào)用的卻是主窗體的PreMessageHandler函數(shù)。
這要從duilib的最底層消息處理函數(shù)說起,他是所以duilib程序消息的出發(fā)點。duilib的最底層消息處理函數(shù)有兩個,1個是CWindowWnd類的ShowModal函數(shù),1個是CPaintManager類的MessageLoop函數(shù),這兩個函數(shù)有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程序的消息需要先調(diào)用GetMessage,然后調(diào)用win32的TranslateMessage和DispatchMessage函數(shù)來分派消息。而duililb在win32的TranslateMessage之前先調(diào)用了CPaintManager中的1個名為TranslateMessage的靜態(tài)函數(shù)來過濾消息。而這個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控件的情況。函數(shù)進入后調(diào)用者兩行代碼判斷發(fā)送消息的窗體是否是子窗體
UINT uStyle = GetWindowStyle(pMsg->hwnd);
UINT uChildRes = uStyle & WS_CHILD;
如果沒有
CEditUI或CWebBrowserUI控件,通常情況下就不會有子窗體,那末TranslateMessage往下履行后if (uChildRes != 0)判斷就不會成功,也就是會調(diào)用else里面的代碼。在else里面,會遍歷m_aPreMessages數(shù)組中的元素(m_aPreMessages是全局變量,里面保存了所有窗體的CPaintManager對象的指針),然后調(diào)用每一個元素的PreMessageHandler函數(shù),直到消息被處理。
而如果包括CEditUI或CWebBrowserUI控件,那末他們內(nèi)部就會創(chuàng)建win32原生的控件(也就是子窗體),那末if
(uChildRes != 0)判斷就會成功,任然是順次遍歷m_aPreMessages數(shù)組的元素,但是代碼有些不同
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循環(huán)中被GetParent函數(shù)修改。問題就在這里了!當遍歷到m_aPreMessages的的元素,也就是主窗體的CPaintManager時
if(pMsg->hwnd == pT->GetPaintWindow() || hTempParent == pT->GetPaintWindow())
這句代碼的hTempParent == pT->GetPaintWindow()會被判斷為成功,由于win32原生控件句柄屢次GetParent后就會得到主窗體的句柄,這時候hTempParent的值就和m_aPreMessages的第1個元素,也就是pT->GetPaintWindow()的結(jié)構(gòu)相同。
判斷成功后,會調(diào)用pT->PreMessageHandler,履行主窗體的PreMessageHandler函數(shù),然后通過PreMessageHandler的代碼可以知道,主窗體設(shè)置了自己的Tab焦點后,履行了return true。而PreMessageHandler返回true,在這個TranslateMessage里面也就返回了true,這時候TranslateMessage就結(jié)束了。明顯看到,這類情況下,彈出窗體的CPaintManager根本沒法履行PreMessageHandler函數(shù),這就解析了為何子窗體的CEditUI和CWebBrowserUI沒法切換焦點而主窗體可以。
這下子找到了本源,分析進程1的修復(fù)代碼就是沒必要的,這里這樣修改代碼后,bug就修復(fù)了。(注意,終究的bug修復(fù)代碼只需要修改這1個函數(shù)就好了,之前分析進程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;
}
修復(fù)代碼很簡單,不讓他return,而是繼續(xù)把消息傳遞下去。附效果圖:

幾經(jīng)波折,前后我分析和調(diào)試了4個多小時duilib,終究只要修改3行代碼,bug就修復(fù)了。
總結(jié):
實際的修復(fù)進程其實不是文章描寫的這么順利,期間修改過量次CEditUI的控件代碼也實現(xiàn)了焦點切換,還該多其他地方的很多代碼,我就不在文章中描寫了。而在后續(xù)的調(diào)試進程中才發(fā)現(xiàn)了原來問題的根本在于CPaintManager中的TranslateMessage消息處理。幾次周轉(zhuǎn)總算修復(fù)了bug。但是我還沒有對這個修復(fù)的代碼進行完全的測試,不知道他會不會引發(fā)甚么新的問題。所以如果有打算修復(fù)這個bug的朋友建議你多做1些測試,如果發(fā)現(xiàn)有甚么問題,請在博客中留言或QQ上告知我1下,謝謝~~
Redrain 2014.11.28
QQ:491646717
生活不易,碼農(nóng)辛苦
如果您覺得本網(wǎng)站對您的學習有所幫助,可以手機掃描二維碼進行捐贈