程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 實現類似Excel和Visual C++裡文件夾式樣的標簽控制(一)

實現類似Excel和Visual C++裡文件夾式樣的標簽控制(一)

編輯:關於VC++

眾所周知,Excel中一個工作簿可以有多個工作表單(worksheet),每個表單可以通過左下角的標簽控制靈活切換(如圖一),Visual C++也有類似的控制,如在Output窗口中設置有:Build,Debug,Find in Files和Results等標簽控制(如圖二)。

圖一Excel中的標簽控制

圖二 Visual C++中的標簽控制

我們將這種界面稱為文件夾式樣的標簽控制,以下簡稱標簽控制,而將MFC中的Tab Control稱為標簽控件。那麼標簽控制是如何實現的呢?MFC中有沒有現成的控件可以利用?

論壇中有很多人提出過這個問題。看了本文以後,我想這個問題應該有一個圓滿的答案。MFC固然給編程帶來了極大的方便,但是它並不能代替程序員的編程,MFC只是提供了一個編程框架,應用的實質性代碼還是必須由程序員自己來寫。同時,MFC的問題也是顯而易見的,那就是其GUI素材太豐富,以至於程序員們過分依賴MFC,當想要實現MFC中沒有的GUI特性時便不知所措。

下面我們就來看看如何實現圖一和圖二所示的文件夾式樣的標簽控制界面。有人可能想到了從現成的標簽控件(Tab Control)入手。但是經驗證明:為了使用的方便性和更好的可重用性起見,還是不要采取這種方法。我是一個熱衷於可重用性的家伙,但是這方面我們在自己的代碼中做得還很不夠。所以我寧願自己創建一個窗口類,這樣做還有一個好處是你能完全控制代碼的修改,不必顧及因現有控件版本的變化而對自己的代碼造成的巨大影響和麻煩。我想微軟的家伙肯定也希望你這麼做。如果你用Spy++查看一下Excel和Visual C++的界面就會發現其文件夾式樣的標簽控制並不是SysTabControl32s,而是另外創建的窗口類。為什麼我們不也來創建一個呢?

請看圖三所示的畫面,這就是我編寫的一個程序FldrTab,它實現了我們所要的界面功能。實現這個UI的C++類是我自己創建的,它叫CFolderTabCtrl。

圖三 FldrTab 程序運行畫面

有關CFolderTabCtrl的實現細節請參考源代碼。其頭文件為Ftab.h,實現文件為Ftab.cpp。在分析CFolderTabCtrl的實現原理之前,讓我先來說明一下這個類的使用方法。當FldrTab程序的InitInstance函數獲得控制權時,它創建一個主對話框的實例,並運行這個對話框:

BOOL CApp::InitInstance()
{
   CMyDialog dlg;
   m_pMainWnd = &dlg;
   dlg.DoModal();
   return FALSE;
} 

    CMyDialog有兩個控制:一個是m_wndStaticInfo,另一個是m_wndFolderTab。顧名思義,第一個控制為一個靜態文本窗口,它顯示選中的標簽,第二個是標簽控制本身,即CFolderTabCtrl實例。通過調用SubclassDlgItem,CMyDialog::OnInitDialog以常規方式子類化靜態文本,遺憾的是它不能子類化標簽控制,因為對話框中並沒有實際的標簽控制窗口。此外也沒有辦法借助COM技術將此標簽控制實現為一個帶運行時接口的定制控件。我的辦法是在對話框想要放置標簽控制的地方創建一個靜態文本控件。如圖五所示:

圖五 用靜態文本控件定位標簽控制

在OnDialogInit中通過調用一個特殊的函數,在運行時將靜態文本替換成標簽控制。

m_wndFolderTab.CreateFromStatic(IDC_FOLDERTAB, this); 

    CFolderTabCtrl::CreateFromStatic 在靜態文本控件的位置上創建一個標簽控制,然後刪除靜態文本控件。這是我創建特殊對話框控制常用的絕招,我認為這個訣竅是超一流的。在調用Create之前,CreateFromStatic調用CFolderTab::GetDesiredHeight來獲得控制的高度,而忽略靜態文本控件的高度。在非對話框應用中不能調用CreateFromStatic;而是要直接調用CFolderTab::Create。創建了標簽控制後,接下來你必須設置標簽名字。這裡是在CMyDialog中調用現成的Load函數。

