Windows消息
众所周知,Windows消息有两种:队列话消息和非队列话消息。队列话消息是被Windows操作系统放入消息队列的,程序通过主消息循环不断的从消息队列中取出消息并分发到各自的窗体调用消息处理函数处理。而非队列话消息是不传入消息队列,直接呼叫消息处理函数处理的。
WTL消息接收与分发
对于非队列话的消息,windows会直接调用对应窗体的消息处理函数进行处理,所以不需要进行任何包装,只要注册消息处理函数即可,这里不讨论。
在上一骗文章中讲到每个界面线程会对应一个CMessageLoop,在线程启动的时候加入到全局的_Module对象中(维护了一个ATL::CSimpleMap<DWORD, CMessageLoop*>类型的map)。在CMessageLoop内部维护了一个CMessageFilter和一个CIdleHandler的列表具体定义为:ATL::CSimpleArray<CMessageFilter*> m_aMsgFilter;
ATL::CSimpleArray<CIdleHandler*> m_aIdleHandler;
在任何地方都可以通过全局的_Module对象获得当前线程的CMessageLoop,通过调用AddMessageFilter方法向其中添加CMessageFilter,在主消息循环内部,会在调用TranslateMessage和DispatchMessage之前遍历CMessageFilter的列表,调用其内部唯一的成员函数PreTranslateMessage对消息进行处理。这是WTL消息处理的主要特性之一,所有实现了CMessageFilter的类都可以实现对队列话消息的处理,这大大增加了消息处理的灵活性和可封装性。
WTL的另一个特性是在消息循环内部添加了空闲处理,所有实现了CIdleHandler的类都可以通过AddIdleHandler添加到CIdleHandler列表内,在消息循环内部,当程序通过::PeekMessage检测消息队列中没有消息时,就会遍历这个列表,调用其中的OnIdle函数进行空闲处理。下面来看具体的调用过程:
1int Run()
2 {
3 BOOL bDoIdle = TRUE;
4int nIdleCount =0;
5 BOOL bRet;
6
7for(;;)
8 {
9//如果消息队列中没有消息,则进行空闲处理
10while(bDoIdle &&!::PeekMessage(&m_msg, NULL, 0, 0, PM_NOREMOVE))
11 {
12if(!OnIdle(nIdleCount++))
13 bDoIdle = FALSE;
14 }
15
16 bRet = ::GetMessage(&m_msg, NULL, 0, 0);
17
18if(bRet ==-1)
19 {
20 ATLTRACE2(atlTraceUI, 0, _T("::GetMessage returned -1 (error) "));
21continue; // error, don't process
22 }
23elseif(!bRet)
24 {
25 ATLTRACE2(atlTraceUI, 0, _T("CMessageLoop::Run - exiting "));
26break; // WM_QUIT, exit message loop
27 }
28//在分发消息之前,进行消息处理
29if(!PreTranslateMessage(&m_msg))
30 {
31 ::TranslateMessage(&m_msg);
32 ::DispatchMessage(&m_msg);
33 }
34
35if(IsIdleMessage(&m_msg))
36 {
37 bDoIdle = TRUE;
38 nIdleCount =0;
39 }
40 }
41
42return (int)m_msg.wParam;
43 }
PreTranslateMessage的实现:
1 // Override to change message filtering
2 virtual BOOL PreTranslateMessage(MSG* pMsg)
3 {
4 // loop backwards
5 for(int i = m_aMsgFilter.GetSize() -1; i >=0; i--)
6 {
7 CMessageFilter* pMessageFilter = m_aMsgFilter[i];
8 if(pMessageFilter != NULL && pMessageFilter->PreTranslateMessage(pMsg))
9 return TRUE;
10 }
11 return FALSE; // not translated
12 }
OnIdle函数的实现:
1 // override to change idle processing
2 virtual BOOL OnIdle(int/*nIdleCount*/)
3 {
4 for(int i =0; i < m_aIdleHandler.GetSize(); i++)
5 {
6 CIdleHandler* pIdleHandler = m_aIdleHandler[i];
7 if(pIdleHandler != NULL)
8 pIdleHandler->OnIdle();
9 }
10 return FALSE; // don't continue
11 }
代码灰常简单易懂,应该不需要再解释了。
以上是WTL的主消息循环,实现了对消息的接收、分发、分发前的消息处理和空闲处理。下面来看WTL窗体的消息处理。
WTL消息处理
上面提到的在主消息循环内部分发前的消息处理和空闲处理,较为简单,此处不再讨论。
在上篇文章中提到,windows在创建窗体的时候会要求提供一个消息处理函数,这个窗体的接收到的所有消息都会调用这个消息处理函数进行处理,他的定义为:
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
这个一个全局静态函数,这大大破坏了程序的封装性,因此WTL(确切的说应该是ATL)引入了Thunk机制来解决这个问题,具体流程如下:
首先每个窗体类内部都定义了一个CWndProcThunk对象,窗体在创建时,会注册WNDCLASS,在CWindowImpl内部是通过宏DECLARE_WND_CLASS(NULL)完成的,这里注册的消息处理函数为StartWindowProc。然后,通过调用全局_Module对象的AddCreateWndData方法对CWndProcThunk对象中的_AtlCreateWndData成员变量进行赋值并将它加入到全局_Module对象的链表中,这个对象内部保存了当前窗口对象的指针和当前线程ID。完成窗体创建之后,窗体接收到第一个消息,会调用StartWindowProc进行消息处理,StartWindowProc具体定义为:
1 template <class TBase, class TWinTraits>
2 LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
3 {
4 //获得当前对象指针
5 CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_Module.ExtractCreateWndData();
6 ATLASSERT(pThis != NULL);
7 pThis->m_hWnd = hWnd;
8 //Thunk初始化
9 pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);
10 WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk);
11 //设置新的消息处理函数
12 WNDPROC pOldProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc);
13 #ifdef _DEBUG
14 // check if somebody has subclassed us already since we discard it
15 if(pOldProc != StartWindowProc)
16 ATLTRACE2(atlTraceWindowing, 0, _T("Subclassing through a hook discarded. "));
17 #else
18 pOldProc; // avoid unused warning
19 #endif
20 //调用新的消息处理函数处理这个消息
21 return pProc(hWnd, uMsg, wParam, lParam);
22 }
首先从全局_Module对象中取出当前对象指针,并对这个指针m_hWnd赋值,m_thunk进行初始化。初始化后的m_thunk的首地址变成了初始化时传入的消息处理函数,并且此函数的第一个参数变成了pThis指针。因此在新的消息处理函数内强制转化一下,就可以使用这个pThis指针了。得到新的消息处理函数之后,调用SetWindowLong把当前窗体的消息处理函数设置为新的消息处理函数。新的消息处理函数定义如下:
1 template <class TBase, class TWinTraits>
2 LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
3 {
4 //强制转化为pThis指针
5 CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;
6 // set a ptr to this message and save the old value
7 MSG msg = { pThis->m_hWnd, uMsg, wParam, lParam, 0, { 0, 0 } };
8 const MSG* pOldMsg = pThis->m_pCurrentMsg;
9 pThis->m_pCurrentMsg =&msg;
10 // pass to the message map to process
11 LRESULT lRes;
12 //调用pThis的ProcessWindowMessage消息处理函数进行消息处理
13 BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);
14 // restore saved value for the current message
15 ATLASSERT(pThis->m_pCurrentMsg ==&msg);
16 pThis->m_pCurrentMsg = pOldMsg;
17 //如果消息没有被处理,调用默认的消息处理函数
18 if(!bRet)
19 {
20 if(uMsg != WM_NCDESTROY)
21 lRes = pThis->DefWindowProc(uMsg, wParam, lParam);
22 else
23 {
24 // unsubclass, if needed
25 LONG pfnWndProc = ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC);
26 lRes = pThis->DefWindowProc(uMsg, wParam, lParam);
27 if(pThis->m_pfnSuperWindowProc != ::DefWindowProc && ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC) == pfnWndProc)
28 ::SetWindowLong(pThis->m_hWnd, GWL_WNDPROC, (LONG)pThis->m_pfnSuperWindowProc);
29 // clear out window handle
30 HWND hWnd = pThis->m_hWnd;
31 pThis->m_hWnd = NULL;
32 // clean up after window is destroyed
33 pThis->OnFinalMessage(hWnd);
34 }
35 }
36 return lRes;
37 }
首先将第一个参数hwnd强制转化为CWindowImplBaseT< TBase, TWinTraits >的指针pThis,然后调用pThis的ProcessWindowMessage函数进行消息处理。所以,在窗体类的内部只需要重写此函数就可以实现对消息的处理。为了方便起见,WTL为这个方法定义了一些相关的宏。这些宏以BEGIN_MSG_MAP (theClass)开头,以END_MSG_MAP结束,中间加入对相应消息处理宏,如:COMMAND_HANDLER(id, code, func) 、NOTIFY_HANDLER(id, cd, func)等。以下是这几个宏的定义:
BEGIN_MSG_MAP(theClass)的定义
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 case0:
COMMAND_HANDLER(id, code, func)的定义
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 }
NOTIFY_HANDLER(id, cd, func) 的定义:
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 }
END_MSG_MAP()的定义:
1 #define END_MSG_MAP()
2 break;
3 default:
4 ATLTRACE2(atlTraceWindowing, 0, _T("Invalid message map ID (%i) "), dwMsgMapID);
5 ATLASSERT(FALSE);
6 break;
7 }
8 return FALSE;
9 }
BEGIN_MSG_MAP (theClass)和END_MSG_MAP分别组成了ProcessWindowMessage函数体和switch语句开始部分和结束部分,中间的具体消息处理宏添加相应的Swithc分支,这样就组成了一个完成的ProcessWindowMessage函数。
WTL消息反射
为什么需要消息反射呢?大家都知道,Windows为我们提供了很多标准控件,如Button,Editbox等,这些所谓的标准控件,其本质还是一个windows窗体,只不过他们的WNDCLASS是系统预定义好的,他们的消息处理函数也是系统预定义好的。一个标准控件接收到一个消息之后,首先进行默认的处理和响应,然后会发送给他的父窗体,由父窗体就行消息处理。这样就很难在标准控件的内部对消息进行响应。因此一个普遍的做法就是在父窗体消息处理函数内将消息再转发给那个控件,当然这个消息必须以某种固定的规律转换成自定义消息。
WTL的做法是在父窗体里通过REFLECT_NOTIFICATIONS()宏反射给子控件,定义如下:
1 #define REFLECT_NOTIFICATIONS()
2 {
3 bHandled = TRUE;
4 lResult = ReflectNotifications(uMsg, wParam, lParam, bHandled);
5 if(bHandled)
6 return TRUE;
7 }
其中ReflectNotifications从各个消息中获得子控件的HWND,然后调用SendMessage方法将自定义的消息发送给子控件,具体的定义为:
1 template <class TBase>
2 LRESULT CWindowImplRoot< TBase >::ReflectNotifications(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
3 {
4 HWND hWndChild = NULL;
5
6 switch(uMsg)
7 {
8 case WM_COMMAND:
9 if(lParam != NULL) // not from a menu
10 hWndChild = (HWND)lParam;
11 break;
12 case WM_NOTIFY:
13 hWndChild = ((LPNMHDR)lParam)->hwndFrom;
14 break;
15 case WM_PARENTNOTIFY:
16 switch(LOWORD(wParam))
17 {
18 case WM_CREATE:
19 case WM_DESTROY:
20 hWndChild = (HWND)lParam;
21 break;
22 default:
23 hWndChild = GetDlgItem(HIWORD(wParam));
24 break;
25 }
26 break;
27 case WM_DRAWITEM:
28 if(wParam) // not from a menu
29 hWndChild = ((LPDRAWITEMSTRUCT)lParam)->hwndItem;
30 break;
31 case WM_MEASUREITEM:
32 if(wParam) // not from a menu
33 hWndChild = GetDlgItem(((LPMEASUREITEMSTRUCT)lParam)->CtlID);
34 break;
35 case WM_COMPAREITEM:
36 if(wParam) // not from a menu
37 hWndChild = GetDlgItem(((LPCOMPAREITEMSTRUCT)lParam)->CtlID);
38 break;
39 case WM_DELETEITEM:
40 if(wParam) // not from a menu
41 hWndChild = GetDlgItem(((LPDELETEITEMSTRUCT)lParam)->CtlID);
42 break;
43 case WM_VKEYTOITEM:
44 case WM_CHARTOITEM:
45 case WM_HSCROLL:
46 case WM_VSCROLL:
47 hWndChild = (HWND)lParam;
48 break;
49 case WM_CTLCOLORBTN:
50 case WM_CTLCOLORDLG:
51 case WM_CTLCOLOREDIT:
52 case WM_CTLCOLORLISTBOX:
53 case WM_CTLCOLORMSGBOX:
54 case WM_CTLCOLORSCROLLBAR:
55 case WM_CTLCOLORSTATIC:
56 hWndChild = (HWND)lParam;
57 break;
58 default:
59 break;
60 }
61
62 if(hWndChild == NULL)
63 {
64 bHandled = FALSE;
65 return1;
66 }
67
68 ATLASSERT(::IsWindow(hWndChild));
69 return ::SendMessage(hWndChild, OCM__BASE + uMsg, wParam, lParam);
70 }
在子控件内部,可以调用与消息对应的宏进行处理,这个消息响应过程与窗体类似。