程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 使用一個CWnd空閒池創建一個動態用戶界面

使用一個CWnd空閒池創建一個動態用戶界面

編輯:關於VC++

介紹

本文提出了一組可以用來動態創建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基類(抽象)
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);
};

可以只使用new操作符通過應用程序代碼創建繼承於CWndControl類的實例。然而,一個CWndFactory類已被提供來允許為CWndControl實例的創建而給定一個類型字符串。該工廠類主要被設計用來允許從XML清單中動態創建控件。

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 接口.
class IWndEventHandler
{
public:
virtualvoid HandleWndEvent(const CWndEvent& ev) = 0;
};

事件的屬性通過CWndEvent類被封裝:

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

使用動態UI類

下面的代碼示例顯示了如何為一個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

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

控件皮膚層(Surface Layer)

對話框示例是相當簡單的,它展示了如何動態地創建一個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控件或者甚至你自己的自定義控件添加支持。在演示項目文件夾中有一個文本文件,它概述了添加一個新的控件支持的步驟。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved