Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Building a Dynamic UI using a CWnd Free Pool

0.00/5 (No votes)
6 Jan 2007 3  
Classes for building MFC-based user interfaces dynamically, with a focus on minimizing resource usage.

Introduction

This article presents a set of classes which can be used to build UIs dynamically. The code is centered around the use of a free pool manager of CWnd-derived controls, which helps to reduce GDI resource usage in certain UI scenarios. To demonstrate the classes in action, I've included a demo MDI application which simply allows you to open XML files. Each XML file defines the layout and properties of UI controls for a single MDI child window. Although the code is for VC6, the demo project can be converted to VS 2003 and VS 2005 as well.

UI Scenarios

There are a couple of common UI scenarios that may benefit from the free pool concept. The first example is that of a network management application which allows operators to control many different types of remote devices. Each device has a set of parameters which can be read or set in near real-time. One possible UI model for this type of application is your basic MDI shell that allows you to open one MDI child window for controlling a single device instance. Because each device may have numerous (tens or even hundreds of) parameters, the UI controls within each MDI child (or device) window are organized into logical groupings using tabs as shown in the figure below.

The typical approach to implementing the UI for each device type is to create a separate dialog or property page of controls for each tab. This method is straightforward to implement but it doesn't scale well. Consider a situation where you need to support a device type with 200 parameters. Assume each tab in a device window can accommodate a layout of controls for at most 20 parameters. Thus, 10 tabs or dialogs need to be created. Now, if you consider that each parameter may need to be paired with its own descriptive text label, the number of UI controls required to represent the entire device could possibly exceed 400. In addition, for certain parameters, the UI control may not be as simple as your basic CButton or CEdit. It could conceivably be a third party gauge ActiveX control (that you are required to use for your project), or an aggregate similar to a Windows Forms user control. Thus, the GDI resources required to implement a single device window could be quite high and become a limiting factor when the operator needs to open many of these device windows at the same time.

The second example is that of an options dialog (such as the Options dialog in VS 2005). This type of dialog typically consists of a tree view on the left-hand side, and a set of UI controls on the right. As the selection in the tree view is changed, the set of controls on the right-hand side changes dynamically. This UI scenario is actually quite similar to the first example of a tabbed device window. The main difference is in the selection or grouping mechanism (e.g., tree view selection versus tab selection).

CWnd Free Pool

One way to reduce the resource requirements of the tabbed device window is to eliminate the need for separate dialogs or property pages. This can be achieved by using just a single dialog and implementing a mechanism whereby UI controls are hidden or shown depending on which tab is currently selected. The same number of UI controls need to be created, but we save on the number of dialogs required.

Further reductions in resource usage can be achieved if we realize that the same types of UI controls are often present in multiple tabs. In other words, instead of just hiding controls when the tab selection changes, we can store the hidden controls in a free pool or cache so that they can be reused when switching to a different tab. This allows us to reuse UI control instances across tab selections. For example, if one tab uses a CButton and a second tab also uses a CButton, it should only be necessary to create one instance of a CButton and use the same UI instance for both tabs. With this approach, the savings in the number of UI controls required per device window can be significant. As an example of the best case scenario, consider a device with 10 parameter groupings (tabs) and 200 parameters, where each parameter is represented by a trackbar control. If we also pair each trackbar with a corresponding text label control, then a total of 400 UI controls are required using a typical multi-dialog implementation. However, if we reuse trackbar and label controls from one tab to the next, the device window will need at most 20 trackbar and 20 text label controls, thus reducing the resource usage by a factor of 10.

To implement this reuse mechanism, we begin by defining a CWndFreePool class that simply keeps track of which CWnd instances are free and available for use. Each CWnd referenced within the pool is paired with a string indicating the type of UI control that corresponds to the CWnd. For example, a type string of "Button" indicates the paired CWnd is actually a CButton instance (that was created with the BS_PUSHBUTTON style). Besides the built-in MFC controls such as CButton, the free pool can also reference ActiveX controls, since Visual Studio can generate MFC wrapper classes for ActiveX controls which are derived from CWnd. The public interface of the CWndFreePool class is shown below.

// CWndFreePool keeps references to CWnds which have been

// created but are unused (hidden). The pool maintains ownership

// of the CWnds which are still in the pool and deletes them in

// its destructor.

class CWndFreePool
{
public:
    // Constructor / destructor.

    CWndFreePool();
    ~CWndFreePool();
    
    // Public methods.

