Wtl的sdi應用,視圖默認鋪滿框架的客戶區。視圖通常用modeless對話框,所有的界面元素都擁擠在左上角,這明顯很丑陋。我們嘗試讓視圖居中顯示,保持原始大小,這是個很典型的問題,看似簡單,諸多細節,逐一解決后,對Wtl的理解程度,馬上能達到通透的水平。
Wtl比較臭名昭著的一點:沒有官方資料。許多問題只能靠分析源代碼來解決。本文詳細的描述整個解決過程,以及如何快速的閱讀、分析Wtl源代碼。
一、Google之路:
本世紀只要有最低智商的人,首先的方式肯定是Google,我們來看看能否通過Google來找到答案。非常遺憾,我們只能找到一篇Mfc領域的文章:
Making the SDI view smaller than the CFrameWnd:
http://www.codeproject.com/Articles/13621/Making-the-SDI-view-smaller-than-the-CFrameWnd
這篇文章講解了在mfc中,怎樣讓視圖"比框架窗口小",他花費了大量的精力解決閃爍的問題...事后我發現他完全走錯了路子:1、閃爍的問題並非因為他所理解的原因;2、他的解決方法,如果將視圖設置得更小一些,仍然會閃爍。使用Wtl center view sdi之類的關鍵詞,無論你怎么搜索,相信你也找不到一篇...任何一篇文章,說明如何處理。你不必再嘗試,因為我整整花費了兩個小時,專注,中途絕對沒有轉移視線。Google大師沒有找到的,你肯定也找不到。偷懶沒用的時候,你只有動用終極手段,讀代碼、理解,然后自己搞定。當然,這種終極手段遠沒有那么辛勞,只要隨時注意大而化之...
二、第一步:創建時居中顯示:
在CMainFrame類的OnCreate函數中:MESSAGE_HANDLER(WM_CREATE, OnCreate)
m_hWndClient=m_view.Create(m_hWnd);
這里m_view創建了視圖窗口,m_hWndClient保留了視圖的句柄。我們在這里居中顯示,試試看...
m_view.CenterWindow(m_hWnd);//當然,這個實在整個窗體居中,我們可以自行寫函數處理在客戶區居中。為了快速實現,我們暫時忽略細節。
你會很失望,因為運行之后,這行代碼沒有發生任何作用。原因何在?CMainFrame的基類,肯定對視圖的顯示做了處理,讓對話框鋪滿窗體,需要改變其大小。我們先用一個暴力的方法,讓基類不知道這是視圖:將m_hWndClient=m_view.Create(m_hWnd)修改為m_view.Create(m_hWnd)。再看看...果然,對話框居中顯示,很正常。基類明顯針對m_hWndClent處理,當m_hWndClient為NULL的時候,代碼也肯定做了判斷,因此程序能正常運行。
我們當然不能用這種粗暴的方式,程序員一般總要裝得紳士一些...那么雅致一點,就意味着大量的工作,我們首先要做的,是找到基類里這部份內容。
三、第二步:CMainFrame的繼承關系
先簡單閱讀一下向導生成的CMainFrame代碼,方式很簡單,看繼承自哪些類,看消息映射,看函數的名字...除非必要,不要過多的看函數的細節。
1、CMainFrame類的繼承關系:
在vs2013中,鼠標指向類名,然后右鍵在快捷菜單中點擊"轉向定義",很容易查出CMainFrame的繼承關系。
class CMainFrame : public CFrameWindowImpl<CMainFrame>, public CUpdateUI<CMainFrame>, public CMessageFilter, public CIdleHandler template <class T, class TBase = ATL::CWindow, class TWinTraits = ATL::CFrameWinTraits>
class ATL_NO_VTABLE CFrameWindowImpl : public CFrameWindowImplBase< TBase, TWinTraits > template <class TBase = ATL::CWindow, class TWinTraits = ATL::CFrameWinTraits>
class ATL_NO_VTABLE CFrameWindowImplBase : public ATL::CWindowImplBaseT< TBase, TWinTraits >
很清晰,CMainFrame<-CFrameWindowImpl<-CFrameWindowImplBase<-ATL::CWindowImplBaseT,到Atl一層我們暫時不用管了...Wtl的提供的框架基類,包括兩層CFrameWindowImpl<-CFrameWindowImplBase,向導創建的CMainFrame和這兩個基類,就是Wtl關於框架類的全部源代碼。
四、第三步:找到修改視圖大小的地方
1、查看CMainFrame和兩個基類的消息映射表:
可以看到CMainframe的映射表最后,鏈接了CFrameWindowImpl。同樣,后者鏈接了CFrameWindowImplBase。解釋一下所謂的鏈接
CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)
意思是,本類的消息映射表中沒有處理的,將在鏈接的CFrameWindowImpl<CMainFrame>的映射表中繼續響應。本類的消息映射表中處理了的,如果bHandled為false,表示沒有處理完成,消息仍然會在CFrameWindowImpl<CMainFrame>的映射表中繼續響應。如果為bHanded如果為true,則表示消息處理完成,映射表中就不會往下傳遞,即使鏈接了CFrameWindowImplBase,且CFrameWindowImplBase的消息映射表有響應函數,它也不會執行。
可以直觀的設置斷點,然后單步執行,能看到在消息映射表中從上到下執行的過程。
因此,消息映射表的順序是非常重要的,如果CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)放在最前面...這個順序會倒過來,派生類就不太好覆蓋基類的處理。
2、分別查看三個類的消息映射表:
那么,和創建、位置有關的,我們在CFrameWindowImpl中,看到Onsize函數,視圖的位置、大小就是在這里改變的:
OnSize(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled){ if(wParam != SIZE_MINIMIZED) { T* pT = static_cast<T*>(this); pT->UpdateLayout(); } bHandled = FALSE; return 1; }
3、理解模板多態:
這部分代碼比較好理解,這就是"模板多態" T* pT = static_cast<T*>(this);這里按繼承關系,T是我們傳入的CMainFrame類型,將this轉化成CMainFrame類的指針,然后執行pT->UpdateLayout();而UpdateLayout()在CFrameWindowImplBase中實現,因此:
首先,我們的CMainFrame中沒有重新定義UpdateLayout,但由於CMainFrame歸根揭底是從CFrameWindowImplBase中繼承下來的,擁有這個函數
所以這種情形下,執行的是CFrameWindowImplBase的UpdateLayout()
然后,假設我們在CMainFrame里定義了完全同型的UpdateLayout函數,那么,指向CMainFrame指針的pT,當然只會執行我們定義的UpdateLayout,基類定義的函數就成為擺設。這就是所謂的模板多態...基類不知道派生類會有多少種、各自什么名稱,所以繼承的時候要將派生類名稱傳遞給基類
這也是這種繼承方式的來由:class CMainFrame : public CFrameWindowImpl<CMainFrame>
我們可以看看UpdateLayout的代碼,繼續動用"轉向定義"
4、UpdateLayout代碼分析:
多數情況下,我們在翻看代碼的時候,不必深入函數的實現細節。比如UpdateLayout,從名字上可以看到,是更新窗體的布局。函數在父類CFrameWindowImpl中調用,在祖父類CFrameWindowImplBase實現,調用是采用模板多態。由於CMainFrame和父類中都沒有覆蓋,因此調用的就是祖父類CFrameWindowImplBase中定義的函數。一般情況下這么理解基本就可以了。
只有在特殊情況下,我們才需要詳細分析某個函數,因為我們要弄清它如何改變視圖大小、我們也要阻止它。
下面就是祖父類中的UpdateLayout的代碼,注釋比較清晰。
void UpdateLayout(BOOL bResizeBars = TRUE) { RECT rect = { 0 }; GetClientRect(&rect); //獲取整個應用的客戶區rect,這只是除去窗口的標題、邊框之后,剩下的窗體工作區域 // position bars and offset their dimensions
UpdateBarsPosition(rect, bResizeBars); //該rect減去菜單、工具欄、狀態欄所占區域 //此處得到的rect是全部客戶區,可以在這個范圍內居中顯示 //如果不要鋪滿視圖,則注釋掉下面的語句,會出現狀態欄殘痕,這是UpdateBarsPosition要處理的 // resize client window
if(m_hWndClient != NULL) //這里將客戶區鋪滿。如果注釋掉,則大小變化的時候,狀態欄會出現異常,前面部分區域沒有消除
::SetWindowPos(m_hWndClient, NULL, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOZORDER | SWP_NOACTIVATE); } void UpdateBarsPosition(RECT& rect, BOOL bResizeBars = TRUE) { // resize toolbar
if(m_hWndToolBar != NULL && ((DWORD)::GetWindowLong(m_hWndToolBar, GWL_STYLE) & WS_VISIBLE)) { if(bResizeBars != FALSE) { ::SendMessage(m_hWndToolBar, WM_SIZE, 0, 0); //相當於調用函數,消息執行完后才執行下一條,是同步代碼,可以理解為調用某個函數
::InvalidateRect(m_hWndToolBar, NULL, TRUE); } RECT rectTB = { 0 }; ::GetWindowRect(m_hWndToolBar, &rectTB); rect.top += rectTB.bottom - rectTB.top; } // resize status bar
if(m_hWndStatusBar != NULL && ((DWORD)::GetWindowLong(m_hWndStatusBar, GWL_STYLE) & WS_VISIBLE)) { //這里沒讓原來區域失效,因為鋪滿地窗體將覆蓋它,但我們若沒有鋪滿窗體,則這里必須同樣失效。
if(bResizeBars != FALSE) ::SendMessage(m_hWndStatusBar, WM_SIZE, 0, 0); RECT rectSB = { 0 }; ::GetWindowRect(m_hWndStatusBar, &rectSB); rect.bottom -= rectSB.bottom - rectSB.top; } }
五、解決方案一:在CMainFrame中覆蓋UpdateLayout
我們將代碼拷貝到CMainFrame,注釋掉改變視圖大小的幾行語句
void UpdateLayout(BOOL bResizeBars = TRUE) { RECT rect = { 0 }; GetClientRect(&rect); //獲取整個應用的客戶區rect,這只是除去窗口的標題、邊框之后,剩下的窗體工作區域 // position bars and offset their dimensions UpdateBarsPosition(rect, bResizeBars); //該rect減去菜單、工具欄、狀態欄所占區域 //此處得到的rect是全部客戶區,可以在這個范圍內居中顯示 //如果不要鋪滿視圖,則注釋掉下面的語句,會出現狀態欄殘痕,這是UpdateBarsPosition要處理的 // resize client window //if(m_hWndClient != NULL) //這里將客戶區鋪滿。如果注釋掉,則大小變化的時候,狀態欄會出現異常,前面部分區域沒有消除 // ::SetWindowPos(m_hWndClient, NULL, rect.left, rect.top, // rect.right - rect.left, rect.bottom - rect.top, // SWP_NOZORDER | SWP_NOACTIVATE); }
重新運行,顯然,我們看到視圖居中顯示了。效果如下:
但遺憾的是,當我們將應用最大化,或者改變大小的時候,狀態欄出現了殘留痕跡,視圖原來的位置也沒有擦除,仍然保留殘痕:
當然,改變大小后,視圖保持了以前在框架中的位置,沒有居中,這是因為我們沒有在onsize中處理。我們先解決簡單的,為CMainFrame響應WM_ONSIZE消息,在消息映射表加上MESSAGE_HANDLER(WM_SIZE, OnSize),然后消息處理函數:
LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { if (m_view.IsWindow()) { m_view.CenterWindow(m_hWnd); } bHandled = False; return 1; }
這里設置bHandled = False;這樣基類的onsize會執行UpdateLayout,這里就不用多此一舉。至於殘留痕跡問題,我們繼續看代碼。
六、第四步:找出主窗體大小改變時,殘痕產生的原因
上面說的殘痕,很明顯,整個界面都紊亂了,我們首先要找到原因。按照上面同樣的方式,我們可以看到,父類只有個OnSize函數,祖父類消息映射中則處理了兩個:擦除背景的消息,是return 1,也就是說,如果存在視圖,默認的背景擦除就不調用了,這里直接處理。
這等於屏蔽了背景擦除或者重畫。
LRESULT OnEraseBackground(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { if(m_hWndClient != NULL) // view will paint itself instead就是說這由視圖來做
return 1; bHandled = FALSE; return 0; }
為什么要屏蔽?因為,視圖鋪滿客戶區的情形下...根本無需擦除背景。同時,前面在UpdateLayout中,狀態欄沒有發消息重畫,也是同樣的原因。所以,這里可以看出,Wtl的Frame類設計的基礎,就是視圖鋪滿客戶區。
題外話,祖父類中還處理了一個消息OnSetFocus,即程序啟動之后,視圖即獲得焦點
LRESULT OnSetFocus(UINT, WPARAM, LPARAM, BOOL& bHandled) { if(m_hWndClient != NULL) ::SetFocus(m_hWndClient); bHandled = FALSE; return 1; }
七、解決方案之二:解決背景擦除問題,讓祖父類的OnEraseBackground失去作用
我們為CMainFrame響應WM_ERASEBKGND消息
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground) LRESULT OnEraseBackground(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { return DefWindowProc(uMsg, wParam, lParam); }
這里的代碼很簡單,即對於WM_ERASEBKGND消息,調用默認的消息處理函數。注意這是一個宏,調用的也不是win32 api而是基類的函數。由於bHandle默認為true,因此該消息對Chain的父類、祖父類不再可見。這樣,重新運行,不錯,現在視圖能夠正常的居中,無論最大化、改變主窗體大小,都能正常居中,不再有殘影。到此為止,我們的居中小視圖事業...算是圓滿解決?
No,接下來還剩下一個大的問題,閃爍!
我們最大化主窗體,或者改變大小,都能看到明顯的閃爍。細節看出態度,態度決定一切...我們,怎么能閃爍不已呢?
八、第五步:找出閃爍產生的原因
首先,閃爍之Google大計。很痛苦,很痛苦,當百度活得滋潤的時候,你完全是在受它的折磨和蹂躪。當我終於發現bing也能找到不少國外的資料時,你發現它確實很弱智。最終,你還得使用Google,無論你用什么辦法。一時手癢,搜索了一下閃爍,嘩啦啦,鋪天蓋地,無論是Win32、mfc、Wtl、Duilib、Qt...閃爍無所不在。
同時,解決的方法也無奇不有,雙緩沖?自行處理擦除?局部擦除?盡量避免重畫?一個悲哀的結論是:即使微軟自己的程序,閃爍也幾乎無所不在。
我悄悄地嘗試了各種方式...對不起,沒有一種能夠消除剛才的閃爍...上面提到的哪篇Mfc居中顯示視圖的文章,用Wtl原樣實現,閃爍還是很明顯,毫無變化。雖然奇怪,后來發現,他的視圖設置得比較大,幾乎鋪滿了框架...而我的視圖很小,遮擋不住啊。
將這位老先生的視圖改小,我那個...去!這位費勁九牛二虎之力,致力於消除閃爍,號稱比微軟普通軟件都要不閃亮的兄弟...這視圖仍然是閃爍滴,你說這事兒鬧的。
既然各種方法沒用,我們反過來思考,多數閃爍現象,是因為窗體控件太多,在屏幕不同刷新周期顯示,各種法門大體從快速、一次顯示角度出發,或者減少擦除出發。但我們這里遇到的問題,整個框架,只有一個窗體,也就是我們的視圖,沒道理閃爍。
我再仔細觀察了一下,閃爍的現象:最大化時,顯示視圖的同時,視圖原來的位置跳動了一下,看到原位置視圖、視圖內的文字都跳動一下然后消失,再正常的顯示居中的視圖本身。這說明什么呢?月黑風高,一道閃電從窗外怯生生的探進頭來...Onsize中居中,此時在正確的位置顯示視圖。但居中之前,很明顯在原來的位置已經顯示了視圖,只是瞬間被擦除。
瞬間,自動擦除背景、原位置顯示再瞬間消失...這樣怎可能不閃爍?
九、解決方案之三:消除閃爍
那么...當窗體大小變化時,我們先隱藏之...OnSize先令其居中,然后顯示之...問題豈非解決?
//Hide it here
LRESULT OnGetMinMaxInfo(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { if (m_view.IsWindow()) { m_view.ShowWindow(SW_HIDE); } bHandled = false; return TRUE; } //center it here
LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { if (m_view.IsWindow()) { m_view.CenterWindow(m_hWnd); m_view.ShowWindow(SW_SHOW); } bHandled = False; return 1; } //fix position changed here // LRESULT OnWindowPosChanged(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled) { if (m_view.IsWindow() && !m_view.IsWindowVisible()) { m_view.ShowWindow(SW_SHOW); } bHandled = False; return 1; }
解決了這個問題后,很悲哀的看到了一篇:duilib啟動程序時會閃一下(一閃而過)的解決方案。這也說明,但凡Win32編程,道理都是互通的。
這個幾乎是相同的問題...但這哥們只是處理了創建之初,沒有遇到主窗體大小改變的情形。
所以,設計器中創建modeless對話框的時候,默認visble屬性為false,不是沒有道理的,我們手工的Show...可以避免這類啟動時的問題。最后的效果,在原來居中的情形下,主窗體變大后,正常的居中顯示視圖:
十、留下作業?
我們當然不是打算僅僅使視圖居中,我們還需要切換不同的視圖,這些視圖基本上是modeless對話框,這就是簡單的界面框架,讓wtl能夠實現主要流行界面。那么接下來我們還剩下哪些工作?
1、將m_view改為指針?這意味着視圖必需自刪除
2、切換視圖,這需要有通用的方式處理PreTranslateMessage
3、視圖能夠在框架中指定位置,並隨大小移動?
這需要提供相對位置、相對大小的函數,或者在CMainFrame中使用CDialogResize
4、最最重要的是:將上述內容寫成嵌入類?
這絕對是必要的...但大家能夠看到,我一向遵循從具體到抽象、有必要才抽象的次序。尤其是界面相關的編程中,先實現效果,再抽象就是件很簡 單的事情。嵌入類是什么?前面我們看了兩個基類的代碼,嵌入類就很明顯...使用模板多態的類。我們CMainFrame繼承自該類,並將消息映射Chain到該類...上面出現的大量重載函數、消息映射、消息處理函數,就不用再寫了。當然,要保證消息映射表中,嵌入類在 CFrameWindowImpl之前。
十一、有Wtl的書籍嗎?
我還真沒找到,即使可憐的如Mfc程序員的Wtl指南、Wtl指導教程之類,都存在版本嚴重滯后的問題,也存在知識陳舊的問題,Win98干我們甚是?
C++ 11之后,Wtl也成為較好的UI選擇之一,模板編程的思維比較接近。
如果真沒有...或許空閑的時候,我准備就Wtl最新的版本,結合C++ 11,結合一個具體項目的一部分...用本文的風格來完成?考慮到Wtl的冷僻程度,這或許是個注定虧本的買賣。沒有契機...很遺憾,Wtl的小世界,暫時,仍然要生活在碎片化資料、不同版本資料之中,與Google相伴。
本文作者:畢丹軍(11084184@qq.com),轉載請略禮貌些,保留出處。