ATL & WTL 实现分析 (三)

超类化

Windows窗口对象模型定义窗口类并创建窗口对象实例,和c++的对象模型十分相似。HWND之于WNDCLASSEX结构体正如同this指针之于c++的类。如果仍然按照c++的面向对象模型来进一步用于Windows,那么我们也可以延伸出继承这个特性:Windows superclassing。

超类化技术可以复制一个已有窗口类的WNDCLASSEX结构体并可赋予一个新的命名,重新给定一个WndProc。当窗口接收到一个消息,首先传给这个新的WndProc,如果这个新的Proc未处理这个消息,则该消息再由原来的WndProc来处理,而非直接交给DefWndProc。可以将原来的WndProc想象成为基类中一个虚函数,超类化继承了这个基类并重新定义了该虚函数,新实现的这个虚函数中,你可以决定是否再访问基类的方法或者直接宣告结束。显然,超类化作为Windows编程的面向对象实现是不可或缺的,我们可以继承一个已有的窗口类,并重写或改写基类已有的方法。

ATL对超类化的支持通过DECLARE_WND_SUPERCLASS宏实现:

 1:  #define DECLARE_WND_SUPERCLASS(WndClassName, OrigWndClassName) \
 2:  static ATL::CWndClassInfo& GetWndClassInfo() \
 3:  { \
 4:      static ATL::CWndClassInfo wc = \
 5:      { \
 6:          { sizeof(WNDCLASSEX), 0, StartWindowProc, \
 7:            0, 0, NULL, NULL, NULL, NULL, NULL, WndClassName, NULL }, \
 8:          OrigWndClassName, NULL, NULL, TRUE, 0, _T("") \
 9:      }; \
10:      return wc; \
11:  }

和DECLARE_WND_CLASS的唯一区别,是增加了一个被继承的原窗口类的名称,而窗口类名称在ATL中常为NULL。使用超类化很简单:

 

 1:  class CLetterBox : public CWindowImpl<CLetterBox> {
 2:  public:
 3:      DELCARE_WND_SUPCLASS(0, "EDIT");
 4:   
 5:      virtual BOOL
 6:      ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, 
 7:                          LPARAM lParam, LRESULT &lResult, DWORD /*dwMsgMapID*/)
 8:      {
 9:          BOOL bHandled = TRUE;
10:          switch (uMsg) {
11:          case WM_CHAR :
12:              lResult = OnChar((TCHAR)wParam, bHandled);break;
13:          default:
14:              bHandled = FALSE; break;
15:          }
16:   
17:          return bHandled;
18:      }
19:   
20:  private:
21:      LRESULT OnChar(TCHAR c, BOOL &bHandled) {
22:          if (isalpha(c)) bHandled = FALSE;
23:          else MessageBeep(0xFFFFFFFF);
24:          return 0;
25:      }
26:  };

虽然超类化看似强大,实则用的很少,更好的替代方案是subclassing,在随后的CContainedWindow中再做详细描述。

消息处理

注册窗口类的任务主要在于提供WndProc,正是WndProc对不同的消息的响应决定了程序的行为。前面的分析中,我们已经看到ATL如何将在默认的WndProc中将消息传递给继承自CWindowImpl的窗口对象的ProcessWindowMessage成员函数,也看到了该函数书写的乏味,现在看看ATL\WTL如何通过宏BEGIN_MSG_MAP和END_MSG_MAP来实现该成员函数的实现框架:

 1:  #define BEGIN_MSG_MAP(theClass) \
 2:  public: \
 3:      BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult, DWORD dwMsgMapID = 0) \
 4:      { \
 5:          BOOL bHandled = TRUE; \
 6:          (hWnd); \
 7:          (uMsg); \
 8:          (wParam); \
 9:          (lParam); \
10:          (lResult); \
11:          (bHandled); \
12:          switch(dwMsgMapID) \
13:          { \
14:          case 0:

只不过将这些模板代码封装到宏里,简化书写,减少出错。

1:  #define END_MSG_MAP() \
2:              break; \
3:          default: \
4:              ATLTRACE(ATL::atlTraceWindowing, 0, _T("Invalid message map ID (%i)\n"), dwMsgMapID); \
5:              ATLASSERT(FALSE); \
6:              break; \
7:          } \
8:          return FALSE; \
9:      }

