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

MFC程序員WTL指南(5)對話框與控件

編輯:關於VC++

對第四章的介紹

MFC 的對話框和控件的封裝真得可以節省你很多時間和功夫。沒有MFC對控件的封裝,你要操作控件就得耐著性子填寫各種結構並寫很多的SendMessage調用。MFC還提供了對話框數據交換(DDX),它可以在控件和變量之間傳輸數據。WTL 當然也提供了這些功能,並對控件的封裝做了很多改進。本文將著眼於一個基於對話框的程序演示你以前用MFC實現的功能,除此之外還有WTL消息處理的增強功能。第五章將介紹高級界面特性和WTL對新控件的封裝。

回顧一下ATL的對話框

現在回顧一下第一章 提到的兩個對話框類,CDialogImpl 和 CAxDialogImpl。CAxDialogImpl用於包含ActiveX控件的對話框。本文不准備介紹ActiveX控件,所以只使用CDialogImpl。

創建一個對話框需要做三件事:

  1. 創建一個對話框資源
  2. 從CDialogImpl類派生一個新類
  3. 添加一個公有成員變量IDD,將它設置為對話框資源的ID.

然後就像主框架窗口那樣添加消息處理函數,WTL沒有改變這些,不過確實添加了一些其他能夠在對話框中使用得特性。

通用控件的封裝類

WTL有許多控件的封裝類對你應該比較熟悉,因為它們使用與MFC相同(或幾乎相同)的名字。控件的方法的命名也和MFC一樣,所以你可以參照MFC的文檔使用這些WTL的封裝類。不足之處是F12鍵不能方便地跳到類的定義代碼處。

下面是Windows內建控件的封裝類:

  • 用戶控件: 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中沒有的封裝類: CPagerCtrl, CFlatScrollBar, CLinkCtrl (clickable hyperlink, available on XP only)

還有一些是WTL特有的類:CBitmapButton, CCheckListViewCtrl (帶檢查選擇框的list控件), CTreeViewCtrlEx 和 CTreeItem (通常一起使用, CTreeItem 封裝了HTREEITEM), CHyperLink (類似於網頁上的超鏈接對象,支持所有操作系統)

需要注意得一點是大多數封裝類都是基於CWindow接口的,和CWindow一樣,它們封裝了HWND並對控件的消息進行了封裝(例如,CListBox::GetCurSel()封裝了LB_GETCURSEL消息)。所以和CWindow一樣,創建一個控件的封裝對象並將它與已經存在的控件關聯起來只占用很少的資源,當然也和CWindow一樣,控件封裝對象銷毀時不銷毀控件本身。也有一些例外,如CBitmapButton, CCheckListViewCtrl和CHyperLink。

由於這些文章定位於有經驗的MFC程序員,我就不浪費時間介紹這些封裝類,它們和MFC相應的控件封裝相似。當然我會介紹WTL的新類:CBitmapButtonCBitmapButton類與MFC的同名類有很大的不同,CHyperLink則完全是新事物。

用應用程序向導生成基於對話框的程序

運行VC並啟動WTL應用向導,相信你在做時鐘程序時已經用過它了,為我們的新程序命名為ControlMania1。在向導的第一頁選擇基於對話框的應用,還要選擇是使用模式對話框還是使用非模式對話框。它們有很大的區別,我將在第五章介紹它們的不同,現在我們選擇簡單的一種:模式對話框。如下所示選擇模式對話框和生成CPP文件選項:

第二頁上所有的選項只對主窗口是框架窗口時有意義,現在它們是不可用狀態,單擊"Finish",再單擊"OK"完成向導。

正如你想的那樣,向導生成的基於對話框程序的代碼非常簡單。_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並創建一個單線程公寓,這對於使用ActiveX控件的對話框是有必要得,接著調用WTL的功能函數AtlInitCommonControls(),這個函數是對InitCommonControlsEx()的封裝。全局對象_Module被初始化,主對話框顯示出來。(注意所有使用DoModal()創建的ATL對話框實際上是模式的,這不像MFC,MFC的所有對話框是非模式的,MFC通過代碼禁用對話框的父窗口來模擬模式對話框的行為)最後,_Module和COM被釋放,DoModal()的返回值被用來作為程序的結束碼。

