介紹
從零開發自定義控件常常是不需要的,因為標准控件組是相當全面的,如果不夠用,子類化或自繪等方法就可以搞掂這個工作。這是一個不應被忽略的要點。在從零開發一個自定義控件時,千辛萬苦獲得的控件往往會不如標准(控件)。
那就是說,這裡只有少數真地缺少的控件,如果我們想要在我們的應用程序中部署它們,除了無中生有地構建它們別無他法。有一個這樣的情況就是名稱為“層疊式窗體控件”,或無論什麼它的其他稱呼,例如:Spybot或Outlook。因為它不在標准控件之中並且因為它是一個有趣的練習,本指南講解了如何開發這類控件,並一步一步地給予講解。
本指南的目標讀者為程序員新手,在開始之前,我想挑戰你一下:即在不閱讀本文的情況下先嘗試自己開發這個控件。盡管這看起來可能會讓人退縮或你可能不知道從哪裡開始,它不是像你想像得那樣難。嘗試一下,看看你能走多遠,這時再回來看看本指南並檢驗一下我所說的話。提示:它完全與窗體的重新恢復尺寸和重新復位有關,沒有其他。
我們要完成什麼
目標是一個“層疊式窗體控件”。就是它。它將會被盡可能地泛型化並會闡明如何聚集該類控件的一個。
熱心的讀者可能希望知道我在寫這個演示工程時寫了這個指南。下面的指導、解釋和代碼實際上就是在上面的截屏中的層疊窗體控件(准確地說來就是圖中左邊那個控件)的開發。
讓我們從代碼開始。
過程詳解
工程開始
創建工作是簡單的。創建一個新的基於對話框的工程,並設置警告級別為4(工程設置,C/C++標簽)。級別4將確保任何可疑事物給我們帶來注意以使得由我們來決定要做什麼“這裡提示的警告在絕大多數情況下可以被安全地忽略”(此語出自文檔)
讓我們在該控件上開始工作。創建一個用CStatic作為基類的新的MFC類命名為CStackedWndCtrl。
在資源編輯器中,添加一個圖片控件ID號為IDC_SWC。保留Type的值為缺省的Frame並將Color置為Black。
使用MFC ClassWizard添加一個數據變量到IDC_SWC命名為m_StackedWndCtrl,確保選擇了Control作為Category以及CStackedWndCtrl作為變量類型。
在OK上點擊,彈出一個消息框提示我們確保我們已經為類CStackedWndCtrl包含頭文件在我們的對話框代碼中。如果你沒有包含它現在就要做了。
數據結構
任何控件的主要部分就是一個數據結構,數據結構可以保持將要顯示的信息。
好的,什麼將會被顯示?該控件用面板制作出來,每個面板包含兩個窗體,一個標題窗體和一個內容窗體。下面的圖片說明了這個概念。
控件的機制要求只有一個面板的內容窗體在一個時間內顯示。在一個面板上點擊標題窗體將觸發其相應的內容窗體顯示,並且也隱藏了當前顯示面板的內容窗體。
因此,數據結構將包含一對指向CWnd 對象的指針和一個布爾標識值以指出是否顯示或隱藏這個面板的內容窗體。不需要任何其他的東西了。
#include <afxtempl.h>
對於這些結構的保存、檢索和操作,用一個數組是一個方便的且足夠的方法。記住為了使用這個數組模版,我們需要包含相應的頭文件。
class CStackedWndCtrl : public CStatic
{
....
....
// Attributes
protected:
typedef struct
{
CWnd* m_pwndRubric;
CWnd* m_pwndContent;
BOOL m_bOpen;
} TDS_PANE, *PTDS_PANE;
CArray<PTDS_PANE, PTDS_PANE> m_arrPanes;
....
....
}
下一個任務是寫一個允許我們添加面板到控件上的public方法。這沒有什麼困難。我們使窗體對象的指針作為參數傳遞,並設置新的面板如其所顯示的一樣。
int CStackedWndCtrl::AddPane( CWnd* pwndRubric, CWnd* pwndContent )
{
// 隱藏無論哪一個正在顯示面板的內容窗體
//我們將總是顯示最近添加的面板的內容窗體
for( int i = 0; i < m_arrPanes.GetSize(); i++ )
if( m_arrPanes[ i ]->m_bOpen )
m_arrPanes[ i ]->m_bOpen = FALSE;
//創建一個新的面板結構
PTDS_PANE pPane = new TDS_PANE;
if( pPane == NULL )
{
AfxMessageBox( "Failed to add a new pane to"
" the stack.\n\n Out of memory." );
return -1;
}
// 拷貝指針到標題和內容窗體
//同時,設置這個面板為打開狀態
pPane->m_pwndRubric = pwndRubric;
pPane->m_pwndContent = pwndContent;
pPane->m_bOpen = TRUE;
// 添加該新面板到棧的尾部
int iIndex = m_arrPanes.Add( pPane );
// 重新排列棧
RearrangeStack();
// 返回新面板的索引號
return iIndex;
}
在我們擔心排列和顯示面板之前(如果你想要測試這個代碼,只要參考RearrangeStack
方法的調用 ),我們要確保在退出時該結構體被完全刪除是非常重要的,以免內存洩漏。我們在析構器中執行該任務,如下所示:
CStackedWndCtrl::~CStackedWndCtrl()
{
for( int i = 0; i < m_arrPanes.GetSize(); i++ )
{
//刪除標題窗體
m_arrPanes[ i ]->m_pwndRubric->DestroyWindow();
delete m_arrPanes[ i ]->m_pwndRubric;
// 刪除內容窗體
m_arrPanes[ i ]->m_pwndContent->DestroyWindow();
delete m_arrPanes[ i ]->m_pwndContent;
// 刪除結構體
delete m_arrPanes[ i ];
}
m_arrPanes.RemoveAll();
}
簡單填充。我們遍歷該面板上的數組,銷毀每個窗體,然後刪除每個窗體對象,然後刪除每個面板對象,並且最後,從數組中移除所有指針。
這個功能足以使得CStackedWndCtrl類可以做它的工作。我們可以添加面板,同時它們(譯注:指面板)在控件被銷毀時被適當釋放。
可視的魔力
None of it, 我想.排列和顯示控件的算法是相當簡單的。
我們遍歷面板,通過一個預先估量消除頂部框架,m_iRubricHeight,它在演示程序中被設置為一個默認的值(可以自由測試)當我們點擊打開的面板,我們用余下來要顯示的標題窗體的數量來計算該面板的內容窗體的尺寸。請看下面的代碼。
void CStackedWndCtrl::RearrangeStack()
{
CRect rFrame;
GetClientRect( &rFrame );
for( int i = 0; i < m_arrPanes.GetSize(); i++ )
{
// 標題窗體總是顯示
m_arrPanes[ i ]->m_pwndRubric->SetWindowPos(
NULL,
0,
rFrame.top,
rFrame.Width(),
m_iRubricHeight,
SWP_NOZORDER | SWP_SHOWWINDOW );
// 只有已標記面板的內容窗體被顯示
// 如果它們沒有准備好,所有其他的都隱藏
if( m_arrPanes[ i ]->m_bOpen )
{
// 從框架的底部,去掉余下要顯示的那些標題窗體的一樣高度的尺寸
int iContentWndBottom = rFrame.bottom -
( ( m_arrPanes.GetSize() - i ) * m_iRubricHeight );
m_arrPanes[ i ]->m_pwndContent->SetWindowPos(
NULL,
0,
rFrame.top + m_iRubricHeight,
rFrame.Width(),
iContentWndBottom - rFrame.top,
SWP_NOZORDER | SWP_SHOWWINDOW );
//下一個標題窗體將被放置於該面板內容窗體的正下方
rFrame.top = iContentWndBottom;
}
else
m_arrPanes[ i ]->m_pwndContent->ShowWindow( SW_HIDE );
//框架的頂部偏移一個標題窗體的高度
rFrame.top += m_iRubricHeight;
}
}
以上處理了控件的排列和顯示。
讓我們現在添加一個調用到PreSubclassWindow以去除圖片控件周圍的黑框。在資源編輯器工作時這是有效的,在應用程序運行時它是不必的且難看。
void CStackedWndCtrl::PreSubclassWindow()
我們已經獲得機會添加WS_CLIPCHILDREN 標志以在重新改變控件尺寸時減少閃爍,這提醒我…
{
// 移除黑框並夾住子控件以避免閃爍
ModifyStyle( SS_BLACKFRAME, WS_CLIPCHILDREN );
CStatic::PreSubclassWindow();
}
…確保該控件能在需要時改變自己的尺寸大小總是一個好主意。在此情況中,該功能是相當容易實現的。調出Classwizard,為WM_SIZE添加一個消息句柄,並做一個調用到RearrangeStack。
void CStackedWndCtrl::OnSize(UINT nType, int cx, int cy)
{
CStatic::OnSize(nType, cx, cy);
RearrangeStack();
}
我們幾乎已經做好了。如果你添加一些測試面板,編譯並運行;這個層疊式控件將顯示所有標題窗體和最後的面板的內容窗體。
當然,這個控件不會對用戶點擊標題窗體做出反應。我們還沒有為其寫響應代碼啊。它是我們任務清單上的下一個也是最後一個任務了。
標題窗體的惟一需求
至於我們的控件,標題和內容窗體可以是任何一種窗體。照字面意思,可以是對話框、static控件、列表框/控件、樹控件、日歷控件、編輯框/ richedit控件、generic窗體、甚至自定義控件。如果我們可以獲得一個指向它的CWnd指針,CStackedWndCtrl類會如預期一樣工作。這裡惟一的限制是常識,而不是一個技術問題。舉個例子,一個組合框可能被設置為標題或內容窗體,但是其適宜性相當值得懷疑。
然而,這裡有一個必要條件,同時它被應用於標題窗體。當它被點擊,它必須通知其父(一個CStackedWndCtrl 對象)以使得相關內容窗體可以被顯示。我們將通過發送一個消息完成這個任務。
為了簡化,我將用按鈕作為標題窗體。它們畢竟是絕大多數可能的選擇。我們將從CButton繼承一個類,並且添加這個有點特別的功能。
那麼,我們現在創建一個繼承於CButton的名為CTelltaleButton的類。添加下面的消息定義到它的頭文件,和一個BN_CLICKED(反射消息)的消息處理程序。
// In TelltaleButton.h
標題窗體將發送一個包含其自己句柄的消息,如wParam,有了這個信息,它的父控件將可以了解到哪一個標題窗體已經被點擊了。
#define WM_RUBRIC_WND_CLICKED_ON ( WM_APP + 04100 )
// In TelltaleButton.cpp
void CTelltaleButton::OnClicked()
{
GetParent()->SendMessage( WM_BUTTON_CLICKED, (WPARAM)this->m_hWnd );
}
現在,我們通過手工添加一個方法到其消息映射在CStackedWndCtrl中處理這個消息如下:
// In StackedWndCtrl.h
#define WM_RUBRIC_WND_CLICKED_ON ( WM_APP + 04100 )
...
...
// 生成消息映射函數
protected:
//{{AFX_MSG(CStackedWndCtrl)
afx_msg void OnSize(UINT nType, int cx, int cy);
//}}AFX_MSG
afx_msg LRESULT OnRubricWndClicked(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()
// In StackedWndCtrl.cpp
...
...
BEGIN_MESSAGE_MAP(CStackedWndCtrl, CStatic)
//{{AFX_MSG_MAP(CStackedWndCtrl)
ON_WM_SIZE()
//}}AFX_MSG_MAP
ON_MESSAGE(WM_RUBRIC_WND_CLICKED_ON, OnRubricWndClicked)
END_MESSAGE_MAP()
...
...
LRESULT CStackedWndCtrl::OnRubricWndClicked(WPARAM wParam, LPARAM /*lParam*/)
{
HWND hwndRubric = (HWND)wParam;
BOOL bRearrange = FALSE;
for( int i = 0; i < m_arrPanes.GetSize(); i++ )
if( m_arrPanes[ i ]->m_pwndRubric->m_hWnd == hwndRubric )
{
// 只有除了屬於當前已打開面板的一個標題窗體被點擊時才重新排列控件
if( m_arrPanes[ i ]->m_bOpen == FALSE )
{
m_arrPanes[ i ]->m_bOpen = TRUE;
bRearrange = TRUE;
}
}
else
m_arrPanes[ i ]->m_bOpen = FALSE;
if( bRearrange )
RearrangeStack();
// 如果已發送消息的標題窗體希望知道是否控件已被重新排列,返回標志
return bRearrange;
}
它完全歸結為遍歷面板以尋找已被點擊的標題窗體。如果它不同於當前打開的面板的那個(標題窗體),就重新排列控件。
一些Eye Candy
因為對於它的標題和內容窗體所可以被使用的控件來說CStackedWndCtrl是非常靈活,這就很容易使其樣式活潑起來。為了演示如何做到這樣,我已經在演示工程中包含了一個"普通"控件和一個Davide Calabro的陰影按鈕及Everaldo Coelho的圖標的控件。正如你能看到的,通過檢查演示工程中的代碼,沒有一行在CStackedWndCtrl中的代碼需要被修改。正如其所應該的那樣。
我們的短暫的旅程就要結束了,我的朋友;我們從此會各走各路了。我希望你已經用我已向你展示的東西來播下你想像力的種子,而且我們的quiet dealings會對你有益。
反饋
我的意圖是提供一個編碼清楚的指南,它理解和學習起來務求盡可能的簡單。我確信會有比我這裡這個功能的實現更好的解決方案。任何關於改進、簡單化或更好解釋代碼的建議我都歡迎。