天天看點

修複duilib CEditUI控件和CWebBrowserUI控件中按Tab鍵無法切換焦點的bug

轉載請說明原出處,謝謝~~:http://blog.csdn.net/zhuhongshu/article/details/41556615

        在duilib中,按tab鍵會讓焦點在Button一類的控件中切換,但是切換焦點一直存在bug,具體的描述如下:

        1、在主窗體裡彈出新的窗體,當新窗體中存在CEditUI控件并且焦點在此CEditUI控件上,那麼按tab鍵将無法切換焦點而一直處于CEditUI中。(隻在新窗體中有此bug,主創體中沒有,原因會在後面分析)

        2、CWebBrowserUI控件同CEditUI

        之間在群裡就看到有人問這個問題,而且也一直沒解決。

       這幾天在用duilib寫一個注冊界面時(如圖,此頁面便是在主窗體上面的一個彈出窗體),上面有多個CEditUI控件,按照我們的習慣,輸入完第一個edit的内容後會按tab切換到下一個edit。而由于duilib的bug導緻這個焦點無法切換。我自己一般是需要什麼功能就摸索什麼功能,之前用duilib是沒有遇到edit切換焦點的需求,是以就沒有考慮過這個bug,今天碰到了這個需求,就得先解決這個bug了。

修複duilib CEditUI控件和CWebBrowserUI控件中按Tab鍵無法切換焦點的bug

分析過程一:

        很明顯可以看出來,這個bug隻存在于CEditUI和CWebBrowserUI控件中,而這兩個控件與其他控件的差別就在于他們都是用了原生的wini32控件,我這裡就隻分析CEditUI控件了。

        在CEditUI控件的源碼裡可以很容易看到,當他的DoEvent函數裡收到擷取焦點的UIEVENT_SETFOCUS消息或者滑鼠按下的UIEVENT_BUTTONDOWN消息後,他就會建立一個子窗體并且維護這個子窗體的相關資料。而這個子窗體會自動通過CreateWindowEx函數建立一個原生的win32的edit控件,當子窗體失去焦點時自動銷毀自身,這也就是CEditUI控件的實作原理。

       焦點切換的處理是由CPaintManager類管理的,當我們在界面中按下Tab鍵打算切換焦點後,CPaintManager會攔截鍵盤消息然後去管理焦點切換,那麼我修複起點就從焦點管理函數開始。焦點管理的函數是PreMessageHandler,原型如下:

        可以看到函數裡接活VK_TAB按鍵後,會去調用SetNtextTabControl函數去設定下一個控件擷取焦點,然後傳回true。而SetNtextTabControl函數的原型如下:

       函數裡調用FindControl函數,根據__FindControlFromTab函數和bForward參數來決定搜尋下一個焦點的控件,__FindControlFromTab函數的代碼我就不分析了,當找到了下一個應該擷取焦點的控件後,調用CPaintManager的SetFocus函數讓新控件擷取焦點。而SetFocus函數裡,首先對舊的擷取焦點的控件發送UIEVENT_KILLFOCUS消息讓他失去焦點,然後将新的擷取焦點的控件指針指派給m_pFocus變量(CPaintManager中儲存目前擷取焦點的控件指針的成員變量),并且給新的擷取焦點的控件發送UIEVENT_SETFOCUS消息讓他擷取焦點。

      從代碼中看,理論上沒有什麼問題,我就針對CEditUI來進行修改。在CEditUI的内嵌子窗體類CEditWnd中的HandleMessage函數裡加入如下代碼,讓CEditWnd收到Tab消息後來主動調用CPaintManager的SetNextTabControl函數來切換焦點:

      這樣修改後還不起作用,原因是PreMessageHandler函數中處理WM_KEYDOWN消息後直接reutrn true導緻了消息的截斷,進而無法傳遞到CEditWnd,是以再把return true語句注釋掉,這時會驚喜的發現,可以切換焦點了!

