原作 :Michael Dunn [英文原文] 翻譯 :Orbit(桔皮干了) [http://www.winmsg.com/cn] 下載演示程序代碼 本章內容 對第四章的介紹 MFC 的對話(huà)框和控件的封裝真得可以節省你很多時(shí)間和功夫。沒(méi)有MFC對控件的封裝,你要操作控件就得耐著(zhù)性子填寫(xiě)各種結構并寫(xiě)很多的SendMessage調用。MFC還提供了對話(huà)框數據交換(DDX),它可以在控件和變量之間傳輸數據。WTL 當然也提供了這些功能,并對控件的封裝做了很多改進(jìn)。本文將著(zhù)眼于一個(gè)基于對話(huà)框的程序演示你以前用MFC實(shí)現的功能,除此之外還有WTL消息處理的增強功能。第五章將介紹高級界面特性和WTL對新控件的封裝。 回顧一下ATL的對話(huà)框 現在回顧一下第一章 提到的兩個(gè)對話(huà)框類(lèi),CDialogImpl 和 CAxDialogImpl。CAxDialogImpl用于包含ActiveX控件的對話(huà)框。本文不準備介紹ActiveX控件,所以只使用CDialogImpl。 創(chuàng )建一個(gè)對話(huà)框需要做三件事: - 創(chuàng )建一個(gè)對話(huà)框資源
- 從CDialogImpl類(lèi)派生一個(gè)新類(lèi)
- 添加一個(gè)公有成員變量IDD,將它設置為對話(huà)框資源的ID.
然后就像主框架窗口那樣添加消息處理函數,WTL沒(méi)有改變這些,不過(guò)確實(shí)添加了一些其他能夠在對話(huà)框中使用得特性。 通用控件的封裝類(lèi) WTL有許多控件的封裝類(lèi)對你應該比較熟悉,因為它們使用與MFC相同(或幾乎相同)的名字??丶姆椒ǖ拿埠蚆FC一樣,所以你可以參照MFC的文檔使用這些WTL的封裝類(lèi)。不足之處是F12鍵不能方便地跳到類(lèi)的定義代碼處。 下面是Windows內建控件的封裝類(lèi): - 用戶(hù)控件: CStatic, CButton, CListBox, CComboBox, CEdit, CScrollBar, CDragListBox
- 通用控件: CImageList, CListViewCtrl (CListCtrl in MFC), CTreeViewCtrl (CTreeCtrl in MFC), CHeaderCtrl, CToolBarCtrl, CStatusBarCtrl, CTabCtrl, CToolTipCtrl, CTrackBarCtrl (CSliderCtrl in MFC), CUpDownCtrl (CSpinButtonCtrl in MFC), CProgressBarCtrl, CHotKeyCtrl, CAnimateCtrl, CRichEditCtrl, CReBarCtrl, CComboBoxEx, CDateTimePickerCtrl, CMonthCalendarCtrl, CIPAddressCtrl
- MFC中沒(méi)有的封裝類(lèi): CPagerCtrl, CFlatScrollBar, CLinkCtrl (clickable hyperlink, available on XP only)
還有一些是WTL特有的類(lèi):CBitmapButton, CCheckListViewCtrl (帶檢查選擇框的list控件), CTreeViewCtrlEx 和 CTreeItem (通常一起使用, CTreeItem 封裝了HTREEITEM), CHyperLink (類(lèi)似于網(wǎng)頁(yè)上的超鏈接對象,支持所有操作系統)
需要注意得一點(diǎn)是大多數封裝類(lèi)都是基于CWindow接口的,和CWindow一樣,它們封裝了HWND并對控件的消息進(jìn)行了封裝(例如,CListBox::GetCurSel()封裝了LB_GETCURSEL消息)。所以和CWindow一樣,創(chuàng )建一個(gè)控件的封裝對象并將它與已經(jīng)存在的控件關(guān)聯(lián)起來(lái)只占用很少的資源,當然也和CWindow一樣,控件封裝對象銷(xiāo)毀時(shí)不銷(xiāo)毀控件本身。也有一些例外,如CBitmapButton, CCheckListViewCtrl和CHyperLink。 由于這些文章定位于有經(jīng)驗的MFC程序員,我就不浪費時(shí)間介紹這些封裝類(lèi),它們和MFC相應的控件封裝相似。當然我會(huì )介紹WTL的新類(lèi):CBitmapButtonCBitmapButton類(lèi)與MFC的同名類(lèi)有很大的不同,CHyperLink則完全是新事物。 用應用程序向導生成基于對話(huà)框的程序 運行VC并啟動(dòng)WTL應用向導,相信你在做時(shí)鐘程序時(shí)已經(jīng)用過(guò)它了,為我們的新程序命名為ControlMania1。在向導的第一頁(yè)選擇基于對話(huà)框的應用,還要選擇是使用模式對話(huà)框還是使用非模式對話(huà)框。它們有很大的區別,我將在第五章介紹它們的不同,現在我們選擇簡(jiǎn)單的一種:模式對話(huà)框。如下所示選擇模式對話(huà)框和生成CPP文件選項: 第二頁(yè)上所有的選項只對主窗口是框架窗口時(shí)有意義,現在它們是不可用狀態(tài),單擊"Finish",再單擊"OK"完成向導。 正如你想的那樣,向導生成的基于對話(huà)框程序的代碼非常簡(jiǎn)單。_tWinMain()函數在ControlMania1.cpp中,下面是重要的部分: int WINAPI _tWinMain ( HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpstrCmdLine, int nCmdShow ){ HRESULT hRes = ::CoInitialize(NULL); AtlInitCommonControls(ICC_COOL_CLASSES | ICC_BAR_CLASSES); hRes = _Module.Init(NULL, hInstance); int nRet = 0; // BLOCK: Run application { CMainDlg dlgMain; nRet = dlgMain.DoModal(); } _Module.Term(); ::CoUninitialize(); return nRet;}代碼首先初始化COM并創(chuàng )建一個(gè)單線(xiàn)程公寓,這對于使用ActiveX控件的對話(huà)框是有必要得,接著(zhù)調用WTL的功能函數AtlInitCommonControls(),這個(gè)函數是對InitCommonControlsEx()的封裝。全局對象_Module被初始化,主對話(huà)框顯示出來(lái)。(注意所有使用DoModal()創(chuàng )建的ATL對話(huà)框實(shí)際上是模式的,這不像MFC,MFC的所有對話(huà)框是非模式的,MFC通過(guò)代碼禁用對話(huà)框的父窗口來(lái)模擬模式對話(huà)框的行為)最后,_Module和COM被釋放,DoModal()的返回值被用來(lái)作為程序的結束碼。 將CMainDlg變量放在一個(gè)區塊中是很重要的,因為CMainDlg可能有成員使用了ATL和WTL的特性,這些成員在析構時(shí)也會(huì )用到ATL/WTL的特性,如果不使用區塊,CMainDlg將在_Module.Term()(這個(gè)函數完成ATL/WTL的清理工作)調用之后調用析構函數銷(xiāo)毀自己(和成員),并試圖使用ATL/WTL的特性,這將導致程序出現診斷錯誤崩潰。(WTL 3的向導生成的代碼沒(méi)有使用區塊,使得我的一些程序在結束時(shí)崩潰)
你現在可以編譯并運行這個(gè)程序,盡管它只是一個(gè)簡(jiǎn)陋的對話(huà)框: CMainDlg 的代碼處理了WM_INITDIALOG, WM_CLOSE和三個(gè)按鈕的消息,如果你喜歡可以瀏覽一下這些代碼,你應該能夠看懂CMainDlg的聲明,它的消息映射和它的消息處理函數。 這個(gè)簡(jiǎn)單的工程還演示了如何將控件和變量聯(lián)系起來(lái),這個(gè)程序使用了幾個(gè)控件。在接下來(lái)的討論中你可以隨時(shí)回來(lái)查看這些圖表。 由于程序使用了list view控件,所以對AtlInitCommonControls()的調用需要作些修改,將其改為: AtlInitCommonControls ( ICC_WIN95_CLASSES ); 雖然這樣注冊的控件類(lèi)比我們用到的多,但是當我們向對話(huà)框添加不同類(lèi)型的控件時(shí)就不用隨時(shí)記得添加名為ICC_*的常量(譯者加:以ICC_開(kāi)頭的一系列常量)。 使用控件的封裝類(lèi) 有幾種方法將一個(gè)變量和控件建立關(guān)聯(lián),可以使用CWindows(或其它Window接口類(lèi),如CListViewCtrl),也可以使用CWindowImpl的派生類(lèi)。如果只是需要一個(gè)臨時(shí)變量就用CWindow,如果需要子類(lèi)化一個(gè)控件并處理發(fā)送給該控件的消息就需要使用CWindowImpl。 ATL 方式 1 - 連接一個(gè)CWindow對象最簡(jiǎn)單的方法是聲明一個(gè)CWindow或其它window接口類(lèi),然后調用Attach()方法,還可以使用CWindow的構造函數直接將變量與控件的HWND關(guān)聯(lián)起來(lái)。 下面的代碼三種方法將變量和一個(gè)list控件聯(lián)系起來(lái): HWND hwndList = GetDlgItem(IDC_LIST);CListViewCtrl wndList1 (hwndList); // use constructorCListViewCtrl wndList2, wndList3; wndList2.Attach ( hwndList ); // use Attach method wndList3 = hwndList; // use assignment operator 記住CWindow的析構函數并不銷(xiāo)毀控件窗口,所以在變量超出作用域時(shí)不需要將其脫離控件,如果你愿意的話(huà)還可以將其作為成員變量使用:你可以在OnInitDialog()處理函數中建立變量與控件的聯(lián)系。 ATL 方式 2 - 包容器窗口(CContainedWindow) CContainedWindow是介于CWindow和CWindowImpl之間的類(lèi),它可以子類(lèi)化控件,在控件的父窗口中處理控件的消息,這使得所有的消息處理都放在對話(huà)框類(lèi)中,不需要為為每個(gè)控件生成一個(gè)單獨的CWindowImpl派生類(lèi)對象。需要注意的是不能用CContainedWindow 處理WM_COMMAND, WM_NOTIFY和其他通知消息,因為這些消息是發(fā)給控件的父窗口的。 CContainedWindow只是CContainedWindowT定義的一個(gè)數據類(lèi)型,CContainedWindowT才是真正的類(lèi),它是一個(gè)模板類(lèi),使用window接口類(lèi)的類(lèi)名作為模板參數。這個(gè)特殊的CContainedWindowT<CWindow>和CWindow功能一樣, CContainedWindow只是它定義的一個(gè)簡(jiǎn)寫(xiě)名稱(chēng),要使用不同的window接口類(lèi)只需將該類(lèi)的類(lèi)名作為模板參數就行了,例如CContainedWindowT<CListViewCtrl>。 鉤住一個(gè)CContainedWindow對象需要做四件事: - 在對話(huà)框中創(chuàng )建一個(gè)CContainedWindowT 成員變量。
- 將消息處理添加到對話(huà)框消息映射的ALT_MSG_MAP小節。
- 在對話(huà)框的構造函數中調用CContainedWindowT 構造函數并告訴它哪個(gè)ALT_MSG_MAP小節的消息需要處理。
- 在OnInitDialog()中調用CContainedWindowT::SubclassWindow() 方法與控件建立關(guān)聯(lián)。
在ControlMania1中,我對三個(gè)按鈕分別使用了一個(gè)CContainedWindow,對話(huà)框處理發(fā)送到每一個(gè)按鈕的WM_SETCURSOR消息,并改變鼠標指針形狀。 現在仔細看看這一步,首先,我們在CMainDlg中添加了CContainedWindow成員。 class CMainDlg : public CDialogImpl<CMainDlg>{// ...protected: CContainedWindow m_wndOKBtn, m_wndExitBtn;};其次,我們添加了ALT_MSG_MAP小節,OK按鈕使用1小節,Exit按鈕使用2小節。這意味著(zhù)所有發(fā)送給OK按鈕的消息將由ALT_MSG_MAP(1)小節處理,所有發(fā)給Exit按鈕的消息將由ALT_MSG_MAP(2)小節處理。 class CMainDlg : public CDialogImpl<CMainDlg>{public: BEGIN_MSG_MAP_EX(CMainDlg) MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout) COMMAND_ID_HANDLER(IDOK, OnOK) COMMAND_ID_HANDLER(IDCANCEL, OnCancel) ALT_MSG_MAP(1) MSG_WM_SETCURSOR(OnSetCursor_OK) ALT_MSG_MAP(2) MSG_WM_SETCURSOR(OnSetCursor_Exit) END_MSG_MAP() LRESULT OnSetCursor_OK(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg); LRESULT OnSetCursor_Exit(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg);};接著(zhù),我們調用每個(gè)CContainedWindow的構造函數,告訴它使用ALT_MSG_MAP的哪個(gè)小節。 CMainDlg::CMainDlg() : m_wndOKBtn(this, 1), m_wndExitBtn(this, 2){}構造函數的參數是消息映射鏈的地址和ALT_MSG_MAP的小節號碼,第一個(gè)參數通常使用this,就是使用對話(huà)框自己的消息映射鏈,第二個(gè)參數告訴對象將消息發(fā)給ALT_MSG_MAP的哪個(gè)小節。 最后,我們將每個(gè)CContainedWindow對象與控件關(guān)聯(lián)起來(lái)。 LRESULT CMainDlg::OnInitDialog(...){// ... // Attach CContainedWindows to OK and Exit buttons m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) ); m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) ); return TRUE;}下面是新的WM_SETCURSOR消息處理函數: LRESULT CMainDlg::OnSetCursor_OK (HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg ){static HCURSOR hcur = LoadCursor ( NULL, IDC_HAND ); if ( NULL != hcur ) { SetCursor ( hcur ); return TRUE; } else { SetMsgHandled(false); return FALSE; }} LRESULT CMainDlg::OnSetCursor_Exit ( HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg ){static HCURSOR hcur = LoadCursor ( NULL, IDC_NO ); if ( NULL != hcur ) { SetCursor ( hcur ); return TRUE; } else { SetMsgHandled(false); return FALSE; }}如果你還想使用按鈕類(lèi)的特性,你需要這樣聲明變量: CContainedWindowT<CButton> m_wndOKBtn; 這樣就可以使用CButton類(lèi)的方法。 當你把鼠標光標移到這些按鈕上就可以看到WM_SETCURSOR消息處理函數的作用結果: ATL 方式 3 - 子類(lèi)化(Subclassing) 第三種方法創(chuàng )建一個(gè)CWindowImpl派生類(lèi)并用它子類(lèi)化一個(gè)控件。這和第二種方法有些相似,只是消息處理放在CWindowImpl類(lèi)內部而不是對話(huà)框類(lèi)中。 ControlMania1使用這種方法子類(lèi)化主對話(huà)框的About按鈕。下面是CButtonImpl類(lèi),他從CWindowImpl類(lèi)派生,處理WM_SETCURSOR消息: class CButtonImpl : public CWindowImpl<CButtonImpl, CButton>{ BEGIN_MSG_MAP_EX(CButtonImpl) MSG_WM_SETCURSOR(OnSetCursor) END_MSG_MAP() LRESULT OnSetCursor(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg) { static HCURSOR hcur = LoadCursor ( NULL, IDC_SIZEALL ); if ( NULL != hcur ) { SetCursor ( hcur ); return TRUE; } else { SetMsgHandled(false); return FALSE; } }};接著(zhù)在主對話(huà)框聲明一個(gè)CButtonImpl成員變量: class CMainDlg : public CDialogImpl<CMainDlg>{// ...protected: CContainedWindow m_wndOKBtn, m_wndExitBtn; CButtonImpl m_wndAboutBtn;};最后,在OnInitDialog()種子類(lèi)化About按鈕。 LRESULT CMainDlg::OnInitDialog(...){// ... // Attach CContainedWindows to OK and Exit buttons m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) ); m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) ); // CButtonImpl: subclass the About button m_wndAboutBtn.SubclassWindow ( GetDlgItem(ID_APP_ABOUT) ); return TRUE;}WTL 方式 - 對話(huà)框數據交換(DDX) WTL的DDX(對話(huà)框數據交換)很像MFC,可以使用很簡(jiǎn)單的方法將變量和控件關(guān)聯(lián)起來(lái)。首先,和前面的例子一樣你需要從CWindowImpl派生一個(gè)新類(lèi),這次我們使用一個(gè)新類(lèi)CEditImpl,因為這次我們使用得是Edit控件。你還需要將#include atlddx.h 添加到stdafx.h中,這樣就可以使用DDX代碼。 要使主對話(huà)框支持DDX,需要將CWinDataExchange添加到繼承列表中: class CMainDlg : public CDialogImpl<CMainDlg>, public CWinDataExchange<CMainDlg>{//...};接著(zhù)在對話(huà)框類(lèi)中添加DDX鏈,這和MFC的類(lèi)向導使用的DoDataExchange()函數功能相似。對于不同類(lèi)型的數據可以使用不同的DDX宏,我們使用DDX_CONTROL用來(lái)連接變量和控件,這次我們使用CEditImpl處理WM_CONTEXTMENU消息,使它能夠在你右鍵單控件時(shí)做一些事情。 class CEditImpl : public CWindowImpl<CEditImpl, CEdit>{ BEGIN_MSG_MAP_EX(CEditImpl) MSG_WM_CONTEXTMENU(OnContextMenu) END_MSG_MAP() void OnContextMenu ( HWND hwndCtrl, CPoint ptClick ) { MessageBox("Edit control handled WM_CONTEXTMENU"); }}; class CMainDlg : public CDialogImpl<CMainDlg>, public CWinDataExchange<CMainDlg>{//... BEGIN_DDX_MAP(CMainDlg) DDX_CONTROL(IDC_EDIT, m_wndEdit) END_DDX_MAP() protected: CContainedWindow m_wndOKBtn, m_wndExitBtn; CButtonImpl m_wndAboutBtn; CEditImpl m_wndEdit;};最后,在OnInitDialog()中調用DoDataExchange()函數,這個(gè)函數是繼承自CWinDataExchange。DoDataExchange()第一次被調用時(shí)完成相關(guān)控件的子類(lèi)化工作,所以在這個(gè)例子中,DoDataExchange()子類(lèi)化ID為IDC_EDIT的控件,將其與m_wndEdit建立關(guān)聯(lián)。 LRESULT CMainDlg::OnInitDialog(...){// ... // Attach CContainedWindows to OK and Exit buttons m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) ); m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) ); // CButtonImpl: subclass the About button m_wndAboutBtn.SubclassWindow ( GetDlgItem(ID_APP_ABOUT) ); // First DDX call, hooks up variables to controls. DoDataExchange(false); return TRUE;}DoDataExchange()的參數與MFC的UpdateData()函數的參數意義相同,我會(huì )在下一節詳細介紹。 現在運行ControlMania1程序,可以看到子類(lèi)化的效果。鼠標右鍵單擊編輯框將彈出消息框,當鼠標通過(guò)按鈕上時(shí)鼠標形狀會(huì )改變。 DDX的詳細內容 當然,DDX是用來(lái)做數據交換的,WTL支持在Edit控件和字符串之間交換數據,也可以將字符串解析成數字,轉換成整型或浮點(diǎn)型變量,還支持Check box和Radio button組的狀態(tài)與int型變量之間的轉換。 DDX 宏 DDX可以使用6種宏,每一種宏都對應一個(gè)CWinDataExchange類(lèi)的方法支持其工作,每一種宏都用相同的形式:DDX_FOO(控件ID, 變量),每一種宏都可以支持多種類(lèi)型的變量,例如DDX_TEXT的重載就支持多種類(lèi)型的數據。 - DDX_TEXT
- 在字符串和edit box控件之間傳輸數據,變量類(lèi)型可以是CString, BSTR, CComBSTR或者靜態(tài)分配的字符串數組,但是不能使用new動(dòng)態(tài)分配的數組。
- DDX_INT
- 在edit box控件和數字變量之間傳輸int型數據。
- DDX_UINT
- 在edit box控件和數字變量之間傳輸無(wú)符號int型數據。
- DDX_FLOAT
- 在edit box控件和數字變量之間傳輸浮點(diǎn)型(float)數據或雙精度型數據(double)。
- DDX_CHECK
- 在check box控件和int型變量之間轉換check box控件的狀態(tài)。
- DDX_RADIO
- 在radio buttons控件組和int型變量之間轉換radio buttons控件組的狀態(tài)。
DDX_FLOAT宏有一些特殊,要使用DDX_FLOAT宏需要在stdafx.h文件的所有WTL頭文件包含之前添加一行定義: #define _ATL_USE_DDX_FLOAT 這個(gè)定義是必要的,因為默認狀態(tài)為了優(yōu)化程序的大小而不支持浮點(diǎn)數。 有關(guān) DoDataExchange()的詳細內容 調用DoDataExchange()方法和在MFC中使用UpdateData()一樣,DoDataExchange()的函數原型是: BOOL DoDataExchange ( BOOL bSaveAndValidate = FALSE, UINT nCtlID = (UINT)-1 ); 參數: - bSaveAndValidate
- 指示數據傳輸方向的標志。TRUE表示將數據從控件傳輸給變量,FALSE表示將數據從變量傳輸給控件。需要注意得是這個(gè)參數的默認值是FALSE,而MFC的UpdateData()函數的默認值是TRUE。為了方便記憶,你可以使用DDX_SAVE 和 DDX_LOAD標號(它們分別被定義為T(mén)RUE和FALSE)。
- nCtlID
- 使用-1可以更新所有控件,如果只想DDX宏作用于一個(gè)控件就使用控件的ID。
如果控件更新成功DoDataExchange()會(huì )返回TRUE,如果失敗就返回FALSE,對話(huà)框類(lèi)有兩個(gè)重載函數處理數據交換錯誤。一個(gè)是OnDataExchangeError(),無(wú)論什么原因的錯誤都會(huì )調用這個(gè)函數,這個(gè)函數的默認實(shí)現在CWinDataExchange中,它僅僅是驅動(dòng)PC喇叭發(fā)出一聲蜂鳴并將出錯的控件設為當前焦點(diǎn)。另一個(gè)函數是OnDataValidateError(),但是要到本文的第五章介紹DDV時(shí)才用得到。 使用DDX 在CMainDlg中添加幾個(gè)變量,演示DDX的使用方法。 class CMainDlg : public ...{//... BEGIN_DDX_MAP(CMainDlg) DDX_CONTROL(IDC_EDIT, m_wndEdit) DDX_TEXT(IDC_EDIT, m_sEditContents) DDX_INT(IDC_EDIT, m_nEditNumber) END_DDX_MAP() protected: // DDX variables CString m_sEditContents; int m_nEditNumber;};在OK按鈕的處理函數中,我們首先調用DoDataExchange()將將edit控件的數據傳送給我們剛剛添加的兩個(gè)變量,然后將結果顯示在列表控件中。 LRESULT CMainDlg::OnOK ( UINT uCode, int nID, HWND hWndCtl ){CString str; // Transfer data from the controls to member variables. if ( !DoDataExchange(true) ) return; m_wndList.DeleteAllItems(); m_wndList.InsertItem ( 0, _T("DDX_TEXT") ); m_wndList.SetItemText ( 0, 1, m_sEditContents ); str.Format ( _T("%d"), m_nEditNumber ); m_wndList.InsertItem ( 1, _T("DDX_INT") ); m_wndList.SetItemText ( 1, 1, str );}如果編輯控件輸入的不是數字,DDX_INT將會(huì )失敗并觸發(fā)OnDataExchangeError()的調用,CMainDlg重載了OnDataExchangeError()函數顯示一個(gè)消息框: void CMainDlg::OnDataExchangeError ( UINT nCtrlID, BOOL bSave ){CString str; str.Format ( _T("DDX error during exchange with control: %u"), nCtrlID ); MessageBox ( str, _T("ControlMania1"), MB_ICONWARNING ); ::SetFocus ( GetDlgItem(nCtrlID) );}作為最后一個(gè)使用DDX的例子,我們添加一個(gè)check box演示DDX_CHECK的使用: DDX_CHECK使用的變量類(lèi)型是int型,它的可能值是0,1,2,分別對應check box的未選擇狀態(tài),選擇狀態(tài)和不確定狀態(tài)。你也可以使用常量BST_UNCHECKED,BST_CHECKED,和 BST_INDETERMINATE代替,對于check box來(lái)說(shuō)只有選擇和未選擇兩種狀態(tài),你可以將其視為布爾型變量。 以下是為使用check box的DDX而做的改動(dòng): class CMainDlg : public ...{//... BEGIN_DDX_MAP(CMainDlg) DDX_CONTROL(IDC_EDIT, m_wndEdit) DDX_TEXT(IDC_EDIT, m_sEditContents) DDX_INT(IDC_EDIT, m_nEditNumber) DDX_CHECK(IDC_SHOW_MSG, m_nShowMsg) END_DDX_MAP() protected: // DDX variables CString m_sEditContents; int m_nEditNumber; int m_nShowMsg;};在OnOK()的最后,檢查m_nShowMsg的值看看check box是否被選中。 void CMainDlg::OnOK ( UINT uCode, int nID, HWND hWndCtl ){ // Transfer data from the controls to member variables. if ( !DoDataExchange(true) ) return;//... if ( m_nShowMsg ) MessageBox ( _T("DDX complete!"), _T("ControlMania1"), MB_ICONINFORMATION );}使用其它DDX_*宏的例子代碼包含在例子工程中。 處理控件發(fā)送的通知消息 在WTL中處理通知消息與使用API方式編程相似,控件以WM_COMMAND 或 WM_NOTIFY 消息的方式向父窗口發(fā)送通知事件,父窗口相應并做相應處理。少數其它的消息也可以看作是通知消息,例如:WM_DRAWITEM,當一個(gè)自畫(huà)控件需要畫(huà)自己時(shí)就會(huì )發(fā)送這個(gè)消息,父窗口可以自己處理這個(gè)消息,也可以再將它反射給控件,MFC采用得就是消息反射方式,使得控件能夠自己處理通知消息,提高了代碼的封裝性和可重用性。 在父窗口中響應控件的通知消息 以WM_NOTIFY和WM_COMMAND消息形式發(fā)送的通知消息包含各種信息。WM_COMMAND消息的參數包含發(fā)送通知消息的控件ID,控件的窗口句柄和通知代碼,WM_NOTIFY消息的參數還包含一個(gè)NMHDR數據結構的指針。ATL和WTL有各種消息映射宏用來(lái)處理這些通知消息,我在這里只介紹WTL宏,因為本文就是講WTL得。使用這些宏需要在消息映射鏈中使用BEGIN_MSG_MAP_EX并包含atlcrack.h文件。 消息映射宏 要處理WM_COMMAND通知消息需要使用COMMAND_HANDLER_EX宏: - COMMAND_HANDLER_EX(id, code, func)
- 處理從某個(gè)控件發(fā)送得某個(gè)通知代碼。
- COMMAND_ID_HANDLER_EX(id, func)
- 處理從某個(gè)控件發(fā)送得所有通知代碼。
- COMMAND_CODE_HANDLER_EX(code, func)
- 處理某個(gè)通知代碼得所有消息,不管是從那個(gè)控件發(fā)出的。
- COMMAND_RANGE_HANDLER_EX(idFirst, idLast, func)
- 處理ID在idFirst和idLast之間得控件發(fā)送的所有通知代碼。
- COMMAND_RANGE_CODE_HANDLER_EX(idFirst, idLast, code, func)
- 處理ID在idFirst和idLast之間得控件發(fā)送的某個(gè)通知代碼。
例子: - COMMAND_HANDLER_EX(IDC_USERNAME, EN_CHANGE, OnUsernameChange): 處理從ID是IDC_USERNAME的edit box控件發(fā)出的EN_CHANGE通知消息。
- COMMAND_ID_HANDLER_EX(IDOK, OnOK): 處理ID是IDOK的控件發(fā)送的所有通知消息。
- COMMAND_RANGE_CODE_HANDLER_EX(IDC_MONDAY, IDC_FRIDAY, BN_CLICKED, OnDayClicked): 處理ID在IDC_MONDAY和IDC_FRIDAY之間控件發(fā)送的BN_CLICKED通知消息。
還有一些宏專(zhuān)門(mén)處理WM_NOTIFY消息,和上面的宏功能類(lèi)似,只是它們的名字開(kāi)頭以“NOTIFY_”代替“COMMAND_”。 WM_COMMAND 消息處理函數的原型是: void func ( UINT uCode, int nCtrlID, HWND hwndCtrl ); WM_COMMAND通知消息不需要返回值,所以處理函數也不需要返回值,WM_NOTIFY消息處理函數的原型是: LRESULT func ( NMHDR* phdr ); 消息處理函數的返回值用作消息相應的返回值,這不同于MFC,MFC的消息響應通過(guò)消息處理函數的LRESULT*參數得到返回值。發(fā)送通知消息的控件的窗口句柄和通知代碼包含在NMHDR結構中,分別是code和hendFrom成員。和MFC一樣的是如果通知消息發(fā)送的不是普通的NMHDR結構,你的消息處理函數應該將phdr參數轉換成正確的類(lèi)型。 我們將為CMainDlg添加LVN_ITEMCHANGED通知的處理函數,處理從list控件發(fā)出的這個(gè)通知,在對話(huà)框中顯示當前選擇的項目,先從添加消息映射宏和消息處理函數開(kāi)始: class CMainDlg : public ...{ BEGIN_MSG_MAP_EX(CMainDlg) NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged) END_MSG_MAP() LRESULT OnListItemchanged(NMHDR* phdr);//...};下面是消息處理函數: LRESULT CMainDlg::OnListItemchanged ( NMHDR* phdr ){NMLISTVIEW* pnmlv = (NMLISTVIEW*) phdr;int nSelItem = m_wndList.GetSelectedIndex();CString sMsg; // If no item is selected, show "none". Otherwise, show its index. if ( -1 == nSelItem ) sMsg = _T("(none)"); else sMsg.Format ( _T("%d"), nSelItem ); SetDlgItemText ( IDC_SEL_ITEM, sMsg ); return 0; // retval ignored}該處理函數并未用到phdr參數,我將他強制轉換成NMLISTVIEW*只是為了演示用法。 反射通知消息 如果你是用CWindowImpl的派生類(lèi)封裝控件,比如前面使用的CEditImpl,你可以在類(lèi)的內部處理通知消息而不是在對話(huà)框中,這就是通知消息的反射,它和MFC的消息反射相似。不同的是在WTL中父窗口和控件都可以處理通知消息,而在MFC中只有控件能處理通知消息(譯者加:除非你重載 WindowProc函數,在MFC反射這些消息之前截獲它們)。 如果需要將通知消息反射給控件封裝類(lèi),只需在對話(huà)框的消息映射鏈中添加REFLECT_NOTIFICATIONS()宏: class CMainDlg : public ...{public: BEGIN_MSG_MAP_EX(CMainDlg) //... NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged) REFLECT_NOTIFICATIONS() END_MSG_MAP()};這個(gè)宏向消息映射鏈添加了一些代碼處理那些未被前面的宏處理的通知消息,它檢查消息傳遞的HWND窗口句柄是否有效并將消息轉發(fā)給這個(gè)窗口,當然,消息代碼的數值被改變成OLE控件所使用的值,OLE控件有與之相似的消息反射系統。新的消息代碼值用OCM_xxx代替了WM_xxx,但是消息的處理方式和未反射前一樣。 有18中被反射的消息: - 控件通知消息: WM_COMMAND, WM_NOTIFY, WM_PARENTNOTIFY
- 自畫(huà)消息: WM_DRAWITEM, WM_MEASUREITEM, WM_COMPAREITEM, WM_DELETEITEM
- List box 鍵盤(pán)消息: WM_VKEYTOITEM, WM_CHARTOITEM
- 其它: WM_HSCROLL, WM_VSCROLL, WM_CTLCOLOR*
在你想添加反射消息處理的控件類(lèi)內不要忘了使用DEFAULT_REFLECTION_HANDLER()宏,DEFAULT_REFLECTION_HANDLER()宏確保將未被處理的消息交給DefWindowProc()正確處理。 下面的例子是一個(gè)自畫(huà)按鈕類(lèi),它相應了從父窗口反射的WM_DRAWITEM消息。 class CODButtonImpl : public CWindowImpl<CODButtonImpl, CButton>{public: BEGIN_MSG_MAP_EX(CODButtonImpl) MSG_OCM_DRAWITEM(OnDrawItem) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() void OnDrawItem ( UINT idCtrl, LPDRAWITEMSTRUCT lpdis ) { // do drawing here... }};用來(lái)處理反射消息的WTL宏 我們現在只看到了WTL的消息反射宏中的一個(gè):MSG_OCM_DRAWITEM,還有17個(gè)這樣的反射宏。由于WM_NOTIFY和WM_COMMAND消息帶的參數需要展開(kāi),WTL提供了特殊的宏MSG_OCM_COMMAND和MSG_OCM_NOTIFY做這些事情。這些宏所作的工作與COMMAND_HANDLER_EX和NOTIFY_HANDLER_EX宏相同,只是前面加了“REFLECTED_”,例如,一個(gè)樹(shù)控件類(lèi)可能存在這樣的消息映射鏈:
class CMyTreeCtrl : public CWindowImpl<CMyTreeCtrl, CTreeViewCtrl>{public: BEGIN_MSG_MAP_EX(CMyTreeCtrl) REFLECTED_NOTIFY_CODE_HANDLER_EX(TVN_ITEMEXPANDING, OnItemExpanding) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() LRESULT OnItemExpanding ( NMHDR* phdr );};在ControlMania1對話(huà)框中用了一個(gè)樹(shù)控件,和上面的代碼一樣處理TVN_ITEMEXPANDING消息,CMainDlg類(lèi)的成員m_wndTree使用DDX連接到控件上,CMainDlg反射通知消息,樹(shù)控件的處理函數OnItemExpanding()是這樣的: LRESULT CBuffyTreeCtrl::OnItemExpanding ( NMHDR* phdr ){NMTREEVIEW* pnmtv = (NMTREEVIEW*) phdr; if ( pnmtv->action & TVE_COLLAPSE ) return TRUE; // don‘‘t allow it else return FALSE; // allow it}運行ControlMania1,用鼠標點(diǎn)擊樹(shù)控件上的+/-按鈕,你就會(huì )看到消息處理函數的作用-節點(diǎn)展開(kāi)后就不能再折疊起來(lái)。 容易出錯和混淆的地方 對話(huà)框的字體 如果你像我一樣對界面非常講究并且正在只用windows 2000或XP,你就會(huì )奇怪為什么對話(huà)框使用MS Sans Serif字體而不是Tahoma字體,因為VC6太老了,它生成的資源文件在NT 4上工作的很好,但是對于新的版本就會(huì )有問(wèn)題。你可以自己修改,需要手工編輯資源文件,據我所知VC 7不存在這個(gè)問(wèn)題。 在資源文件中對話(huà)框的入口處需要修改3個(gè)地方: - 對話(huà)框類(lèi)型: 將DIALOG改為DIALOGEX
- 窗口類(lèi)型: 添加DS_SHELLFONT
- 對話(huà)框字體: 將MS Sans Serif改為MS Shell Dlg
不幸的是前兩個(gè)修改會(huì )在每次保存資源文件時(shí)丟失(被VC又改回原樣),所以需要重復這些修改,下面是改動(dòng)之前的代碼: IDD_ABOUTBOX DIALOG DISCARDABLE 0, 0, 187, 102STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENUCAPTION "About"FONT 8, "MS Sans Serif"BEGIN ...END 這是改動(dòng)之后的代碼: IDD_ABOUTBOX DIALOGEX DISCARDABLE 0, 0, 187, 102STYLE DS_SHELLFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENUCAPTION "About"FONT 8, "MS Shell Dlg"BEGIN ...END 這樣改了之后,對話(huà)框將在新的操作系統上使用Tahoma字體,而在老的操作系統上仍舊使用MS Sans Serif字體。 _ATL_MIN_CRT 本文的論壇 FAQ已經(jīng)做過(guò)解釋, ATL包含的優(yōu)化設置讓你創(chuàng )建一個(gè)不使用C運行庫(CRT)的程序,使用這個(gè)優(yōu)化需要在預處理設置中添加_ATL_MIN_CRT標號,向導生成的代碼在Release配置中默認使用了這個(gè)優(yōu)化。由于我寫(xiě)程序總是會(huì )用到CRT函數,所以我總是去掉這個(gè)標號,如果你在CString類(lèi)或DDX中用到了浮點(diǎn)運算特性,你也要去掉這個(gè)標號。 繼續 在第五章,我將介紹對話(huà)框數據驗證(DDV),WTL對新控件的封裝和自畫(huà)控件、自定外觀(guān)控件等一些高級界面特性。 修改記錄 2003年4月27日,本文第一次發(fā)表。 |