ATL & WTL 实现分析(五)

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中代码的分析,以及自定义控件等进行学习。

转载于:https://www.cnblogs.com/macrolee/archive/2013/04/22/3036609.html

你可能感兴趣的:(ATL & WTL 实现分析(五))