原作 :Michael Dunn [英文原文] 翻譯 :Orbit(桔皮干了) [http://www.winmsg.com/cn/orbit.htm] 下載演示程序代碼 本章內容 介紹 甚至在成為Windows 95的通用控件之前,使用屬性表來(lái)表示一些選項就已經(jīng)成為一種很流行的方式。向導模式的屬性表通常用來(lái)引導用戶(hù)安裝軟件或完成其他復雜的工作。WTL對這兩種方式的屬性表都提供了很好的支持,可以使用前面介紹的與對話(huà)框相關(guān)的特性,如DDX和DDV。在本章我將演示如何創(chuàng )建一個(gè)基本的屬性表和向導,如何處理屬性頁(yè)發(fā)送的通知消息和事件。 WTL 的屬性表類(lèi) 實(shí)現一個(gè)屬性表需要CPropertySheetWindow和CPropertySheetImpl兩個(gè)類(lèi)聯(lián)合使用,它們都定義在atldlgs.h頭文件中。CPropertySheetWindow類(lèi)是一個(gè)窗口接口類(lèi)(也就是說(shuō)是一個(gè)CWindow派生類(lèi)),CPropertySheetImpl有消息映射鏈和窗口的完整實(shí)現,這和ATL的基本窗口類(lèi)相似,它需要CWindow和CWindowImpl兩個(gè)類(lèi)聯(lián)合使用。 CPropertySheetWindow類(lèi)封裝了對各種PSM_* 消息的處理,例如,SetActivePageByID()封裝了PSM_SETCURSELID消息。CPropertySheetImpl類(lèi)管理一個(gè)PROPSHEETHEADER結構和一個(gè)HPROPSHEETPAGE類(lèi)型的數組,CPropertySheetImpl類(lèi)還提供了 一些方法用來(lái)填充PROPSHEETHEADER結構,添加或刪除屬性頁(yè),你也可以使用m_psh成員變量直接操作PROPSHEETHEADER結構。 最后,CPropertySheet類(lèi)是CPropertySheetImpl類(lèi)的一個(gè)特例,你可以直接使用它而不需要定制整個(gè)屬性表。 CPropertySheetImpl 的方法 下面是CPropertySheetImpl類(lèi)的一些重要方法。由于許多方法僅僅是對窗口消息的封裝,所以就不在這里列出,你可以查看atldlgs.h中完整的函數清單。
CPropertySheetImpl(_U_STRINGorID title = (LPCTSTR) NULL, UINT uStartPage = 0, HWND hWndParent = NULL) CPropertySheetImpl類(lèi)的構造函數允許你使用一些常用的屬性(默認值),所以就不需要在調用其他的方法設置它們。title指定顯示在屬性表的標題欄的文字,_U_STRINGorID是一個(gè)WTL的工具類(lèi),它可以自動(dòng)轉換LPCTSTR和資源ID,例如,下面的兩行代碼都是正確的:
CPropertySheetImpl mySheet ( IDS_SHEET_TITLE ); CPropertySheetImpl mySheet ( _T("My prop sheet") );IDS_SHEET_TITLE 是字符串的ID。 uStartPage 是屬性表啟動(dòng)時(shí)激活的屬性頁(yè),是一個(gè)從0開(kāi)始的索引。hWndParent 是屬性表的父窗口的句柄。 BOOL AddPage(HPROPSHEETPAGE hPage) BOOL AddPage(LPCPROPSHEETPAGE pPage) 添加一個(gè)屬性頁(yè)。如果這個(gè)屬性頁(yè)已經(jīng)創(chuàng )建了,你可以使用第一個(gè)重載函數,使用屬性頁(yè)的句柄(HPROPSHEETPAGE)作為參數。通常是使用第二個(gè)重載函數,使用這個(gè)重載函數只需設置一個(gè)PROPSHEETPAGE數據結構(后面會(huì )講到,它和CPropertyPageImpl一起協(xié)同工作),CPropertySheetImpl會(huì )為你創(chuàng )建并管理這個(gè)屬性頁(yè)。 BOOL RemovePage(HPROPSHEETPAGE hPage) BOOL RemovePage(int nPageIndex) 移除一個(gè)屬性頁(yè),可以使用屬性頁(yè)的句柄或索引。 BOOL SetActivePage(HPROPSHEETPAGE hPage) BOOL SetActivePage(int nPageIndex) 設置屬性表的活動(dòng)頁(yè)面??梢允褂脤傩皂?yè)的句柄或索引。你可以在屬性表創(chuàng )建(顯示)之前使用這個(gè)方法動(dòng)態(tài)的設置處于激活的屬性頁(yè)。 void SetTitle(LPCTSTR lpszText, UINT nStyle = 0) 使之屬性表窗口的標題文字。nStyle可以是0或PSH_PROPTITLE,如果是PSH_PROPTITLE,則屬性表就具有PSH_PROPTITLE樣式,這樣系統會(huì )在你通過(guò)lpszText參數指定的窗口標題前添加字符串“Properties for”。 void SetWizardMode() 設置PSH_WIZARD樣式,將屬性表改稱(chēng)向導模式,這個(gè)函數必須在屬性表顯示之前調用。 void EnableHelp() 設置PSH_HASHELP樣式,將在屬性表中添加幫助按鈕。需要注意的是你還要在每個(gè)屬性頁(yè)中使幫助按鈕可用并提供幫助才能使之生效。 INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow()) 創(chuàng )建并顯示一個(gè)模式的屬性表,返回正值表示操作成功,有關(guān)PropertySheet() API的幫助文檔有有關(guān)返回值的詳細解釋?zhuān)绻l(fā)生錯誤,屬性表無(wú)法創(chuàng )建,DoModal()返回-1。 HWND Create(HWND hWndParent = NULL) 創(chuàng )建并顯示一個(gè)無(wú)模式的屬性表,返回值是窗口的句柄,如果發(fā)生錯誤,屬性表無(wú)法創(chuàng )建,Create()返回NULL。 WTL 的屬性頁(yè)類(lèi) WTL對屬性頁(yè)的封裝類(lèi)與屬性表的封裝類(lèi)相似,有一個(gè)窗口接口類(lèi) CPropertyPageWindow 和一個(gè)實(shí)現類(lèi) CPropertyPageImpl 。CPropertyPageWindow 很小,包含最常用的需要在作為父窗口的屬性表中調用的方法。 CPropertyPageImpl 是從 CDialogImplBaseT派生,由于屬性頁(yè)是從對話(huà)框資源中創(chuàng )建的,這就意味著(zhù)所有可以在對話(huà)框中使用的WTL的特性都可以在屬性頁(yè)中使用,如DDX和DDV。CPropertyPageImpl 有兩個(gè)主要作用:管理一個(gè)PROPSHEETPAGE數據結構(保存在成員變量m_psp中),處理所有PSN_開(kāi)頭的通知消息。對于很簡(jiǎn)單的屬性頁(yè)可以直接使用CPropertyPage類(lèi),這個(gè)類(lèi)只適合與用戶(hù)沒(méi)有任何交互的屬性頁(yè),例如“關(guān)于”頁(yè)面或者向導中的介紹頁(yè)面 也可以創(chuàng )建含有ActiveX控件的屬性頁(yè)。首先,這需要在stdafx.h文件中添加對atlhost.h的包含,還要使用CAxPropertyPageImpl代替CPropertyPageImpl。對于簡(jiǎn)單的頁(yè)面可以使用CAxPropertyPage代替CPropertyPage。 CPropertyPageImpl 的方法 CPropertyPageImpl 管理著(zhù)一個(gè) PROPSHEETPAGE 結構,也就是公有成員 m_psp。CPropertyPageImpl還重載了PROPSHEETPAGE*操作符,所以你可以將CPropertyPageImpl傳遞給需要LPPROPSHEETPAGE 或 LPCPROPSHEETPAGE 類(lèi)型的參數的方法,例如CPropertySheetImpl::AddPage()。 CPropertyPageImpl的構造函數允許你設置頁(yè)面的標題,標題通常顯示在頁(yè)面的Tab標簽上: CPropertyPageImpl(_U_STRINGorID title = (LPCTSTR) NULL) 如果你不想讓屬性表創(chuàng )建屬性頁(yè)面而是想手工創(chuàng )建頁(yè)面,你可以調用Create(): HPROPSHEETPAGE Create() Create() 只是調用用m_psp做參數調用了 CreatePropertySheetPage() 。如果你向一個(gè)已經(jīng)創(chuàng )建的屬性表添加屬性頁(yè)或者向另一個(gè)不在控制的屬性表添加屬性頁(yè)(例如,處理系統Shell擴展的屬性表),那就只需要調用Create()函數。 下面的三個(gè)方法用于設置屬性頁(yè)的各種標題文本: void SetTitle(_U_STRINGorID title) void SetHeaderTitle(LPCTSTR lpstrHeaderTitle) void SetHeaderSubTitle(LPCTSTR lpstrHeaderSubTitle) 第一個(gè)方法改變頁(yè)面標簽的文字,另外幾個(gè)用來(lái)設置Wizard97樣式的向導中屬性頁(yè)頂部的文字。 void EnableHelp() 設置m_psp中的PSP_HASHELP標志,當本頁(yè)面激活時(shí)使屬性表的幫助按鈕可用。 處理通知消息 CPropertyPageImpl有一個(gè)消息映射處理WM_NOTIFY。如果通知代碼是PSN_*的值,OnNotify()就會(huì )調用相應的通知處理函數。這使用了編譯階段虛函數機制,從而使得派生類(lèi)可以很容易的重載這些處理函數。 由于WTL 3和WTL 7設計的改變,從而存在兩套不同的通知處理機制。在WTL 3中通知處理函數返回的值與PSN_*消息的返回值不同,例如,WTL 3是這樣處理PSN_WIZFINISH的:
case PSN_WIZFINISH: lResult = !pT->OnWizardFinish(); break; OnWizardFinish()期望返回TRUE結束向導,FALSE阻止關(guān)閉向導。這個(gè)方法很簡(jiǎn)陋,但是IE5的通用控件對PSN_WIZFINISH處理的返回值添加了新解釋?zhuān)祷匦枰@得焦點(diǎn)的窗口的句柄。WTL 3的程序將不能使用這個(gè)特性,因為它對所有非0的返回值都做相同的處理。 在WTL 7中,OnNotify() 沒(méi)有改變 PSN_* 消息的返回值,處理函數返回任何文檔中規定的合法數值和正確的行為。當然,為了向前兼容,WTL 3 仍然使用當前默認的工作方式,要使用WTL 7的消息處理方式,你必須在中including atldlgs.h一行之前添加一行定義: #define _WTL_NEW_PAGE_NOTIFY_HANDLERS 編寫(xiě)新的代碼沒(méi)有理由不使用WTL 7的消息處理函數,所以這里就不介紹WTL 3的消息處理方式。 CPropertyPageImpl 為所有消息提供了默認的通知消息處理函數,你可以重載與你的程序有關(guān)的消息處理函數完成特殊的操作。默認的消息處理函數和相應的行為如下: int OnSetActive() - 允許頁(yè)面成為激活狀態(tài) BOOL OnKillActive() - 允許頁(yè)面成為非激活狀態(tài) int OnApply() - 返回 PSNRET_NOERROR 表示應用操作成功完成 void OnReset() - 無(wú)相應的動(dòng)作 BOOL OnQueryCancel() - 允許取消操作 int OnWizardBack() - 返回到前一個(gè)頁(yè)面 int OnWizardNext() - 進(jìn)行到下一個(gè)頁(yè)面 INT_PTR OnWizardFinish() - 允許向導結束 void OnHelp() - 無(wú)相應的動(dòng)作 BOOL OnGetObject(LPNMOBJECTNOTIFY lpObjectNotify) - 無(wú)相應的動(dòng)作 int OnTranslateAccelerator(LPMSG lpMsg) - 返回 PSNRET_NOERROR 表示消息沒(méi)有被處理 HWND OnQueryInitialFocus(HWND hWndFocus) - 返回 NULL 表示將按Tab Order順序的第一個(gè)控件設為焦點(diǎn)狀態(tài) 創(chuàng )建一個(gè)屬性表 關(guān)于這些類(lèi)的解釋就全部講完了,現在需要一個(gè)例子程序演示如何使用它們。本章的例子工程是一個(gè)簡(jiǎn)單的SDI程序,它在客戶(hù)區顯示一幅圖片并使用一總顏色填充背景,使用的圖片和顏色可以通過(guò)一個(gè)選項對話(huà)框(一個(gè)屬性表)來(lái)設置,還有一個(gè)向導(稍后會(huì )介紹)。 最簡(jiǎn)單的屬性表 首先用WTL的向導創(chuàng )建一個(gè)SDI工程,然后為關(guān)于對話(huà)框添加一個(gè)屬性表。首先改變向導創(chuàng )建的關(guān)于對話(huà)框樣式,使它用起來(lái)像個(gè)屬性頁(yè)。 第一步就是去除OK按鈕,因為屬性表不希望屬性頁(yè)自己關(guān)閉。在Style Tab中,將對話(huà)框樣式改為Child,Thin Border,選擇Title Bar,在More Styles tab,選擇Disabled。 第二步(也是最后一步)是在OnAppAbout()的處理函數中創(chuàng )建一個(gè)屬性表,我們使用非定制的CPropertySheet 和 CPropertyPage類(lèi): LRESULT CMainFrame::OnAppAbout(...){CPropertySheet sheet ( _T("About PSheets") );CPropertyPage<IDD_ABOUTBOX> pgAbout; sheet.AddPage ( pgAbout ); sheet.DoModal(); return 0;}結果看起來(lái)向下面這樣: 創(chuàng )建一個(gè)有用的屬性頁(yè) 并不是每一個(gè)屬性表中的每一個(gè)屬性頁(yè)都像關(guān)于對話(huà)框這么簡(jiǎn)單,大多數屬性頁(yè)需要使用CPropertyPageImpl的派生類(lèi),所以我們現在就看一個(gè)這樣的類(lèi)。我們創(chuàng )建了一個(gè)新的屬性頁(yè)用來(lái)設置客戶(hù)區背景顯示的圖片,它是這個(gè)樣子的: 這個(gè)對話(huà)框的樣式和關(guān)于頁(yè)面相同,我們需要一個(gè)新類(lèi)來(lái)和這個(gè)屬性頁(yè)協(xié)同工作,我們將其命名為CBackgroundOptsPage。這個(gè)類(lèi)是從CPropertyPageImpl類(lèi)派生的,它有一個(gè)CWinDataExchange來(lái)支持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;};關(guān)于這個(gè)類(lèi)需要注意幾點(diǎn): - 有一個(gè)名為IDD的公有成員將對話(huà)框于資源聯(lián)系起來(lái)。
- 消息映射鏈和CDialogImpl相似。
- 消息映射鏈將消息鏈入CPropertyPageImpl,從而使我們能夠處理與屬性表相關(guān)的通知消息。
- 有一個(gè)OnApply()處理函數在單擊屬性表中的OK按鈕時(shí)保存用戶(hù)的選擇。
OnApply() 非常簡(jiǎn)單,它調用 DoDataExchange() 更新 DDX 變量,然后返回一個(gè)代碼標識是否可以關(guān)閉這個(gè)屬性表: int CBackgroundOptsPage::OnApply(){ return DoDataExchange(true) ? PSNRET_NOERROR : PSNRET_INVALID;}我們還要在主窗口添加一個(gè)Tools|Options菜單來(lái)打開(kāi)屬性表,這個(gè)菜單的處理函數創(chuàng )建一個(gè)屬性表,但是添加了一個(gè)新屬性頁(yè)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 );}屬性表的構造函數的第二個(gè)參數是0,表示將索引是0的頁(yè)面初始是可見(jiàn)的,你可以將其設為1,使得屬性表第一次顯示時(shí)顯示關(guān)于頁(yè)面。既然是演示代碼,我就偷個(gè)懶,使用一個(gè)公有變量與CBackgroundOptsPage屬性頁(yè)的radio button建立關(guān)聯(lián),在主窗口中直接為其賦初始值,當用戶(hù)單擊屬性表的OK按鈕時(shí)在將其讀出來(lái)。 如果用戶(hù)點(diǎn)擊OK按鈕,DoModal()發(fā)揮IDOK,我們通知視圖窗口使用新的圖片和背景顏色。下面是幾個(gè)屏幕截圖顯示幾個(gè)不同的樣式的視圖: 創(chuàng )建一個(gè)更好的屬性表類(lèi) 在OnOptions()中創(chuàng )建屬性表是個(gè)好主意,但是在這里使用很多初始化代碼卻非常糟糕,這不是CMainFrame應該做得事情。更好的方法是從CPropertySheetImpl派生一個(gè)新類(lèi),在這個(gè)類(lèi)中完成這些任務(wù)。 #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;};我們使用這個(gè)類(lèi)封裝屬性表中各個(gè)屬性頁(yè)的細節,將初始化代碼移到屬性表內部完成,構造函數完成添加頁(yè)面,并設置其他必需的標志: 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 );}這樣一來(lái),OnOptions()處理函數就變得簡(jiǎn)單了一些: 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 );}創(chuàng )建一個(gè)向導樣式的屬性表 創(chuàng )建一個(gè)向導和創(chuàng )建一個(gè)屬性表很相似,這并不奇怪,只需稍做修改添加“上一步”和“下一步”按鈕就行了。和MFC一樣,你需要重載OnSetActive()函數并調用SetWizardButtons()使相應的按鈕可用。我們先從一個(gè)簡(jiǎn)單的介紹頁(yè)面開(kāi)始,它的ID是IDD_WIZARD_INTRO: 注意這個(gè)頁(yè)面沒(méi)有標題欄文字,因為向導中的所有的頁(yè)面通常都有相同的標題,我更愿意在CPropertySheetImpl的構造函數中設置這些文字,然后每個(gè)頁(yè)面使用這個(gè)字符串資源。這就是為什么我只需要改變一個(gè)字符串就能改變所有頁(yè)面標題文字的原因。 關(guān)于這個(gè)頁(yè)面的實(shí)現代碼在CWizIntroPage類(lèi)中: 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();};構造函數使用(引用)一個(gè)字符串資源ID來(lái)設置頁(yè)面的文字: CWizIntroPage::CWizIntroPage() : CPropertyPageImpl<CWizIntroPage>( IDS_WIZARD_TITLE ){}當這個(gè)頁(yè)面激活時(shí),字符串IDS_WIZARD_TITLE ("PSheets Options Wizard")將出現在向導的標題欄。OnSetActive()僅僅使“下一步”按鈕可用: int CWizIntroPage::OnSetActive(){ SetWizardButtons ( PSWIZB_NEXT ); return 0;}為了實(shí)現一個(gè)向導,我們需要創(chuàng )建一個(gè)類(lèi)COptionsWizard,還要在主窗口添加菜單Tools|Wizard。COptionsWizard類(lèi)的構造函數和CAppPropertySheet類(lèi)的構造函數一樣,只是設置必要的樣式標志和添加頁(yè)面。 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類(lèi)的Tools|Wizard菜單處理函數是這個(gè)樣子: void CMainFrame::OnOptionsWizard ( UINT uCode, int nID, HWND hwndCtrl ){COptionsWizard wizard; wizard.DoModal();}這就是向導的效果: 添加更多的屬性頁(yè),使用DDV 為了使這個(gè)向導能夠有點(diǎn)用處,我們要為其添加一個(gè)設置視圖背景顏色的頁(yè)面。這個(gè)頁(yè)面還將有一個(gè)checkbox演示如何處理DDV驗證失敗并阻止向導進(jìn)行到下一頁(yè)。下面就是新的頁(yè)面,ID是IDD_WIZARD_BKCOLOR: 這個(gè)類(lèi)的實(shí)現代碼在CWizBkColorPage類(lèi)中,下面是相關(guān)的部分代碼 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()的工作和前面的介紹頁(yè)面相同,它使“上一步”和“下一步”按鈕可用。OnKillActive()是個(gè)新的處理函數,它觸發(fā)DDV,然后檢查m_bFailDDV的值,如果是TRUE就表示checkbox處于選中狀態(tài),OnKillActive()將阻止向導進(jìn)行到下一頁(yè)。 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()中完成,因為這兩個(gè)處理函數都可以使向導維持在當前頁(yè)面。它們的不同之處在于OnKillActive()在用戶(hù)單擊“上一步”和“下一步”按鈕時(shí)被調用,而OnWizardNext()只是在用戶(hù)單擊“下一步”按鈕時(shí)被調用。OnWizardNext()還被用來(lái)完成其它目的,比如,它可以直接將向導引導到指定的頁(yè)面而不是按順序的下一頁(yè)。 例子工程的向導還有另外兩個(gè)頁(yè)面,CWizBkPicturePage 和 CWizFinishPage,由于它們和前面的兩個(gè)頁(yè)面相似,我就不再詳細介紹它們,想了解它們的細節可以查看源代碼。 其他的界面考慮 置中一個(gè)屬性表 屬性頁(yè)和向導的默認位置是出現在父窗口的左上角: 這看起來(lái)有點(diǎn)不爽,還好有方法可以補救。第一種方法是重載CPropertySheetImpl::PropSheetCallback()函數,在這個(gè)函數中將屬性表置中。PropSheetCallback()是MSDN中介紹的PropSheetProc()的回調函數,操作系統在屬性表創(chuàng )建時(shí)調用這個(gè)函數,WTL也是利用這個(gè)時(shí)間子類(lèi)化屬性表窗口的。所以我們的第一種嘗試是: 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; }};正如你看到的,我們遇到了棘手的問(wèn)題。PropSheetCallback()是一個(gè)靜態(tài)方法,不能使用this指針訪(fǎng)問(wèn)屬性表窗口。那將這些代碼從CPropertySheetImpl::PropSheetCallback()中拷貝出來(lái),然后添加我們自己的方法行不行呢?撇開(kāi)剛才將代碼和特定版本的WTL聯(lián)系在一起的方法(這已經(jīng)被證明不是各好方法),現在代碼應該是這樣的: 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; }};這從理論上講很完美,但是我試過(guò),屬性表的位置并未改變。顯然,通用控件的代碼在我們調用CenterWindow()之后又改變了屬性表窗口的位置。 必須放棄這個(gè)將代碼封裝到屬性表類(lèi)的方法,盡管它是個(gè)好的解決方案。我又回到原來(lái)的方案,即使用屬性頁(yè)窗口和屬性表窗口相互協(xié)作是屬性表窗口置中。我添加了一個(gè)用戶(hù)定義消息UWM_CENTER_SHEET: #define UWM_CENTER_SHEET WM_APP CAppPropertySheet 在它的消息映射鏈中處理這個(gè)消息: 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;}然后,每個(gè)屬性頁(yè)的OnInitDialog() 方法發(fā)送這個(gè)消息到屬性表窗口: BOOL CBackgroundOptsPage::OnInitDialog ( HWND hwndFocus, LPARAM lParam ){ GetPropertySheet().SendMessage ( UWM_CENTER_SHEET ); DoDataExchange(false); return TRUE;}添加m_bCentered標志確保屬性表窗口只響應收到的第一個(gè)UWM_CENTER_SHEET消息。 在屬性頁(yè)中添加圖標 如果要使用屬性表和屬性頁(yè)的未被成員函數封裝的特性,就需要直接訪(fǎng)問(wèn)相關(guān)的數據結構:CPropertySheetImpl類(lèi)中的PROPSHEETHEADER類(lèi)型(結構)成員m_psh和CPropertyPageImpl類(lèi)中的PROPSHEETPAGE類(lèi)型(結構)成員m_psp。 例如:為例子中Option屬性表中的Background頁(yè)面添加一個(gè)圖標,就需要添加一個(gè)標志并設置屬性頁(yè)的PROPSHEETPAGE結構中的幾個(gè)成員: CBackgroundOptsPage::CBackgroundOptsPage(){ m_psp.dwFlags |= PSP_USEICONID; m_psp.pszIcon = MAKEINTRESOURCE(IDI_TABICON); m_psp.hInstance = _Module.GetResourceInstance();}下 |