將CMainDlg變量放在一個區塊中是很重要的,因為CMainDlg可能有成員使用了ATL和WTL的特性,這些成員在析構時也會用到ATL/WTL的特性,如果不使用區塊,CMainDlg將在_Module.Term()(這個函數完成ATL/WTL的清理工作)調用之後調用析構函數銷毀自己(和成員),並試圖使用ATL/WTL的特性,這將導致程序出現診斷錯誤崩潰。(WTL 3的向導生成的代碼沒有使用區塊,使得我的一些程序在結束時崩潰)

你現在可以編譯並運行這個程序,盡管它只是一個簡陋的對話框:

CMainDlg 的代碼處理了WM_INITDIALOG, WM_CLOSE和三個按鈕的消息,如果你喜歡可以浏覽一下這些代碼,你應該能夠看懂CMainDlg的聲明,它的消息映射和它的消息處理函數。

這個簡單的工程還演示了如何將控件和變量聯系起來,這個程序使用了幾個控件。在接下來的討論中你可以隨時回來查看這些圖表。

由於程序使用了list view控件,所以對AtlInitCommonControls()的調用需要作些修改,將其改為:

AtlInitCommonControls ( ICC_WIN95_CLASSES );

雖然這樣注冊的控件類比我們用到的多,但是當我們向對話框添加不同類型的控件時就不用隨時記得添加名為ICC_*的常量(譯者加:以ICC_開頭的一系列常量)。

使用控件的封裝類

有幾種方法將一個變量和控件建立關聯,可以使用CWindows(或其它Window接口類,如CListViewCtrl),也可以使用CWindowImpl的派生類。如果只是需要一個臨時變量就用CWindow,如果需要子類化一個控件並處理發送給該控件的消息就需要使用CWindowImpl。

ATL 方式 1 - 連接一個CWindow對象最簡單的方法是聲明一個CWindow或其它window接口類,然後調用Attach()方法,還可以使用CWindow的構造函數直接將變量與控件的HWND關聯起來。

下面的代碼三種方法將變量和一個list控件聯系起來:

HWND hwndList = GetDlgItem(IDC_LIST);
CListViewCtrl wndList1 (hwndList); // use constructor
CListViewCtrl wndList2, wndList3;
  wndList2.Attach ( hwndList );   // use Attach method
  wndList3 = hwndList;       // use assignment operator

記住CWindow的析構函數並不銷毀控件窗口,所以在變量超出作用域時不需要將其脫離控件,如果你願意的話還可以將其作為成員變量使用:你可以在OnInitDialog()處理函數中建立變量與控件的聯系。

ATL 方式 2 - 包容器窗口(CContainedWindow)

CContainedWindow是介於CWindow和CWindowImpl之間的類,它可以子類化控件,在控件的父窗口中處理控件的消息,這使得所有的消息處理都放在對話框類中,不需要為為每個控件生成一個單獨的CWindowImpl派生類對象。需要注意的是不能用CContainedWindow 處理WM_COMMAND, WM_NOTIFY和其他通知消息,因為這些消息是發給控件的父窗口的。

CContainedWindow只是CContainedWindowT定義的一個數據類型,CContainedWindowT才是真正的類,它是一個模板類,使用window接口類的類名作為模板參數。這個特殊的CContainedWindowT<CWindow>和CWindow功能一樣,

CContainedWindow只是它定義的一個簡寫名稱,要使用不同的window接口類只需將該類的類名作為模板參數就行了,例如CContainedWindowT<CListViewCtrl>。

鉤住一個CContainedWindow對象需要做四件事:

  1. 在對話框中創建一個CContainedWindowT 成員變量。
  2. 將消息處理添加到對話框消息映射的ALT_MSG_MAP小節。
  3. 在對話框的構造函數中調用CContainedWindowT 構造函數並告訴它哪個ALT_MSG_MAP小節的消息需要處理。
  4. 在OnInitDialog()中調用CContainedWindowT::SubclassWindow() 方法與控件建立關聯。

