備注:Rimi是我們用的一個(gè)分布式機(jī)制。
在進(jìn)行設(shè)備樹(shù)(也就是一個(gè)CTreeCtrl控件)更新修改的時(shí)候,遇到了一個(gè)比較bug的問(wèn)題。
為了提供更好的用戶體驗(yàn),甲方希望設(shè)備樹(shù)更新之后滾動(dòng)條位置能夠保持與更新前一致。設(shè)備樹(shù)的更新過(guò)程是這樣的:更新消息來(lái)自Rimi的通知機(jī)制,類似于函數(shù)回調(diào),客戶端在收到消息之后調(diào)用Rimi的對(duì)象方法來(lái)獲取新的設(shè)備樹(shù)信息,然后更新樹(shù)。乍看之下,要完成這個(gè)修改好像很簡(jiǎn)單,只要更新前先記錄滾動(dòng)條的滾動(dòng)位置,更新后還原位置,如果更新后滾動(dòng)條滾動(dòng)范圍變化了還要微調(diào)一下位置,邏輯上來(lái)講就這么幾個(gè)步驟。
我一開(kāi)始也是按照這樣的思路,GetScrollPos()獲取當(dāng)前滾動(dòng)條的滾動(dòng)位置,然后更新樹(shù)(先刪除所有節(jié)點(diǎn)再逐個(gè)添加,其他省略...),GetScrollRange()獲得新的滾動(dòng)范圍,最后SetScrollPos()將舊的位置與滾動(dòng)范圍最大值中最小的一個(gè)設(shè)回去(這里用到的ScrollBar是CTreeCtrl自動(dòng)產(chǎn)生的,注意不是兩個(gè)控件,這里調(diào)用的函數(shù)都是CTreeCtrl的方法)。但實(shí)際效果是,樹(shù)更新后滾動(dòng)條滾到準(zhǔn)確的位置,但樹(shù)的視圖到了最頂,點(diǎn)擊一下滾動(dòng)條的那個(gè)方塊才能回到之前的位置。也就是說(shuō),滾動(dòng)位置的更新與樹(shù)的視圖分離了。
之后,我一直以為是我控件的方法用錯(cuò)了,對(duì)著MSDN和CSDN糾結(jié)了很久。最后忍無(wú)可忍,自己寫了個(gè)測(cè)試Demo,里面就一Dialog,一CTreeCTrl,樹(shù)上隨便加了些東西,然后又一按鍵,按鍵后會(huì)重新刷新樹(shù),再滾動(dòng)到原來(lái)的位置,結(jié)果居然是對(duì)的,視圖跟著滾動(dòng)條的位置變化了。為了更好的模擬設(shè)備樹(shù)節(jié)點(diǎn)增刪的效果,我在按鍵響應(yīng)上又作了處理,按一下重刷樹(shù)的時(shí)候會(huì)隱藏幾個(gè)節(jié)點(diǎn),再按一下這些節(jié)點(diǎn)顯示出來(lái),滾動(dòng)位置按照客戶端里面的一個(gè)處理方法,結(jié)果居然也是正確的。問(wèn)題變得玄乎了!
無(wú)意間發(fā)現(xiàn)客戶端里面有個(gè)手動(dòng)刷新設(shè)備樹(shù)的快捷鍵,估計(jì)是當(dāng)年pb做調(diào)試的時(shí)候留下來(lái)的。快捷鍵的響應(yīng)直接調(diào)用更新樹(shù)的函數(shù),重刷后的顯示出人意料地是對(duì)的。比較一下兩種更新方式的過(guò)程:
Rimi: 通知到來(lái)—>更新樹(shù)(Rimi回調(diào)函數(shù),Rimi自己維護(hù)了一個(gè)線程池,遠(yuǎn)程調(diào)用在被調(diào)用端的發(fā)起者都是Rimi自己的線程)
快捷鍵: 按鍵響應(yīng)—>更新樹(shù)(MFC消息處理函數(shù))
更新樹(shù)所用到的是同一個(gè)函數(shù),但調(diào)用者卻是不同的。因?yàn)镽imi用了boost::function,那我也在按鍵響應(yīng)的時(shí)候?qū)σ{(diào)的函數(shù)用function來(lái)包裝一下,造成兩者在調(diào)用棧上調(diào)用的函數(shù)、順序大部分是一致的,只有最底層不同,一邊是Rimi,一邊是MFC消息傳遞。
后來(lái)jianhao說(shuō),以前在Rimi的回調(diào)函數(shù)里面調(diào)Rimi對(duì)象的方法出過(guò)問(wèn)題,然后我又順道回憶起之前zxb在Rimi函數(shù)(還是對(duì)象方法)里面調(diào)system()也有問(wèn)題。
難道說(shuō)Rimi線程就是“萬(wàn)惡之源”?好吧,我把更新代碼移到另外一個(gè)線程里面,Rimi回調(diào)的時(shí)候喚醒更新線程,更新后視圖還是不能跟著滾動(dòng)位置變;將快捷鍵的響應(yīng)也修改一下,自己不作更新,也是喚醒更新線程,這個(gè)方法也變得不靈了,囧!這可以說(shuō)明問(wèn)題跟Rimi線程無(wú)關(guān)。
難道說(shuō)線程調(diào)用才是“萬(wàn)惡之源”?把之前做的那個(gè)Demo小改了一把,線程做刷新,按鍵響應(yīng)只喚醒更新線程,果然不靈了!上網(wǎng)google了一把,關(guān)鍵字“mfc 線程 操作控件”,首先映入眼簾的是《MFC中跨線程操作控件會(huì)不會(huì)出現(xiàn)像C#中的異常問(wèn)題?》。這時(shí)候我也不關(guān)心這個(gè)帖子的內(nèi)容了,線程操作控件有異常是吧,那就不用線程做咯!這時(shí)候我才回想起WIN32里面有自定義消息這玩意,MFC里面給定一個(gè)消息ID,ON_MESSAGE綁定一個(gè)處理函數(shù),PostMessage或SendMessage來(lái)發(fā)消息,然后由WIN32自己的消息循環(huán)來(lái)調(diào)用處理函數(shù),這樣應(yīng)該是可以保證用非Rimi線程來(lái)更新設(shè)備樹(shù)的。再一次把Demo小改了一把,按鍵響應(yīng)Post一個(gè)自定義消息,消息處理函數(shù)做刷新,結(jié)果是對(duì)的;再改,按鍵響應(yīng)喚醒線程,線程里面Post自定義消息,結(jié)果也是對(duì)的。
原以為是控件使用問(wèn)題,又以為是Rimi不兼容問(wèn)題,最后實(shí)質(zhì)為MFC跨線程使用控件的問(wèn)題。其實(shí)我也不清楚這是不是真正的問(wèn)題,畢竟我MFC既不懂又用得少。That's all!
最后附上我的測(cè)試代碼
http://m.shnenglu.com/Files/neverwinter/testtree.rar