ATL & WTL 实现分析(四)
消息链(Message chaining)
当我们一次又一次用相同的方式来处理消息时,一定想到了重用消息处理的实现。一种直接而简单的想法实现如下:
1: template <typename Deriving>
2: class CFileHandler {
3: public:
4: LRESULT OnFileNew(WORD, WORD, HWND, BOOL&);
5: LRESULT OnFileOpen(WORD, WORD, HWND, BOOL&);
6: ...
7: };
8:
9: class CMainWindow :
10: public CWindowImpl<CMainWindow, CWindow, CMainWinTraits>,
11: public CFileHandler<CMainWindows>
12: {
13: public:
14: BEGIN_MSG_MAP(CMainWindow)
15: COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew)
16: COMMAND_ID_HANDLER(ID_FILE_OPEN, OnFileOpen)
17: ...
18: END_MSG_MAP
19: };
然而,当我们想要扩充基类的方法时,不得不修改每个继承自它的子类中的entry。显然很不方便。我们想要的是既继承消息处理函数,也继承消息映射,ATL提供了“消息链“(message chaining)正是为此。最简单的消息链宏如下:
1: #define CHAIN_MSG_MAP(theChainClass) \
2: { \
3: if(theChainClass::ProcessWindowMessage(hWnd, uMsg, wParam, lParam, lResult)) \
4: return TRUE; \
5: }
使用时,基类的实现如下:
1: template <typename Deriving>
2: class CFileHandler {
3: public:
4: //message map in base class
5: BEGIN_MSG_MAP(CFileHandler<Deriving>)
6: COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew)
7: COMMAND_ID_HANDLER(ID_FILE_OPEN, OnFileOpen)
8: ...
9: END_MSG_MAP()
10:
11: LRESULT OnFileNew(WORD, WORD, HWND, BOOL&);
12: LRESULT OnFileOpen(WORD, WORD, HWND, BOOL&);
13: ...
14: };
在子类中使用消息链的方法:
1: class CMainWindow :
2: public ...
3: public CFileHandler<CMainWindow>
4: {
5: public:
6: BEGIN_MSG_MAP(CMainWindow)
7: ...
8: CHAIN_MSG_MAP(CFileHandler<CMainWindow>)
9: END_MSG_MAP()
10: ...
11: };
任何实现了ProcessWindowMessage的基类都可以被用于消息链,但此时基类仅仅是”单独“处理消息,与子类完全无关。而上面例子中,子类使用自己来参数化基类,利用模板的特性实现了”多态“,此时基类的消息处理函数定义方法如下:
1: template <typename Deriving>
2: LRESULT CFileHandler<Deriving>::OnFileExit(WORD, WORD, HWND, BOOL&)
3: {
4: static_cast<Deriving*>(this)->DestroyWindow();
5: ...
6: }
通过static_cast实现了编译期的多态,避免的c++多态通过查vtable的方式,节省空间和时间,这个是理解WTL的关键,也是里面最常用的trick。可以将任意多的基类加入到消息链中,实际效果类似下图所示:
不仅仅是基类可以加入消息链,数据成员也可以作为消息的handler,当然类似上面的static_cast实现的多态效果是不可能了。使用数据成员的消息链宏及使用方法如下:
1: #define CHAIN_MSG_MAP_MEMBER(theChainMember) \
2: { \
3: if(theChainMember.ProcessWindowMessage(hWnd, uMsg, wParam, lParam, lResult)) \
4: return TRUE; \
5: }
而数据成员所属类的定义应如下,此时类中应包含可被子类实例化的数据成员的指针:
1: template <typename TWindow>
2: class CFileHandler {
3: public:
4: //message map in base class
5: BEGIN_MSG_MAP(CFileHandler<Deriving>)
6: COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew)
7: COMMAND_ID_HANDLER(ID_FILE_OPEN, OnFileOpen)
8: ...
9: END_MSG_MAP()
10:
11: //constructor
12: CFileHandler(TWindow* pwnd) :m_pwnd(pwnd) {}
13:
14: LRESULT OnFileNew(WORD, WORD, HWND, BOOL&);
15: LRESULT OnFileOpen(WORD, WORD, HWND, BOOL&);
16: ...
17: private:
18: TWindow* m_pwnd; //pointer 缓存了链入对象中,该类的指针。
19: };
1: template <typename TWindow>
2: LRESULT CFileHandler<TWindow>::OnFileExit(WORD, WORD, HWND, BOOL&)
3: {
4: m_pwnd->DestroyWindow();
5: ...
6: }
使用数据成员消息链的类,实现如下:
1: public:
2: ...
3: CHAIN_MSG_MAP_MEMBER(m_handlerFile)
4: ...
5:
6: CMainWindow() :m_handlerFile(this) {}
7: private:
8: CFileHandler<CMainWindow> m_handlerFile;
9: ...
10:
也就是说,我们想要使用数据成员的消息映射,那么必须在构造函数中将自己的指针传给消息链数据成员,只有这样它才能在自己的消息处理函数中使用这个指针(怎么看着像设计模式里的什么东东?)
交替消息映射(Alternate Message Map)
消息映射还可以被分割成多个段,每一段都是一个alternate message map。在(分析二),介绍BEGIN_MSG_MAP时,提到了消息分发的switch-case语句switch的并不是消息,而是一个叫dwMsgMapID的东东。而我们之前提到的所有消息映射宏都默认的dwMsgMapID=0,而实际上BEGIN_MSG_MAP宏展开时,该参数默认被赋值0。一旦这个参数不为0又会怎样?显然消息首席据该ID进行分发,然后才是消息具体处理。
1: #define ALT_MSG_MAP(msgMapID) \
2: break; \
3: case msgMapID:
显然适合放到消息映射的最后,否则会打乱预期执行。使用如下:
1: BEGIN_MSG_MAP(CView)
2: MESSAGE_HANDLER(WM_PAINT, OnPaint)
3: ...
4: ALT_MSG_MAP(1)
5: COMMAND_HANDLER(ID_EDIT_COPY)
6: END_MSG_MAP(CView)
为将消息处理链接alternate到其它消息map,ATL提供两个宏:
1: #define CHAIN_MSG_MAP_ALT(theChainClass, msgMapID) \
2: { \
3: if(theChainClass::ProcessWindowMessage(hWnd, uMsg, wParam, lParam, lResult, msgMapID)) \
4: return TRUE; \
5: }
6:
7: #define CHAIN_MSG_MAP_ALT_MEMBER(theChainMember, msgMapID) \
8: { \
9: if(theChainMember.ProcessWindowMessage(hWnd, uMsg, wParam, lParam, lResult, msgMapID)) \
10: return TRUE; \
11: }
一个是到类,一个是到数据成员。后者使用如下:
1: class CMainWindow :...
2: {
3: public:
4: BEGIN_MSG_MAP(CMainWindow)
5: MESSAGE_HANDLER(WM_PAINT, OnPaint)
6: MESSAGE_HANDLER(WM_CREATE, OnCreate)
7: ...
8: //route unhandled message to child window
9: CHAIN_MSG_MAP_MEMBER(m_view, 1)
10: END_MSG_MAP()
11: ...
12: LRESULT OnCreate(UNIT, WPARAM, LPARAM, BOOL&) {
13: m_view.create(...);
14: ...
15: }
16: private:
17: CView m_view;
18: };
未处理的消息会由成员m_view来处理,且必须是CView类的dwMpaID = 1的部分才能处理。(Alternate的这个用法真的很古怪,之前也没真正用过这个,没有很清晰的认识)
动态链入(Dynamic Chaining)
消息映射可以链入基类或者数据成员有用,但不够灵活。如果发出消息的窗口,和处理消息的部分是松耦合的,将会更加易用。例如在MFC中,WM_COMMAND消息路由就是如此,view首先接收全部的WM_COMMAND消息,然后document处理和文件部分相关的消息。ATL提供了一个这样的宏来试图提供这样的功能:
1: #define CHAIN_MSG_MAP_DYNAMIC(dynaChainID) \
2: { \
3: if(CDynamicChain::CallChain(dynaChainID, hWnd, uMsg, wParam, lParam, lResult)) \
4: return TRUE; \
5: }
动态链入支持两个对象同时处理一个消息,如果第一个对象未处理,则交由第二个进行处理。这一关系是由dynamic chain ID来建立的,并且主消息处理对象通过这个ID来寻找第二消息处理对象来处理未处理的消息。欲实现这样的动态链关系,第二个消息处理对象必须继承自CMessageMap:
1: class ATL_NO_VTABLE CMessageMap
2: {
3: public:
4: virtual BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
5: LRESULT& lResult, DWORD dwMsgMapID) = 0;
6: };
CMessageMap的存在仅仅是保证继承自它的类必须实现ProcessWindowMessage的方法。比如,CWindowImpl同样继承自它。这里,只有使用CMessageMap类作为第二消息处理器的原因就是它可以放在ATL_CHAIN_ENTRY结构体中,并由主消息处理器控制:
1: struct ATL_CHAIN_ENTRY
2: {
3: DWORD m_dwChainID;
4: CMessageMap* m_pObject;
5: DWORD m_dwMsgMapID;
6: };
而主消息处理器则继承自CDynamicChain,这个类包含了ATL_CHAIN_ENTRY结构体。因此当它处理不了的消息就可以从该结构体中找到第二消息处理器进行处理,思想很简单。CDynamicChain提供了两个成员函数:
1: BOOL SetChainEntry(DWORD dwChainID, CMessageMap* pObject, DWORD dwMsgMapID = 0)
1: BOOL CallChain(DWORD dwChainID, HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult)
第一个用来设置ATL_CHAIN_ENTRY结构体,第二个。。。
动态链的一个应用是Document/View结构:主窗口作为框架,保存了菜单条等、一个document和一个view。view管理了主窗口的客户区并处理由document管理的数据的绘制(MVC?),此外view也相应和其相关的菜单命令,例如Edit|Copy。document用来管理当前的状态以及和其相关的菜单命令。当把菜单消息路由给view时,主窗口将使用alternate-message-map。然而,当欲将命令继续从view路由给document时,需要将二者hook到一起,这里可以使用SetChainEntry,任意未被view处理的消息将会通过第二消息处理器传递给document,最后未被处理的消息将会传递给DefWindowProc处理。【我竟然对文档/试图模式毫无概念,额……】代码框架如下:
1: //main window, act as the frame
2: class CMainWindow : public CWindowImpl<CMainWindow, CWindow, CMainWinTraits>
3: {
4: public:
5: //Handle main window message
6: BEGIN_MSG_MAP(CMainWindow)
7: MESSAGE_HANDLER(WM_CREATE, OnCreate)
8: ...
9: //Route unhandled message to the view
10: // (i)主窗口到view的消息传递用了chain message
11: CHAIN_MSG_MAP_ALT_MEMBER(m_view, 1) //主消息处理器:使用了前面介绍的chain message
12: COMMAND_ID_HANDLER(ID_HELP_ABOUT, OnHelpAbout)
13: END_MSG_MAP()
14:
15: CMainWindow() :m_doc(this), m_view(&m_doc) {
16: //Hook up the document to receive message frome view;
17: m_view.SetChainEntry(1, &m_doc, 1); //辅助消息处理器:view 和document使用了dynamic chain,设入口
18: //注意ID为,和后面的对应。
19: }
20:
21: private:
22: LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&)
23: {
24: return (m_view.Create(m_hWnd, CWindow::rcDefault) ? 0 : -1);
25: }
26:
27: LRESULT OnHelpAbout(WORD, WORD, HWND, BOOL&);
28: virtual void OnFinalMessage(HWND);
29:
30: ...
31: private:
32: //main window contains these two parts.
33: CDocument<CMainWindow> m_doc;
34: CView<CMainWindow> m_view;
35: };
36:
37: //view: 主消息处理器,处理不了的再传递给document
38: template <typename TMainWindow>
39: class CView :
40: public CWindowImpl<CView>,
41: public CDynamicChain //这样才是可以使用dynamic chain
42: {
43: public:
44: CView(CDocument<TMainWindow>* pdoc) :m_pdoc(pdoc) {
45: m_pdoc->SetString(__T("ATL Doc/View"));
46: }
47:
48: BEGIN_MSG_MAP(CView)
49: //Handle view message
50: MESSAGE_HANDLER(WM_PAINT, OnPaint)
51:
52: ALT_MSG_MAP(1) //处理来自主窗口chain过来的消息。
53: CHAIN_MSG_MAP_DYNAMIC(1) //若能自己处理自己处理,否则使用预设的第二处理器处理。
54: END_MSG_MAP()
55:
56: private:
57: LRESULT OnPaint(UINT, WPARAM, LPARAM, BOOL&);
58: private:
59: //View cache its own document
60: CDocument<TMainWindow> *m_pdoc;
61: };
62:
63: //Document: handle any message it receives from the main frame
64: template <typename TMainWindow>
65: class CDocument:
66: public CMessageMap//这样才可用作第二消息处理器。
67: {
68: public:
69: BEGIN_MSG_MAP(CDocument)
70: //Handle the message remained in View
71: ALT_MSG_MAP(1)
72: COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew)
73: COMMAND_ID_HANDLER(ID_FILE_OPEN, OnFileOpen)
74: ...
75: END_MSG_MAP()
76:
77: CDocument(TMainWindow *pwnd) : m_pwnd(pwnd) { *m_sz = 0;}
78: void SetString(LPCTSTR psz);
79:
80: private:
81: LRESULT OnFileNew(WORD, WORD, HWND, BOOL&);
82: LRESULT OnFileOpen(WORD, WORD, HWND, BOOL&);
83: ...
84:
85: private:
86: TMainWindow* m_pwnd;
87: TCHAR m_sz[64];
88: };