注意到这个宏是一个巨大的switch-case语句,很容易以为swich的是消息,但实际上是一个message map ID。消息处理宏块不仅仅可以处理父窗口消息(当dwMsgMapID = 0,即当前这些消息响应成员函数所在的窗口对象),也可以处理子窗口消息(dwMsgMapID非0,此时实际是通过ALT_MSG_MAP宏来转移)稍后分析

通用消息处理

窗口所要处理的每个消息对应于消息映射中的一个entry。最简单的entry是MESSAGE_HANDLER,它仅处理一条消息:

1:  #define MESSAGE_HANDLER(msg, func) \
2:      if(uMsg == msg) \
3:      { \
4:          bHandled = TRUE; \
5:          lResult = func(uMsg, wParam, lParam, bHandled); \
6:          if(bHandled) \
7:              return TRUE; \
8:      }

如果要处理某个区间内的消息,则用MESSAGE_RANGE_HANDLER,仅需改动第2条语句为:

1:  if(uMsg >= msgFirst && uMsg <= msgLast) 

使用消息映射宏后的ProcessWindowMessage程序即成为如下形式:

1:  BEGIN_MESSAGE_MAP (CMainWindow)
2:      MESSAGE_HANDLER (WM_PAINT, OnPaint)
3:  END_MSG_MAP (CMainWindow)

很熟悉,不是吗?而实际上也没有什么神秘。注意到在MESSAGE_HANDLER中,默认消息已经被处理,即bHandled = TRUE。但是你可以将其设为FALSE,这在超类化和子类化中会有用,也可以让随后的entrys来进行处理(比如message map chaining中)。如果最终消息也没被处理,则被交给DefWindowProc。还需要注意的一点是消息处理函数的“签名”:

1:  LRESULT MessageHandler (UINT nMsg, WPARAM wparam, LPARAM lparam, BOOL& bHandled);

虽然使用了message handler,但使用时仍然需要从消息中crack出自己要使用的参数:

1:  LRESULT OnPaint (UINT nMsg, WPARAM wparam, LPARAM lparam, BOOL& bHandled)
2:  {
3:      HDC hdc = (HDC)wparam;
4:      ...
5:  }

这显然仍是一个繁重的工作,因为windows中至少有300个标准消息,每个消息都有自己对WPARAM和LPARAM的解释。“希望后续的ATL版本能够提供对此解决方案。

WMCOMMAND & WM_NOTIFY

windows为以上二者提供了cracking assistance。窗口控件正是通过二者与其父窗口进行通信的。知识补充:窗口控件并不是OLE或者ActiveX控件,标准窗口控件是由windows提供的一些窗口子类,如btton、scrollbars、edit boxes、list boxes、combo boxes(windows1.0开始提供);toolbars、status bars、tree views、list views、rich tet edit boxes(windows95开始提供);IE、rebars、date picker IP address control。

窗口控件的创建和创建其它窗口几乎一样,唯一不同在于需要提供正确的窗口类名,如”EDIT“。(这些就交给OS去解析)与子窗口控件交流的方式也是SendMessage并附以相应参数(主动通信?),或者通过窗口消息进行通信,即WMCOMMAND和WM_NOTIFY(被动,对OS的响应?)。这些消息提供了足以包装在WPARAM和LPARAM来描述响应事件。