在ControlMania1中,我對三個按鈕分別使用了一個CContainedWindow,對話框處理發送到每一個按鈕的WM_SETCURSOR消息,並改變鼠標指針形狀。

現在仔細看看這一步,首先,我們在CMainDlg中添加了CContainedWindow成員。

class CMainDlg : public CDialogImpl<CMainDlg>
{
// ...
protected:
   CContainedWindow m_wndOKBtn, m_wndExitBtn;
};

其次,我們添加了ALT_MSG_MAP小節,OK按鈕使用1小節,Exit按鈕使用2小節。這意味著所有發送給OK按鈕的消息將由ALT_MSG_MAP(1)小節處理,所有發給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);
};

接著,我們調用每個CContainedWindow的構造函數,告訴它使用ALT_MSG_MAP的哪個小節。

CMainDlg::CMainDlg() : m_wndOKBtn(this, 1),
            m_wndExitBtn(this, 2)
{
}

構造函數的參數是消息映射鏈的地址和ALT_MSG_MAP的小節號碼,第一個參數通常使用this,就是使用對話框自己的消息映射鏈,第二個參數告訴對象將消息發給ALT_MSG_MAP的哪個小節。

最後,我們將每個CContainedWindow對象與控件關聯起來。

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;
     }
}

如果你還想使用按鈕類的特性,你需要這樣聲明變量:

CContainedWindowT<CButton> m_wndOKBtn;

這樣就可以使用CButton類的方法。

當你把鼠標光標移到這些按鈕上就可以看到WM_SETCURSOR消息處理函數的作用結果:

ATL 方式 3 - 子類化(Subclassing)

第三種方法創建一個CWindowImpl派生類並用它子類化一個控件。這和第二種方法有些相似,只是消息處理放在CWindowImpl類內部而不是對話框類中。

ControlMania1使用這種方法子類化主對話框的About按鈕。下面是CButtonImpl類,他從CWindowImpl類派生,處理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;
       }
   }
};

接著在主對話框聲明一個CButtonImpl成員變量:

class CMainDlg : public CDialogImpl<CMainDlg>
{
// ...
protected:
   CContainedWindow m_wndOKBtn, m_wndExitBtn;
   CButtonImpl m_wndAboutBtn;
};

最後,在OnInitDialog()種子類化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 方式 - 對話框數據交換(DDX)

WTL的DDX(對話框數據交換)很像MFC,可以使用很簡單的方法將變量和控件關聯起來。首先,和前面的例子一樣你需要從CWindowImpl派生一個新類,這次我們使用一個新類CEditImpl,因為這次我們使用得是Edit控件。你還需要將#include atlddx.h 添加到stdafx.h中,這樣就可以使用DDX代碼。

要使主對話框支持DDX,需要將CWinDataExchange添加到繼承列表中:

class CMainDlg : public CDialogImpl<CMainDlg>,
         public CWinDataExchange<CMainDlg>
{
//...
};

接著在對話框類中添加DDX鏈,這和MFC的類向導使用的DoDataExchange()函數功能相似。對於不同類型的數據可以使用不同的DDX宏,我們使用DDX_CONTROL用來連接變量和控件,這次我們使用CEditImpl處理WM_CONTEXTMENU消息,使它能夠在你右鍵單控件時做一些事情。

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()函數,這個函數是繼承自CWinDataExchange。DoDataExchange()第一次被調用時完成相關控件的子類化工作,所以在這個例子中,DoDataExchange()子類化ID為IDC_EDIT的控件,將其與m_wndEdit建立關聯。

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()函數的參數意義相同,我會在下一節詳細介紹。

現在運行ControlMania1程序,可以看到子類化的效果。鼠標右鍵單擊編輯框將彈出消息框,當鼠標通過按鈕上時鼠標形狀會改變。

DDX的詳細內容

當然,DDX是用來做數據交換的,WTL支持在Edit控件和字符串之間交換數據,也可以將字符串解析成數字,轉換成整型或浮點型變量,還支持Check box和Radio button組的狀態與int型變量之間的轉換。

