四章 對話框和控件
對于Win32 GUI的程序設計來說,其實大部分的情況下我們都不需要自己進行窗口類的設計,而是可以使用Win32中與用戶交互的標準方式——對話框(Dialog Box)。我們可以在VC IDE的資源設計器中設計對話框資源,并在其上放置各種控件資源——的確是非常方便。在本章里,李馬將要向諸位介紹如何利用ATL來操作對話框,以及如何操作對話框上的各種控件。
題外話先
ATL,是的,正是由于我所講的是“ATL的GUI程序設計”,所以我才可能將內容直接經由CWindowImpl過渡到CDialogImpl——而不是過渡到你先前所熟悉的CFrameWnd和Doc/View體系。況且,即使這之后我深入到了CDialogImpl之中,我也不會講到你所熟悉的DDX/DDV機制。再三考慮之下,我還是決定把這些東西在CDialogImpl前一并當作題外話說出來,先。
再來回顧一下ATL的性質。它是一個被設計用來開發COM組件的Framework,所以對GUI部分的支持——套用一句2006年的流行語來說:那是相~~當~~(加重且延長聲音地)少。于是,它沒有“框架窗口”這個概念,更不會有Doc/View體系。其實我對MFC的這一設計特點感覺不錯,畢竟它可以通過一個簡單的CFrameWnd類來實現一個標準的SDI/MDI框架,而且其中帶有工具欄、狀態欄和一個用來容納視圖的標準的工作區域。我們可以通過控制框架窗口中的View及其相關的Doc類型來完成特定文檔類型的讀寫與顯示?!?,很不幸,這一切都只屬于偉大的MFC;在ATL中,我們什么都沒有。
另外,在對話框的技術領域中,使用ATL的我們也不會享有數據交換與驗證(DDX/DDV)的支持。這一所謂的缺憾我并不想多加評價,一是因為我并不了解MFC中DDX/DDV的內部機制,二是因為我直覺上認為這是影響MFC效率的罪魁之一。在MFC中,我們可以通過向導的支持輕易地為表單的輸入域加入輸入校驗與限制,而且表現在源代碼上的僅僅是幾個宏而已——我自認天下沒有免費的午餐,這幾個簡單的宏既然能為我們包辦一切,那我們勢必會相應地失去些東西,要不然忒便宜了也就。
題外話的最后不免落入俗套,我將會向諸位介紹解決以上缺憾的方法?!苍S你猜到了,就是從WTL中尋找解決方案。WTL是對ATL的擴展,所以它的很多代碼可以直接拿過來用(當然可能需要一些小小的修改)。而且,不知道WTL的設計者是不是為了拉攏MFC的開發人員,總之它里面添加了很多與MFC相似的元素,例如以上所說的框架窗口和DDX/DDV。
CDialogImpl
與ATL窗口類CWindowImpl相對應,ATL的對話框類名為CDialogImpl。它的定義如下:
template <class T, class TBase = CWindow> class ATL_NO_VTABLE CDialogImpl : public CDialogImplBaseT< TBase > { // ... }; |
你可以從上面的代碼看到,CDialogImpl與CWindowImpl類似,也經歷了一系列的繼承鏈。不過,它較之CWindowImpl的模板參數要簡單得多——畢竟是標準對話框,有些東西是不用操心的。
CDialogImpl的使用方法大致如下:
class CYourDlg : public CDialogImpl< CYourDlg > { public: enum { IDD = IDD_YOUR_DLG }; public: BEGIN_MSG_MAP( CYourDlg ) // 消息映射 END_MSG_MAP() public: // 消息響應函數 /////////////////// // 其余的部分... }; |
和CWindowImpl不一樣,CDialogImpl不需要使用DECLARE_WND_CLASS來定義窗口類。在原來DECLARE_WND_CLASS的位置,一個枚舉代替了原來窗口類定義的部分。這里的枚舉列表必須有一個被命名為IDD,并且它的值要被設置為相應的對話框資源ID。呃……寫到這里,我仿佛已經感覺到了你的不快,但CDialogImpl的實現即是如此(以CDialogImpl::DoModal為例):
// from CDialogImpl::DoModal return ::DialogBoxParam(_Module.GetResourceInstance(), MAKEINTRESOURCE(T::IDD), hWndParent, (DLGPROC)T::StartDialogProc, dwInitParam); |
當然,如果你不喜歡這么做的話,也可以自己從CDialogImplBaseT派生出屬于你的對話框類。
再回到CDialogImpl的話題上來。這個類主要有以下幾個常用的成員函數:
成員函數 |
說明 |
DoModal |
顯示一個模態對話框 |
EndDialog |
銷毀一個模態對話框 |
Create |
創建一個非模態對話框 |
DestroyWindow |
銷毀一個非模態對話框 |
這樣看來是不是和MFC十分相似?事實上,如果你已經定義好了一個對話框類,那么它的使用和MFC的對話框類的確沒什么兩樣:
CYourDlg dlg; dlg.DoModal(); |
控件的使用
從與用戶交互的角度來看,控件是對話框上必不可少的元素。在Win32 GUI程序設計中,對控件的操作大可歸為兩個方面:一是對控件進行操作,二是響應控件的事件。排除子類化的事件響應(后面我會專門介紹如何在ATL中進行控件的子類化),那么這兩方面的具體實現就是:
- 使用窗口操作的API函數或發送消息來操作控件。
- 處理WM_COMMAND或WM_NOTIFY來響應控件的事件。
根據順序,李馬來為大家介紹一下如何對控件進行操作先。這通??梢越浻蒀Window及其派生類實現,以下代碼示范了如何禁用一個控件:
CWindow ctrl = GetDlgItem( IDC_CONTROL ); ctrl.EnableWindow( FALSE ); |
如果你要操作的控件需要用到特定的特性(也就是通過發送消息來實現的特有行為),當然你可以通過使用CWindow::SendMessage來實現,不過我并不推薦你使用這種方法,因為SendMessage是不會對消息參數進行類型檢查的。而且,考慮到代碼的可復用性,你可以對CWindow進行派生以達到目的。例如,對于列表控件的封裝可以是類似下面這個樣子:
class CListBox : public CWindow { public: int AddString( LPCTSTR lpszString ) { return ::SendMessage( m_hWnd, LB_ADDSTRING, 0, (LPARAM)lpszString ); } }; |
然后,這樣進行調用:
CListBox list; list.Attach( GetDlgItem( IDC_LIST ) ); list.AddString( _T("This is a test line") ); |
可能你會有所疑問:為什么CWindow的例子直接使用了“=”來進行賦值,而CListBox則要使用Attach來初始化。當然,其實這兩者并沒有實質上的區別,只不過是CWindow重載了operator=操作符,而CListBox沒有這樣做罷了(嚴格說來,派生自CWindow的CListBox當然繼承了CWindow的operator=,但是它并不能用于CListBox對象,如果強行使用則會得到一個“error C2679: binary '=' : no operator defined which takes a right-hand operand of type 'struct HWND__ *' (or there is no acceptable conversion)”的錯誤)。如果你也希望CListBox支持operator=的初始化方式,可以這樣來對CListBox進行封裝:
class CListBox : public CWindow { public: CListBox& operator=( HWND hWnd ) { m_hWnd = hWnd; return *this; } public: int AddString( LPCTSTR lpszString ) { return ::SendMessage( m_hWnd, LB_ADDSTRING, 0, (LPARAM)lpszString ); } }; |
下面來介紹對控件事件的處理。通??丶谀承┦录l生時會以發送WM_COMMAND(普通控件)或WM_NOTIFY(公共控件)消息的方式通知其父窗口,然后我們在其父窗口的窗口過程中處理這些消息即可。WM_COMMAND和WM_NOTIFY的參數意義如下:
|
WM_COMMAND |
WM_NOTIFY |
wParam |
HIWORD(wParam)為通知消息代碼,LOWORD(wParam)為控件ID |
發生通知消息的控件ID,不過仍建議使用lParam參數中的ID |
lParam |
發生通知消息的控件句柄 |
一個指向NMHDR結構的指針,這個結構中包含了通知消息的各種信息 |
在ATL中,可以使用如下的宏來進行各種消息的分流(在此將Windows消息分流的宏也一并加上):
消息分流宏 |
說明 |
MESSAGE_HANDLER |
用于將某個特定消息分流至一個消息處理函數。 |
MESSAGE_RANGE_HANDLER |
用于將某個范圍內的消息一并分流至同一個消息處理函數。 |
COMMAND_HANDLER |
用于將來自特定ID、特定通知碼的WM_COMMAND消息分流至一個消息處理函數。 |
COMMAND_ID_HANDLER |
用于將來自特定ID的WM_COMMAND消息分流至一個消息處理函數。 |
COMMAND_CODE_HANDLER |
用于將來自特定通知碼的WM_COMMAND消息分流至一個消息處理函數。 |
COMMAND_RANGE_HANDLER |
用于將來自某個ID范圍內的WM_COMMAND消息分流至一個消息處理函數。 |
NOTIFY_HANDLER |
用于將來自特定ID、特定通知碼的WM_NOTIFY消息分流至一個消息處理函數。 |
NOTIFY_ID_HANDLER |
用于將來自特定ID的WM_NOTIFY消息分流至一個消息處理函數。 |
NOTIFY_CODE_HANDLER |
用于將來自特定通知碼的WM_NOTIFY消息分流至一個消息處理函數。 |
NOTIFY_RANGE_HANDLER |
用于將來自某個ID范圍內的WM_NOTIFY消息分流至一個消息處理函數。 |
另外,處理Windows消息、WM_COMMAND消息、WM_NOTIFY消息的消息處理函數應該分別滿足如下規格要求:
// atlwin.h // Handler prototypes: // LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); // LRESULT CommandHandler(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled); // LRESULT NotifyHandler(int idCtrl, LPNMHDR pnmh, BOOL& bHandled); |
李馬牌通訊錄管理系統

別誤會,這并不是什么正兒八經的所謂“信息管理系統”,而只是我為本章寫下的一個簡單示例而已。這里面并不涉及數據的存儲,而只是為演示本章的內容而實現了必要的流程而已。在此李馬并不打算對這個程序的代碼進行過多解說,僅僅點出幾點需要特殊說明的。
- 由于程序中使用了公共控件ListView,所以在WinMain的開頭需要對公共控件庫進行初始化:
// 初始化公共控件先 INITCOMMONCONTROLSEX init; init.dwSize = sizeof( init ); init.dwICC = ICC_LISTVIEW_CLASSES; InitCommonControlsEx( &init ); |
在此我有必要指出,對公共控件庫的初始化應該盡量使用InitCommonControlsEx,即使InitCommonControls貌似更加方便一些。我曾經做過測試,一個使用了DateTime控件并由InitCommonControls初始化的應用程序在WinXP sp2 + VC 6.0編譯完成后,在Win2K下是不能運行的。
- CMainDlg::OnRadioSex是為了演示COMMAND_RANGE_HANDLER而寫的一個消息處理函數,其實針對這個示例并不用編寫之——因為Windows系統會自動對Radio按鈕進行檢選狀態的處理;但如若考慮到多組Radio按鈕存在的情況,CMainDlg::OnRadioSex這樣的處理函數便會凸顯出它的用處。
- LListView::GetSelectionMark并不能用來準確判斷ListView的選中項,尤其是在選中項被刪除之后。