1:  WM_COMMAND
2:      wNotifyCode = HIWORD(wParam);//notification code
3:      wID         = LOWORD(wParam);//item, control or accelerator identifier
4:      hwndCtl     = (HWND)LPARAM;  //handle of control
5:   
6:  WM_NOTIFY
7:      idCtrl      = (int)WPARAM;   //等于pnmh->idFrom
8:      pnmh        = (LPNMHDR)lParam;
1:  typedef struct tagNMHDR
2:  {
3:      HWND      hwndFrom;     //handle of contrl  --> WM_COMMAND:hwndCtrl
4:      UINT_PTR  idFrom;       //control identifier—>             wID?
5:      UINT      code;         //notification code –>             wNotifyCode
6:  }   NMHDR;

COMMAND消息是早期出现的,随着windows进化发现WPARAM和LPARAM已经不足以包括所有消息中要传递的信息。所以后来给出了更强大的WM_NOTIFY,扩充在于令lParam指向了一个NMHDR结构体,”间接“所以可以传递更多。注意到NMHDR结构的域对应于WM_COMMAND解析出来的所有信息。(而结构体是可以任意扩充的,只要向尾部追加)。

ALT提供的对WM_COMMAND和WM_NOTIFY的crack如下:

1:  #define COMMAND_HANDLER(id, code, func) \
2:      if(uMsg == WM_COMMAND && id == LOWORD(wParam) && code == HIWORD(wParam)) \
3:      { \
4:          bHandled = TRUE; \
5:          lResult = func(HIWORD(wParam), LOWORD(wParam), (HWND)lParam, bHandled); \
6:          if(bHandled) \
7:              return TRUE; \
8:      }

1:  #define NOTIFY_HANDLER(id, cd, func) \
2:      if(uMsg == WM_NOTIFY && id == ((LPNMHDR)lParam)->idFrom && cd == ((LPNMHDR)lParam)->code) \
3:      { \
4:          bHandled = TRUE; \
5:          lResult = func((int)wParam, (LPNMHDR)lParam, bHandled); \
6:          if(bHandled) \
7:              return TRUE; \
8:      }

可以通过这些参数(控件的ID,控件所发送的command或notify)直接对应到相应消息响应函数。消息响应函数签名如下:

1:  LRESULT CommandHandler(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled);
2:  LRESULT NotifyHandler(int idCtrl, LPNMHDR pnmh, BOOL& bHandled);

注意,在编写CommandHandler时,实际上WPARAM中的参数我们都是已知的,就对应于message map中entry的参数,只有LPARM中的参数是需要得到的;在使用NotifyHandler时类似。可以认为消息映射是一个先分解参数(用于找到回调响应消息响应函数)再还原参数(消息响应函数中得到的仍然是原封不动的lPARAM和WPARAM)过程。

处理edit control(windows1.0已有)的entry如下:

1:  COMMAND_HANDLER(IDC_EDIT1, EN_CHANGE, OnEditChange)

处理toolbar control(window95后又)的entry如下:

1:  NOTIFY_HANDLER(IDC_TOOLBAR1, TBN_BEGINDRAG, OnToolbarBeginDrag)

 

menu control不同于大多数的控件,它不是用子窗口ID作为第一个参数,而是用menu item的ID(实际上menu control的句柄在窗口创建时就已经赋给了窗口。),并且不需要关注code,如:

1:  COMMAND_HANDLER(ID_FILE_ABOUT, 0, OnHelpAbout)

其它类似menu control不需要关注所发送的code,即对该控件的所有事件都响应,我们可以使用COMMAND_ID_HANDLER或NOTIFY_ID_HANDLER:

1:  #define COMMAND_ID_HANDLER(id, func) \
2:      if(uMsg == WM_COMMAND && id == LOWORD(wParam)) \
3:      { \
4:          bHandled = TRUE; \
5:          lResult = func(HIWORD(wParam), LOWORD(wParam), (HWND)lParam, bHandled); \
6:          if(bHandled) \
7:              return TRUE; \
8:      }

此时menu control的map entry可写为:

1:  COMMAND_ID_HANDLER(ID_FILE_ABOUT, onHelpAbout)

还可以映射某一个范围的control到一个entry中,COMMAND_RANGE_HANDLER, NOTIFY_RANGE_HANDLER:

1:  #define NOTIFY_RANGE_HANDLER(idFirst, idLast, func) \
2:      if(uMsg == WM_NOTIFY && ((LPNMHDR)lParam)->idFrom >= idFirst && ((LPNMHDR)lParam)->idFrom <= idLast) \
3:      { \
4:          bHandled = TRUE; \
5:          lResult = func((int)wParam, (LPNMHDR)lParam, bHandled); \
6:          if(bHandled) \
7:              return TRUE; \
8:      }

还可以在不考虑控件ID的情况下处理同一个消息code,这对于用同一个handler处理多个控件的情况十分有用。COMMAND_CODE_HANDLER, NOTIFY… :

1:  #define NOTIFY_CODE_HANDLER(cd, func) \
2:      if(uMsg == WM_NOTIFY && cd == ((LPNMHDR)lParam)->code) \
3:      { \
4:          bHandled = TRUE; \
5:          lResult = func((int)wParam, (LPNMHDR)lParam, bHandled); \
6:          if(bHandled) \
7:              return TRUE; \
8:      }

然后,你可以在消息响应函数内部,根据WPARM解析出相应的控件ID并作相应处理。

TIPS:WM_COMMAND vs. WM_NOTIFY

在windows1.0~windows3.x中,只存在WM_COMMAND。然而,当后来的shell team(补充:无论是GUI还是command interface都属于UI,即shell)构建新的控件时,他们想在送出的消息中包含除control ID、notifition code之外更多的信息,不幸的是WPARAM和LPARAM所有位都已用完。所以他们”发明“了新的消息WM_NOTIFY(窃以为这种做法不可取,应该是向后兼容,同时存在COMMAND和NOTIFY让新手很是迷惑,如我。),WPARAM保存控件ID,NMHDR保存其它消息(定义见前面),NMHDR实际上是一个结构体头(可以认为NMHDR是基类,实际不同的消息继承自它;也可认为它是协议头),其后可以根据不同的控件附着不同的消息在其尾部。例如,当接收到TBN_BEGIN_DRAG时,NMHDR实际指向的是一个NMTOOLBAR结构体:

 1:  typedef struct tagNMTOOLBARW {
 2:      NMHDR   hdr;
 3:      int     iItem;
 4:      TBBUTTON tbButton;
 5:      int     cchText;
 6:      LPWSTR   pszText;
 7:  #if (_WIN32_IE >= 0x500)
 8:      RECT    rcButton;
 9:  #endif
10:  } NMTOOLBARW, *LPNMTOOLBARW;

使用时,实际上是LPARM->NMHDR->NMTOOLBAR这样一个转换过程。

 

小结:

1、超类化,实现了窗口的”继承“并可定制相应的消息相应,不过实际中更多用的是CContainedWindow。

2、介绍了ATL对实际窗口过程ProcessWindowMessage的封装:

     BEGIN_MSG_MAP

          MESSAGE_[RANGE_]HANDLER                   //过滤uMsg

          [COMMAND | NOTIFY]_HANDLER             //uMsg固定,过滤ID、Code

          [COMMAND | NOTIFY]_ID_HANDLER        //uMsg固定,过滤ID

          [COMMAND | NOTIFY]_RANGE_HANDLER//uMsg固定,过滤ID(范围)

          [COMMAND | NOTIFY]_CODE_HANLDER  //uMsg固定,过滤Code(范围)

     END_MSG_MAP

3、分析了消息的参数,WM_COMMAND(notification code, ID, handle of control), WM_NOTIFY(ID, NMHDR ptr),而NMHDR根据不同的控件会有不同的组成。

posted @ 2013-04-05 14:27  MacroLee  阅读(517)  评论(0编辑  收藏  举报