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.
class CWndFreePool
{
public:
CWndFreePool();
~CWndFreePool();
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.
class CWndControl : public IWndEventHandler
{
public:
CWndControl();
virtual ~CWndControl();
const CString& GetTypeName() const;
const CString& GetName() const;
void SetName(const CString& name);
bool IsVisible() const;
void SetVisible(bool visible);
bool IsEnabled() const;
void SetEnabled(bool enabled);
bool IsReadOnly() const;
void SetReadOnly(bool readOnly);
const CPoint& GetLocation() const;
void SetLocation(const CPoint& location);
const CSize& GetSize() const;
void SetSize(const CSize& size);
CRect GetRect() const;
UINT GetResourceId() const;
void AttachWnd(CWnd* pWnd);
void DetachWnd();
CWnd* GetAttachWnd();
void AttachFont(CFont* pFont);
void EnableEvents(bool enable);
void SuspendEvents();
void RestoreEvents();
void AddEventHandler(IWndEventHandler* pEventHandler);
void RemoveEventHandler(IWndEventHandler* pEventHandler);
void RemoveAllEventHandlers();
void AddLinkedControl(CWndControl* pControl);
void RemoveLinkedControl(CWndControl* pControl);
void RemoveAllLinkedControls();
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;
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.
class CWndContainer
{
public:
CWndContainer();
~CWndContainer();
void AttachWnd(CWnd* pWnd);
void DetachWnd();
void SetResourceIdRange(UINT minResourceId, UINT maxResourceId);
void AddControl(CWndControl* pControl);
void AddControls(const std::list<CWndControl*>& controlList);
void RemoveControl(CWndControl* pControl);
void RemoveAllControls();
CWndControl* GetControl(const CString& controlName);
CWndControl* GetControl(UINT resourceId);
void GetControls(std::list<CWndControl*>& controlList) const;
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.
class IWndEventHandler
{
public:
virtual void HandleWndEvent(const CWndEvent& ev) = 0;
};
The properties of an event are encapsulated by the CWndEvent
class:
class CWndEvent
{
public:
CWndEvent(CWndControl* sender, const CString& text);
~CWndEvent();
CWndControl* GetSender() const;
CString GetText() const;
void AddProperty(const CString& name, const CString& value);
bool GetProperty(const CString& name, CString& value) const;
};
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:
...
#include "WndEvent.h"
class CWndContainer;
class CWndButton;
class CMyDlg : public CDialog, public IWndEventHandler
{
DECLARE_DYNAMIC(CMyDlg)
public:
CMyDlg(CWnd* pParent = NULL);
virtual ~CMyDlg();
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:
#include "stdafx.h"
#include "MyDlg.h"
#include "WndContainer.h"
#include "WndControl.h"
...
CMyDlg::CMyDlg(CWnd* pParent )
: CDialog(CMyDlg::IDD, pParent)
{
m_button = NULL;
m_container = new CWndContainer;
m_container->AttachWnd(this);
}
CMyDlg::~CMyDlg()
{
m_container->DetachWnd();
delete m_container;
delete m_button;
}
BOOL CMyDlg::OnInitDialog()
{
CDialog::OnInitDialog();
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));
m_button->AddEventHandler(this);
m_container->AddControl(m_button);
return TRUE;
}
BOOL CMyDlg::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo)
{
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