Adding Behavior to Classes, Part II – Resizing Dialogs and Property Pages

However, it is not entirely good news.

Let’s say that you have written class CMyDialog derived from CDialog that implements some handy features. You derive all your own dialogs from CMyDialog instead of CDialog, so they all have your new features. All is fine with the world.

Now you want to add those same nice features to your property pages. Why not? After all, dialogs and property pages are very similar. But…oh, dear. You cannot simply change the MFC CPropertyPage class so it derives from your CMyDialog instead. So what do you do?

Well, you could duplicate your code and write a CMyPropertyPage class derived from CPropertyPage. However, as discussed in my last article, it is not good form. More importantly, it is the start of a maintenance nightmare, as you’d have to make the same fixes and changes to both classes

The next obvious choice is to use a template, so you only have to write the code once; but as we found out in my last article, the MFC message map macros do not get along well with templates. We can make our own versions of the macros that do work in a template class, but that method has its limitations as well.

Templates are only a good solution for trivial extensions. Templates expose their internal workings to the world as inline functions. This defeats the purpose of encapsulation and increases dependencies within a project. Therefore, we use multiple-inheritance to neatly wrap up the internals of the class.

Let us have a look at a practical example.

Template-Friendly Message Maps

First, let’s have a quick look at the template-friendly message-map macros again.

I copied the MFC message-map macros and added ‘template <class baseClass>’ in front of the variable and function definition lines in the new BEGIN_MESSAGE_MAP_FOR_TEMPLATE. I also removed the ‘const’s to get around a compiler bug. Yes, it looks ugly. But then, it is an MFC macro.

File “XMessageMapsForTemplates.h”:

#ifndef _XMESSAGEMAPSFORTEMPLATES_
#define _XMESSAGEMAPSFORTEMPLATES_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#ifdef _AFXDLL
#define DECLARE_MESSAGE_MAP_FOR_TEMPLATE() \
private: \
 static /*const*/ AFX_MSGMAP_ENTRY _messageEntries[]; \
protected: \
 static AFX_DATA /*const*/ AFX_MSGMAP messageMap; \
 static const AFX_MSGMAP* PASCAL _GetBaseMessageMap(); \
 virtual const AFX_MSGMAP* GetMessageMap() const; \

#else
#define DECLARE_MESSAGE_MAP_FOR_TEMPLATE() \
private: \
 static /*const*/ AFX_MSGMAP_ENTRY _messageEntries[]; \
protected: \
 static AFX_DATA /*const*/ AFX_MSGMAP messageMap; \
 virtual const AFX_MSGMAP* GetMessageMap() const; \

#endif