m_wndFolderTab.Load(IDR_FOLDERTABS); 

    IDR_FOLDERTABS是串資源的ID,它是一個包含新行指示符(“\n”)分割的標簽名(“在線雜志第一期\n在線雜志第二期\n……”),一旦創建了控制並調用Load,那麼你的標簽控制就完全happy了。它看起來就象圖三所示的那樣。

當然,這時它還不能做任何事情,你還必須處理它們的通知消息。當用戶按下一個標簽時,CFolderTab便用特殊代碼FTN_TABCHANGED向對話框發送一個WM_NOTIFY消息。然後對話框處理這條消息,也就是在上面的靜態文本控件中顯示一條信息。

void CMyDialog::OnChangedTab(NMFOLDERTAB* nmtab,LRESULT* pRes)
{
  CString s;
  s.Format(_T("選中 %d: %s"),nmtab->iItem,nmtab->pItem->GetText());
  m_wndStaticInfo.SetWindowText(s);  
}   
NMFOLDERTAB 結構在FTab.h. 文件中定義。 struct NMFOLDERTAB : public NMHDR {
   int iItem;        // 項目索引
   const CFolderTab* pItem; // 標簽
}; 

    這個結構除了NMHDR所包含的成員之外,還有項目索引和指向當前標簽CFolderTab的指針,它與CFolderTabCtrl有所不同,從CFolderTab中你可以得到標簽的文本。以上就是CFolderTabCtrl的使用方法。

下面我們就來揭示這個C++類的實現原理。前面已經對CreateFromStatic進行了描述,那麼CFolderTabCtrl::Load是個什麼樣的函數呢,這個函數的功能是加載一個串標簽名,這個串是用新行指示符(“\n”)分割的字符串,吸取其中的子串,並調用CFolderTabCtrl::AddItem將它添加到每一個標簽上。

int CFolderTabCtrl::AddItem(LPCTSTR lpszText)
{
   m_lsTabs.AddTail(new CFolderTab(lpszText));
   return m_lsTabs.GetCount() - 1;
} 

    就這麼簡單,創建一個新的CFolderTab對象並將它添加到一個列表中。與AddItem相對的是RemoveItem函數,它們的實現都在Ftab.cpp文件中,這兩個函數分別負責動態添加和刪除標簽頁,而不是存取資源串。然後是GetItem和GetItemCount函數,一看它們的名字你就應該明白它們的作用,前者用來獲取CFolderTab標簽的索引號(從0開始),後者則返回m_lsTabs.GetCount,即總共有多少標簽。此外,你一定想需要有個函數來獲取和設置標簽文本,沒問題,每一個CFolderTab對象都有一個m_sText成員變量來存儲標簽名,存取方法是GetText和SetText,我想你閉著眼睛都能寫出這些代碼!

接下來要做的事情很重要,首先是繪制標簽。CFolderTabCtrl::OnPaint在循環中遍歷所有標簽,對每一個標簽調用CFolderTab::Draw來進行繪制處理。這裡有兩個技巧:一個是必須在最後繪制當前選中的標簽(m_iCurItem),以便它看起來重疊在最上面。另一個是要繪制其它標簽,必須讓其它標簽知道自己的位置——也就是定義標簽的梯形坐標。這是此標簽控制的重點所在。下面就來看看實際代碼是怎麼做的。

CFolderTabCtrl有一個RecomputeLayout函數,它計算所有標簽的位置。只要你改變控制的版面,則必須調用它,如添加或刪除某個標簽以及修改某個標簽的名字(它會影響標簽大小)。RecomputeLayout的關鍵代碼如下:

int x = 0;
for (int i=0; i<GetItemCount(); i++) {
   CFolderTab* pTab = GetTab(i);
   if (pTab) x += pTab->ComputeRgn(dc, x) - CXOFFSET;
} 

    RecomputeLayout為每一個標簽調用CFolderTab的成員函數ComputeRgn。ComputeRgn計算出標簽的梯形大小並返回算出的寬度,RecomputeLayout將它與當前x軸坐標相加,然後作為下一個標簽的起始x軸坐標進行參數傳遞,最後減去形狀修飾因子CXOFFSET,使得它們看起來有重疊的效果。之所以這麼做是因為給定的標簽只能決定其大小,不能決定其絕對位置,它需要更多的x軸信息。 一旦ComputeRgn有了x軸坐標,它就可以計算出一個足夠大的梯形來容納標簽文本,注意要加一些邊空,使文本的顯示不會產生混亂。用DT_CALCRECT 調用CDC::DrawText計算文本所占的矩形,然後用結果計算梯形的大小。私有函數GetTrapezoid計算與文本矩形相配的梯形。用象素進行計算確實是一件麻煩和頭疼的事情,我不想讓你為此也痛苦一番。當CFolderTab::ComputeRgn計算出梯形的坐標,它調用CRgn::CreatePolygonRgn函數強行創建一個多邊形區域。

