使用一个CWnd空闲池创建一个动态用户界面
介绍
本文提出了一组可以用来动态创建UI的类。该代码是集中于一个空闲池使用管理器的CWnd继承控件,该管理器可以帮助我们减少在特定UI场景中GDI资源的使用。为了在运行中演示这些类,我已经在此提供了一个MDI应用示例,它只是让你来打开XML文件。每个XML文件为单个MDI子窗体定义了布局和UI控件属性。尽管代码是用VC6写的,示例项目也可以被转换为VS 2003 和VS 2005项目。
UI场景
这里有两个普通的UI场景可能从空闲池的概念中受益。第一个例子是一个允许操作员来控制一些不同类型的远程设备的网络管理应用程序。每个设备有一组可以被几乎实时地读取或设置的参数。对于这种类型的应用程序有一个可能的UI模式是你的基本MDI框架允许你打开一个MDI子窗体以控制单个设备实例。因为每个设备可能有众多的(数以十计或甚至数以百计)参数,每个MDI子(或设备)窗体中的UI控件用如下图所示标签被组织为逻辑上的分组。
为每个设备类型实现UI的典型方法是为每个标签创建控件的一个不同的对话框或属性页。这个方法实现起来简单但是它不能很好地工作。考虑这样一个状况:你需要支持一个有着200个参数的设备类型。假定在一个设备窗体中每个标签可以为最多20个参数的控件提供的一个布局。因此,需要创建10个标签或对话框。现在,如果你认为每个参数也许需要配以它自我描述的文本标签,那么表示完整设备所必需的UI控件的数量可能会超过400个。另外,对于特定参数,UI控件可能并不是像你的基本CButton或CEdit那样简单。它也许可能是一个第三方测量的ActiveX控件(你必需在你的项目用到的),或一个类似于Windows Forms用户控件的聚集。因此,必需实现单个设备窗体的GDI资源耗费可能会很高并在操作员需要在同一时间打开很多这些设备窗体时变成一个限制的因素。
第二个例子是选项对话框(比如在VS2005中的“选项(Options)”对话框)。这一类型对话框的代表是在左手边包含一个树视图,右边是一组UI控件。每当树视图中的选择项改变,右手的那组控件就会动态改变。这个UI场景实际上与有着标签设备常窗体的第一个例子很相似。主要的不同之处是在选择和分组机制上(例如,树视图选择对应标签选择)。CWnd空闲池
去除对不同的对话框或属性页的需要是减少标签设备窗体的资源需求的一个方法。可以通过只用一个对话框并实现一个机制,并由此依靠当前选择了哪一个标签决定UI控件被隐藏或显示。相同数量的UI控件需要被创建,但是我们以对话框所需要的数量保存这些控件。
如果我们认识到相同类型的UI控件常常在多个标签中被显示,那就可以在资源使用中获得更大节约。换句话说,不是只在标签选择改变时隐藏控件,我们可以在空闲池或cache中存储隐藏控件以便它们可以在转换到一个不同标签时被复用。这允许我们通过标签选择复用UI控件实例。举个例子,如果一个标签使用了一个CButton和另一个标签也使用一个CButton,为这两个标签它应该只需要创建一个CButton的实例并使用相同的UI实例。用此方法,每个设备窗体所需UI控件节约的数量会是相当大的。正如最佳案例场景的一个例子,考虑一个有者10个参数组(标签)和200个参数的设备,每个参数用一个trackbar(滑块)控件表示。如果我们也要用一个相应的文本标签控件为每个trackbar配对,这时就需要总数400个UI控件使用一个典型的多对话框实现。然而,如果我们从一个标签到另一个标签复用trackbar和label控件,设备窗体将需要至少20个trackbar和20个label控件,由此可以10倍减少资源的使用。
为了实现复用机制,我们首先通过定义一个只是保存空闲和可利用的CWnd实例track的CWndFreePool类。池中引用的每个CWnd配以一个字符串标识与UI控件的类型相应的CWnd。比如,"Button"类型字符串标识配对CWnd实际上是一个CButton实例,(以BS_PUSHBUTTON样式创建的)。除了MFC内建控件比如CButton,空闲池也可以引用ActiveX控件,因为Visual Studio可以为继承于CWnd的ActiveX控件生成MFC包装类。CWndFreePool类的public接口如下所示。
// CWndFreePool保持引用到已被创建但没有使用的(隐藏)的CWnds。
控件类
//该池包括仍在池中的CWnds的所有者并在其析构体删除它们中。
class CWndFreePool
{
public:
//构造器/析构器
CWndFreePool();
~CWndFreePool();
// Public 方法。
CWnd* GetWnd(const CString& strType);
voidAddWnd(const CString& strType, CWnd* pWnd);
};
为了复用一个UI控件实例,我们需要另一个在控件被返回到空闲池前保存控件状态,并且也要保存这个控件从池中被再次获得时状态的机制。为获得这样的机制,我们可以定义与一组MFC控件类支持的如CButton和CSliderCtrl相似的一层类。这层类的基类是CWndControl并且它的public接口如下所示以供参考。你可以认为这些CWndControl类是为它们的MFC副本而做的简单封装。
// CWndControl基类(抽象)
可以只使用new操作符通过应用程序代码创建继承于CWndControl类的实例。然而,一个CWndFactory类已被提供来允许为CWndControl实例的创建而给定一个类型字符串。该工厂类主要被设计用来允许从XML清单中动态创建控件。
class CWndControl : public IWndEventHandler
{
public:
//构造器/析构器
CWndControl();
virtual ~CWndControl();
//类型字符串
const CString& GetTypeName() const;
//生成目标名称标识
const CString& GetName() const;
voidSetName(const CString& name);
//可见性
bool IsVisible() const;
void SetVisible(bool visible);
// Enabled状态
bool IsEnabled() const;
void SetEnabled(bool enabled);
// Read-only状态
bool IsReadOnly() const;
void SetReadOnly(bool readOnly);
//位置
const CPoint& GetLocation() const;
voidSetLocation(const CPoint& location);
// 尺寸大小
const CSize& GetSize() const;
voidSetSize(const CSize& size);
CRect GetRect() const;
// CWnd资源ID
UINT GetResourceId() const;
// CWnd装置
voidAttachWnd(CWnd* pWnd);
voidDetachWnd();
CWnd* GetAttachWnd();
// CFont装置
void AttachFont(CFont* pFont);
//事件
void EnableEvents(bool enable);
void SuspendEvents();
void RestoreEvents();
void AddEventHandler(IWndEventHandler* pEventHandler);
void RemoveEventHandler(IWndEventHandler* pEventHandler);
void RemoveAllEventHandlers();
//连接到其它CWndControl
void AddLinkedControl(CWndControl* pControl);
void RemoveLinkedControl(CWndControl* pControl);
void RemoveAllLinkedControls();
//纯虚方法
virtualbool CreateWnd(CWnd* pParentWnd, UINT resourceId) = 0;
virtualvoid UpdateWnd() = 0;
virtual BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo) = 0;
// IWndEventHandler覆写
virtualvoid HandleWndEvent(const CWndEvent& ev);
};
CWnd 容器
实际的复用逻辑通过CWndContainer类实现。该类是动态UI布局的核心因为它管理了空闲池的更新、使用工厂类和事件调度。CWndContainer可以被认为是一个附着在CDialog以添加动态UI支持的帮助类。例如,在一个CDialog类,只是创建一个CWndContainer实例并将其附着到this指针。一旦该容器被附加到对话框,CWndControl实例便可以被创建并同时添加到容器(如这里的代码示例所示)。
当一个CWndControl实例被添加,该容器使用其内部的空闲池闯⑹曰竦靡桓鲆延械挠凶攀实崩嘈偷腃Wnd。如果找到一个,CWnd从池中被移出,显示,并且CWndControl的属性此时就被应用到这个CWnd实例。另一方面,如果在此池中没有找到适合的CWnd,容器将用工厂类创建一个新的CWnd实例。
当一个CWndControl实例从容器中被移出时,与它关联的CWnd被剥离,隐藏,并返回到空闲池以供复用。CWndContainer类的public接口如下所示以供参考。
// CWndContainer 管理一个CWndControl 实例的集合并且
事件处理
// 被设计来附着到一个CDialog比如CControlDlg。
// 当一个控件被添加到这个容器,空闲池被用来获得一个适当的Cwnd
// 以附着到这个控件。
// 如果没有可用的(CWnd),该容器将通过使用工厂类为之
//创建一个新的CWnd。当一个控件从容器中被移出时,
//它的CWnd被剥离并添加到空闲池,以供稍后复用。
class CWndContainer
{
public:
CWndContainer();
~CWndContainer();
// 附着到 CDialog.
void AttachWnd(CWnd* pWnd);
void DetachWnd();
// 为控件的CWnd们设置资源ID范围
void SetResourceIdRange(UINT minResourceId, UINT maxResourceId);
// 控件管理
void AddControl(CWndControl* pControl);
void AddControls(const std::list& controlList);
void RemoveControl(CWndControl* pControl);
void RemoveAllControls();
//寻找控件
CWndControl* GetControl(const CString& controlName);
CWndControl* GetControl(UINT resourceId);
voidGetControls(std::list& controlList) const;
//消息处理
BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo);
};
当MFC控件在一个对话框中被动态创建(比如,通过使用new然后调用Create()方法,这些控件发出的消息可以通过改写CDialog类中的OnCmdMsg()虚方法来截取。这就是为什么CWndContainer类也定义一个OnCmdMsg()方法。在任何一个附着了CWndContainer实例的CDialog,你可以改写这个对话框的OnCmdMsg()方法并简单转交这个调用给CWndContainer的OnCmdMsg()实现。容器的实现将派发这个消息给存储于容器中的适当的CWndControl。这个CWndControl将发送一个CWndEvent通知到它的每个事件处理程序。
对于任何CWndControl实例,你可以添加一个或更多的事件处理程序来接受通过其相应的MFC控件发送的事件。如下所示,事件处理程序是实现IWndEventHandler接口的目标。
// IWndEventHandler 接口.
事件的属性通过CWndEvent类被封装:
class IWndEventHandler
{
public:
virtualvoid HandleWndEvent(const CWndEvent& ev) = 0;
}; // CWndEvent 类.
使用动态UI类
class CWndEvent
{
public:
//构造器/析构器。
CWndEvent(CWndControl* sender, const CString& text);
~CWndEvent();
//公共方法。
CWndControl* GetSender() const;
CStringGetText() const;
voidAddProperty(const CString& name, const CString& value);
boolGetProperty(const CString& name, CString& value) const;
};
下面的代码示例显示了如何为一个CDialog类添加动态UI支持。在此例中,我们简单添加了一个"Hello World!"按钮到一个对话框上。当这个按钮被按下,一个消息框就显示出来,如下面的截图所示:对话框相应改变首先是包含文件://文件名:MyDlg.h
还有就是下面的对话框源文件的相应改变:
...
#include "WndEvent.h"
//转交声明
class CWndContainer;
class CWndButton;
// CMyDlg 类.
class CMyDlg : public CDialog, public IWndEventHandler
{
DECLARE_DYNAMIC(CMyDlg)
public:
CMyDlg(CWnd* pParent = NULL);
virtual ~CMyDlg();
// IWndEventHandler 改写.
virtualvoid 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;
...
};
...// Filename: MyDlg.cpp
控件皮肤层(Surface Layer)
#include "stdafx.h"
#include "MyDlg.h"
#include "WndContainer.h"
#include "WndControl.h"
...
CMyDlg::CMyDlg(CWnd* pParent /*=NULL*/)
: 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();
//创建一个CWndButton并设置其属性
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;// 返回TRUE除非你设置焦点到一个控件
// 异常:OCX属性页应该返回FALSE
}
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"));
}
}
...
对话框示例是相当简单的,它展示了如何动态地创建一个UI。然而,为了证明资源使用方面从空闲池机制获得的好处,我们需要一个在运行时从容器添加或移除CWndControl实例的方法。最好的说明就是使用一个控件被分成小组(但是一次只有一组控件可以被显示)场景,并且分组的选择这里有一个机制(比如使用一个树视图或一个标签控件)。最后,我添加了另一个层的类:它实现包含可以通过XML被定义的内容的一个“控件窗体”。我用这一组类的主要目的是展示给一个非常特别的UI场景所带来的资源节约。下面简要描述一下控件皮肤类:
CTreeWnd: 一个树控件的CWnd包装类。用来实现在控件窗体中的树视图;
CListWnd: 一个列表控件的CWnd包装类。用来在控件窗体中实现事件区域;
CControlDlg: 这就是使用CWndContainer实例的对话框类。它就是CWnd控件被创建、显示或隐藏所在的实际控件皮肤;
CMarkup: 来自Ben Bryant文章的XML 析构器类。这是一个易用的没有额外依赖的类,它只包括两个源文件(release 6.5 Lite version);
CControlGroup: 代表一个“控件组”,它与文件系统中的一个文件夹相似。一个控件组可以包含其它组,并且也可能包含控件(这里的控件就好像文件系统中的文件);
CControlXml: 这是一个使用CMarkup来解析XML文件和生成控件组和控件实例的XML引擎;
ControlWnd: 一个继承于CWnd的类,它实现一个包括一个左手边上的树视图、在右边的内容控件和一个小的来展示事件处理的事件窗体。这是TestFreePool 演示应用程序用到的顶层类。
TestFreePool应用程序
该演示项目(TestFreePool)是一个MDI应用程序,我是用Visual Studio开始生成的。该应用程序只允许你打开为MDI子窗体而定义UI内容的XML文件。在每个子窗体内部,你可以访问一个包含选项:"Show CWnd Count"的上下文菜单。这个功能计算窗体在CChildView实例层次上实际使用的CWnd对象(当做一个资源使用的粗略估计)。CChildView由Visual Studio生成并且它是以控件皮肤层整合MDI应用程序代码的基本点。下面的截屏显示了演示项目是如何构成的。
本文提供下载的zip文件包括TestFreePool应用程序的一个release版本。如果你希望自己创建演示项目,请注意由于许可限制的原因我已经从zip文件中剔除了两个源文件:Markup.h和Markup.cpp。请首先从CMarkup文章下载该源代码,并在使用Visual Studio生成解决方案之前将Markup.h和Markup.cpp文件置入 TestFreePool 项目文件夹中去。如果你使用VS 2005来转换并生成演示项目,你可能会因为Markup.cpp的 Line 725而碰到一个编译器错误C2440。为了解决之,你可以只添加一个适当的强制类型转换(_TCHAR *)(译注:即改为if ( (pFound=(_TCHAR *)_tcschr(pFind,cSource)) != NULL ) 就可)以避免这个错误。
下面的图展示了在演示应用程序中的每个MDI子窗体的窗体继承图表。
XML 文件
在TestFreePool文件夹中,有三个可以由演示应用程序打开的示例XML文件。下表描述了每个文件并且也给出了一个关于使用空闲池机制获得的资源节约指标(基于CWnd统计总数)。XML格式选择是相当武断的-它主要允许你定义每组可以包含零个或更多子组和零或更多控件的一个控件组层次。
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 |
注意为Example1.xml计算的CWnd最大数可能会随着你系统上配置的Internet Explorer而变化(因为其中一个支持控件是Microsoft WebBrowser2 ActiveX 控件)。下面是演示应用程序中载入Example2.xml文件时的截图。
总结
本文的目的是演示如何动态创建UI同时在特定场景下最小化资源使用。开发出的代码是为了阐明这个概念而不是为了一个一般的或完整的XML表格库,等等。比如,现在只有一些有限的控件和属性被支持,并且事件处理机制是非常简单的。加入XML支持是作为一个方便的说明和测试的方法和但不是我要本文要表达的重点。虽然如果你能将其改编以适应你自己的特定应用程序需要,源代码对你来说将可能更有用。比如,你可能想为更多的MFC控件或者甚至你自己的自定义控件添加支持。在演示项目文件夹中有一个文本文件,它概述了添加一个新的控件支持的步骤。