WTL学习之对话框和控件
作者:朱金灿
来源:http://blog.csdn.net/clever101
继续学习WTL程序中的对话框程序的使用。程序的主要设置如下图:
在对话框和控件学习中我们主要解决下面几个问题。
一.变量如何关联控件。
和MFC的通过增加控件成员变量或者数据成员变量不同,WTL的变量关联控件的方法有四种。一个值得注意的地方是不同的控件绑定方法处理控件的消息各不相同。下面会逐一进行介绍。
1.绑定一个CWindow
最简单的方法是声明一个CWindow 或者其他的窗口接口类,然后调用其 Attach() 方法。你也可以使用CWindow 的构造函数或者赋值操作符把变量关联到控件的 HWND。
下面的代码演示了把变量关联到列表控件的全部三种方法:
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() 处理器中关联变量。
不过这种绑定控件的方法经过我研究,似乎只可以处理WM_COMMAND、WM_NOTIFY或者其他通知消息。要处理更多的消息,需要用到下面两种绑定方法。
2.使用CContainedWindow类
CContainedWindow 是使用CWindow 和 CWindowImpl 的一个折中。它允许你子类化一个控件,并在其父窗口中处理控件的消息。这就允许你把所有的消息处理器置于对话框类里,而不必再为每个控件写独立的CWindowImpl 类。注意,不要使用 CContainedWindow 来处理 WM_COMMAND、WM_NOTIFY 或者其他通知消息,因为这些消息总是发送给控件的父窗口。就是说这种绑定方法可以处理专门发给控件自身窗口的消息。
具体的做法是:
a. 使用CContainedWindow或CContainedWindowT类定义一个变量。其中CContainedWindow为一个基类窗口,可以用它来处理一般发给控件的消息,如WM_SETCURSOR消息,如果要实现更使用更具体的窗口的用法,需要CContainedWindowT类来包装WTL已有的控件类。CContainedWindowT为一个模板类,如需定义一个列表控件类,CContainedWindowT<CListViewCtrl>。
b. 在对话框的初始化函数里使用接口SubclassWindow关联控件,例子如下:
m_wndAboutBtn.SubclassWindow ( GetDlgItem(ID_APP_ABOUT) );
c.使用ALT_MSG_MAP来指定控件消息处理函数。假设你定义了一个
CContainedWindow m_wndOKBtn;
然后在消息处理宏添加ALT_MSG_MAP节:
BEGIN_MSG_MAP_EX(CMainDlg) MESSAGE_HANDLER(WM_INITDIALOG,OnInitDialog) COMMAND_ID_HANDLER_EX(ID_APP_ABOUT,OnAppAbout) COMMAND_ID_HANDLER_EX(IDOK,OnOK) COMMAND_ID_HANDLER_EX(IDCANCEL,OnCancel) ALT_MSG_MAP(1) MSG_WM_SETCURSOR(OnSetCursor_OK) END_MSG_MAP()
注意这里你必须使用BEGIN_MSG_MAP_EX代替BEGIN_MSG_MAP,具体原因请参考MFC 程序员的 WTL 教程(五)。
最后在对话框的构造函数里指定控件使用的消息处理段,如
CMainDlg::CMainDlg() : m_wndOKBtn(this, 1)//这里对应ALT_MSG_MAP(1)中的1 { }
3.子类化
这种方法实际上是定义一个控件派生类,和第二种方法有些类似,不同的是消息处理部分不在对话框类的实现里,而是在控件派生类的实现部分。
具体做法我举一个例子来说明。比如创建一个CWindowImpl 派生类并使用它来子类化控件。这与方法 2 相似,但是消息处理器在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() 里子类化按钮:
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 中处理通知与API 级编程类似。控件以 WM_COMMAND 或者WM_NOTIFY 消息的形式向其父窗口发送通知,其父窗口负责处理。另外还有几个消息也可以视作为通知,例如WM_DRAWITEM,该消息在属主绘制控件需要绘制的时候发送。父窗口既可以自己处理通知消息,也可将消息反射回控件。反射像在MFC 中一样工作 - 控件可以自己处理通知,使得代码具有自包容的形态,易于移到其他的工程中。那什么叫反射通知呢?简单来说就是有一个消息发给控件,你想在控件的实现部分处理这个消息而不是在对话框类处理这个消息。这就意味着你要通知对话框类:这个消息你不用处理,留给控件自身处理吧。具体的做法是向对话框的消息映射中添加一个宏REFLECT_NOTIFICATIONS()。
4.使用DDX_CONTROL
这种做法和MFC程序的做法一样,就是使用DDX_CONTROL宏绑定一个控件派生类。值得注意的是你不能在DDX_CONTROL宏使用WTL的原生类CListViewCtrl、CContainedWindow、以及CContainedWindowT,你需要使用一个CWindowImpl 的派生类。
为了向CMainDlg 添加 DDX 支持,把CWinDataExchange 加入到继承列表中:class CMainDlg : public CDialogImpl<CMainDlg>, public CWinDataExchange<CMainDlg> { //... };
接下来在类中创建一个DDX 映射,它与 MFC 应用中 ClassWizard 生成的 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() 中,我们调用继承于 CWinDataExchange 的 DoDataExchange() 函数。在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() 函数的参数具有相同的含义。我们在下一节中讨论其更多细节。
如前所述,这种方法我们仍然需要REFLECT_NOTIFICATIONS()进行反射通知。
一.数据交换问题
我们知道在MFC程序中控件资源除了绑定上面的提到的控件变量,也可以绑定数据成员,这样就可以在对话框类中进行数据交换。在WTL程序中也可以这样做。具体做法以一个简单的例子说明:
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() 将数据从编辑框中传输到我们刚刚添加的两个变量中,然后再把结果显示到列表控件中。
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 ); }
在这里你可以看到WTL的DoDataExchange函数实际上和MFC的UpdateData函数的作用是一样的。
如果你在编辑框中输入了非数字文本,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) ); }
这其实已带有数据校验(DDV)的色彩了。关于DDV我们以后还会提到,本次学习就到这里。
参考文献:
1.《WTL for MFC Programmers》,作者迈克尔.敦 (Michael Dunn), 珠穆朗玛 译