程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> MFC程序員WTL指南(9)屬性頁與向導

MFC程序員WTL指南(9)屬性頁與向導

編輯:關於VC++

介紹

甚至在成為Windows 95的通用控件之前,使用屬性表來表示一些選項就已經成為一種很流行的方式。向導模式的屬性表通常用來引導用戶安裝軟件或完成其他復雜的工作。WTL對這兩種方式的屬性表都提供了很好的支持,可以使用前面介紹的與對話框相關的特性,如DDX和DDV。在本章我將演示如何創建一個基本的屬性表和向導,如何處理屬性頁發送的通知消息和事件。

WTL 的屬性表類

實現一個屬性表需要CPropertySheetWindow和CPropertySheetImpl兩個類聯合使用,它們都定義在atldlgs.h頭文件中。CPropertySheetWindow類是一個窗口接口類(也就是說是一個CWindow派生類),CPropertySheetImpl有消息映射鏈和窗口的完整實現,這和ATL的基本窗口類相似,它需要CWindow和CWindowImpl兩個類聯合使用。

CPropertySheetWindow類封裝了對各種PSM_* 消息的處理,例如,SetActivePageByID()封裝了PSM_SETCURSELID消息。CPropertySheetImpl類管理一個PROPSHEETHEADER結構和一個HPROPSHEETPAGE類型的數組,CPropertySheetImpl類還提供了

一些方法用來填充PROPSHEETHEADER結構,添加或刪除屬性頁,你也可以使用m_psh成員變量直接操作PROPSHEETHEADER結構。

最後,CPropertySheet類是CPropertySheetImpl類的一個特例,你可以直接使用它而不需要定制整個屬性表。

CPropertySheetImpl 的方法

下面是CPropertySheetImpl類的一些重要方法。由於許多方法僅僅是對窗口消息的封裝,所以就不在這裡列出,你可以查看atldlgs.h中完整的函數清單。

CPropertySheetImpl(_U_STRINGorID title = (LPCTSTR) NULL,
          UINT uStartPage = 0, HWND hWndParent = NULL)

CPropertySheetImpl類的構造函數允許你使用一些常用的屬性(默認值),所以就不需要在調用其他的方法設置它們。title指定顯示在屬性表的標題欄的文字,_U_STRINGorID是一個WTL的工具類,它可以自動轉換LPCTSTR和資源ID,例如,下面的兩行代碼都是正確的:

CPropertySheetImpl mySheet ( IDS_SHEET_TITLE );
  CPropertySheetImpl mySheet ( _T("My prop sheet") );

IDS_SHEET_TITLE 是字符串的ID。 uStartPage 是屬性表啟動時激活的屬性頁,是一個從0開始的索引。hWndParent 是屬性表的父窗口的句柄。

BOOL AddPage(HPROPSHEETPAGE hPage)

BOOL AddPage(LPCPROPSHEETPAGE pPage)

添加一個屬性頁。如果這個屬性頁已經創建了,你可以使用第一個重載函數,使用屬性頁的句柄(HPROPSHEETPAGE)作為參數。通常是使用第二個重載函數,使用這個重載函數只需設置一個PROPSHEETPAGE數據結構(後面會講到,它和CPropertyPageImpl一起協同工作),CPropertySheetImpl會為你創建並管理這個屬性頁。

BOOL RemovePage(HPROPSHEETPAGE hPage)

BOOL RemovePage(int nPageIndex)

移除一個屬性頁,可以使用屬性頁的句柄或索引。

BOOL SetActivePage(HPROPSHEETPAGE hPage)

BOOL SetActivePage(int nPageIndex)

設置屬性表的活動頁面。可以使用屬性頁的句柄或索引。你可以在屬性表創建(顯示)之前使用這個方法動態的設置處於激活的屬性頁。

void SetTitle(LPCTSTR lpszText, UINT nStyle = 0)

使之屬性表窗口的標題文字。nStyle可以是0或PSH_PROPTITLE,如果是PSH_PROPTITLE,則屬性表就具有PSH_PROPTITLE樣式,這樣系統會在你通過lpszText參數指定的窗口標題前添加字符串“Properties for”。

