Turn Any CWnd-Derived Control Class into a View


This article was contributed by Tom Archer.

Introduction

One pretty cool little class that Microsoft introduced back in version 4 of MFC was
the CCtrlView class. However, it amazes me to see how misunderstood and
underutilized this class is with regards to being
used as a base class. In other words, even now, some 4 years after this class first saw the light of day,
I still hear lots of incorrect statements regarding how a class such as the CListView
works internally, I also continue to receive many article submissions where in order to make a
control emcompass the entire client
area of given frame, the programmer goes through all sorts of unnecessary work to make that happen
(e.g., embedding the control in the view, manually doing the sizing code, etc.)
Therefore, I decided to dig out an article I wrote some time ago that illustrates exactly
how to derive your own CWnd classes from CCtrlView in order to 1) have the control take up the
entire client area of a frame and 2) have the control behave as a view in the document/view scheme of things.

General Misconceptions

Let’s take an example of a CCtrlView-dervied class: CListView. The first misconception
that I hear revolves around the fact that there are two MFC classes that deal with
the Explorer like listing of objects: CListCtrl and CListView. Contrary to what a lot of
beginners might logically think, these two classes do not encapsulate two different
Windows classes. They both deal with one and only one Windows class called a
ListView. In fact, the CListCtrl is the encapsulation of the ListView. So why didn’t the
MFC designers call it a CListView? Because even though the common controls team
decided to call their window class a ListView, the suffix “View” has a very specific meaning
to us MFC developers and conjures up images of working within the context of the
document/view architecture. Therefore, the MFC team decided to instead call it a CListCtrl
since to us it is a “control” and not a “view” (in our way of thinking of views).

The next misconception is that the CListView is a CView-derived class that contains
an embedded CListCtrl object. This misconception is certainly the result of two things: 1)
it would be natural to think of a view as containing a control and 2) the CListView has a
member function called GetListCtrl which returns a (seemingly contained) CListCtrl reference.

However, what is happening here (and you’ll see this code shortly) is that when you
open a view with MFC (typically associated with a given SDI or MDI document template),
the MFC framework creates a frame for you and then within that frame creates a window. It is
that window that serves as your view and what you perceive as the window’s client area.
However, with the CCtrlView-derived classes (such as CListView) what is happening is
that you can specify any type of window to be used as the client area! In the case of the
CListView class, that class’ author simply specified that a ListView window class be used and
voila! when the view comes up its entire client area is taken up by a ListView and
the framework automatically handles the different things such as making sure that the
ListView resizes appropriately when the frame is resized.

How it Works

Now let’s look at the code of how the CListView gets created so that we can
see how this thing works.

The first thing we’ll look at is the CListView constructor (found in AFXCVIEW.INL).
As you can see below, the CListView constructor simply calls the CCtrlView constructor
and passes it a value of WC_LISTVIEW.

_AFXCVIEW_INLINE CListView::CListView()
: CCtrlView(WC_LISTVIEW, AFX_WS_DEFAULT_VIEW)
{ }

A little more digging turns up the following in the COMMCTRL.H file.

#ifdef _WIN32

#define WC_LISTVIEWA            "SysListView32"
#define WC_LISTVIEWW            L"SysListView32"

#ifdef UNICODE
#define WC_LISTVIEW             WC_LISTVIEWW
#else
#define WC_LISTVIEW             WC_LISTVIEWA
#endif

#else
#define WC_LISTVIEW             "SysListView"
#endif

So now all we know is that the CListView constructor is going to pass a value of
“SysListView32” to the CCtrlView constructor. From there let’s dig a bit deeper to
how simple this whole thing really is.

CCtrlView::CCtrlView(LPCTSTR lpszClass, DWORD dwStyle)
{
 m_strClass = lpszClass;
 m_dwDefaultStyle = dwStyle;
}

BOOL CCtrlView::PreCreateWindow(CREATESTRUCT& cs)
{
 ASSERT(cs.lpszClass == NULL);
 cs.lpszClass = m_strClass;

 // initialize common controls
 VERIFY(AfxDeferRegisterClass(AFX_WNDCOMMCTLS_REG));
 AfxDeferRegisterClass(AFX_WNDCOMMCTLSNEW_REG);

 // map default CView style to default style
 // WS_BORDER is insignificant
 if ((cs.style | WS_BORDER) == AFX_WS_DEFAULT_VIEW)
  cs.style = m_dwDefaultStyle & (cs.style | ~WS_BORDER);

 return CView::PreCreateWindow(cs);
}

