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.
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
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.
CWnd* GetWnd(const CString& strType);
void AddWnd(const CString& strType, CWnd* pWnd);
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
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
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 AttachFont(CFont* pFont);
void EnableEvents(bool enable);
void AddEventHandler(IWndEventHandler* pEventHandler);
void RemoveEventHandler(IWndEventHandler* pEventHandler);
void AddLinkedControl(CWndControl* pControl);
void RemoveLinkedControl(CWndControl* pControl);
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);
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.
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).
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.
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.
void AttachWnd(CWnd* pWnd);
void SetResourceIdRange(UINT minResourceId, UINT maxResourceId);
void AddControl(CWndControl* pControl);
void AddControls(const std::list<CWndControl*>& controlList);
void RemoveControl(CWndControl* pControl);
CWndControl* GetControl(const CString& controlName);
CWndControl* GetControl(UINT resourceId);
void GetControls(std::list<CWndControl*>& controlList) const;
BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra,
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
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.
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.
virtual void HandleWndEvent(const CWndEvent& ev) = 0;
The properties of an event are encapsulated by the
CWndEvent(CWndControl* sender, const CString& text);
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:
class CMyDlg : public CDialog, public IWndEventHandler
CMyDlg(CWnd* pParent = NULL);
virtual void HandleWndEvent(const CWndEvent& ev);
virtual BOOL OnInitDialog();
virtual BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra,
And here are the relevant changes to the dialog's source file:
CMyDlg::CMyDlg(CWnd* pParent )
: CDialog(CMyDlg::IDD, pParent)
m_button = NULL;
m_container = new CWndContainer;
m_button = new CWndButton;
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 CDialog::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
void CMyDlg::HandleWndEvent(const CWndEvent& ev)
if ( ev.GetSender()->GetName() == _T("Button1") )
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.
CWnd wrapper for a tree control. Used to implement the tree view in the control window.
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.
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.
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.
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.