void SetWizardMode()

設置PSH_WIZARD樣式,將屬性表改稱向導模式,這個函數必須在屬性表顯示之前調用。

void EnableHelp()

設置PSH_HASHELP樣式,將在屬性表中添加幫助按鈕。需要注意的是你還要在每個屬性頁中使幫助按鈕可用並提供幫助才能使之生效。

INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow())

創建並顯示一個模式的屬性表,返回正值表示操作成功,有關PropertySheet() API的幫助文檔有有關返回值的詳細解釋,如果發生錯誤,屬性表無法創建,DoModal()返回-1。

HWND Create(HWND hWndParent = NULL)

創建並顯示一個無模式的屬性表,返回值是窗口的句柄,如果發生錯誤,屬性表無法創建,Create()返回NULL。

WTL 的屬性頁類

WTL對屬性頁的封裝類與屬性表的封裝類相似,有一個窗口接口類 CPropertyPageWindow 和一個實現類 CPropertyPageImpl 。CPropertyPageWindow 很小,包含最常用的需要在作為父窗口的屬性表中調用的方法。

CPropertyPageImpl 是從 CDialogImplBaseT派生,由於屬性頁是從對話框資源中創建的,這就意味著所有可以在對話框中使用的WTL的特性都可以在屬性頁中使用,如DDX和DDV。CPropertyPageImpl 有兩個主要作用:管理一個PROPSHEETPAGE數據結構(保存在成員變量m_psp中),處理所有PSN_開頭的通知消息。對於很簡單的屬性頁可以直接使用CPropertyPage類,這個類只適合與用戶沒有任何交互的屬性頁,例如“關於”頁面或者向導中的介紹頁面

也可以創建含有ActiveX控件的屬性頁。首先,這需要在stdafx.h文件中添加對atlhost.h的包含,還要使用CAxPropertyPageImpl代替CPropertyPageImpl。對於簡單的頁面可以使用CAxPropertyPage代替CPropertyPage。

CPropertyPageImpl 的方法

CPropertyPageImpl 管理著一個 PROPSHEETPAGE 結構,也就是公有成員 m_psp。CPropertyPageImpl還重載了PROPSHEETPAGE*操作符,所以你可以將CPropertyPageImpl傳遞給需要LPPROPSHEETPAGE 或 LPCPROPSHEETPAGE 類型的參數的方法,例如CPropertySheetImpl::AddPage()。

CPropertyPageImpl的構造函數允許你設置頁面的標題,標題通常顯示在頁面的Tab標簽上:

CPropertyPageImpl(_U_STRINGorID title = (LPCTSTR) NULL)

如果你不想讓屬性表創建屬性頁面而是想手工創建頁面,你可以調用Create():

HPROPSHEETPAGE Create()

Create() 只是調用用m_psp做參數調用了 CreatePropertySheetPage() 。如果你向一個已經創建的屬性表添加屬性頁或者向另一個不在控制的屬性表添加屬性頁(例如,處理系統Shell擴展的屬性表),那就只需要調用Create()函數。

下面的三個方法用於設置屬性頁的各種標題文本:

void SetTitle(_U_STRINGorID title)

void SetHeaderTitle(LPCTSTR lpstrHeaderTitle)

void SetHeaderSubTitle(LPCTSTR lpstrHeaderSubTitle)

第一個方法改變頁面標簽的文字,另外幾個用來設置Wizard97樣式的向導中屬性頁頂部的文字。

void EnableHelp()

設置m_psp中的PSP_HASHELP標志,當本頁面激活時使屬性表的幫助按鈕可用。

處理通知消息

CPropertyPageImpl有一個消息映射處理WM_NOTIFY。如果通知代碼是PSN_*的值,OnNotify()就會調用相應的通知處理函數。這使用了編譯階段虛函數機制,從而使得派生類可以很容易的重載這些處理函數。

由於WTL 3和WTL 7設計的改變,從而存在兩套不同的通知處理機制。在WTL 3中通知處理函數返回的值與PSN_*消息的返回值不同,例如,WTL 3是這樣處理PSN_WIZFINISH的:

case PSN_WIZFINISH:

lResult = !pT->OnWizardFinish();

break;

OnWizardFinish()期望返回TRUE結束向導,FALSE阻止關閉向導。這個方法很簡陋,但是IE5的通用控件對PSN_WIZFINISH處理的返回值添加了新解釋,他返回需要獲得焦點的窗口的句柄。WTL 3的程序將不能使用這個特性,因為它對所有非0的返回值都做相同的處理。

在WTL 7中,OnNotify() 沒有改變 PSN_* 消息的返回值,處理函數返回任何文檔中規定的合法數值和正確的行為。當然,為了向前兼容,WTL 3 仍然使用當前默認的工作方式,要使用WTL 7的消息處理方式,你必須在中including atldlgs.h一行之前添加一行定義:

#define _WTL_NEW_PAGE_NOTIFY_HANDLERS

編寫新的代碼沒有理由不使用WTL 7的消息處理函數,所以這裡就不介紹WTL 3的消息處理方式。

CPropertyPageImpl 為所有消息提供了默認的通知消息處理函數,你可以重載與你的程序有關的消息處理函數完成特殊的操作。默認的消息處理函數和相應的行為如下:

int OnSetActive() - 允許頁面成為激活狀態

BOOL OnKillActive() - 允許頁面成為非激活狀態

int OnApply() - 返回 PSNRET_NOERROR 表示應用操作成功完成

void OnReset() - 無相應的動作

BOOL OnQueryCancel() - 允許取消操作

int OnWizardBack() - 返回到前一個頁面

int OnWizardNext() - 進行到下一個頁面

INT_PTR OnWizardFinish() - 允許向導結束

void OnHelp() - 無相應的動作

BOOL OnGetObject(LPNMOBJECTNOTIFY lpObjectNotify) - 無相應的動作

int OnTranslateAccelerator(LPMSG lpMsg) - 返回 PSNRET_NOERROR 表示消息沒有被處理

HWND OnQueryInitialFocus(HWND hWndFocus) - 返回 NULL 表示將按Tab Order順序的第一個控件設為焦點狀態

創建一個屬性表

關於這些類的解釋就全部講完了,現在需要一個例子程序演示如何使用它們。本章的例子工程是一個簡單的SDI程序,它在客戶區顯示一幅圖片並使用一總顏色填充背景,使用的圖片和顏色可以通過一個選項對話框(一個屬性表)來設置,還有一個向導(稍後會介紹)。

最簡單的屬性表

首先用WTL的向導創建一個SDI工程,然後為關於對話框添加一個屬性表。首先改變向導創建的關於對話框樣式,使它用起來像個屬性頁。

第一步就是去除OK按鈕,因為屬性表不希望屬性頁自己關閉。在Style Tab中,將對話框樣式改為Child,Thin Border,選擇Title Bar,在More Styles tab,選擇Disabled。

第二步(也是最後一步)是在OnAppAbout()的處理函數中創建一個屬性表,我們使用非定制的CPropertySheet 和 CPropertyPage類:

LRESULT CMainFrame::OnAppAbout(...)
{
CPropertySheet sheet ( _T("About PSheets") );
CPropertyPage<IDD_ABOUTBOX> pgAbout;
   sheet.AddPage ( pgAbout );
   sheet.DoModal();
   return 0;
}

結果看起來向下面這樣:

創建一個有用的屬性頁

並不是每一個屬性表中的每一個屬性頁都像關於對話框這麼簡單,大多數屬性頁需要使用CPropertyPageImpl的派生類,所以我們現在就看一個這樣的類。我們創建了一個新的屬性頁用來設置客戶區背景顯示的圖片,它是這個樣子的:

這個對話框的樣式和關於頁面相同,我們需要一個新類來和這個屬性頁協同工作,我們將其命名為CBackgroundOptsPage。這個類是從CPropertyPageImpl類派生的,它有一個CWinDataExchange來支持DDX。

