ATL的GUI程序設計(4)

第四章 對話框和控件

對於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中,我們可以通過嚮導的支持輕易地爲表單的輸入域加入輸入校驗與限制,而且表現在源代碼上的僅僅是幾個宏而已——我自認天下沒有免費的午餐,這幾個簡單的宏既然能爲我們包辦一切,那我們勢必會相應地失去些東西,要不然忒便宜了也就。

題外話的最後不免落入俗套,我將會向諸位介紹解決以上缺憾的方法。——也許你猜到了,就是從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來響應控件的事件。

根據順序,李馬來爲大家介紹一下如何對控件進行操作先。這通常可以經由CWindow及其派生類實現,以下代碼示範瞭如何禁用一個控件:

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

下面來介紹對控件事件的處理。通常控件在某些事件發生時會以發送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);

李馬牌通訊錄管理系統

別誤會,這並不是什麼正兒八經的所謂“信息管理系統”,而只是我爲本章寫下的一個簡單示例而已。這裏面並不涉及數據的存儲,而只是爲演示本章的內容而實現了必要的流程而已。在此李馬並不打算對這個程序的代碼進行過多解說,僅僅點出幾點需要特殊說明的。

  1. 由於程序中使用了公共控件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下是不能運行的。
  2. CMainDlg::OnRadioSex是爲了演示COMMAND_RANGE_HANDLER而寫的一個消息處理函數,其實針對這個示例並不用編寫之——因爲Windows系統會自動對Radio按鈕進行檢選狀態的處理;但如若考慮到多組Radio按鈕存在的情況,CMainDlg::OnRadioSex這樣的處理函數便會凸顯出它的用處。
  3. LListView::GetSelectionMark並不能用來準確判斷ListView的選中項,尤其是在選中項被刪除之後。

點這裏下載本章配套代碼

發佈了32 篇原創文章 · 獲贊 3 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章