分析過程二:

      這樣稀裡糊塗的修複了bug,并且測試正常。但是我心裡很疑惑為什麼這樣在CEditWnd裡面調用SetNextTabControl可以切換焦點但是在CpaintManager的PreMessageHandler裡面調用SetNextTabControl函數卻失效。而且這也無法解釋為什麼這個bug隻存在于彈出窗體而不是主窗體中,後來才意識到問題的原因根本不在于CEditWnd和PreMessageHandler!

      接着分析過程一之後,我一直調試SetNextTabControl函數和SetFocus函數,下了很多條件斷點和資料斷點,試圖找到在CPaintManager的PreMessageHandler裡面調用SetNextTabControl函數失效的原因。最後發現執行PreMessageHandler的CpaintManager類根本不是彈出窗體的CPaintManager,而是主窗體的CPaintManager!主窗體的CPaintManager調用了SetNextTabControl,他是給主窗體的控件切換了焦點!而彈出的子窗體的CPaintManager根本沒有執行PreMessageHandler函數,是以他的SetNextTabControl失效了,而我稀裡糊塗的在CEditWnd裡面調用了SetNextTabControl歪打正着的調用了彈出窗體的SetNextTabControl。這就解析了分析過程一中為什麼看上去修複了bug。

      那麼現在就要分析一下為什麼明明在彈出窗體中按了Tab鍵,最後調用的卻是主窗體的PreMessageHandler函數。

      這要從duilib的最底層消息處理函數說起,他是是以duilib程式消息的起點。duilib的最底層消息處理函數有兩個,一個是CWindowWnd類的ShowModal函數,一個是CPaintManager類的MessageLoop函數,這兩個函數有一個共同點,共同的代碼如下:

      大家都知道win32程式的消息需要先調用GetMessage,然後調用win32的TranslateMessage和DispatchMessage函數來分派消息。而duililb在win32的TranslateMessage之前先調用了CPaintManager中的一個名為TranslateMessage的靜态函數來過濾消息。而這個TranslateMessage才是bug的出處!他的代碼如下:

       我來分析一下導緻bug的原因。首先說一下當窗體中沒有CEditUI或者CWebBrowserUI控件的情況。函數進入後調用者兩行代碼判斷發送消息的窗體是不是子窗體

      如果沒有CEditUI或者CWebBrowserUI控件,通常情況下就不會有子窗體,那麼TranslateMessage往下執行後if (uChildRes != 0)判斷就不會成功,也就是會調用else裡面的代碼。在else裡面,會周遊m_aPreMessages數組中的元素(m_aPreMessages是全局變量,裡面儲存了所有窗體的CPaintManager對象的指針),然後調用每個元素的PreMessageHandler函數,直到消息被處理。

      而如果包含CEditUI或者CWebBrowserUI控件,那麼他們内部就會建立win32原生的控件(也就是子窗體),那麼if

(uChildRes != 0)判斷就會成功,任然是依次周遊m_aPreMessages數組的元素,但是代碼有些不同

         其中的hTempParent句柄會在while循環中被GetParent函數修改。問題就在這裡了!當周遊到m_aPreMessages的的元素,也就是主窗體的CPaintManager時

         這句代碼的hTempParent == pT->GetPaintWindow()會被判斷為成功,因為win32原生控件句柄多次GetParent後就會得到主窗體的句柄,這時hTempParent的值就和m_aPreMessages的第一個元素,也就是pT->GetPaintWindow()的結構相同。

         判斷成功後,會調用pT->PreMessageHandler,執行主窗體的PreMessageHandler函數,然後通過PreMessageHandler的代碼可以知道,主窗體設定了自己的Tab焦點後,執行了return true。而PreMessageHandler傳回true,在這個TranslateMessage裡面也就傳回了true,這時TranslateMessage就結束了。明顯看到,這種情況下,彈出窗體的CPaintManager根本沒法執行PreMessageHandler函數,這就解析了為什麼子窗體的CEditUI和CWebBrowserUI無法切換焦點而主窗體可以。

          這下子找到了根源,分析過程一的修複代碼就是沒必要的,這裡這樣修改代碼後,bug就修複了。(注意,最終的bug修複代碼隻需要修改這一個函數就行了,之前分析過程一的不需要修改了!)

        修複代碼很簡單,不讓他return,而是繼續把消息傳遞下去。附效果圖:

修複duilib CEditUI控件和CWebBrowserUI控件中按Tab鍵無法切換焦點的bug

        幾經波折,前後我分析和調試了4個多小時duilib,最終隻要修改三行代碼,bug就修複了。

總結:

       實際的修複過程并不是文章描述的這麼順利,期間修改過多次CEditUI的控件代碼也實作了焦點切換,還該多其他地方的很多代碼,我就不在文章中描述了。而在後續的調試過程中才發現了原來問題的根本在于CPaintManager中的TranslateMessage消息處理。幾次周轉總算修複了bug。但是我還沒有對這個修複的代碼進行完整的測試,不知道他會不會引起什麼新的問題。是以如果有打算修複這個bug的朋友建議你多做一些測試,如果發現有什麼問題,請在部落格中留言或者QQ上告訴我一下,謝謝~~

Redrain   2014.11.28

QQ:491646717