深入瞭解CCtrlView

 如果我們要將一個控件轉換成視圖類,我們一般會想到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那部分的成員,因而這種調用是可行的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章