在第一部分我們創建了一個類CFolderTabCtrl,用這個類實現了類似Excel和Visual C++應用中文件夾式樣的標簽控制。在閱讀本文之前,最好先看上一篇文章及其例子代碼FldrTab。FldrTab純粹是一個測試CFolderTabCtrl類的例子,沒有什麼實質性的用途。為了真正模仿出Excel和Visual C++的文件夾式樣標簽控制的效果,本文擬將CFolderTabCtrl應用到一個實際的MFC程序中。並且標簽的旁邊也象Excel一樣有水平滾動條,如圖一所示:
圖一 標簽和滾動條
本文的例子原來是一個顯示位圖(bitmap)文件及其BITMAPINFOHEADER結構信息的程序。圖像及其BITMAPINFOHEADER結構信息都是顯示在同一個視圖畫面裡。如圖二所示:
圖二 圖像和信息在同一畫面
為了將CFolderTabCtrl標簽控制類引入到這個程序,我們創建了兩個新類,CFolderFrame和CFolderView。此外,我們還要對原來的程序進行改進,使它能夠在不同的標簽頁裡分別顯示圖像和BITMAPINFOHEADER結構信息。如圖三和圖四:
圖三 顯示位圖圖像
圖四 顯示位圖文件格式頭結構信息
另外標簽控制頁中還有一個用於顯示原始圖像十六進制數據的Hex標簽,不過這是一個虛設的標簽,我並沒有實現它,如果哪位朋友有興趣,可以自己去完成,做好後別忘了把源代碼也給我一份哦!
從個人的角度來講,我很懷疑用這樣的方法來改進程序的可行性,因為我覺得將信息顯示在一個畫面中會更直觀。但是,本文的目的是示范,並不指望拿它去獲得UI設計的獎項,僅僅是用它來作為例子,示范如何同時實現CFolderCtrlTab標簽控制和滾動條控制,僅此而已。
此外,要記住一點,每當你要修改或增強MFC應用程序框架特性時,盡量少的觸及MFC框架本身。模仿它而不要去破壞它。以此為原則,我采取的策略是在框架和視圖之間插入新窗口。如圖五所示:
圖五 插入的新框架與主框架的關系
上圖說明了基本的框架關系。主框架(或MDI子框架)包含作為子窗口的CFolderFrame,然後CFolderFrame包含水平滾動條和標簽控制並管理它們之間的交互。
CFolderFrame的使用很簡單,自己要做的事情並不多。首先必須改寫主框架的OnCreateClient函數以便創建CFolderFrame表示的窗口。
BOOL CMainFrame::OnCreateClient(...,
CCreateContext* pcc)
{
return m_wndFolderFrame.Create(this,
RUNTIME_CLASS(CDIBView), pcc,
IDR_FOLDERTABS);
}
因為原來的位圖顯示程序同時支持SDI和MDI版本。對於MDI的情況,你要在MDI子框架中改寫OnCreateClient,而OnCreateClient函數通常是MFC創建視圖的地方,如今應該在此創建CFolderFrame子框架。不要調用基類的OnCreateClient!然後,CFolderFrame子框架用運行時類創建一個視圖和你要傳遞的上下文信息。IDR_FOLDERTABS是串資源的ID,用它存儲標簽名。如果你想指定動態的標簽名,可以省略這個參數並調用CFolderFrame::GetFolderTabCtrl來獲得標簽控制,然後用CFolderTabCtrl::AddItem添加標簽頁。有關細節請參考上一篇文章和附帶的源代碼。
接下來,你必須自己添加一些視圖代碼。其中最重要的事情是從CFolderView派生自己的視類,而不是從CScrollView類派生。因為要讓標簽控制和滾動條協調操作,所以這是問題的重點所在。如果你不使用滾動視圖,那就不存在這個問題——你可以將標簽控制創建成一個子視圖。一旦你從CFolderView派生了自己的視類,那麼便可以處理來自標簽控制的通知消息。為此改寫虛擬函數CFolderView::OnChangedFolder即可。當用戶點擊新的標簽時,CFolderView便調用這個函數。這個函數的實現細節很簡單,主要負責存儲新標簽頁並重畫視圖:
void CDIBView::OnChangedFolder(int iPage)
{
m_iPage = iPage;
UpdateScrollSizes();
Invalidate();
}
記住不要忘了修改視圖的OnDraw函數,讓它繪制正確的標簽頁,改進後的程序需要在標簽頁之間來回切換,m_iPage表示頁索引,它的值分別為0,1,2,三個標簽頁分別用來繪制圖像、顯示BITMAPINFOHEADER結構信息和顯示十六進制數據。最後,你必須在CDIBView::OnInitialUpdate中加一行顯示CFolderFrame框架控制的代碼:
// 在 CDIBView::OnInitialUpdate 函數中
GetFolderFrame()->ShowControls(pDIB ? CFolderFrame::bestFit : CFolderFrame::hide);
CFolderFrame::ShowControls可以讓你隱藏和顯示標簽控制和滾動條。這樣當程序為SDI並且啟動空框架時——也就是說沒有文檔/視。這時程序中的pDIB==NULL,CDIBView::OnInitialUpdate傳遞CFolderFrame::hide來隱藏控制;否則傳遞CFolderFrame::bestFit來指示CFolderFrame根據需要的寬度顯示所有標簽,然後用剩下的寬度顯示滾動條。如果你想用其它的算法也未嘗不可,你可以計算寬度,然後用這個調用CFolderFrame::ShowControls。
綜上所述,你必須在主框架的OnCreateClient中創建CFolderFrame,此外還必需從CFolderView派生自己的視類,並按照前面所講的方法進行必要的修改。這樣不用費太多的周折就可以將標簽控制應用到MFC程序框架中。
下面就讓我們到幕後看一看CFolderFrame和CFolderView時如何運作的。CFolderFrame 中有保存滾動條和標簽控制寬度的成員數據。當你調用CFolderFrame::Create的時候,它首先創建CFolderFrame框架,然後創建視圖。它象MFC所做的那樣用上下文信息來創建視圖。但是要注意CFolderFrame創建的是自身的子視圖,不是主框架(或者MDI子框架)。對於框架中的標簽控制和水平/垂直滾動條以及調整大小的機關,都由CFolderFrame在OnCreate來創建,實現起來也不難,細節請參見源代碼。
在代碼中有一個小地方雖然不起眼,但是它很重要,就是將CFolderFrame框架的式樣設置成WS_EX_CLIENTEDGE,以便使你的窗口保持與Windows 9x及Windows NT一致的凹陷效果。CFolderFrame在PreCreateWindow中做這件事。
一旦CFolderFrame創建了屬於自己的窗口,你就必須管理它們的大小。通常這是OnSize的事情。具體細節純粹是一些算法問題,我就不再這裡贅言了,請自己參考源代碼。
CFolderFrame從m_cxFolderTabCtrl獲取標簽的寬度。用SetFolderTabWidth函數來設置寬度,但一般情況下你不必使用這個函數,因為CFolderFrame總是試圖用CFolderTabCtrl::GetDesiredWidth返回理想的寬度。只要OnSize工作正常,你就能看到CFolderFrame是如何仿真文件夾式樣標簽控制的,就像圖四所示的那樣,效果很好,選中的標簽上邊緣白色邊緣與其上視圖的背景無縫連接,盡管它們是兩個不同的窗口,但看不出痕跡。
這一切好象挺簡單,不過我還有最精彩的一部分沒有講到,那就是滾動條如何工作?派生的視圖如何讓MFC在CFolderFrame中操作滾動條,而不是象MFC所想象的那樣使用滾動條?當我剛開始實現單獨的滾動條時——我預料到會碰上很頭疼的問題,因為原來的位圖顯示程序中CDIBView2視類是從CScrollView派生的,它是以子窗口方式隱藏或顯示滾動條。但我此時想讓視圖使用自己的滾動條,一個在CFolderFrame中的滾動條——至少水平滾動條是這樣。如何改變它的大小,使它與標簽控制共處一室呢?於是我開始鑽研CScrollView的內部運作機制,看看能否找到解決問題的辦法。最後我發現只需要改寫一下虛函數CWnd::GetScrollBarCtrl就能搞掂。這個函數有一個參數,其取值要麼是SB_HORZ,要麼是SB_VERT,最後返回相應的滾動條窗口。缺省實現返回Null。MFC走了一條很長的邏輯鏈來創建它需要的滾動條(通過調用::ShowScrollBar)。而CFolderView對GetScrollBarCtrl的改寫很簡單:
CScrollBar* CFolderView::GetScrollBarCtrl(int nBar) const
{return GetFolderFrame()->GetScrollBar(nBar);}
它將控制傳遞到CFolderFrame:
CScrollBar* CFolderFrame::GetScrollBar (int nBar)
{
return nBar==SB_HORZ ? &m_wndSBHorz
: nBar==SB_VERT ? &m_wndSBVert : NULL;
}
這比預料的要簡單多了!MFC設計的靈活性真是令人吃驚。要想使用自己創建的與CScrollView一致的滾動條,僅僅改寫GetScrollBarCtrl就可以了。例如,你可以發明有一個有迷幻色彩的超級滾動條……哈哈,帥呆了。
標簽控制還有一個地方要說明:因為滾動條是CFolderFrame框架的一個子窗口,而不是視圖的子窗口,CFolderFrame傳遞滾動消息(WM_HSCROLL 和 WM_VSCROLL),所以還必須自己編寫代碼將它們轉發到視圖:
void CFolderFrame::OnHScroll(...)
{
GetView()->SendMessage(WM_HSCROLL, ...);
}
對於WM_VSCROLL消息也同樣如法炮制。一旦你解決的這些雞毛蒜皮似的問題,滾動條的運行便OK了,它工作起來就像CScrollView一樣。
其實,GetScrollBarCtrl是CFolderView存在一個主要理由,雖然CFolderView也將通知消息FTN_TABCHANGED從標簽控制轉換成輕松的OnChangedFolder虛函數調用,這使得通知消息的處理也變成了僅僅在視圖中改寫虛函數這麼簡單。
好了,看了這篇文章,你肯定覺得CFolderFrame和CFolderView並沒有實現每一個想要的有關標簽的UI特性,例如,沒有提供左右以及開始/結束按鈕,就像Excel界面那樣(如圖一所示)——說得沒錯,但是CFolderFrame和CFolderView已經提供了基本的框架,你可以在此基礎上添加想要的特性。把按鈕作為標簽控制的附加控制添加進去,並在上下文的邏輯鏈中傳遞BN_CLICKED即可。總之,沒有做不到的,只有想不到的,你可以對自己的程序進行所隨心所欲的控制。下一部分我們將對標簽頁的數量進行擴展,並加上移動控制。(待續)