From the November 2001 issue of MSDN Magazine.

MSDN Magazine

Understanding CControlView, Changing Scroll Bar Color in MFC Apps
Paul DiLascia
Download the code for this article:CQA0111.exe (201KB)
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();
}

 

      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.

Q

I have read your Q&A columns on using WM_CTLCOLOR to change a control color in MFC, but still I cannot change the scroll bar color in my MFC AppWizard-generated program. I override OnCtlColor in my CScrollView and I return an HBRUSH with the desired color, but nothing happens. Can you help?

Nasser Hosseiniyar


A

The problem is that Windows has two kinds of scroll bars. There are the "built-in" scroll bars that you get when you use the WS_HSCROLL and/or WS_VSCROLL window styles, and there are scroll bar controls, actual child windows you can create within a window. Only the latter send WM_CTLCOLOR messages. Figure 1 and Figure 2 illustrate this situation. Figure 1 shows a Spy listing of my ImgView program from last month's column.

Figure 1 Scroll Bar without Scroll Bar Controls
Figure 1 Scroll Bar without Scroll Bar Controls

While ImgView does, in fact, display scroll bars, there are no scroll bar controls anywhere to be seen with Spy. Figure 2 shows a modified version of ImgView (described later) that uses scroll bar controls; this time the Spy window shows the two scroll bar controls as children of the main window.

Figure 2 Scroll Bar Control as Child
Figure 2 Scroll Bar Control as Child

      Inquiring minds might wonder why there are two kinds of scroll bars. The answer lies in history. In the early days when Windows was a wee lad, there was a pressing need to save every bit of memory, and since each window took up considerable space relative to what was available, and scroll bars were so ubiquitous, the designers figured it would be efficient to build them in as a window option instead of requiring programmers to create separate windows and hook them up. It also made life easier for programmers. At least, I am guessing that's the reason; perhaps the real reason is something else entirely. In any case, the built-in scroll bars are easier to work with and require less code from you, the gentle programmer; but they don't allow much in the way of customization, and they don't send WM_CTLCOLOR messages, as you probably discovered to your great surprise.
      What about MFC? By default, MFC's CScrollView uses the built-in scroll bars, which means that you can't change their colors. Never fear, there's a not-too-painful way to get CScrollView to use your own scroll bar controls. But before I show you how to do that, let me point out a potentially major caveat: WM_CTLCOLOR lets you set only the background color of the scroll bar, not the scroll thumb and line up/line down buttons. So if you're hoping for a fully color-customized scroll bar, WM_CTLCOLOR is probably useless for you. I'll give you some ideas how to get around this later, but for now, the first thing is to get CScrollView to use your scroll bar controls as opposed to the built-in ones you get with WS_HSCROLL/WS_VSCROLL.
      First you have to instantiate a couple of CScroll bars in your main frame:

  class CMainFrame : public CFrameWnd {
protected:
CScrollbar m_wndHScroll;
CScrollBar m_wndVScroll;
•••
};

 

Then, create the windows in the usual way, when your frame is created:

  int CMainFrame::OnCreate(...)
{
•••
CRect rc(0,0,0,0);
VERIFY(m_wndSBHorz.Create(WS_VISIBLE|WS_CHILD|SBS_HORZ,
rc, this, AFX_IDW_HSCROLL_FIRST));
VERIFY(m_wndSBVert.Create(WS_VISIBLE|WS_CHILD|SBS_VERT,
rc, this, AFX_IDW_HSCROLL_FIRST+1));
VERIFY(m_wndSBBox.Create(WS_VISIBLE|WS_CHILD|SBS_SIZEBOX,
rc, this,-1));
return 0;
}

 

      Oh, I forgot to mention: you also have to create a scroll bar box, the little thingy that goes where the horizontal and vertical scroll bars meet, so you can size your window and avoid a blank spot.
      Once you've created the scroll bars, there are several things to take care of. You have to size them, process notifications, and get the view to use them. The last part is easy. CScrollView provides a virtual function, GetScrollBarCtrl, which you can override.

  CScrollBar* CPictureView::GetScrollBarCtrl(int nBar) const
{
CWnd* pParent = GetParent();
UINT nID = AFX_IDW_HSCROLL_FIRST;
if (nBar==SB_VERT)
nID++;
return (CScrollBar*)pParent->GetDlgItem(nID);
}

 

This gets the appropriate control from the parent using the convenient IDs AFX_IDW_HSCROLL_FIRST and AFX_IDW_HSCROLL_FIRST+1.
      What about scroll notifications? When the user scrolls, the scroll bar sends a WM_HSCROLL or WM_VSCROLL to its parent, and it's up to you to do something. Well, fortunately, you're in luck because if you put your scroll bars in the frame as I have suggested, CFrameWnd already has OnHScroll and OnVScroll handlers that route WM_HSCROLL and WM_VSCROLL to the view:

  // From winfrm.cpp 
void CFrameWnd::OnHScroll(...)
{
CWnd* pActiveView = GetActiveView();
if (pActiveView != NULL) {
pActiveView->SendMessage(WM_HSCROLL, ...);
}
}

 

      If the view is a scroll view—that is, if you have derived your view from CScrollView—it does the scrolling. You don't have to do anything to handle scroll messages; MFC does it automatically!
      What about sizing? That's where you have to do a little work. To make everything fly, you must manage the positions of the scroll bars whenever your frame is resized. Remember, the scroll bars are children of the frame, not the view. Normally you'd handle sizing in OnSize, but for frame windows the place to do it is in CFrameWnd::RecalcLayout.

  void CMainFrame::RecalcLayout(BOOL)
{
CFrameWnd::RecalcLayout();
CView* pView = GetActiveView();
if (pView) {
// adjust view and scrollbars...
}
}

 

      The details are a tedious exercise in pixel arithmetic; Figure 3 reveals all. I modified the ImgView program from last month's column to produce the ugly blue scroll bars in Figure 4. You can get the code from the link at the top of this article.

Figure 4 Blue Scroll Bars
Figure 4 Blue Scroll Bars

      Before I leave the topic of scroll bars, I should point out that post Microsoft® Internet Explorer 4.0 versions of ComCtrl32.dll support something called "flat scroll bars." There's a special API you can use to initialize and create these scroll bars without the 3D-look. For more info, see " Flat Scroll Bars" in the User Interface Design and Development section of the Platform SDK documentation.
      Whether you use scroll bar controls as I've shown here, or flat scroll bars, you still can only set the background color of the scroll bar, so I question what value all this has. How useful is it to change only the background color? Most apps that have custom-colored scroll bars have custom-colored thumb slider and buttons as well. For that you have to write your own scroll bar control from scratch. But really, a scroll bar is pretty simple. All it does is let the user drag a box, send messages like WM_HSCROLL and WM_VSCROLL, and handle others like SB_PAGEUP and SB_THUMBPOSITION. It's not that hard. Another possible change to a simpler option is to use a normal scroll bar and override WM_ PAINT. I personally have never tried this, but in theory it might work. If anyone has ever been brave enough to try, let me know.... Happy programming!

Send questions and comments for Paul to cppqa@microsoft.com.

Paul DiLascia is a freelance writer, consultant, and Web/UI designer-at-large. He is the author of Windows++: Writing Reusable Windows Code in C++(Addison-Wesley, 1992). Paul can be reached at askpd@pobox.com or https://www.dilascia.com.