MFC程式員的WTL指南: Part VIII - 屬性頁面與嚮導
原作 :Michael Dunn [英文原文]
翻譯 :Orbit(桔皮幹了) [www.winmsg.com]
下載示範程式碼
本章內容
- 介紹
- WTL 的屬性工作表類
- WTL 的屬性頁面類
- CPropertyPageWindow 的方法
- CPropertyPageImpl 的方法
- 處理通知訊息
- 建立一個屬性工作表
- 最簡單的屬性工作表
- 建立一個有用的屬性頁面
- 建立一個更好的屬性工作表類
- 建立嚮導樣式的屬性工作表
- 其他的介面考慮
- 繼續
- 修改記錄
介紹
甚至在成為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: 文章第一次發布。