DDX 宏

DDX可以使用6種宏,每一種宏都對應一個CWinDataExchange類的方法支持其工作,每一種宏都用相同的形式:DDX_FOO(控件ID, 變量),每一種宏都可以支持多種類型的變量,例如DDX_TEXT的重載就支持多種類型的數據。

DDX_TEXT 在字符串和edit box控件之間傳輸數據,變量類型可以是CString, BSTR, CComBSTR或者靜態分配的字符串數組,但是不能使用new動態分配的數組。 DDX_INT 在edit box控件和數字變量之間傳輸int型數據。 DDX_UINT 在edit box控件和數字變量之間傳輸無符號int型數據。 DDX_FLOAT 在edit box控件和數字變量之間傳輸浮點型(float)數據或雙精度型數據(double)。 DDX_CHECK 在check box控件和int型變量之間轉換check box控件的狀態。 DDX_RADIO 在radio buttons控件組和int型變量之間轉換radio buttons控件組的狀態。

DDX_FLOAT宏有一些特殊,要使用DDX_FLOAT宏需要在stdafx.h文件的所有WTL頭文件包含之前添加一行定義:

#define _ATL_USE_DDX_FLOAT

這個定義是必要的,因為默認狀態為了優化程序的大小而不支持浮點數。

有關 DoDataExchange()的詳細內容

調用DoDataExchange()方法和在MFC中使用UpdateData()一樣,DoDataExchange()的函數原型是:

BOOL DoDataExchange ( BOOL bSaveAndValidate = FALSE, UINT nCtlID = (UINT)-1 );

參數:

bSaveAndValidate 指示數據傳輸方向的標志。TRUE表示將數據從控件傳輸給變量,FALSE表示將數據從變量傳輸給控件。需要注意得是這個參數的默認值是FALSE,而MFC的UpdateData()函數的默認值是TRUE。為了方便記憶,你可以使用DDX_SAVE 和 DDX_LOAD標號(它們分別被定義為TRUE和FALSE)。 nCtlID 使用-1可以更新所有控件,如果只想DDX宏作用於一個控件就使用控件的ID。

如果控件更新成功DoDataExchange()會返回TRUE,如果失敗就返回FALSE,對話框類有兩個重載函數處理數據交換錯誤。一個是OnDataExchangeError(),無論什麼原因的錯誤都會調用這個函數,這個函數的默認實現在CWinDataExchange中,它僅僅是驅動PC喇叭發出一聲蜂鳴並將出錯的控件設為當前焦點。另一個函數是OnDataValidateError(),但是要到本文的第五章介紹DDV時才用得到。

使用DDX

在CMainDlg中添加幾個變量,演示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控件的數據傳送給我們剛剛添加的兩個變量,然後將結果顯示在列表控件中。

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將會失敗並觸發OnDataExchangeError()的調用,CMainDlg重載了OnDataExchangeError()函數顯示一個消息框:

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) );
}

作為最後一個使用DDX的例子,我們添加一個check box演示DDX_CHECK的使用:

DDX_CHECK使用的變量類型是int型,它的可能值是0,1,2,分別對應check box的未選擇狀態,選擇狀態和不確定狀態。你也可以使用常量BST_UNCHECKED,BST_CHECKED,和 BST_INDETERMINATE代替,對於check box來說只有選擇和未選擇兩種狀態,你可以將其視為布爾型變量。

以下是為使用check box的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)
     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_*宏的例子代碼包含在例子工程中。

處理控件發送的通知消息

在WTL中處理通知消息與使用API方式編程相似,控件以WM_COMMAND 或 WM_NOTIFY 消息的方式向父窗口發送通知事件,父窗口相應並做相應處理。少數其它的消息也可以看作是通知消息,例如:WM_DRAWITEM,當一個自畫控件需要畫自己時就會發送這個消息,父窗口可以自己處理這個消息,也可以再將它反射給控件,MFC采用得就是消息反射方式,使得控件能夠自己處理通知消息,提高了代碼的封裝性和可重用性。