class CBackgroundOptsPage :
   public CPropertyPageImpl<CBackgroundOptsPage>,
   public CWinDataExchange<CBackgroundOptsPage>
{
public:
   enum { IDD = IDD_BACKGROUND_OPTS };
   // Construction
   CBackgroundOptsPage();
   ~CBackgroundOptsPage();
   // Maps
   BEGIN_MSG_MAP(CBackgroundOptsPage)
     MSG_WM_INITDIALOG(OnInitDialog)
     CHAIN_MSG_MAP(CPropertyPageImpl<CBackgroundOptsPage>)
   END_MSG_MAP()
   BEGIN_DDX_MAP(CBackgroundOptsPage)
     DDX_RADIO(IDC_BLUE, m_nColor)
     DDX_RADIO(IDC_ALYSON, m_nPicture)
   END_DDX_MAP()
   // Message handlers
   BOOL OnInitDialog ( HWND hwndFocus, LPARAM lParam );
   // Property page notification handlers
   int OnApply();
   // DDX variables
   int m_nColor, m_nPicture;
};

關於這個類需要注意幾點:

  • 有一個名為IDD的公有成員將對話框於資源聯系起來。
  • 消息映射鏈和CDialogImpl相似。
  • 消息映射鏈將消息鏈入CPropertyPageImpl,從而使我們能夠處理與屬性表相關的通知消息。
  • 有一個OnApply()處理函數在單擊屬性表中的OK按鈕時保存用戶的選擇。

OnApply() 非常簡單,它調用 DoDataExchange() 更新 DDX 變量,然後返回一個代碼標識是否可以關閉這個屬性表:

int CBackgroundOptsPage::OnApply()
{
   return DoDataExchange(true) ? PSNRET_NOERROR : PSNRET_INVALID;
}

我們還要在主窗口添加一個Tools|Options菜單來打開屬性表,這個菜單的處理函數創建一個屬性表,但是添加了一個新屬性頁CBackgroundOptsPage。

void CMainFrame::OnOptions ( UINT uCode, int nID, HWND hwndCtrl )
{
CPropertySheet sheet ( _T("PSheets Options"), 0 );
CBackgroundOptsPage pgBackground;
CPropertyPage<IDD_ABOUTBOX> pgAbout;
   pgBackground.m_nColor = m_view.m_nColor;
   pgBackground.m_nPicture = m_view.m_nPicture;
   sheet.m_psh.dwFlags |= PSH_NOAPPLYNOW;
   sheet.AddPage ( pgBackground );
   sheet.AddPage ( pgAbout );
   if ( IDOK == sheet.DoModal() )
     m_view.SetBackgroundOptions ( pgBackground.m_nColor,
                    pgBackground.m_nPicture );
}

屬性表的構造函數的第二個參數是0,表示將索引是0的頁面初始是可見的,你可以將其設為1,使得屬性表第一次顯示時顯示關於頁面。既然是演示代碼,我就偷個懶,使用一個公有變量與CBackgroundOptsPage屬性頁的radio button建立關聯,在主窗口中直接為其賦初始值,當用戶單擊屬性表的OK按鈕時在將其讀出來。

如果用戶點擊OK按鈕,DoModal()發揮IDOK,我們通知視圖窗口使用新的圖片和背景顏色。下面是幾個屏幕截圖顯示幾個不同的樣式的視圖:

創建一個更好的屬性表類

在OnOptions()中創建屬性表是個好主意,但是在這裡使用很多初始化代碼卻非常糟糕,這不是CMainFrame應該做得事情。更好的方法是從CPropertySheetImpl派生一個新類,在這個類中完成這些任務。

#include "BackgroundOptsPage.h"
class CAppPropertySheet : public CPropertySheetImpl<CAppPropertySheet>
{
public:
   // Construction
   CAppPropertySheet ( _U_STRINGorID title = (LPCTSTR) NULL,
             UINT uStartPage = 0, HWND hWndParent = NULL );
   // Maps
   BEGIN_MSG_MAP(CAppPropertySheet)
     CHAIN_MSG_MAP(CPropertySheetImpl<CAppPropertySheet>)
   END_MSG_MAP()
   // Property pages
   CBackgroundOptsPage     m_pgBackground;
   CPropertyPage<IDD_ABOUTBOX> m_pgAbout;
};