    CWnd* GetWnd(const CString& strType);
    void  AddWnd(const CString& strType, CWnd* pWnd);
};

Control Classes

In order to reuse a UI control instance, we need another mechanism for saving the state of the control before it is returned to the free pool, and also for restoring the state when the control is acquired again from the pool. To achieve this, we can define a hierarchy of classes that parallels the set of supported MFC control classes such as CButton and CSliderCtrl. The base class for this hierarchy is CWndControl and its public interface is shown below for reference. You can think of these CWndControl classes as being simple wrappers for their MFC counterparts.

// CWndControl base class (abstract).

class CWndControl : public IWndEventHandler
{
public:
    // Constructor / destructor.

    CWndControl();
    virtual ~CWndControl();
    
    // Type string.

    const CString& GetTypeName() const;
    
    // General purpose name identifier.

    const CString& GetName() const;
    void  SetName(const CString& name);
    
    // Visibility.

    bool IsVisible() const;
    void SetVisible(bool visible);
    
    // Enabled state.

    bool IsEnabled() const;
    void SetEnabled(bool enabled);
    
    // Read-only state.

    bool IsReadOnly() const;
    void SetReadOnly(bool readOnly);
    
    // Location.

    const CPoint& GetLocation() const;
    void  SetLocation(const CPoint& location);
    
    // Size. 

    const CSize& GetSize() const;
    void  SetSize(const CSize& size);
    CRect GetRect() const;
    
    // CWnd resource ID.

    UINT GetResourceId() const;
    
    // CWnd attachment.

    void  AttachWnd(CWnd* pWnd);
    void  DetachWnd();
    CWnd* GetAttachWnd();
    
    // CFont attachment.

    void AttachFont(CFont* pFont);
    
    // Events.

    void EnableEvents(bool enable);
    void SuspendEvents();
    void RestoreEvents();
    void AddEventHandler(IWndEventHandler* pEventHandler);
    void RemoveEventHandler(IWndEventHandler* pEventHandler);
    void RemoveAllEventHandlers();

    // Link to other CWndControls.

    void AddLinkedControl(CWndControl* pControl);
    void RemoveLinkedControl(CWndControl* pControl);
    void RemoveAllLinkedControls();

    // Pure virtual methods.

    virtual bool CreateWnd(CWnd* pParentWnd, UINT resourceId) = 0;
    virtual void UpdateWnd() = 0;
    virtual BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra, 
                          AFX_CMDHANDLERINFO* pHandlerInfo) = 0;
    
    // IWndEventHandler overrides.

    virtual void HandleWndEvent(const CWndEvent& ev);
};

Instances of CWndControl-derived classes can be created by application code simply by using the new operator. However, a CWndFactory class has also been provided to allow for the creation of CWndControl instances given a type string. This factory class has been designed primarily to allow for the dynamic creation of controls from XML specifications.

CWnd Container

The actual reuse logic is implemented by the CWndContainer class. This class is the heart of the dynamic UI layer as it manages updates to the free pool, uses the factory class, and dispatches events. CWndContainer can be thought of as a helper class which can be attached to any CDialog in order to add dynamic UI support. For example, in a CDialog class, simply create an instance of CWndContainer and attach it to the this pointer. Once the container has been attached to the dialog, CWndControl instances can be created and then added to the container (as shown in the code example here).

When a CWndControl instance is added, the container uses its internal free pool to try and acquire an existing CWnd of the appropriate type. If one is found, the CWnd is removed from the pool, made visible, and the properties of the CWndControl are then applied to this CWnd instance. On the other hand, if no appropriate CWnd was found in the pool, the container will create a new CWnd instance using the factory class.

When a CWndControl instance is removed from the container, its associated CWnd is detached, hidden, and returned to the free pool for reuse. The public interface of the CWndContainer class is shown below for reference.

// CWndContainer manages a collection of CWndControl instances and

// is designed for attachment to a CDialog such as CControlDlg.

// When a control is added to the container, the free pool is used

// to acquire an appropriate CWnd for attachment to the control.

// If none is available, the container will create a new CWnd for

// it by using the factory class. When a control is removed from

// the container, its CWnd is detached and added to the free pool

// for later reuse.

class CWndContainer
{
public:
    CWndContainer();
    ~CWndContainer();
    
    // Attach to CDialog.

    void AttachWnd(CWnd* pWnd);
    void DetachWnd();
    
    // Set resource ID range for control CWnds.

    void SetResourceIdRange(UINT minResourceId, UINT maxResourceId);
    
    // Control management.

    void AddControl(CWndControl* pControl);
    void AddControls(const std::list<CWndControl*>& controlList);
    void RemoveControl(CWndControl* pControl);
    void RemoveAllControls();
    
    // Find controls.

    CWndControl* GetControl(const CString& controlName);
    CWndControl* GetControl(UINT resourceId);
    void         GetControls(std::list<CWndControl*>& controlList) const;

    // Message handling.

    BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra, 
                  AFX_CMDHANDLERINFO* pHandlerInfo);
};

Event Handling

When MFC controls are created dynamically in a dialog (e.g., by using new and then invoking the Create() method), messages from those controls can be intercepted by overriding the OnCmdMsg() virtual method in the CDialog class. That is why the CWndContainer class also defines an OnCmdMsg() method. In any CDialog with an attached CWndContainer instance, you can override the dialog's OnCmdMsg() method and simply forward the call to the CWndContainer's OnCmdMsg() implementation. The container's implementation will dispatch the message to the appropriate CWndControl that is stored within the container. This CWndControl then sends out a CWndEvent notification to each of its event handlers.

For any CWndControl instance, you can add one or more event handlers that will receive events sent out by its corresponding MFC control. Event handlers are objects which implement the IWndEventHandler interface as shown below.

// IWndEventHandler interface.

class IWndEventHandler
{
public:
    virtual void HandleWndEvent(const CWndEvent& ev) = 0;
};

The properties of an event are encapsulated by the CWndEvent class:

// CWndEvent class.

class CWndEvent
{
public:
    // Constructor / destructor.

    CWndEvent(CWndControl* sender, const CString& text);
    ~CWndEvent();
    
    // Public methods.

    CWndControl* GetSender() const;
    CString      GetText() const;
    void         AddProperty(const CString& name, const CString& value);
    bool         GetProperty(const CString& name, CString& value) const;
};

Using The Dynamic UI Classes

The following code example shows how to add dynamic UI support to a CDialog class. In the example, we simply add a "Hello World!" button to a dialog. When the button is pressed, a message box is displayed as shown in the screenshot below.

The relevant changes to the dialog's include file are presented first:

// Filename: MyDlg.h


...

#include "WndEvent.h"


// Forward declarations.

class CWndContainer;
class CWndButton;

// CMyDlg class.

class CMyDlg : public CDialog, public IWndEventHandler
{
    DECLARE_DYNAMIC(CMyDlg)

public:
    CMyDlg(CWnd* pParent = NULL);
    virtual ~CMyDlg();

    // IWndEventHandler overrides.

    virtual void HandleWndEvent(const CWndEvent& ev);

    ...

protected:
    virtual BOOL OnInitDialog();    
    virtual BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra,
                          AFX_CMDHANDLERINFO* pHandlerInfo);
    ...
        
private:
    CWndContainer* m_container;
    CWndButton*    m_button;
    
    ...
};

...

And here are the relevant changes to the dialog's source file:

// Filename: MyDlg.cpp


#include "stdafx.h"

#include "MyDlg.h"

#include "WndContainer.h"

#include "WndControl.h"


...

CMyDlg::CMyDlg(CWnd* pParent /*=NULL*/)
    : CDialog(CMyDlg::IDD, pParent)
{
    m_button = NULL;

    // Create an instance of the container

    // and attach it to the dialog.

    m_container = new CWndContainer;
    m_container->AttachWnd(this);    
}

CMyDlg::~CMyDlg()
{
    // Detach the container from the dialog

    // and then delete it.

    m_container->DetachWnd();
    delete m_container;

    // Delete the button.

    delete m_button;
}

BOOL CMyDlg::OnInitDialog()
{
    CDialog::OnInitDialog();
    
    // Create a CWndButton and set its properties.

    m_button = new CWndButton;
    m_button->SetName(_T("Button1"));
    m_button->SetText(_T("Hello World!"));
    m_button->SetLocation(CPoint(10,10));
    m_button->SetSize(CSize(100,24));

    // Attach an event handler to the button.

    m_button->AddEventHandler(this);

    // Add the button to the container.

    m_container->AddControl(m_button);

    return TRUE;  // return TRUE unless you set the focus to a control

                  // EXCEPTION: OCX Property Pages should return FALSE

}

BOOL CMyDlg::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo)
{
    // Let the container handle the message.

    if ( m_container != NULL )
    {
        BOOL isHandled = m_container->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
        if ( isHandled )
            return TRUE;
    }
    
    return CDialog::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);    
}