在父窗口中響應控件的通知消息

以WM_NOTIFY和WM_COMMAND消息形式發送的通知消息包含各種信息。WM_COMMAND消息的參數包含發送通知消息的控件ID,控件的窗口句柄和通知代碼,WM_NOTIFY消息的參數還包含一個NMHDR數據結構的指針。ATL和WTL有各種消息映射宏用來處理這些通知消息,我在這裡只介紹WTL宏,因為本文就是講WTL得。使用這些宏需要在消息映射鏈中使用BEGIN_MSG_MAP_EX並包含atlcrack.h文件。

消息映射宏

要處理WM_COMMAND通知消息需要使用COMMAND_HANDLER_EX宏:

COMMAND_HANDLER_EX(id, code, func) 處理從某個控件發送得某個通知代碼。 COMMAND_ID_HANDLER_EX(id, func) 處理從某個控件發送得所有通知代碼。 COMMAND_CODE_HANDLER_EX(code, func) 處理某個通知代碼得所有消息,不管是從那個控件發出的。 COMMAND_RANGE_HANDLER_EX(idFirst, idLast, func) 處理ID在idFirst和idLast之間得控件發送的所有通知代碼。 COMMAND_RANGE_CODE_HANDLER_EX(idFirst, idLast, code, func) 處理ID在idFirst和idLast之間得控件發送的某個通知代碼。

例子:

  • COMMAND_HANDLER_EX(IDC_USERNAME, EN_CHANGE, OnUsernameChange): 處理從ID是IDC_USERNAME的edit box控件發出的EN_CHANGE通知消息。
  • COMMAND_ID_HANDLER_EX(IDOK, OnOK): 處理ID是IDOK的控件發送的所有通知消息。
  • COMMAND_RANGE_CODE_HANDLER_EX(IDC_MONDAY, IDC_FRIDAY, BN_CLICKED, OnDayClicked): 處理ID在IDC_MONDAY和IDC_FRIDAY之間控件發送的BN_CLICKED通知消息。

還有一些宏專門處理WM_NOTIFY消息,和上面的宏功能類似,只是它們的名字開頭以“NOTIFY_”代替“COMMAND_”。

WM_COMMAND 消息處理函數的原型是:

void func ( UINT uCode, int nCtrlID, HWND hwndCtrl );

WM_COMMAND通知消息不需要返回值,所以處理函數也不需要返回值,WM_NOTIFY消息處理函數的原型是:

LRESULT func ( NMHDR* phdr );

消息處理函數的返回值用作消息相應的返回值,這不同於MFC,MFC的消息響應通過消息處理函數的LRESULT*參數得到返回值。發送通知消息的控件的窗口句柄和通知代碼包含在NMHDR結構中,分別是code和hendFrom成員。和MFC一樣的是如果通知消息發送的不是普通的NMHDR結構,你的消息處理函數應該將phdr參數轉換成正確的類型。

我們將為CMainDlg添加LVN_ITEMCHANGED通知的處理函數,處理從list控件發出的這個通知,在對話框中顯示當前選擇的項目,先從添加消息映射宏和消息處理函數開始:

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的派生類封裝控件,比如前面使用的CEditImpl,你可以在類的內部處理通知消息而不是在對話框中,這就是通知消息的反射,它和MFC的消息反射相似。不同的是在WTL中父窗口和控件都可以處理通知消息,而在MFC中只有控件能處理通知消息(譯者加:除非你重載 WindowProc函數,在MFC反射這些消息之前截獲它們)。

如果需要將通知消息反射給控件封裝類,只需在對話框的消息映射鏈中添加REFLECT_NOTIFICATIONS()宏:

class CMainDlg : public ...
{
public:
   BEGIN_MSG_MAP_EX(CMainDlg)
     //...
     NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged)
     REFLECT_NOTIFICATIONS()
   END_MSG_MAP()
};

