如果我們要將一個控制項轉換成視圖類,我們一般會想到CCtrlView,用它實現的控制項視圖一般添加一個GetXXXCtrl函數,函數的作用是返回視圖中控制項的引用,如果在MFC程式中跟蹤它的調用我們會發現它的實現是這樣的(以CEdit控制項為例)
_AFXEXT_INLINE CEdit& CEditView::GetEditCtrl() const
{ return *(CEdit*)this; }
這個轉換讓人覺得很疑惑,因為CEdit和CEditView是來自兩條不同繼承鏈上的類,一般來說這樣的轉換是要出問題的,但是為什麼可以進行這種轉換?MSDN上有一篇文章對這種轉換做了比較深入的解釋,下面把這篇文章翻出來給大家看看。
問題:
我設計了一個從CWnd派生而來的自訂控制項,現在我想把它當成視圖來使用。我想到的的第一個辦法是把控制項嵌入到視圖中然後操控視圖中的OnSize函數以使控制項覆蓋客戶區。但問題是傳給控制項的滑鼠訊息無法在視圖中重載。而傳給視圖的擊鍵訊息必須手動傳遞給控制項。我瞭解到CCtrlView可以作為公用控制項的基類。於是我試圖圍繞著它設計檢視(我記得你在MSJ的某一期上討論了這個問題),但是我無法讓它與我自訂的CWnd派生控制項類一起工作。請問這樣做可以嗎?應該怎樣做?
Mateo Anderson(回答者)
回答:
CCtrlView是一個MFC將控制項類轉換為視圖類的技巧。比如從CTreeCtrl轉換成CTreeView或者從CListCtrl轉換成CListView。在CCtrlView的文檔注釋中提到:“CCtrlView幾乎允許把任何控制項轉換成視圖。”。不幸的是,說“幾乎”稍微有些誇大其詞了,除非作者所說的“任何控制項”只是關於像CEdit和CTreeCtrl這樣的Windows內建控制項。CCtrlView用了一個只能在特定環境下工作的技巧。要理解CCtrlView如何工作,首先讓我們看看CTreeView,CTreeView派生於CCtrlView。其中有三個重要的函數需要考慮:建構函式,PreCreateWindow函數和GetTreeCtrl函數。建構函式告訴CCtrlView要建立哪種Windows控制項。
CTreeView::CTreeView() :
CCtrlView(WC_TREEVIEW, dwStyle)
在這個例子中,WC_TREEVIEW(在commctrl.h中定義)是樹型控制項類的類名,也就是“SysTreeView32”。CCtrlView會將此類名儲存在一個資料成員中稍後使用:
CCtrlView::CCtrlView(LPCTSTR lpszClass,
DWORD dwStyle)
{
m_strClass = lpszClass;
m_dwDefaultStyle = dwStyle;
}
下一個起作用的函數是PreCreateWindows,這是一個CTreeCtrl從CCtrlView繼承來的函數。CCtrlView::PreCreateWindow在視窗剛好建立完成之前使用m_strClass在CREATESTRUCT種設定類名。
// CCtrlView uses stored class name
BOOL CCtrlView::PreCreateWindow(CREATESTRUCT& cs)
{
cs.lpszClass = m_strClass;
•••
return CView::PreCreateWindow(cs);
}
現在建立的視窗就是使用所期望的類建立的了-在這個例子中就是SysTreeView32。到目前為止,一切都很好。但是如果CTreeCtrl派生於CCtrlView,而CCtrlView又派生於CView,那麼它為什麼能又派生於CTreeCtrl?難道是MFC類封裝了樹型控制項?CTreeView和CTreeCtrl是完全獨立的,有著不同的繼承鏈。CTreeCtrl直接派生於CWnd,而CTreeView派生於CCtrlView/CView。這就是技巧發生作用的地方了。要讓樹型視映像樹型控制項一樣操控,CCtreeView提供了特殊的函數GetTreeCtrl來獲得樹型控制項。
CTreeCtrl& CTreeView::GetTreeCtrl() const
{
return *(CTreeCtrl*)this;
}
GetTreeCtrl只是簡單的將CtreeView轉換為CTreeCtrl。但是等等-怎麼會發生這麼詭異的事?這兩個類完全不同,有著不同的資料成員和虛函數表-你根本無法將一個轉換成另一個同時還指望著能夠正常工作!答案就是:CTreeCtrl沒有虛函數也沒有資料成員。你可以把它稱為一個純封裝類。CTreeCtrl不對它的基類CWnd添加任何東西(不添加資料成員也不添加虛函數),所有添加的內容只是一些封裝函數,一些將訊息發送給內在HWND的有形函數。
HTREEITEM CTreeCtrl::InsertItem(...)
{
return (HTREEITEM)::SendMessage(m_hWnd,
TVM_INSERTITEM, ...);
}
InsertItem訪問的唯一資料成員是m_hWnd,所有的CWnd衍生類別都有該成員。InsertItem和所有其它的封裝函數只是將它們的參數傳遞給了內在的HWND,將C++風格的成員函數轉換成Windows風格的SendMessage調用。對象本身(“this”指標)可以是任何CWnd衍生類別的執行個體,只要m_hWnd處於正確的位置(也即是類的第一個資料成員),並且HWND(實際上就是如此)是一個樹型控制項的控制代碼。相同的原因發生在下面的寫法中:
pEdit = (CEdit*)GetDlgItem(ID_FOO);
即便在這裡GetDlgItem返回的是一個指向CWnd的指標,不是CEdit。但是這樣是允許的,因為CEdit也是一個封裝類,而且對於它從CWnd繼承來的內容沒有添加額外的資料和虛函數。所以注釋中“CCtrlView幾乎允許將任何控制項轉換為視圖”的說法中所說的“幾乎任何”意思是特指對CWnd沒有添加資料成員和虛函數的這些控制項,也就是我所說的“純封裝類。”如果你的控制項類有它自己的資料或虛函數,你無法使用CCtrlView,因為在CCtrlView/CView中不存在這些額外的資料成員和虛函數。
舉例來說,CView中的第一個虛函數是CView::IsSelected。如果你的控制項類有其它虛函數,在你把CCtrlView類轉換成你的CFooCtrl類調用那個虛函數的時候就肯定會爆發問題了,因為這個函數根本不存在。類似的,CView中的第一個資料成員是m_pDocument。如果你的控制項類需要一些其它的資料成員,那你在訪問它們的時候就要倒黴了,因為對象事實上調用的是CCtrlView,而不是CFooCtrl。多麼糟糕,多麼令人沮喪。
簡而言之,唯一可以使用CCtrlView技巧的時候是CWnd派生控制項類沒有它自己的虛函數和資料成員之時。這就是生活。
看到這裡,大家應該就很明白了,這裡的轉換有一個與普通類類型之間的轉換有一個重要區別,控制項類中沒有虛函數和新的資料成員,因此控制項類具有和CWnd一樣的虛函數表和資料擺放,所謂將CEditView轉換成CEdit控制項,實際CEdit控制項對象只會調用CEditView中CWnd那部分的成員,因而這種調用是可行的。