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

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