這個宏向消息映射鏈添加了一些代碼處理那些未被前面的宏處理的通知消息,它檢查消息傳遞的HWND窗口句柄是否有效並將消息轉發給這個窗口,當然,消息代碼的數值被改變成OLE控件所使用的值,OLE控件有與之相似的消息反射系統。新的消息代碼值用OCM_xxx代替了WM_xxx,但是消息的處理方式和未反射前一樣。

有18中被反射的消息:

  • 控件通知消息: WM_COMMAND, WM_NOTIFY, WM_PARENTNOTIFY
  • 自畫消息: WM_DRAWITEM, WM_MEASUREITEM, WM_COMPAREITEM, WM_DELETEITEM
  • List box 鍵盤消息: WM_VKEYTOITEM, WM_CHARTOITEM
  • 其它: WM_HSCROLL, WM_VSCROLL, WM_CTLCOLOR*

在你想添加反射消息處理的控件類內不要忘了使用DEFAULT_REFLECTION_HANDLER()宏,DEFAULT_REFLECTION_HANDLER()宏確保將未被處理的消息交給DefWindowProc()正確處理。 下面的例子是一個自畫按鈕類,它相應了從父窗口反射的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...
   }
};

用來處理反射消息的WTL宏

我們現在只看到了WTL的消息反射宏中的一個:MSG_OCM_DRAWITEM,還有17個這樣的反射宏。由於WM_NOTIFY和WM_COMMAND消息帶的參數需要展開,WTL提供了特殊的宏MSG_OCM_COMMAND和MSG_OCM_NOTIFY做這些事情。這些宏所作的工作與COMMAND_HANDLER_EX和NOTIFY_HANDLER_EX宏相同,只是前面加了“REFLECTED_”,例如,一個樹控件類可能存在這樣的消息映射鏈:

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對話框中用了一個樹控件,和上面的代碼一樣處理TVN_ITEMEXPANDING消息,CMainDlg類的成員m_wndTree使用DDX連接到控件上,CMainDlg反射通知消息,樹控件的處理函數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,用鼠標點擊樹控件上的+/-按鈕,你就會看到消息處理函數的作用-節點展開後就不能再折疊起來。

容易出錯和混淆的地方 對話框的字體

如果你像我一樣對界面非常講究並且正在只用windows 2000或XP,你就會奇怪為什麼對話框使用MS Sans Serif字體而不是Tahoma字體,因為VC6太老了,它生成的資源文件在NT 4上工作的很好,但是對於新的版本就會有問題。你可以自己修改,需要手工編輯資源文件,據我所知VC 7不存在這個問題。

在資源文件中對話框的入口處需要修改3個地方:

  1. 對話框類型: 將DIALOG改為DIALOGEX
  2. 窗口類型: 添加DS_SHELLFONT
  3. 對話框字體: 將MS Sans Serif改為MS Shell Dlg

不幸的是前兩個修改會在每次保存資源文件時丟失(被VC又改回原樣),所以需要重復這些修改,下面是改動之前的代碼:

IDD_ABOUTBOX DIALOG DISCARDABLE 0, 0, 187, 102
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About"
FONT 8, "MS Sans Serif"
BEGIN
  ...
END

這是改動之後的代碼:

IDD_ABOUTBOX DIALOGEX DISCARDABLE 0, 0, 187, 102
STYLE DS_SHELLFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About"
FONT 8, "MS Shell Dlg"
BEGIN
  ...
END

這樣改了之後,對話框將在新的操作系統上使用Tahoma字體,而在老的操作系統上仍舊使用MS Sans Serif字體。

_ATL_MIN_CRT

本文的論壇 FAQ已經做過解釋, ATL包含的優化設置讓你創建一個不使用C運行庫(CRT)的程序,使用這個優化需要在預處理設置中添加_ATL_MIN_CRT標號,向導生成的代碼在Release配置中默認使用了這個優化。由於我寫程序總是會用到CRT函數,所以我總是去掉這個標號,如果你在CString類或DDX中用到了浮點運算特性,你也要去掉這個標號。

繼續

在第五章,我將介紹對話框數據驗證(DDV),WTL對新控件的封裝和自畫控件、自定外觀控件等一些高級界面特性。

修改記錄

2003年4月27日,本文第一次發表。

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