CDialogImpl
对话框本质上是布局和行为受约束的窗口。最原始的模态对话框API是:
1: WINUSERAPI
2: INT_PTR
3: WINAPI
4: DialogBoxParamW(
5: __in_opt HINSTANCE hInstance, //applicaton instance
6: __in LPCWSTR lpTemplateName, //IDD : dialog template resource identifies
7: __in_opt HWND hWndParent, //hWndParent
8: __in_opt DLGPROC lpDialogFunc, //WndProc ::StartDialogProc
9: __in LPARAM dwInitParam); //initialization value
非模态对话框的唯一区别在于返回值:
1: WINUSERAPI
2: HWND
3: WINAPI
4: CreateDialogIndirectParamW 。。。
对话框的Win32API编程步骤,首先用资源编辑器布局窗口,然后写一个DlgProc,组后创建(模态或非模态)。ATL对对话框的封装和Window类似,类图参考分析一,在实现DlgProc时,同样用到了两级跳转以及thunk,从StartDiallgProc到实际的ProcessWindowMessage跳转。而窗口的布局则使用资源描述符IDD所预先布置好的位置图来得到。
CSimpleDlg是一个简化版本的对话框,用以简单的弹出式对话框。
DataExchange和验证
对话框使用时还是比较复杂的,主要原因有将数据写给对话框所包含的子控件,对话框从子控件读取数据。
模态数据交互流程如下:
1、创建继承自CDialogImpl的类实例;
2、将数据拷贝进对话框类的数据成员;
3、调用DoModal。
4、在WM_INITDIALOG时将数据传递给子控件;
5、当对话框处理OK键消息时,对子控件获得的数据进行验证;
6、当数据有效,把数据拷贝到对话框的数据成员,结束对话框;
7、应用从对话框DoModal返回获得IDOK,并从中把数据返回。
非模态对话框的交互流程类似,不同在于第6、7步:
6、当数据有效,并把数据拷贝到对话框的数据成员,应用接受到通知并从对话框数据成员获取这些数据;
7、应用在收到通知后接收数据,拷贝到应用自己的数据成员中。
不同在于,模态时对数据的验证工作室对话框自己做,非模态则将接收数据的消息传给了应用,由应用进行处理。消息处理的位置不同。
ATL没有提供类似MFC中DDX、DDV的功能,不过很容易模仿实现;而在WTL中随机对这一功能进行了补充。
Windows Control Wrappers
Child Window Management
操作一个子控件时,例如设定edit control的文本显示或禁用OK按钮等,我们使用到的函数都是来自CWindow,在CWindow中提供了一系列的helper function来操作子控件:
1: class CWindow
2: {
3: public:
4: ...
5: // Dialog-Box Item Functions
6:
7: BOOL CheckDlgButton(int nIDButton, UINT nCheck) throw()
8: {
9: ATLASSERT(::IsWindow(m_hWnd));
10: return ::CheckDlgButton(m_hWnd, nIDButton, nCheck);
11: }
12:
13: BOOL CheckRadioButton(int nIDFirstButton, int nIDLastButton, int nIDCheckButton) throw()
14: {
15: ATLASSERT(::IsWindow(m_hWnd));
16: return ::CheckRadioButton(m_hWnd, nIDFirstButton, nIDLastButton, nIDCheckButton);
17: }
18:
19: int DlgDirList(_Inout_z_ LPTSTR lpPathSpec, _In_ int nIDListBox, _In_ int nIDStaticPath, _In_ UINT nFileType) throw()
20: {
21: ATLASSERT(::IsWindow(m_hWnd));
22: return ::DlgDirList(m_hWnd, lpPathSpec, nIDListBox, nIDStaticPath, nFileType);
23: }
24: ...
25: BOOL SetDlgItemText(int nID, LPCTSTR lpszString) throw()
26: {
27: ATLASSERT(::IsWindow(m_hWnd));
28: return ::SetDlgItemText(m_hWnd, nID, lpszString);
29: }
30: };
这种实现效率上不高,CWindow是一个庞大的类,包含了很多的help functions。如,每次传入一个子控件ID,windows将会查找以得到HWND,然后再调用实际的处理函数,SetDlgItemText的【API】实现“应该”如下:
1: BOOL SetDlgItemText(HWND hwndParent, int nID, LPCTSTR lpszString)
2: {
3: HWND hwndChild = ::GetDlgItem(hwndParent, nID);
4: if (!hwndChild) return FALSE;
5: return ::SetWindowText(HWND, lpszString);
6: }
也即,首先根据子控件ID找到HWND, 然后调用实际的窗口方法。所以,在CWindow中实现的这个help function并不高效,每次调用都有一个查找的步骤。更好的方法是在使用时首先缓存每个控件的HWND,然后调用时就省去这个查找的步骤。使用时如下:
1: LRESULT CStringDlg :: OnInitDialog(UINT, WPARAM, LPARAM, BOOL&)
2: {
3: ::CenterWindow();
4:
5: //cache the HWNDs
6: m_edit.Attach(GetDlgItem(IDC_STRING));
7: m_ok.Attach(GetDlgItem(IDOK));
8:
9: //then we can use it like this
10: m_edit.SetWindowText(m_sz);
11: ...
12: return 1;
13: }
14:
15: LRESULT CStringDlg :: OnOK(WORD, UINT, HWND, BOOL&)
16: {
17: m_edit.GetWindowText(m_sz, lengthof(m_sz));
18:
19: ::EndDialog(IDOK);
20: return 0;
21: }
在OnInitDialog中绑定HWNDs,然后在其他方法中就可以直接调用了。【当然,以上方法需要在类中具体实现,实现很简单,就是直接调用win32API了】
A Better Class of Wrappers
带edit control的对话框无论怎么写,实现起来都很简单。而当使用listbox等控件时,就不只是简单调用::SetWindowText了。操作listbox就要用到Windows messages了。例如填充一个listbox并选中的代码如下:
1: LRESULT CStringListDlg :: OnInitDialog(UINT, WPARAM, LPARAM, BOOL&)
2: {
3: ::CenterWindow();
4:
5: //cache list box HWND
6: m_lb.Attach(GetDlgItem(IDC_LIST));
7:
8: //fullfill the listbox
9: m_lb.SendMessage(LB_ADDSTRING, 0, (LPARAM)__T("Hello, ATL"));
10: m_lb.SendMessage(LB_ADDSTRING, 0, (LPARAM)__T("Ain't ATL cool?"));
11: m_lb.SendMessage(LB_ADDSTRING, 0, (LPARAM)__T("ATL foooooo"));
12:
13: //Set initial selection
14: int n = m_lb.SendMessage(LB_FINDSTRING, 0, (LPARAM)m_sz);
15: if (n == LB_ERR) n = 0;
16: m_lb.SendMessage(LB_SETCURSEL, n);
17:
18: return 1;
19: }
ATL中CWindows确实提供了很多wrapper函数,但是对于windows内置的控件却并没有提供,如listbox等。然而,ATL非官方的提供了一些这样的类,在atlcontrols.h中。例如CListBox,其实现如下:
1: //取自WTL8.0实现
2: template <class TBase>
3: class CListBoxT : public TBase
4: {
5: public:
6: // Constructors
7: CListBoxT(HWND hWnd = NULL) : TBase(hWnd)
8: { }
9:
10: CListBoxT< TBase >& operator =(HWND hWnd)
11: {
12: m_hWnd = hWnd;
13: return *this;
14: }
15:
16: HWND Create(HWND hWndParent, ATL::_U_RECT rect = NULL, LPCTSTR szWindowName = NULL,
17: DWORD dwStyle = 0, DWORD dwExStyle = 0,
18: ATL::_U_MENUorID MenuOrID = 0U, LPVOID lpCreateParam = NULL)
19: {
20: return TBase::Create(GetWndClassName(), hWndParent, rect.m_lpRect, szWindowName, dwStyle, dwExStyle, MenuOrID.m_hMenu, lpCreateParam);
21: }
22:
23: // Attributes
24: static LPCTSTR GetWndClassName()
25: {
26: return _T("LISTBOX");
27: }
28:
29: // for entire listbox
30: int GetCount() const
31: {
32: ATLASSERT(::IsWindow(m_hWnd));
33: return (int)::SendMessage(m_hWnd, LB_GETCOUNT, 0, 0L);
34: }
35:
36: #ifndef _WIN32_WCE
37: int SetCount(int cItems)
38: {
39: ATLASSERT(::IsWindow(m_hWnd));
40: ATLASSERT(((GetStyle() & LBS_NODATA) != 0) && ((GetStyle() & LBS_HASSTRINGS) == 0));
41: return (int)::SendMessage(m_hWnd, LB_SETCOUNT, cItems, 0L);
42: }
这样前面的listbox填充并选中的代码可以改写如下:
1: LRESULT CStringListDlg :: OnInitDialog(UINT, WPARAM, LPARAM, BOOL&)
2: {
3: ::CenterWindow();
4:
5: //cache list box HWND
6: m_lb.Attach(GetDlgItem(IDC_LIST));
7:
8: //fullfill the listbox
9: m_lb.AddString(__T("Hello,ATL"));
10: //m_lb.SendMessage(LB_ADDSTRING, 0, (LPARAM)__T("Hello, ATL"));
11: ...
12: //Set initial selection
13: int n = m_lb.FindString(0, m_sz);
14: if(n ==LB_ERR) n = 0;
15: m_lb.SetCurSel(n);
16:
17: return 1;
18: }
而WTL则对这部分进行了完善,实现了所有的控件类。
CContainedWindow
CContainedWindow可以使父窗口处理其子窗口传递过来的消息,从而可以将消息处理函数集中放到父窗口中。父窗口即可以主动创建这样的子窗口,也可以使用已创建好的然后将其子类化(subclassing)实现父窗口处理子窗口消息的方法是使用Altmessage maps。每个CContainedWindow都包含了一个message map ID,由此实现将消息转移到父窗口的消息映射中。
1: template <class TBase /* = CWindow */, class TWinTraits /* = CControlWinTraits */>
2: class CContainedWindowT : public TBase
3: {
4: public:
5: CWndProcThunk m_thunk;
6: LPCTSTR m_lpszClassName;
7: WNDPROC m_pfnSuperWindowProc;
8: CMessageMap* m_pObject;
9: DWORD m_dwMsgMapID;
10: const _ATL_MSG* m_pCurrentMsg;
11:
12: // If you use this constructor you must supply
13: // the Window Class Name, Object* and Message Map ID
14: // Later to the Create call
15: CContainedWindowT() : m_pCurrentMsg(NULL)
16: { }
17:
18: CContainedWindowT(LPTSTR lpszClassName, CMessageMap* pObject, DWORD dwMsgMapID = 0)
19: : m_lpszClassName(lpszClassName),
20: m_pfnSuperWindowProc(::DefWindowProc),
21: m_pObject(pObject), m_dwMsgMapID(dwMsgMapID),
22: m_pCurrentMsg(NULL)
23: { }
24: ...
CContainedWindow即非继承自CWindowImpl也没有继承CMessageMap,所以CContainedWindow对象没有消息映射,而是通过WindowProc静态成员函数传递给了父窗口,message map ID是由构造函数活着Create函数提供。CContainedWindow提供了很多构造函数和Create方法。举个应用例子,我们是一个edit control只接收字母,实现如下:
1: class CMainWindow :
2: public CWindowImpl<CMainWindow, CWindow, CMainWindowTraits>
3: {
4: public:
5: ...
6: BEGIN_MESSAGE_MAP(CMainWindow)
7: ...
8: //Handle the child edit controls' messages
9: ALT_MSG_MAP(2013) //message map ID
10: MESSAGE_HANDLER(WM_CHAR, OnEditChar)
11: END_MSG_MAP()
12:
13: LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&)
14: {
15: //Create the contained window, routing its message to us
16: if(m_edit.Create("edit", this, 2013, m_hWnd, &CWindow::rcDefault)) {
17: return 0;
18: }
19: /* 第一个参数是控件名,第二个参数是CMessageMap的指针,这样子窗口的的消息才能找到消息处理映射,第三个是消息映射ID, 第四个是HWND。*/
20: return -1;
21: }
22:
23: //child edit message handler
24: LRESULT OnEditChar(UINT, WPARAM, LPARAM, BOOL &bHandled)
25: {
26: if (isalpha((TCHAR)wparam)) bHandled = FALSE;
27: else return 0;
28: }
29: private:
30: CContainedWindow m_edit;
31: };
注意在OnEditChar中,将是字母的输入设为消息未处理,从而可以让CContainedWindow对应控件的默认消息处理procedure来进行响应;而对于非字母输入,则bHandled为TRUE,消息不再传递下去,表现出来就是该字符被忽略。
Subclassing Contained Windows
如果是包含一个已经创建的子控件,就需要用到子类化功能。之前已经讲到了超类化实现了窗口类的继承,而子类化功能则更加温和并且更加常用。子类化不需要完全创建一个新类,而只是对原有窗口的一些消息进行hack,功能表现更像一个filter。子类化的实现是通过创建一个特定的窗口对象,然后替换掉其窗口过程SetWindowLong(GWL_WNDPROC)。替换的窗口过程首先接收所有的消息,然后再决定是否让原有的窗口过程是否也处理。可以认为,超类化是一个类的特化,而子类化是一个对象实例的特化。子类化常用在窗口子控件上,如:
1: class CLetterDlg : public CDialogImpl<CLetterDlg>
2: {
3: public:
4: //Set the CMessageMap* and the message map ID
5: CLetterDlg(): m_edit(this, 2013) {}
6:
7: BEGIN_MESSAGE_MAP(CLetterDlg)
8: ...
9: ALT_MSG_MAP(2013)
10: MESSAGE_HANDLER(WM_CHAR, OnEditChar)
11: END_MSG_MAP()
12:
13: enum {IDD = IDD_LETTER_ONLY};
14:
15: LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&)
16: {
17: //Subclass the exiting child edit control
18: m_edit.SubclassWindow(GetDlgItem(IDC_EDIT));
19:
20: return 1;
21: }
22: ...
23:
24: private:
25: CContainedWindow m_edit;
26:
27: };
因为没有窗口创建的步骤,所以只能在构造函数中将CMessageMap指针和message map ID传递给CContainedWindow对象(CContainedWindow就是一个辅助类),这样在WM_INITDIALOG中只需要获得窗口句柄即可了。那么子类化过程是如何实现的?
1: BOOL SubclassWindow(HWND hWnd)
2: {
3: BOOL result;
4: ATLASSUME(m_hWnd == NULL);
5: ATLASSERT(::IsWindow(hWnd));
6:
7: result = m_thunk.Init(WindowProc, this);
8: if (result == FALSE)
9: {
10: return result;
11: }
12:
13: WNDPROC pProc = m_thunk.GetWNDPROC();
14: WNDPROC pfnWndProc = (WNDPROC)::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)pProc);
15: if(pfnWndProc == NULL)
16: return FALSE;
17: m_pfnSuperWindowProc = pfnWnd
Proc;
18: m_hWnd = hWdnd;
19: return TRUE;
20: }
首先使用thunk技术获得当前父窗口的WndProc保存其中,然后使用SetWindowLongPtr把父窗口的WndProc植入,这样子窗口的消息首先进入父窗口的消息映射中。同时还cache了子窗口的WndProc,可以用来处理父窗口未处理的消息。
Containing the Windows Control Wrappers
在CContainedWindow中默认把CWindow当作了基类,当然不必非这样做。同样可以对ATL Windows control wrapper classes作为基类来创建contained window。如:
1: CContainedWindowT<ATLControls::CEdit> m_edit;
这对于使用Create来创建子类窗口时比较方便,若不这样,就需要在Create中传入window class的字符串名。而是用模板类,则可以自动获取,通过基类(如CEdit)所提供的GetWndClassName成员函数来获得,其实现如下:
1: static LPCTSTR GetWndClassName()
2: {
3: return _T("LISTBOX");
4: }
这样,创建子类化控件时,无需那么多参数了:
1: LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&)
2: {
3: //Create the contained window, routing its message to us
4: if(m_edit.Create( this, 2013, m_hWnd, &CWindow::rcDefault)) {
5: return 0;
6: }
7:
8: return -1;
9: }
后记:基本是《ATL技术内幕》第九章的读书笔记(翻译)。可以看到ATL所提供的窗口框架基本已经成型,WTL只是后续做了一些扩展和完善。通过阅读这一部分,对中间的消息流算是比较清楚了,还有各种类为何如此设计,整个过程有了比较清晰的理解。由于本人对Windows窗口编程本身的知识掌握的都不甚完善,所以只是尽最大努力写出自己的理解和见解。后续若有空,再进行WTL中代码的分析,以及自定义控件等进行学习。