我們使用這個類封裝屬性表中各個屬性頁的細節,將初始化代碼移到屬性表內部完成,構造函數完成添加頁面,並設置其他必需的標志:

CAppPropertySheet::CAppPropertySheet ( _U_STRINGorID title, UINT uStartPage,
                    HWND hWndParent ) :
   CPropertySheetImpl<CAppPropertySheet> ( title, uStartPage, hWndParent )
{
   m_psh.dwFlags |= PSH_NOAPPLYNOW;
   AddPage ( m_pgBackground );
   AddPage ( m_pgAbout );
}

這樣一來,OnOptions()處理函數就變得簡單了一些:

void CMainFrame::OnOptions ( UINT uCode, int nID, HWND hwndCtrl )
{
CAppPropertySheet sheet ( _T("PSheets Options"), 0 );
   sheet.m_pgBackground.m_nColor = m_view.m_nColor;
   sheet.m_pgBackground.m_nPicture = m_view.m_nPicture;
   if ( IDOK == sheet.DoModal() )
     m_view.SetBackgroundOptions ( sheet.m_pgBackground.m_nColor,
                    sheet.m_pgBackground.m_nPicture );
}
創建一個向導樣式的屬性表

創建一個向導和創建一個屬性表很相似,這並不奇怪,只需稍做修改添加“上一步”和“下一步”按鈕就行了。和MFC一樣,你需要重載OnSetActive()函數並調用SetWizardButtons()使相應的按鈕可用。我們先從一個簡單的介紹頁面開始,它的ID是IDD_WIZARD_INTRO:

注意這個頁面沒有標題欄文字,因為向導中的所有的頁面通常都有相同的標題,我更願意在CPropertySheetImpl的構造函數中設置這些文字,然後每個頁面使用這個字符串資源。這就是為什麼我只需要改變一個字符串就能改變所有頁面標題文字的原因。

關於這個頁面的實現代碼在CWizIntroPage類中:

class CWizIntroPage : public CPropertyPageImpl<CWizIntroPage>
{
public:
   enum { IDD = IDD_WIZARD_INTRO };
   // Construction
   CWizIntroPage();
   // Maps
   BEGIN_MSG_MAP(COptionsWizard)
     CHAIN_MSG_MAP(CPropertyPageImpl<CWizIntroPage>)
   END_MSG_MAP()
   // Notification handlers
   int OnSetActive();
};

構造函數使用(引用)一個字符串資源ID來設置頁面的文字:

CWizIntroPage::CWizIntroPage() :
   CPropertyPageImpl<CWizIntroPage>( IDS_WIZARD_TITLE )
{
}

當這個頁面激活時,字符串IDS_WIZARD_TITLE ("PSheets Options Wizard")將出現在向導的標題欄。OnSetActive()僅僅使“下一步”按鈕可用:

int CWizIntroPage::OnSetActive()
{
   SetWizardButtons ( PSWIZB_NEXT );
   return 0;
}

為了實現一個向導,我們需要創建一個類COptionsWizard,還要在主窗口添加菜單Tools|Wizard。COptionsWizard類的構造函數和CAppPropertySheet類的構造函數一樣,只是設置必要的樣式標志和添加頁面。

class COptionsWizard : public CPropertySheetImpl<COptionsWizard>
{
public:
   // Construction
   COptionsWizard ( HWND hWndParent = NULL );
   // Maps
   BEGIN_MSG_MAP(COptionsWizard)
     CHAIN_MSG_MAP(CPropertySheetImpl<COptionsWizard>)
   END_MSG_MAP()
   // Property pages
   CWizIntroPage m_pgIntro;
};
COptionsWizard::COptionsWizard ( HWND hWndParent ) :
   CPropertySheetImpl<COptionsWizard> ( 0U, 0, hWndParent )
{
   SetWizardMode();
   AddPage ( m_pgIntro );
}

