65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 CWnd Free Pool 构建动态 UI

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (37投票s)

2007年1月6日

CPOL

12分钟阅读

viewsIcon

332267

downloadIcon

3250

用于动态构建基于 MFC 的用户界面的类,重点在于最大程度地减少资源使用。

引言

本文介绍了一套可用于动态构建 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 窗体用户控件的集合。因此,实现单个设备窗口所需的 GDI 资源可能很高,当操作员需要同时打开许多这些设备窗口时,这会成为一个限制因素。

第二个例子是选项对话框(例如 VS 2005 中的选项对话框)。这种类型的对话框通常由左侧的树形视图和右侧的一组 UI 控件组成。当树形视图中的选择发生更改时,右侧的控件集会动态更改。这种 UI 场景实际上与第一个选项卡式设备窗口的例子非常相似。主要区别在于选择或分组机制(例如,树形视图选择与选项卡选择)。

CWnd 空闲池

减少选项卡式设备窗口资源需求的一种方法是消除对单独对话框或属性页的需求。这可以通过仅使用单个对话框并实现一种机制来实现,该机制根据当前选择的选项卡隐藏或显示 UI 控件。需要创建相同数量的 UI 控件,但我们节省了所需的对话框数量。

如果我们认识到相同类型的 UI 控件通常存在于多个选项卡中,则可以进一步减少资源使用。换句话说,我们不必在选项卡选择更改时仅隐藏控件,而是可以将隐藏的控件存储在空闲池或缓存中,以便在切换到不同的选项卡时可以重用它们。这使我们能够在选项卡选择之间重用 UI 控件实例。例如,如果一个选项卡使用 `CButton`,第二个选项卡也使用 `CButton`,那么只需要创建一个 `CButton` 实例,并将相同的 UI 实例用于这两个选项卡。使用这种方法,每个设备窗口所需的 UI 控件数量可以显着节省。例如,在最佳情况下,考虑一个具有 10 个参数分组(选项卡)和 200 个参数的设备,其中每个参数由一个滑块控件表示。如果我们还为每个滑块配对一个相应的文本标签控件,那么使用典型的多对话框实现将需要总共 400 个 UI 控件。但是,如果我们重复使用一个选项卡到下一个选项卡的滑块和标签控件,设备窗口最多需要 20 个滑块和 20 个文本标签控件,从而将资源使用量减少了 10 倍。

要实现此重用机制,我们首先定义一个 `CWndFreePool` 类,该类仅跟踪哪些 `CWnd` 实例是空闲且可用的。池中引用的每个 `CWnd` 都与一个指示 UI 控件类型的字符串配对。例如,类型字符串“Button”表示配对的 `CWnd` 实际上是一个 `CButton` 实例(使用 `BS_PUSHBUTTON` 样式创建)。除了内置的 MFC 控件(如 `CButton`)之外,空闲池还可以引用 ActiveX 控件,因为 Visual Studio 可以为派生自 `CWnd` 的 ActiveX 控件生成 MFC 包装类。`CWndFreePool` 类的公共接口如下所示。

// 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);
};

控件类

为了重用 UI 控件实例,我们需要另一种机制来在控件返回到空闲池之前保存其状态,以及在控件再次从池中获取时恢复其状态。为了实现这一点,我们可以定义一个类的层次结构,该层次结构与支持的 MFC 控件类(如 `CButton` 和 `CSliderCtrl`)相对应。此层次结构的基类是 `CWndControl`,其公共接口如下供参考。您可以将这些 `CWndControl` 类视为 MFC 对应项的简单包装器。

// 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);
};

应用程序代码可以通过简单地使用 `new` 运算符来创建 `CWndControl` 派生类的实例。但是,也提供了一个 `CWndFactory` 类,允许根据类型字符串创建 `CWndControl` 实例。此工厂类主要设计用于从 XML 规范动态创建控件。

CWnd 容器

实际的重用逻辑由 `CWndContainer` 类实现。该类是动态 UI 层的心脏,因为它管理对空闲池的更新,使用工厂类,并分派事件。`CWndContainer` 可以被视为一个辅助类,它可以附加到任何 `CDialog` 以添加动态 UI 支持。例如,在 `CDialog` 类中,只需创建一个 `CWndContainer` 实例并将其附加到 `this` 指针。一旦容器附加到对话框,就可以创建 `CWndControl` 实例,然后将其添加到容器中(如下面的代码示例 此处 所示)。

当添加 `CWndControl` 实例时,容器使用其内部空闲池尝试获取相应类型的现有 `CWnd`。如果找到,则从池中删除 `CWnd`,使其可见,然后将 `CWndControl` 的属性应用于此 `CWnd` 实例。另一方面,如果池中没有找到合适的 `CWnd`,容器将使用工厂类创建一个新的 `CWnd` 实例。

当 `CWndControl` 实例从容器中移除时,其关联的 `CWnd` 被分离、隐藏并返回到空闲池以供重用。`CWndContainer` 类的公共接口如下供参考。

// 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);
};

事件处理

当 MFC 控件在对话框中动态创建时(例如,通过使用 `new` 然后调用 `Create()` 方法),来自这些控件的消息可以通过重写 `CDialog` 类中的 `OnCmdMsg()` 虚拟方法来拦截。这就是为什么 `CWndContainer` 类还定义了一个 `OnCmdMsg()` 方法。在任何附加了 `CWndContainer` 实例的 `CDialog` 中,您可以重写对话框的 `OnCmdMsg()` 方法,然后将调用转发给 `CWndContainer` 的 `OnCmdMsg()` 实现。容器的实现会将消息分派到容器内存储的相应 `CWndControl`。然后,此 `CWndControl` 会向其每个事件处理程序发送 `CWndEvent` 通知。