Thus the CCtrlView class creates a window using the class name that is passed to
its constructor. Because this class is derived from CView, the window in question
will have a frame placed around it automatically when the view is created as part
of an SDI or MDI document template. It will also automatically be resized to always
encompass the entire frame when the frame is resized (alleviating you from writing
the sizing code to handle that event).

Creating Your own CCtrlView Classes

So now that you know that bit of MFC triva, how in the world does that help you
in the “real world”? Now that you know how this works, you can see that MFC has only
used a handful of controls with CCtrlView (e.g., CListView, CTreeView, CEditView, etc.).
But let’s imagine that you have another control that want to act similarly. For example,
let’s say that you wanted a ListBox to cover the entire client area and you didn’t want
to muck around with the sizing code when the view gets resized. Using the CCtrlView class
this is incredibly easy. (Note: I used the standard ListBox control here since that can
be tested by everyone. You can easily apply these same rules to your own custom
CWnd-derived classes as well.)

CListBoxView Source Code

To create something we’ll call a CListBoxView,
simply declare the class as follows. (This is also included in the downloadable
demo below)

#pragma once

class CListBoxView : public CCtrlView
{
 DECLARE_DYNCREATE(CListBoxView)

// c'tor
public:
 CListBoxView();

// Attributes
public:
 CListBox& GetListBox() const;

 //{{AFX_MSG(CListBoxView)
 //}}AFX_MSG
DECLARE_MESSAGE_MAP()
};

Next, implement the source as follows.

#include "stdafx.h"
#include "ListBoxView.h"

IMPLEMENT_DYNCREATE(CListBoxView, CCtrlView)

CListBoxView::CListBoxView() :
 CCtrlView(_T("LISTBOX"), AFX_WS_DEFAULT_VIEW) { };

CListBox& CListBoxView::GetListBox() const
{ return *(CListBox*)this; }

BEGIN_MESSAGE_MAP(CListBoxView, CCtrlView)
 //{{AFX_MSG_MAP(CListBoxView)
 ON_WM_NCDESTROY()
 //}}AFX_MSG_MAP
END_MESSAGE_MAP()

Take note of the constructor’s initializer list
and the fact that it passes the class name of the ListBox control
to the CCtrlView constructor. By doing so, this class we call a
CCtrlView reveals that it’s a ListBox control at heart.

The only function of any interest is the GetListBox function. As you can see, this function
simply returns a casted pointer to itself! How can you
return a view pointer when the function requested a ListBox pointer?! Remember that almost all
MFC control classes are really nothing more than very thin wrappers for their associated
Windows classes. As an example, when you call the CListBox::AddString member function,
the CListBox object simply sends a Windows message to its associated window to
add the specified string:

_AFXWIN_INLINE int CListBox::AddString(LPCTSTR lpszItem)
 { ASSERT(::IsWindow(m_hWnd));
   return (int)::SendMessage(m_hWnd,
                             LB_ADDSTRING,
                             0,
                             (LPARAM)lpszItem); }

Therefore, if a user of a CListBoxView requests a CListBox pointer in order to call
CListBox member functions this will work because even though they are not the same object
in memory, they do both have similar layouts in memory (both being derived from CWnd and
therefore, having m_hWnd member variables). Because of this fact, the AddString function
will work because the message (shown above) is simply being sent to the m_hWnd associated
with the CListBoxView. Needless to say, you have to be very careful because if you
attempted to call a CListBox member function that dealt with instance data not associated
with your CListBoxView you will certainly GPF!

Using the CListBoxView Class


When you’ve finished you’ll see how easy it is to use any CWnd-derived
class as a view in your MFC applications

Testing this class is as easy as writing it. Simply follow these steps (
remember that a demo is included with this article if
you don’t want to follow along):

  1. Use the AppWizard to create an SDI application called LbxViewTest
  2. Add the ListBoxView.cpp and ListBoxView.h files from above to your new project
  3. Open the LbxViewTestView.h file and include the ListBoxView.h file
  4. Replace all occurrences of CView with CListBoxView
  5. Open the LbxViewTestView.cpp file and replace all occurrences of CView with CListBoxView
  6. Add the view’s OnInitialUpdate member function as follows:
    void CLbxViewTestView::OnInitialUpdate()
    {
     CListBoxView::OnInitialUpdate();
    
     CListBox* pLbx = (CListBox*)this;
     if (pLbx)
     {
      CString str;
      for (int i = 0; i < 10; i++)
      {
       str.Format("Test %ld", i);
       pLbx->AddString(str);
      }
     }
    }
    

Downloads

Download demo project – 19 Kb

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read