void CMyDlg::HandleWndEvent(const CWndEvent& ev)
{
    if ( ev.GetSender()->GetName() == _T("Button1") )
    {
        MessageBox(ev.GetText(), _T("CMyDlg"));
    }
}

...

Control Surface Layer

The dialog example is fairly basic and shows how to create a UI dynamically. However, in order to demonstrate the resource usage benefits of the free pool mechanism, we need a way to add and remove CWndControl instances from the container during run-time. This is best illustrated using a scenario in which controls are divided into groups (where only one group of controls can be displayed at a time), and there exists a mechanism for group selection (such as using a tree view or a tab control). To this end, I've added a second layer of classes which implements a "control window" containing content that can be defined through XML. My main goal with this set of classes is to show the resource savings that can be achieved for a very specific UI scenario. The control surface classes are described briefly below.

CTreeWnd: A CWnd wrapper for a tree control. Used to implement the tree view in the control window.

CListWnd: A CWnd wrapper for a list control. Used to implement the events area in the control window.

CControlDlg: This is the dialog class which uses the CWndContainer instance. It's the actual control surface where CWnd controls are created, shown, or hidden.

CMarkup: The XML parser class from Ben Bryant's article. This is an easy-to-use class with no external dependencies and it consists of just two source files (release 6.5 Lite version).

CControlGroup: Represents a "group of controls", which is analogous to a folder in a file system. A control group may contain other groups, and also may contain controls (which are analogous to files in a file system).

CControlXml: This is the XML engine that uses CMarkup to parse the XML files and generate control groups and control instances.

CControlWnd: A CWnd-derived class that implements a window consisting of a tree view on the left-hand side, content controls on the right, and a small event window to demonstrate event handling. This is the top-level class that is used by the TestFreePool demo application.

The TestFreePool Application

The demo project (TestFreePool) is a MDI application that I initially generated using Visual Studio. This application simply allows you to open XML files that define the UI content for MDI child windows. Within each child window, you can access a context menu that contains the option, "Show CWnd Count". This function calculates the total number of actual CWnd objects in use by the window starting at the level of the CChildView instance (as a rough estimate of resource usage). The CChildView class was generated by Visual Studio and is the primary point of integration of the MDI application code with the control surface layer (i.e., CControlWnd). The screenshot below shows how the demo project is organized.

The download zip file includes a release build of the TestFreePool application. If you wish to build the demo project yourself, please note that I have excluded the two source files, Markup.h and Markup.cpp, from the zip file due to licensing restrictions. Please download the source code from the CMarkup article first, and then place the Markup.h and Markup.cpp files in the TestFreePool project folder before building with Visual Studio. If you are using VS 2005 to convert and build the demo project, you may also encounter a compile error C2440 for Markup.cpp, Line 725. To resolve this, you can just add an appropriate cast to (_TCHAR *) in order to avoid the error.

The figure below illustrates the window hierarchy for each MDI child window in the demo application.

XML Files

In the TestFreePool folder, there are three example XML files which can be opened by the demo application. The table below describes each of the files and also gives an indication as to the resource savings achieved using the free pool mechanism (based on total CWnd counts). The XML format chosen is fairly arbitrary - it basically allows you to define a control group hierarchy in which each group may contain zero or more child groups, and zero or more controls.

Filename Description Maximum CWnd Count Estimated CWnd count without using free pool
Example1.xml Displays each of the supported UI control types. 30 41
Example2.xml Displays 12 control groups, each containing 10 labels and 10 buttons. 27 259
Example3.xml Displays 3 pages from the VS 2005 Options dialog. 30 48

Note that the maximum CWnd count for Example1.xml may vary depending on how Internet Explorer is configured on your system (since one of the supported controls is the Microsoft WebBrowser2 ActiveX control).

Below is a screenshot of the Example2.xml file as loaded in the demo application.

Summary

The purpose of this article was to demonstrate how to create UIs dynamically while minimizing resource usage in certain scenarios. The code was developed to illustrate this concept and is not intended to be a generic or complete XML forms library, etc. For example, only a limited set of controls and properties are supported currently, and the event handling mechanism is very simplistic. The XML support was added as a convenient means of demonstration and testing but is not the main focus of what I wanted to present. The source code will probably be more useful to you though if you can adapt it to your own specific application requirements. For example, you may want to add support for more MFC controls or even your own custom controls. There is a text file in the demo project folder which outlines the steps for adding new control support.

History

  • January 6th, 2007
    • Initial revision.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here