对于任何 `CWndControl` 实例,您可以添加一个或多个事件处理程序,它们将接收来自其相应 MFC 控件的事件。事件处理程序是实现 `IWndEventHandler` 接口的对象,如下所示。

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

事件的属性被 `CWndEvent` 类封装

// 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;
};

使用动态 UI 类

以下代码示例显示了如何向 `CDialog` 类添加动态 UI 支持。在此示例中,我们仅向对话框添加一个“Hello World!”按钮。按下按钮时,将显示一个消息框,如下面的屏幕截图所示。

首先显示对话框的包含文件的相关更改

// 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;
    
    ...
};

...

以下是对话框源文件的相关更改

// 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"));
    }
}

...

控件表面层

对话框示例非常基础,展示了如何动态创建 UI。但是,为了演示空闲池机制的资源使用优势,我们需要一种在运行时将 `CWndControl` 实例添加到容器或从中移除的方法。这最好通过控件分组(一次只能显示一组控件)并且存在分组选择机制(例如,使用树形视图或选项卡控件)的场景来演示。为此,我添加了第二层类,该类实现了一个“控件窗口”,其中包含可以通过 XML 定义的内容。我使用这些类集的主要目标是展示在非常特定的 UI 场景中可以实现的资源节省。控件表面类简要描述如下。

`CTreeWnd`:树形控件的 `CWnd` 包装器。用于在控件窗口中实现树形视图。

`CListWnd`:列表控件的 `CWnd` 包装器。用于在控件窗口中实现事件区域。

`CControlDlg`:这是使用 `CWndContainer` 实例的对话框类。它是实际的控件表面,可以在其中创建、显示或隐藏 `CWnd` 控件。

`CMarkup`:来自 Ben Bryant 的 文章 的 XML 解析器类。这是一个易于使用的类,没有外部依赖项,并且仅包含两个源文件(6.5 Lite 版本)。

`CControlGroup`:表示一个“控件组”,类似于文件系统中的文件夹。控件组可以包含其他组,也可以包含控件(类似于文件系统中的文件)。

`CControlXml`:这是使用 `CMarkup` 解析 XML 文件并生成控件组和控件实例的 XML 引擎。

`CControlWnd`:一个派生自 `CWnd` 的类,它实现了一个窗口,该窗口在左侧有一个树形视图,在右侧有内容控件,还有一个小的事件窗口来演示事件处理。这是 TestFreePool 演示应用程序使用的顶层类。

TestFreePool 应用程序

演示项目(`TestFreePool`)是一个我最初使用 Visual Studio 生成的 MDI 应用程序。该应用程序允许您打开定义 MDI 子窗口 UI 内容的 XML 文件。在每个子窗口中,您可以访问一个包含“**显示 CWnd 计数**”选项的上下文菜单。此函数计算窗口中使用的实际 `CWnd` 对象的总数,从 `CChildView` 实例的级别开始(作为资源使用情况的粗略估计)。`CChildView` 类由 Visual Studio 生成,是 MDI 应用程序代码与控件表面层(即 `CControlWnd`)集成的关键点。下图显示了演示项目的组织方式。

下载的 zip 文件包含 `TestFreePool` 应用程序的发布版本。如果您想自己构建演示项目,请注意,由于许可限制,我已从 zip 文件中**排除了**两个源文件:*Markup.h* 和 *Markup.cpp*。请首先从 `CMarkup` 文章 下载源代码,然后将 *Markup.h* 和 *Markup.cpp* 文件放入 `TestFreePool` 项目文件夹中,然后使用 Visual Studio 进行构建。如果您使用 VS 2005 来转换和构建演示项目,您还可能会遇到 *Markup.cpp* 文件第 725 行的编译错误 C2440。要解决此问题,您只需添加一个适当的转换 `(_TCHAR *)` 以避免该错误。

下图说明了演示应用程序中每个 MDI 子窗口的窗口层次结构。

XML 文件

在 `TestFreePool` 文件夹中,有三个示例 XML 文件可以被演示应用程序打开。下表描述了每个文件,并给出了使用空闲池机制实现的资源节省(基于 `CWnd` 总数)的指示。选择的 XML 格式相当随意 - 它基本上允许您定义一个控件组层次结构,其中每个组可以包含零个或多个子组,以及零个或多个控件。

文件名 描述 最大 CWnd 计数 未使用空闲池的估计 CWnd 计数
Example1.xml 显示了所有支持的 UI 控件类型。 30 41
Example2.xml 显示 12 个控件组,每个组包含 10 个标签和 10 个按钮。 27 259
Example3.xml 显示 VS 2005 选项对话框的 3 页。 30 48

请注意,*Example1.xml* 的最大 `CWnd` 计数可能因您的系统上 Internet Explorer 的配置方式而异(因为支持的控件之一是 Microsoft WebBrowser2 ActiveX 控件)。

下面是 *Example2.xml* 文件在演示应用程序中加载的屏幕截图。

摘要

本文的目的是演示如何动态创建 UI,同时在某些场景下最大限度地减少资源使用。代码是为了说明这个概念而开发的,并不打算成为一个通用的或完整的 XML 表单库等。例如,目前仅支持有限的控件和属性,并且事件处理机制非常简单。XML 支持是为了方便演示和测试而添加的,但并不是我想要呈现的主要内容。如果您能够将源代码改编到您自己的特定应用程序需求,它可能会更有用。例如,您可能希望添加对更多 MFC 控件或您自己的自定义控件的支持。演示项目文件夹中有一个文本文件,概述了添加新控件支持的步骤。

历史

  • 2007 年 1 月 6 日
    • 初始版本。
© . All rights reserved.