CMainFrame類的Tools|Wizard菜單處理函數是這個樣子:

void CMainFrame::OnOptionsWizard ( UINT uCode, int nID, HWND hwndCtrl )
{
COptionsWizard wizard;
   wizard.DoModal();
}

這就是向導的效果:

添加更多的屬性頁,使用DDV

為了使這個向導能夠有點用處,我們要為其添加一個設置視圖背景顏色的頁面。這個頁面還將有一個checkbox演示如何處理DDV驗證失敗並阻止向導進行到下一頁。下面就是新的頁面,ID是IDD_WIZARD_BKCOLOR:

這個類的實現代碼在CWizBkColorPage類中,下面是相關的部分代碼

class CWizBkColorPage :
   public CPropertyPageImpl<CWizBkColorPage>,
   public CWinDataExchange<CWizBkColorPage>
{
public:
   // some stuff removed for brevity...
   BEGIN_DDX_MAP(CWizBkColorPage)
     DDX_RADIO(IDC_BLUE, m_nColor)
     DDX_CHECK(IDC_FAIL_DDV, m_bFailDDV)
   END_DDX_MAP()
   // Notification handlers
   int OnSetActive();
   BOOL OnKillActive();
   // DDX vars
   int m_nColor;
protected:
   int m_bFailDDV;
};

OnSetActive()的工作和前面的介紹頁面相同,它使“上一步”和“下一步”按鈕可用。OnKillActive()是個新的處理函數,它觸發DDV,然後檢查m_bFailDDV的值,如果是TRUE就表示checkbox處於選中狀態,OnKillActive()將阻止向導進行到下一頁。

int CWizBkColorPage::OnSetActive()
{
   SetWizardButtons ( PSWIZB_BACK | PSWIZB_NEXT );
   return 0;
}
int CWizBkColorPage::OnKillActive()
{
   if ( !DoDataExchange(true) )
     return TRUE;  // prevent deactivation
   if ( m_bFailDDV )
     {
     MessageBox (
      _T("Error box checked, wizard will stay on this page."),
      _T("PSheets"), MB_ICONERROR );
     return TRUE;  // prevent deactivation
     }
   return FALSE;    // allow deactivation
}

需要注意的是OnKillActive()中做的事情也可以在OnWizardNext()中完成,因為這兩個處理函數都可以使向導維持在當前頁面。它們的不同之處在於OnKillActive()在用戶單擊“上一步”和“下一步”按鈕時被調用,而OnWizardNext()只是在用戶單擊“下一步”按鈕時被調用。OnWizardNext()還被用來完成其它目的,比如,它可以直接將向導引導到指定的頁面而不是按順序的下一頁。

例子工程的向導還有另外兩個頁面,CWizBkPicturePage 和 CWizFinishPage,由於它們和前面的兩個頁面相似,我就不再詳細介紹它們,想了解它們的細節可以查看源代碼。

其他的界面考慮 置中一個屬性表

屬性頁和向導的默認位置是出現在父窗口的左上角:

這看起來有點不爽,還好有方法可以補救。第一種方法是重載CPropertySheetImpl::PropSheetCallback()函數,在這個函數中將屬性表置中。PropSheetCallback()是MSDN中介紹的PropSheetProc()的回調函數,操作系統在屬性表創建時調用這個函數,WTL也是利用這個時間子類化屬性表窗口的。所以我們的第一種嘗試是:

class CAppPropertySheet : public CPropertySheetImpl<CAppPropertySheet>
{
//...
   static int CALLBACK PropSheetCallback(HWND hWnd, UINT uMsg, LPARAM lParam)
   {
   int nRet = CPropertySheetImpl<CAppPropertySheet>::PropSheetCallback (
                             hWnd, uMsg, lParam );
     if ( PSCB_INITIALIZED == uMsg )
       {
       // center sheet... somehow?
       }
     return nRet;
   }
};

