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根据不同的控件会有不同的组成。