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
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.
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)
{
}
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;
}
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);
}
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.
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;
}
{
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:
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, ...);
}
{
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
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.
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);
}
{
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:
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(...);
}
{
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();
}
{
m_wndFooCtrl.SetFocus();
}
Now keystrokes go directly to your control. Do not pass view. I told you it was easy! This is the age-old Windows way of doing things, but with all those frameworks doing so much for you nowadays, it's easy to miss the basics.
The upshot is this: if your custom control view class is not a pure wrapper function, that is, if it has so much as one data member or virtual function of its own, then the way to convert your control into a view is to instantiate it as a child window of the view and integrate it in three simple steps.
Handle WM_SIZE in the view to position your control exactly over the view's client area.
Convert mouse messages in the control to higher-level parent WM_NOTIFY notifications.
Handle WM_FOCUS in the view to set focus to your control.
Incidentally—if I may be permitted to muse for just a paragraph or two—this example illustrates one of the drawbacks of the MFC object model, which doesn't allow multiple inheritance. You can't say, "my class is a view and a foo control," which is really what you want to do. It also shows why some programmers may choose to implement custom controls using C only, and not C++. It is possible, you know. All you have to do is register your own window class (in the Windows sense), allocate a little memory block to hold your window's instance data, and implement all your functions as messages—WMFOO_GETTHIS and WMFOO_SETTHAT. This was the only way to implement custom controls before C++ came along, and it still has many benefits.
For example, if you do it this way, some other C++ programmer could come along and write a C++ pure wrapper class for your window, with simple wrapper functions CFooCtrl::GetThis and CFooCtrl::SetThat, which merely passed the parameters to and fro using the proper WMFOO_XXX messages, and then such a programmer could in fact use CCtrlView to convert your control to a view! Or, to put it differently, one way to use CCtrlView is to reimplement your custom control using pure C and the Windows API with messages and subclassing instead of MFC! This would require a bit more typing and type-casting (for all those WPARAMs and LPARAMs), but would leave you feeling satisfied and pure.
The upshot is this: if your custom control view class is not a pure wrapper function, that is, if it has so much as one data member or virtual function of its own, then the way to convert your control into a view is to instantiate it as a child window of the view and integrate it in three simple steps.
Handle WM_SIZE in the view to position your control exactly over the view's client area.
Convert mouse messages in the control to higher-level parent WM_NOTIFY notifications.
Handle WM_FOCUS in the view to set focus to your control.
Incidentally—if I may be permitted to muse for just a paragraph or two—this example illustrates one of the drawbacks of the MFC object model, which doesn't allow multiple inheritance. You can't say, "my class is a view and a foo control," which is really what you want to do. It also shows why some programmers may choose to implement custom controls using C only, and not C++. It is possible, you know. All you have to do is register your own window class (in the Windows sense), allocate a little memory block to hold your window's instance data, and implement all your functions as messages—WMFOO_GETTHIS and WMFOO_SETTHAT. This was the only way to implement custom controls before C++ came along, and it still has many benefits.
For example, if you do it this way, some other C++ programmer could come along and write a C++ pure wrapper class for your window, with simple wrapper functions CFooCtrl::GetThis and CFooCtrl::SetThat, which merely passed the parameters to and fro using the proper WMFOO_XXX messages, and then such a programmer could in fact use CCtrlView to convert your control to a view! Or, to put it differently, one way to use CCtrlView is to reimplement your custom control using pure C and the Windows API with messages and subclassing instead of MFC! This would require a bit more typing and type-casting (for all those WPARAMs and LPARAMs), but would leave you feeling satisfied and pure.