正如你看到的,我們遇到了棘手的問題。PropSheetCallback()是一個靜態方法,不能使用this指針訪問屬性表窗口。那將這些代碼從CPropertySheetImpl::PropSheetCallback()中拷貝出來,然後添加我們自己的方法行不行呢?撇開剛才將代碼和特定版本的WTL聯系在一起的方法(這已經被證明不是各好方法),現在代碼應該是這樣的:

class CAppPropertySheet : public CPropertySheetImpl<CAppPropertySheet>
{
//...
   static int CALLBACK PropSheetCallback(HWND hWnd, UINT uMsg, LPARAM)
   {
     if(uMsg == PSCB_INITIALIZED)
     {
       // Code copied from WTL and tweaked to use CAppPropertySheet
       // instead of T:
       ATLASSERT(hWnd != NULL);
       CAppPropertySheet* pT = (CAppPropertySheet*)
                     _Module.ExtractCreateWndData();
       // subclass the sheet window
       pT->SubclassWindow(hWnd);
       // remove page handles array
       pT->_CleanUpPages();
       // Our own code follows:
       pT->CenterWindow ( pT->m_psh.hwndParent );
     }
     return 0;
   }
};

這從理論上講很完美,但是我試過,屬性表的位置並未改變。顯然,通用控件的代碼在我們調用CenterWindow()之後又改變了屬性表窗口的位置。

必須放棄這個將代碼封裝到屬性表類的方法,盡管它是個好的解決方案。我又回到原來的方案,即使用屬性頁窗口和屬性表窗口相互協作是屬性表窗口置中。我添加了一個用戶定義消息UWM_CENTER_SHEET:

#define UWM_CENTER_SHEET WM_APP

CAppPropertySheet 在它的消息映射鏈中處理這個消息:

class CAppPropertySheet : public CPropertySheetImpl<CAppPropertySheet>
{
//...
   BEGIN_MSG_MAP(CAppPropertySheet)
     MESSAGE_HANDLER_EX(UWM_CENTER_SHEET, OnPageInit)
     CHAIN_MSG_MAP(CPropertySheetImpl<CAppPropertySheet>)
   END_MSG_MAP()
   // Message handlers
   LRESULT OnPageInit ( UINT, WPARAM, LPARAM );
protected:
   bool m_bCentered; // set to false in the ctor
};
LRESULT CAppPropertySheet::OnPageInit ( UINT, WPARAM, LPARAM )
{
   if ( !m_bCentered )
     {
     m_bCentered = true;
     CenterWindow ( m_psh.hwndParent );
     }
   return 0;
}

然後,每個屬性頁的OnInitDialog() 方法發送這個消息到屬性表窗口:

BOOL CBackgroundOptsPage::OnInitDialog ( HWND hwndFocus, LPARAM lParam )
{
   GetPropertySheet().SendMessage ( UWM_CENTER_SHEET );
   DoDataExchange(false);
   return TRUE;
}

添加m_bCentered標志確保屬性表窗口只響應收到的第一個UWM_CENTER_SHEET消息。

在屬性頁中添加圖標

如果要使用屬性表和屬性頁的未被成員函數封裝的特性,就需要直接訪問相關的數據結構:CPropertySheetImpl類中的PROPSHEETHEADER類型(結構)成員m_psh和CPropertyPageImpl類中的PROPSHEETPAGE類型(結構)成員m_psp。

例如:為例子中Option屬性表中的Background頁面添加一個圖標,就需要添加一個標志並設置屬性頁的PROPSHEETPAGE結構中的幾個成員:

CBackgroundOptsPage::CBackgroundOptsPage()
{
   m_psp.dwFlags |= PSP_USEICONID;
   m_psp.pszIcon = MAKEINTRESOURCE(IDI_TABICON);
   m_psp.hInstance = _Module.GetResourceInstance();
}

下面是這些代碼的效果:

繼續

我將在第九章介紹WTL的一些工具類,還有GDI對象和通用對話框的包裝類。

修改記錄

September 13, 2003: 文章第一次發布。

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