#ifdef _AFXDLL
#define BEGIN_MESSAGE_MAP_FOR_TEMPLATE(theClass, baseClass) \
 template <class baseClass> \
  const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() \
  { return &baseClass::messageMap; } \
 template <class baseClass> \
  const AFX_MSGMAP* theClass::GetMessageMap() const \
  { return &theClass::messageMap; } \
 template <class baseClass> \
  AFX_COMDAT AFX_DATADEF /*const*/ AFX_MSGMAP \
  theClass::messageMap = \
  { &theClass::_GetBaseMessageMap, \
  &theClass::_messageEntries[0] }; \
 template <class baseClass> \
  AFX_COMDAT /*const*/ AFX_MSGMAP_ENTRY \
  theClass::_messageEntries[] = \
  { \
#else
#define BEGIN_MESSAGE_MAP_FOR_TEMPLATE(theClass, baseClass) \
 template <class baseClass> \
  const AFX_MSGMAP* theClass::GetMessageMap() const \
  { return &theClass::messageMap; } \
 template <class baseClass> \
  AFX_COMDAT AFX_DATADEF /*const*/ AFX_MSGMAP \
  theClass::messageMap = \
  { &baseClass::messageMap, &theClass::_messageEntries[0] }; \
 template <class baseClass> \
  AFX_COMDAT /*const*/ AFX_MSGMAP_ENTRY \
  theClass::_messageEntries[] = \
  { \
#endif
#define END_MESSAGE_MAP_FOR_TEMPLATE() END_MESSAGE_MAP()

#endif

Something I failed to mention in my last article is how to make these macros work. The trick is in calling the BEGIN_MESSAGE_MAP_FOR_TEMPLATE. The first argument should be the template class name with a dummy parameter within pointy brackets (“<>“). You use the same parameter name as the second parameter.
For example, if you declared your class like this:

template <class TBASE> class TMyClass : public TBASE {
 ...
 DECLARE_MESSAGE_MAP_FOR_TEMPLATE()
};

Then you’d write the following definition for the message map (usually in the same “.h” file):

BEGIN_MESSAGE_MAP_FOR_TEMPLATE(TMyClass<TBASE>, TBASE)

Multiple Inheritance

I use multiple-inheritance to neatly encapsulate and share the implementation of the template. The template class behaves primarily as a CDialog (or whatever) but also inherits from our new class to add the required behavior.

The corresponding C++ code looks like this:

template <class TWND> // TWND will be derived from CWND
class TMyClass : public TWND, public CMyClass {
...
};

class CMyClass {
...
private:
 CWnd* m_pWnd;
};

CMyClass has all the implementation details hidden away in its “.cpp” file. TMyClass forwards its calls to the corresponding CMyClass functions.

It would be nice if we could derive CMyClass from CWnd as well. However, MFC limitations mean we cannot make calls to CWnd member functions, or use message maps etc. As always, there is a way out. In CMyClass, I store a pointer to my CWnd-derived template object, and make CWnd calls through that pointer.

This assumes that the class with which we instantiate TMyClass is derived from CWnd. If it is not, then we will get compilation errors.

Although we strive to not have compilation errors in the first place, it is always preferable to catch them as early as possible in the development process. The best is to catch them while you are writing code. MS Intellisense helps here. Next best is compile time, then link time, and finally at run-time via checking error codes and doing ASSERTS, etc. The worst way to find errors is for your program to compile, link, build, run … and then crash.

Notice that I name my template classes with a capital “T” (for template) rather than “C” (for class). You should adopt conventions and, if needed, make up your own. In a way, it does not really matter which conventions you adopt, as long as you use them consistently. When working with MFC one should try adopt conventions that fit well with MFC’s conventions, otherwise your code ends up with a mixture of the two.

Now, let us get back to the project. I have encapsulated storing the pointer to a CWnd in a separate class. I will use this class as the base class for my implementation classes.

File "XWndMultipleInheritance.h":

// XWndMultipleInheritance.h : header file
//

#ifndef _XWNDMULTIPLEINHERITANCE_
#define _XWNDMULTIPLEINHERITANCE_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

class CXWndMultipleInheritance {
 // Construction
public:
 CXWndMultipleInheritance(CWnd* pWnd) : m_pWnd(pWnd) {}
 // Casting operators etc
public:
 operator CWnd&() { return *m_pWnd; }
 operator const CWnd&() const { return *m_pWnd; }
 operator CWnd*() { return m_pWnd; }
 operator const CWnd*() const { return m_pWnd; }
 CWnd* operator->() { return m_pWnd; }
 const CWnd* operator->() const { return m_pWnd; }

// Encapsulate some common calls (add to this later if required)
public:
 LRESULT CWnd_SendMessage( UINT message,
                           WPARAM wParam = 0,
                           LPARAM lParam = 0 ) {
  return (*this)->SendMessage(message,wParam,lParam);
 }
 LRESULT CWnd_PostMessage( UINT message,
                           WPARAM wParam = 0,
                           LPARAM lParam = 0 ) {
  return (*this)->PostMessage(message,wParam,lParam);
 }
 HWND CWnd_GetSafeHwnd() const {
  return (*this)->GetSafeHwnd();
 }
 void CWnd_ScreenToClient(LPPOINT lpPoint) const {
  (*this)->ScreenToClient(lpPoint);
 }
 void CWnd_ScreenToClient(LPRECT lpRect) const {
  (*this)->ScreenToClient(lpRect);
 }
 void CWnd_ClientToScreen(LPPOINT lpPoint) const {
  (*this)->ClientToScreen(lpPoint);
 }
 void CWnd_ClientToScreen(LPRECT lpRect) const {
  (*this)->ClientToScreen(lpRect);
 }
 // the pointer to the actual window
private:
 CWnd* m_pWnd;
};

#endif

As you can see, the pointer is set up in the constructor. We gain access to it via the casting operators. I also added some shortcuts for some commonly used functions.

The Template Class

With that out of the way, let’s start fleshing out the template class.

I find the easiest way to write a template like this is to use ClassWizard to generate a new MFC-derived class, and then hand-edit it into a proper template class afterwards. These are the steps:

  1. Use ClassWizard to create a new MFC class called “TXResizable” and to derive it from a generic CWnd.
  2. Change the class declaration to be a template (with multiple-inheritance as described above)
  3. Move the body of the generated “.cpp” file near the end of the “.h” file
  4. Make all the functions inline.
  5. Fix the default constructor so that it correctly initializes both base classes.
  6. Add a #pragma to prevent VC from giving warnings about using “this” in the base member initializer list.
  7. Add #include “XMessageMapsForTemplates.h” and change the message map declarations and definitions to use your template-friendly macros.
  8. Delete the “.clw” and the now-unused “.cpp” file.

I end up with a class that looks consistent with MFC and ClassWizard-generated code. And consistency is a wonderful thing. The result of all that tweaking is the following “.h” file:

File: “TXREsizable.h”:

#ifndef _TXRESIZABLE_H_
#define _TXRESIZABLE_H_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
// TXResizable.h : header file
//

#include "XMessageMapsForTemplates.h"

// ========================================
// TXResizable window

template <class TDIALOGORPAGE>
class TXResizable : public TDIALOGORPAGE, public CXResizable {
// Construction
public:
 TXResizable();

// Attributes
public:

// Operations
public:

// Overrides
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(TXResizable)
//}}AFX_VIRTUAL

// Implementation
public:
 virtual ~TXResizable();

// Generated message map functions
protected:
//{{AFX_MSG(TXResizable)
// NOTE - the ClassWizard will add and remove member functions here.
//}}AFX_MSG
DECLARE_MESSAGE_MAP_FOR_TEMPLATE()
};

//==============================================
//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional
// declarations immediately before the previous
// line.

//==============================================
// TXResizable

// 'this' : used in base member initializer list
#pragma warning (disable: 4355)
TXResizable::TXResizable()
: TDIALOGORPAGE()
, CXResizable(this)
{
}
// 'this' : used in base member initializer list
#pragma warning (default: 4355)

TXResizable::~TXResizable()
{
}


BEGIN_MESSAGE_MAP_FOR_TEMPLATE(TXResizable<TDIALOGORPAGE>,
                               TDIALOGORPAGE)
//{{AFX_MSG_MAP(TXResizable)
// NOTE - the ClassWizard will add and remove mapping macros here.
//}}AFX_MSG_MAP
END_MESSAGE_MAP()


//==============================================
// TXResizable message handlers

#endif

You may be surprised to learn that I can still use ClassWizard to add message handlers to this code. ClassWizard appears to be smart enough to handle this because it is actually quite dumb. ClassWizard doesn’t care what macro names are used, nor that I am working with templates; all it looks for are those funny //{{ AFX_XXX comments. The only catch is that I have to move any ClassWizard-generated functions to just before the #endif that is at the end of the “.h” file and edit it to be a legitimate inline member-function definition.

For example, here is the function that ClassWizard generates if I add a WM_SIZE handler:

void TXResizable::OnSize(UINT nType, int cx, int cy)
{
 C???::OnSize(nType, cx, cy);
 // TODO: Add your message handler code here
}

Because ClassWizard cannot determine what the base class is, it just uses “C???”. This is good because it forces me us to correct the code. Again, compilation errors are a good thing.

I then hand-edit the above code to look like this:

template <class TDIALOGORPAGE>
void TXResizable<TDIALOGORPAGE>::OnSize(UINT nType, int cx, int cy)
{
 TDIALOGORPAGE::OnSize(nType, cx, cy);
 // TODO: Add your message handler code here
}

Putting It All Together

Now all I need to do is combine all this with some actual code that performs dialog and property page resizing. This means I need to write my CXResizable class (it had to happen sooner or later).

I derive CXResizable from CXWndMultipleInheritance (described earlier). This class has all the logic for resizing. I also add the appropriate message handlers to the TXResizable class (using ClassWizard) and edit them so they call the appropriate functions of CXResizable.

There is quite a bit of code here, so I will not reproduce it all in this article. The full source is available for download in the demo project. However, I will discuss some of the main points of interest. Let us start at the very beginning of the “.h” file.

Resizing Controls in a Dialog

There are many ways a control can resize itself when you resize the dialog or property page in which it lives. Some controls stay positioned relative to the edges of the dialog or page, whereas others move in proportion with the change in size. Some controls will keep their size, whereas others will grow in one direction or both.

‘enum EResizeFlags’ specifies resizing options for each of the four coordinates that comprise the rectangle for the control position within a dialog or page. By combining these flags, you can determine how the control moves and/or resizes.

enum EResizeFlags {
 RESIZE_LEFT_LFIX = 00000,

 RESIZE_LEFT_LPROP = 00001,

 RESIZE_LEFT_RPROP = 00002,

 RESIZE_LEFT_CPROP = RESIZE_LEFT_LPROP
                   | RESIZE_LEFT_RPROP,

 RESIZE_LEFT_RFIX = 00004,

 RESIZE_LEFT_FLAGS = RESIZE_LEFT_LPROP
                   | RESIZE_LEFT_RPROP
                   | RESIZE_LEFT_RFIX,
... etc for other coordinates and useful combinations
};

The flags with ‘_LFIX’ (and ‘_TFIX’) suffixes anchor the control relative to the left (or top) of the dialog; there is no change in the coordinate. ‘_RFIX’ and ‘_BFIX’ anchor the control relative to the right (or bottom) of the dialog. For example, if the dialog width increases by 100 pixels, then the left coordinate will also move 100 pixels to the right. ‘_LPROP’, ‘_RPROP’, ‘_CPROP’ move the coordinate in proportion to the position of the control across the dialog. LPROP bases that proportion on the left-hand side of the control, CPROP on the center, and RPROP on the right-hand side. ‘_TPROP’, ‘_BPROP’ and ‘_CPROP’ are the equivalent for the top and bottom coordinate.

I used 3-bits to encode the options for each of the four coordinates for 12-bits total. This fits quite nicely into an integer. We will look at the algorithm for resizing using this flag, and its implementation, shortly.

Let us now look at the CXResizable class itself. Notice that most of the functions are virtual, so I can override them in my derived classes to add to or replace their default implementations.

IsResizable() determines whether the dialog or page will have be resizable. By default, it looks at the border style bits of the dialog or page to see if it has a modal dialog frame style (DS_MODALFRAME). If not, then it is resizable.

The GetClientRectWas/Now/Ini() set of functions return the client rectangles before and after sizing, and for the initial dialog size respectively. The GetWindowRectWas/Now/Init() do the same for the window rectangle

To make it obvious that a dialog is resizable, we draw a gripper in the bottom right corner. GetGripperRect() gives the position of the gripper for hit testing and painting. It uses GetSystemMetrics() to make the gripper the same dimensions as the scroll bar width. You should always use GetSystemMetrics(), GetSysColor(), etc., rather than hard-coding sizes.

DrawGripper() does the work of drawing the gripper. I use the very handy DrawFrameControl() to do this; there is no need to reinvent this particular wheel. If you ever have to draw your own user-interface elements, look at DrawFrameControl first.

IsResizeDirectly() determines whether a dialog or page has resized indirectly, because its container changed size, or has been resized directly, by the user. A property page usually only changes size when the property sheet changes size. However, you can directly resize a modeless dialog by dragging its edges. IsResizeDirectly() stops the gripper appearing and being active.

OnSize(), OnNcHitTest(), etc., in CXResizable may look like message handlers, but strictly speaking, they’re not, because CXResizable does not derive from CWnd. However, the message handlers in the TXResizable template call these member functions of the same name.

OnPaint()fills the background to avoid repaint problems and draws the gripper if required. Note that the logic for when to draw the gripper is here, rather than in DrawGripper(). That way, I can override DrawGripper() to change the gripper’s appearance without having to rewrite the logic for when to draw it.

OnGetMinMaxInfo() looks at the GetWindowRectIni to determine the minimum drag size. During resizing, MFC calls OnGetMinMaxInfo, which fills the MINMAXINFO structure to indicate maximum and minimum sizes for the window.

OnNcHitText() handles dragging by the gripper. Note that if it returns HTNOWHERE, then the TXResizable handler code will call the default OnNcHitTest.

OnSize() is where the real work happens. It updates the cached client and screen rectangles. If the size actually changes, it calls ResizeControls() to do the actual resizing. ResizeControls() uses ::EnumChildWindows() to iterate through all the child windows (i.e. controls) in the dialog or page and resize each reposition each one appropriately.

I need to pass quite a bit of information to be able to resize each child window intelligently. However, there is only room for passing a single LPARAM to the enumerating function. So I shove all the information I need into a struct, and pass a pointer to that struct in the LPARAM.

The ::EnumChildWindows() call tells Windows to call EnumChildProc_ResizeControls for each control. This function unpacks the data from the struct, calls a virtual function to work out the new rectangle for the control, and then moves and resizes it. For cleaner repainting, I use the ::Begin/End/DeferWindowPos() API’s. These accumulate all the window size and position changes I make and apply them in one go at the end.

For each control I call the virtual ResizeControl() function. This in turn calls the appropriately named ResizeControlUsingFlags(). However, you can override ResizeControl for any other method of resizing you choose.

ResizeControlUsingFlags uses the flags defined in EResizeFlags. Although there are quite a few lines of code, it is basically just a bunch of simple switch() statements.

void CXResizable::ResizeControlUsingFlags(UINT id,CWnd* pControl,
                                          CRect& rcControl,
                                          const CSize& szNow,
                                          const CSize& szWas,
                                          const CSize& szIni,
                                          const CSize& szDelta) {
 UINT nFlags = GetResizeFlags(id,
                              pControl,
                              rcControl,
                              szNow,
                              szWas,
                              szIni,
                              szDelta);
 if (nFlags != RESIZE_LFIX) {
  {
   int l = rcControl.left;
   int r = rcControl.right;
   int c = (l+r+1)/2;
   int dlprop = ::MulDiv3(l,szNow.cx,szWas.cx,szIni.cx)-l;
   int drprop = ::MulDiv3(r,szNow.cx,szWas.cx,szIni.cx)-r;
   int dcprop = ::MulDiv3(c,szNow.cx,szWas.cx,szIni.cx)-c;
   int dfull = szDelta.cx;

   switch (nFlags & RESIZE_LEFT_FLAGS) {
    case RESIZE_LEFT_LFIX:
    break;

    case RESIZE_LEFT_LPROP:
     rcControl.left += dlprop;
    break;

    case RESIZE_LEFT_RPROP:
     rcControl.left += drprop;
    break;

    case RESIZE_LEFT_CPROP:
     rcControl.left += dcprop;
    break;

    case RESIZE_LEFT_RFIX:
     rcControl.left += dfull;
    break;
   }

   switch (nFlags & RESIZE_RIGHT_FLAGS) {
    case RESIZE_RIGHT_LFIX:
    break;

    case RESIZE_RIGHT_LPROP:
     rcControl.right += dlprop;
    break;

    case RESIZE_RIGHT_RPROP:
     rcControl.right += drprop;
    break;

    case RESIZE_RIGHT_CPROP:
     rcControl.right += dcprop;
    break;

    case RESIZE_RIGHT_RFIX:
     rcControl.right += dfull;
    break;
   }
  }
  {
   int t = rcControl.top;
   int b = rcControl.bottom;
   int c = (t+b+1)/2;
   int dtprop = ::MulDiv3(t,szNow.cy,szWas.cy,szIni.cy)-t;
   int dbprop = ::MulDiv3(b,szNow.cy,szWas.cy,szIni.cy)-b;
   int dcprop = ::MulDiv3(c,szNow.cy,szWas.cy,szIni.cy)-c;
   int dfull = szDelta.cy;

   switch (nFlags & RESIZE_TOP_FLAGS) {
    case RESIZE_TOP_TFIX:
    break;

    case RESIZE_TOP_TPROP:
     rcControl.top += dtprop;
    break;

    case RESIZE_TOP_BPROP:
     rcControl.top += dbprop;
    break;

    case RESIZE_TOP_CPROP:
     rcControl.top += dcprop;
    break;

    case RESIZE_TOP_BFIX:
     rcControl.top += dfull;
    break;
   }

   switch (nFlags & RESIZE_BOTTOM_FLAGS) {
    case RESIZE_BOTTOM_TFIX:
    break;

    case RESIZE_BOTTOM_TPROP:
     rcControl.bottom += dtprop;
    break;

    case RESIZE_BOTTOM_BPROP:
     rcControl.bottom += dbprop;
    break;

    case RESIZE_BOTTOM_CPROP:
     rcControl.bottom += dcprop;
    break;

    case RESIZE_BOTTOM_BFIX:
     rcControl.bottom += dfull;
    break;
   }
  }
 }
}

Although virtual, you would probably not want to override it, as it is highly dependent on EResizeFlags. However, ResizeControlUsingFlags() does call the virtual function GetResizeFlags() that you probably will override in your own class.

GetResizeFlags() is where the dialog or property page specifies how each of its controls is to be resized. The default implementation of GetResizeFlags returns RESIZE_NONE for all controls; all the controls stay huddled up in the top left corner of the dialog or page when you resize it.

I often replace this simple GetResizeFlags() function with a smart version that looks at the Window Class and position of the controls, and use that to work out what resize flag to return.

Using TXResizable

Phew! That’s a lot for one article. But now you can now derive your own dialogs from TXResizable (and your property pages from TXResizable). Indeed, one could use a typedef for these and derive from that:

typedef TXResizable<CDialog> CXResizableDialog;
typedef TXResizable<CPropertyPage> CXResizablePropertyPage;

To use these classes with your own application, you will need to change the dialog resources and select a border style of Resizing from the Style tab of the Dialog Properties.

To see any real resizing of controls, provide your own GetResizeFlags() function that says how to resize each control.

The sample application shows some simple resizing for both the main application and the about box.

Here is what the application looks like when you first start it…

But if you resize it, it looks like this…

Hmmm … I wonder if TXResizable will work with a CFormView?

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read