分类: C/C++
2009-07-28 10:25:36
Q
I made a custom control derived from CWnd, and now I want to use it as a view. My first solution was to embed the control into the view and handle OnSize in the view to position the control over the client area. The problem is that mouse messages go to the control and cannot be overridden in the view. The keystroke messages go to the view and must be manually forwarded to the control.
I read about CCtrlView as a base class for common controls. I've even managed to write the view around it (I believe that you wrote about this in an issue of MSJ), but I could not get it to work with my CWnd-based control. Can this be done, and how?
Mateo Anderson
A
CCtrlView is a trick that MFC uses to convert control classes to view classes. For example, from CTreeCtrl to CTreeView and from CListCtrl to CListView. The comment in the documentation for CCtrlView says, "CCtrlView allows almost any control to be a view." Unfortunately, the "almost" is a bit of an exaggeration, unless by "any control" the author was thinking about any of the built-in Windows? controls like CEdit, CTreeCtrl, and so on. CCtrlView uses a trick that works only in special circumstances.
To understand how CCtrlView works, let's take a look at CTreeView, which is derived from CCtrlView. There are three important functions to consider: the constructor, PreCreateWindow, and GetTreeCtrl.
The constructor tells CCtrlView which kind of Windows control to create.
CTreeView::CTreeView() :
CCtrlView(WC_TREEVIEW, dwStyle)
{
}
In this case, WC_TREEVIEW (#defined in commctrl.h) is the name of the (Windows) tree control class: namely, SysTreeView32. CCtrlView stores this name in a data member for later use.
CCtrlView::CCtrlView(LPCTSTR lpszClass,
DWORD dwStyle)
{
m_strClass = lpszClass;
m_dwDefaultStyle = dwStyle;
}
The next function that comes into play is PreCreateWindow, which CTreeCtrl inherits from CCtrlView. CCtrlView::PreCreateWindow uses m_strClass to set the class name in the CREATESTRUCT just before the window is created.
// CCtrlView uses stored class name
BOOL CCtrlView::PreCreateWindow(CREATESTRUCT& cs)
{
cs.lpszClass = m_strClass;
???
return CView::PreCreateWindow(cs);
}
Now the window created is of the desired class—in this case, SysTreeView32. So far, so good. But if CTreeCtrl is derived from CCtrlView, which is derived from CView, how can it also be derived from CTreeCtrl, the MFC class that wraps the tree control? CTreeView and CTreeCtrl are completely independent, with different inheritance chains. CTreeCtrl is derived from CWnd directly, whereas CTreeView is derived from
CCtrlView/CView! This is where the trick comes in.
To manipulate the tree view as a tree control, CTreeView provides a special function, GetTreeCtrl, to get the tree control.
CTreeCtrl& CTreeView::GetTreeCtrl() const
{
return *(CTreeCtrl*)this;
}
GetTreeCtrl simply casts the CTreeView to a CTreeCtrl. But wait a minute—how on earth can this work? The two classes are entirely different, with different data members and virtual function tables—you can't just cast one class to another and expect it to work!
The answer is that CTreeCtrl has no virtual functions and no member data. You could call it a pure wrapper class. CTreeCtrl doesn't add anything (data or virtual functions) to its base class, CWnd; all it adds is a bunch of wrapper functions, concrete functions that send messages to the underlying HWND. For example:
HTREEITEM CTreeCtrl::InsertItem(...)
{
return (HTREEITEM)::SendMessage(m_hWnd,
TVM_INSERTITEM, ...);
}
The only data member that InsertItem accesses is m_hWnd, which all CWnd-derived classes have. InsertItem and all the other wrapper functions simply pass their arguments to the underlying HWND, converting C++-style
member functions to Windows-style SendMessage calls. The object itself ("this" pointer) could be an instance of any CWnd-derived class, as long as m_hWnd is in the right place (that is, the first data member of the class) and the HWND is, in fact, a handle to a tree control. It's the same reason you can write
pEdit = (CEdit*)GetDlgItem(ID_FOO);
even though GetDlgItem returns a pointer to a CWnd, not a CEdit: because CEdit is also a pure wrapper class, with no extra data or virtual functions beyond what it inherits from CWnd.
So the "almost any" in the statement "CCtrlView allows almost any control to be a view" means specifically any control that adds no member data and no virtual functions to CWnd, what I am calling a "pure wrapper class." If your control class has its own data or virtual functions, you can't use CCtrlView because the extra data/virtual functions won't exist in CCtrlView/CView.
For example, the first virtual function in CView is CView::IsSelected. If your control class has some other virtual function, then things will certainly bomb when you cast CCtrlView to your CFooCtrl and try to call that virtual function. The function simply doesn't exist. Likewise, the first data member in CView is m_pDocument. If your control class expects some other data member, your code will bite the bag when it tries to access it, if the object called is really a CCtrlView, not a CFooCtrl. Too bad, so sad.
In short, the only time you can use the CCtrlView trick is when your CWnd-derived control class has no virtual functions and no member data of its own. C'est la vie.
If you want to use your control in a doc/view app, what can you do—throw your head on the table and weep? Of course not! Your first approach was dandy: create your control as a child of the view and use OnSize to position it exactly over the view's client area.
CFooView::OnSize(..., cx, cy)
{
m_wndFooCtrl.SetWindowPos(NULL,
0,0,cx,cy,SWP_NOZORDER);
}
Those input problems you encountered are easily overcome. Consider the mouse. If you want to let the parent view handle mouse messages sent to your control, the thing to do is abstract the messages into higher-level events. That's a highfalutin way of saying something familiar to us all.
Consider, for example, a button. When the user clicks a button, the button notifies its parent with a BN_CLICKED event. It does not send WM_LBUTTONDOWN; it sends a WM_COMMAND message with subcode = BN_CLICKED.
The button is telling its parent window: the user clicked me. Likewise, list controls don't broadcast WM_LBUTTONDOWN; they do a little processing and notify their parents with LBN_SELCHANGE. (In the case of a double-click, list controls do propagate LBN_DBLCLK, which is little more than WM_LBUTTONDBLCK.) In general, the idea is that controls convert raw events into higher-level events that are meaningful in the context of the control.
If you're doing this at home, you should probably use the more modern way, which is WM_NOTIFY, instead of WM_COMMAND. WM_NOTIFY lets you pass a whole struct of information instead of trying to squish everything into half a DWORD. You can decide which mouse messages your control should propagate.
For example, buttons don't normally send BN_DOUBLECLICKED unless they have the BS_NOTIFY style.
So much for mousing. Now, what about the keyboard? That's even easier. When the user activates your app by clicking on the caption or Alt-TABing to it, Windows normally gives focus to the main frame. MFC, in turn, passes focus to your view:
void CFrameWnd::OnSetFocus(...)
{
if (m_pViewActive != NULL)
m_pViewActive->SetFocus();
else
CWnd::OnSetFocus(...);
}
All you have to do is pass the focus, in turn, to your control:
CFooView::OnSetFocus(...)
{
m_wndFooCtrl.SetFocus();
}
The short answer is - you don't. The long answer is that you derive from CListView instead and add the same functionality to the CListView derived class. The reason for this is that MFC is designed in such a way that only one C++ object can be associated with one window or control. When you use a CListView, the list view control is already associated with the view class so you cannot have another C++ object associated with the control.
The call to GetListCtrl() actually returns a pointer to the CListView object after casting it to a pointer to CListCtrl. So the return value from GetListCtrl() doesn't really point to a CListCtrl but instead it points to the ClistView.
The workaround is that you derive from CListView and add the same functions and method handlers that you have for the CListCtrl. The class wizard supports all the same window message for ClistView derived class that it does for the CListCtrl class. In this scenario, we use the old form of code reuse, we copy and paste the code.
Let's assume that one of the messages that the derived CListCtrl handles is the LVN_ENDLABELEDIT notification. To add this functionality to the CListView derived class, simply use the class wizard to add the message handler, copy the code from the CListCtrl derived class and finally precede all calls to CListCtrl methods with 'GetListCtrl()->'. Here's an example to illustrate the point.
void CMyListCtrl::OnEndLabelEdit(LPNMHDR pnmhdr, LRESULT *pLResult) { LV_DISPINFO *plvDispInfo = (LV_DISPINFO *)pnmhdr; LV_ITEM *plvItem = &plvDispInfo->item; if (plvItem->pszText != NULL) SetItemText(plvItem->iItem, plvItem->iSubItem, plvItem->pszText); } void CMyListView::OnEndLabelEdit(LPNMHDR pnmhdr, LRESULT *pLResult) { LV_DISPINFO *plvDispInfo = (LV_DISPINFO *)pnmhdr; LV_ITEM *plvItem = &plvDispInfo->item; if (plvItem->pszText != NULL) GetListCtrl().SetItemText(plvItem->iItem, plvItem->iSubItem, plvItem->pszText); }
看来想在视图中使用ListCtrl的派生类是不太可能了,我至目前为止还没找到更好的方法,如果你有什么好的方法,请给予于回复,非常感谢你的建议。
注:本帖中引用的内容出处现已无法查出,如果存在版权问题,请告知,我会尽快处理。