int CFolderTab::ComputeRgn(CDC& dc, int x)
{
   CRect& rc = m_rect;
   dc.DrawText(m_sText, &rc, DT_CALCRECT);
   // tweak rc to add margins
   ……
   CPoint pts[4];
   GetTrapezoid(rc, pts);
   m_rgn.CreatePolygonRgn(pts, 4, WINDING);
   return rc.Width();
} 

   當標簽的區域確定後,繪制這些標簽的工作使你又陷入另一個艱難的象素處理環境。CFolderTab::Draw對選中標簽(選中和未選中狀態)要顯示的顏色和字體進行處理。因為標簽將梯形數據存儲在CRgn中,因此只要調用CDC::FillRgn即可繪制標簽。然後用MoveTos 和LineTos以適當的顏色繪制線條。最後調用DrawText繪制文本。

圖六選中/未選中的標簽狀態

圖六是程序運行時標簽選中和未選中的狀態示意圖。注意線條有的是黑色,有的是灰色以便呈現3D效果(這不是我的設計,而是從Excel借用的)。選中標簽為白色(COLOR_WINDOW)並且頂邊沒有黑色邊線。這樣做便於與上面的視窗融於一體。為了簡單起見,例子程序沒有創建這樣的視窗,但是在實際應用中,一般都會象Excel和Visual Studio那樣有一個甚至多個視窗與每個選中的標簽對應,這在後續文章中會慢慢擴充。本文例子程序裡選中標簽的另外一個特點是使用了小字體,這是從Visual Studio借鑒過來。說到字體,到底應該使用哪一種呢?這裡CFolderTabCtrl默認為Arial,你完全可以改用其它的字體,為此CFolderTabCtrl類中提供了一個改變字體的成員函數CFolderTabCtrl::SetFonts。以上討論的都是標簽的繪制問題, 下面來看看事件及行為。有關標簽區域計算的重點和難點問題都已經解決,剩下的問題就簡單多了。通過解決這些問題,我總結出一條經驗,希望與大家分享:無論你是做控制設計還是系統設計,開始都要問一問自己,“我需要什麼樣的數據結構才能使問題更容易處理?”然後確定如何創建這些數據結構。通常應該只在一個地方進行難點和重點處理,其它的問題便會容易得多,至少你應該這樣努力。 當CFolderTabCtrl獲悉OnLButtonDown事件,它調用函數HitTest找出鼠標位於哪一個標簽上。HitTest在每一個CFolderTab對象上循環,調用CFolderTab的同名函數CFolderTab::HitTest,這個函數再用已經計算好的的梯形坐標調用CRgn::PtInRegion。CRgn必須在這裡做好自己的工作。如果HitTest有返回TRUE,則CFolderTabCtrl::HitTest返回標簽的索引,此時OnLButtonDown調用的另一個函數是CFolderTabCtrl::SelectItem,以此選中標簽。SelectItem沒有什麼玄機,它將新的值賦給m_iCurItem並使的值失效,選中新標簽後重畫。完成SelectItem的調用後,OnLButtonDown創建一個NMFOLDERTAB結構並將信息填寫到結構,然後向父窗口發送WM_NOTIF消息。對話框和主應用就是這樣把握著所發生的一切。

OK,大功告成,例子程序雖然粗糙,但所要的功能基本都實現了。有許多實用的特性我作了保留,例如,用鍵盤切換標簽(這一點可以在主應用中以加速鍵的方式實現),以及標簽的禁用——即防止用選中某個標簽。如果你的程序需要這些特性,那就盡一切辦法實現它們吧!重要的是實現你需要的功能,而不要畫蛇添足。下一部分的內容我們將嘗試把CFolderTabCtrl集成到一個實際的MFC程序當中,使得我們的CFolderTabCtrl不再只是一個純粹的例子。(待續)

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved