除尘器原理:深入了解CCtrlView

来源:百度文库 编辑:九乡新闻网 时间:2024/04/29 04:31:03

 如果我们要将一个控件转换成视图类,我们一般会想到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那部分的成员,因而这种调用是可行的。