win32消息产生机制
duilib是基于windows的ui框架,其消息产生处理机制和windows基本一样。一个最简单的duilib程序如下:
// 程序入口及Duilib初始化部分
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPSTR /*lpCmdLine*/, int nCmdShow)
{
// 加载资源
DuilibResourceInit::InitInstance(hInstance, UILIB_ZIP, L"");
DuilibResourceInit::Load();
//创建主窗口
TsetControl* pFrame = new TsetControl();
pFrame->Create(NULL, L"example", UI_WNDSTYLE_FRAME^WS_THICKFRAME, WS_EX_WINDOWEDGE);
pFrame->CenterWindow();
pFrame->ShowWindow(true);
CPaintManagerUI::MessageLoop();
return 0;
}
上图是一个duilib窗体调用过程,和win32消息产生处理机制基本一致。重点看一下**MessageLoop()**中的代码:
void CPaintManagerUI::MessageLoop()
{
MSG msg = { 0 };
while( ::GetMessage(&msg, NULL, 0, 0) ) {
if( !CPaintManagerUI::TranslateMessage(&msg) ) {
::TranslateMessage(&msg);
try{
::DispatchMessage(&msg);
} catch(...) {
DUITRACE(_T("EXCEPTION: %s(%d)\n"), __FILET__, __LINE__);
#ifdef _DEBUG
throw "CPaintManagerUI::MessageLoop";
#endif
}
}
}
}
其中**GetMessage()**方法就是从消息队列中取对应的消息,**TranslateMessage()**将消息进行预处理,**DispatchMessage()**将消息分发至窗体过程函数进行处理。
bool CPaintManagerUI::TranslateMessage(const LPMSG pMsg)
{
// Pretranslate Message takes care of system-wide messages, such as
// tabbing and shortcut key-combos. We'll look for all messages for
// each window and any child control attached.
UINT uStyle = GetWindowStyle(pMsg->hwnd);
UINT uChildRes = uStyle & WS_CHILD;
LRESULT lRes = 0;
if (uChildRes != 0)// 判断子窗口还是父窗口
{
HWND hWndParent = ::GetParent(pMsg->hwnd);
for( int i = 0; i < m_aPreMessages.GetSize(); i++ )
{
CPaintManagerUI* pT = static_cast<CPaintManagerUI*>(m_aPreMessages[i]);
HWND hTempParent = hWndParent;
while(hTempParent)
{
if(pMsg->hwnd == pT->GetPaintWindow() || hTempParent == pT->GetPaintWindow())
{
if (pT->TranslateAccelerator(pMsg))
return true;
// 这里进行消息过滤
pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes);
}
hTempParent = GetParent(hTempParent);
}
}
}
else
{
for( int i = 0; i < m_aPreMessages.GetSize(); i++ )
{
CPaintManagerUI* pT = static_cast<CPaintManagerUI*>(m_aPreMessages[i]);
if(pMsg->hwnd == pT->GetPaintWindow())
{
if (pT->TranslateAccelerator(pMsg))
return true;
if( pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes) )
return true;
return false;
}
}
}
return false;
}
在TranslateMessage()中,看一下PreMessageHandler() 源代码:
bool CPaintManagerUI::PreMessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& /*lRes*/)
{
for( int i = 0; i < m_aPreMessageFilters.GetSize(); i++ )
{
bool bHandled = false;
LRESULT lResult = static_cast<IMessageFilterUI*>(m_aPreMessageFilters[i])->MessageHandler(uMsg, wParam, lParam, bHandled);
if( bHandled ) {
return true;
}
}
......
}
从这里我们可以知道,只要我们窗体实现了IMessageFilterUI()接口,然后调用CPaintManagerUI::AddPreMessageFilter,将我们的窗口类实例指针添加到CPaintManagerUI::m_aPreMessageFilters 数组中。当消息到达窗口过程之前,就会会先调用我们的窗口类的成员函数:MessageHandler。
bool CPaintManagerUI::AddPreMessageFilter(IMessageFilterUI* pFilter)
{
ASSERT(m_aPreMessageFilters.Find(pFilter)<0);
return m_aPreMessageFilters.Add(pFilter);
}
上面AddPreMessageFilter就是往m_aPreMessageFilters数组中添加IMessageFilterUI对象。
回顾函数CPaintManagerUI::TranslateMessage代码中能够看到,这个过滤是在大循环:
for( int i = 0; i < m_aPreMessages.GetSize(); i++ )
中被调用的。如果m_aPreMessages.GetSize()为0,也就不会调用过滤函数。从代码中追溯其定义:
static CStdPtrArray m_aPreMessages;
是个静态变量,MessageLoop,TranslateMessage等也都是静态函数。其值在CPaintManagerUI::Init中被初始化:
void CPaintManagerUI::Init(HWND hWnd, LPCTSTR pstrName)
{
if( m_hWndPaint != hWnd ) {
m_hWndPaint = hWnd;
m_hDcPaint = ::GetDC(hWnd);
m_aPreMessages.Add(this);
}
}
看来,m_aPreMessages存储的类型为CPaintManagerUI* ,也就说,这个静态成员数组里,存储了当前进程中所有的CPaintManagerUI实例指针,所以,如果有多个CPaintManagerUI实例, 也不会存在过滤问题,互不干扰,都能各自过滤。
不知道大家有个疑问没有,CPaintManagerUI是何时进行初始化的,其实是在加载资源的时候进行实例化的。
然后再说,消息抵达窗口过程后,如何处理。首先,要清楚,窗口过程在哪儿?使用DuiLib开发,我们的窗口类无外乎继承俩个基类:一个是功能简陋一点 的:CWindowWnd,一个是功能健全一点的:WindowImplBase(继承于CWindowWnd)。然后,我们实例化窗口类,调用这俩个基 类的Create函数,创建窗口,其内部注册了窗口过程:
LRESULT CALLBACK CWindowWnd::__WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CWindowWnd* pThis = NULL;
if( uMsg == WM_NCCREATE ) {
LPCREATESTRUCT lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam);
pThis = static_cast<CWindowWnd*>(lpcs->lpCreateParams);
pThis->m_hWnd = hWnd;
::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LPARAM>(pThis));
}
else {
pThis = reinterpret_cast<CWindowWnd*>(::GetWindowLongPtr(hWnd, GWLP_USERDATA));
if( uMsg == WM_NCDESTROY && pThis != NULL ) {
LRESULT lRes = ::CallWindowProc(pThis->m_OldWndProc, hWnd, uMsg, wParam, lParam);
::SetWindowLongPtr(pThis->m_hWnd, GWLP_USERDATA, 0L);
if( pThis->m_bSubclassed ) pThis->Unsubclass();
pThis->m_hWnd = NULL;
pThis->OnFinalMessage(hWnd);
return lRes;
}
}
if (pThis != NULL) {
// 先做一遍预处理,以便做一些特殊处理(为了通用性)
if (pThis->PreHandleMessage(hWnd, uMsg, wParam, lParam)){
return true;
}
return pThis->HandleMessage(uMsg, wParam, lParam);
}
else {
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
里面,主要做了一些转换,细节自行研究,最终,他会调用pThis→HandleMessage(uMsg, wParam, lParam);。也即是说,HandleMessage相当于一个窗口过程(虽然它不是,但功能类似)。他是CWindowWnd的虚函数:
virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
所以,如果我们的窗口类实现了HandleMessage,就相当于再次过滤了窗口过程,HandleMessage代码框架如下:
LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if( uMsg == WM_XXX ) {
… …
return 0;
}
else if( uMsg == WM_XXX) {
… …
return 1;
}
LRESULT lRes = 0;
if( m_pm.MessageHandler(uMsg, wParam, lParam, lRes) ) //CPaintManagerUI::MessageHandler
return lRes;
return CWindowWnd::HandleMessage(uMsg, wParam, lParam); // 调用父类HandleMessage
}
注意:CPaintManagerUI::MessageHandler,名称为MessageHandler,而不是HandleMessage。
没有特殊需求,一定要调用此函数,此函数处理了绝大部分常用的消息响应。而且如果你要响应Notify事件,不调用此函数将无法响应,后面会介绍。
好现在我们已经知道,两个地方可以截获消息:
1、实现IMessageFilterUI接口,调用CPaintManagerUI:: AddPreMessageFilter,进行消息发送到窗口过程前的过滤。
2、重载HandleMessage函数,当消息发送到窗口过程中时,最先进行过滤。
下面继续看看**void Notify(TNotifyUI& msg)**是如何响应的。我们的窗口继承于INotifyUI接口,就必须实现此函数:
class INotifyUI
{
public:
virtual void Notify(TNotifyUI& msg) = 0;
};
上面我说了,在我们的HandleMessage要调用CPaintManagerUI::MessageHandler来进行后续处理。下面是一个代码片段:
bool CPaintManagerUI::MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lRes)
{
… …
TNotifyUI* pMsg = NULL;
while( pMsg = static_cast<TNotifyUI*>(m_aAsyncNotify.GetAt(0)) ) {
m_aAsyncNotify.Remove(0);
if( pMsg->pSender != NULL ) {
if( pMsg->pSender->OnNotify ) pMsg->pSender->OnNotify(pMsg);
}
// 先看这里,其它代码先忽略;我们看到一个转换操作static_cast
for( int j = 0; j < m_aNotifiers.GetSize(); j++ ) {
static_cast<INotifyUI*>(m_aNotifiers[j])->Notify(*pMsg);
}
delete pMsg;
}
// Cycle through listeners
for( int i = 0; i < m_aMessageFilters.GetSize(); i++ )
{
bool bHandled = false;
LRESULT lResult = static_cast<IMessageFilterUI*>(m_aMessageFilters[i])->MessageHandler(uMsg, wParam, lParam, bHandled);
if( bHandled ) {
lRes = lResult;
return true;
}
}
… …
}
定义为**CStdPtrArray m_aNotifiers;**数组,目前还看不出其指向的实际类型。看看,什么时候给该数组添加成员:
bool CPaintManagerUI::AddNotifier(INotifyUI* pNotifier)
{
ASSERT(m_aNotifiers.Find(pNotifier)<0);
return m_aNotifiers.Add(pNotifier);
}
不错,正是AddNotifier,类型也有了:INotifyUI。所以,入门教程里会在HandleMessage响应WM_CREATE消息的时候,调用 AddNotifier(this),将自身加入数组中,然后在CPaintManagerUI::MessageHandler就能枚举调用。由于 AddNotifer的参数为INotifyUI*,所以,我们要实现此接口。
所以,当HandleMessage函数被调用后,紧接着会调用我们的Notify函数。如果你没有对消息过滤的特殊需求,实现INotifyUI即可,在Notify函数中处理消息响应。
上面的Notify调用,是响应系统产生的消息。程序本身也能手动产生,其函数为:
void CPaintManagerUI::SendNotify(TNotifyUI& Msg, bool bAsync /*= false*/)
DuiLib将发送的Notify消息分为了同步和异步消息。同步就是立即调用(类似SendMessage),异步就是先放到队列中,下次再处理。(类似PostMessage)。
void CPaintManagerUI::SendNotify(TNotifyUI& Msg, bool bAsync /*= false*/)
{
… …
if( !bAsync ) {
// Send to all listeners
// 同步调用OnNotify,注意不是Notify
if( Msg.pSender != NULL ) {
if( Msg.pSender->OnNotify ) Msg.pSender->OnNotify(&Msg);
}
// 还会再次通知所有注册了INotifyUI的窗口。
for( int i = 0; i < m_aNotifiers.GetSize(); i++ ) {
static_cast<INotifyUI*>(m_aNotifiers[i])->Notify(Msg);
}
}
else {
// 异步调用,添加到m_aAsyncNotify array中
TNotifyUI *pMsg = new TNotifyUI;
pMsg->pSender = Msg.pSender;
pMsg->sType = Msg.sType;
pMsg->wParam = Msg.wParam;
pMsg->lParam = Msg.lParam;
pMsg->ptMouse = Msg.ptMouse;
pMsg->dwTimestamp = Msg.dwTimestamp;
m_aAsyncNotify.Add(pMsg);
}
}
我们CPaintManagerUI::MessageHandler在开始处发现一些代码:
TNotifyUI* pMsg = NULL;
while( pMsg = static_cast<TNotifyUI*>(m_aAsyncNotify.GetAt(0)) ) {
m_aAsyncNotify.Remove(0);
if( pMsg->pSender != NULL ) {
if( pMsg->pSender->OnNotify ) pMsg->pSender->OnNotify(pMsg);
}
可以看到MessageHandler首先从异步队列中一个消息并调用OnNotify。OnNotify和上面的Notify不一样。OnNotify是响应消息的另外一种方式。它的定义为:
CEventSource OnNotify;
class UILIB_API CEventSource
{
typedef bool (*FnType)(void*);
public:
CEventSource();
~CEventSource();
operator bool();
void operator+= (const CDelegateBase& d); // add const for gcc
void operator+= (FnType pFn);
void operator-= (const CDelegateBase& d);
void operator-= (FnType pFn);
bool operator() (void* param);
void Clear();
protected:
CStdPtrArray m_aDelegates;
private:
std::shared_ptr<char> ptr_; //生命周期监控
};
OnNotify属于CControlUI类。重载了一些运算符,如 operator();要让控件响应手动发送(SendNotify)的消息,就要给控件的OnNotify,添加消息代理。在DuiLib的TestApp1中的OnPrepare函数里,有:
CSliderUI* pSilder = static_cast<CSliderUI*>(m_pm.FindControl(_T("alpha_controlor")));
if( pSilder ) pSilder->OnNotify += MakeDelegate(this, &CFrameWindowWnd::OnAlphaChanged);
至于代理的代码实现,大家可以看一下CEventSource源码,这里简单说明,就是将类成员函数,作为回调函数,加入到OnNotify中,然后调用 pMsg→pSender→OnNotify(pMsg)的时候,循环调用所有的类函数,实现通知的效果。代理代码处理的很巧妙,结合多态和模板,能将任何类成员函数作为回调函数。
查阅CSliderUI代码,发现他在自身的DoEvent函数(也是在messageHandler中触发的)内调用了诸如:
m_pManager->SendNotify(this, DUI_MSGTYPE_VALUECHANGED);
类似的代码,调用它,我们就会得到通知。
现在,又多了两种消息处理的方式:
1、实现INotifyUI,调用CPaintManagerUI::AddNotifier,将自身加入Notifier队列。
2、添加消息代理(其实就是将成员函数加入到回调函数数组中),MakeDelegate(this, &CFrameWindowWnd::OnAlphaChanged);,当程序某个地方调用了 CPaintManagerUI::SendNotify,并且Msg.pSender正好是注册的this,我们的类成员回调函数将被调用。
搜寻CPaintManagerUI代码,我们发现还有一些消息过滤再里面:
bool CPaintManagerUI::AddMessageFilter(IMessageFilterUI* pFilter)
{
ASSERT(m_aMessageFilters.Find(pFilter)<0);
return m_aMessageFilters.Add(pFilter);
}
m_aMessageFilters也是IMessageFilterUI array,和m_aPreMessageFilters类似。上面我们介绍的是CPaintManagerUI::AddPreMessageFilter,那这个又是在哪儿做的过滤?还是CPaintManagerUI::MessageHandler中:
……
// Cycle through listeners
for( int i = 0; i < m_aMessageFilters.GetSize(); i++ )
{
bool bHandled = false;
LRESULT lResult = static_cast<IMessageFilterUI*>(m_aMessageFilters[i])->MessageHandler(uMsg, wParam, lParam, bHandled);
if( bHandled ) {
lRes = lResult;
return true;
}
}
… …
这个片段是在,异步OnNotify和Nofity消息响应,被调用后。才被调用的,优先级也就是最低。但它始终会被调用,因为异步OnNotify和 Nofity消息响应没有返回值,不会因为消息已经被处理,而直接退出。DuiLib再次给用户一个处理消息的机会。用户可以选择将bHandled设置 为True,从而终止消息继续传递。我觉得,这个通常是为了弥补OnNotify和Nofity没有返回值的问题,在m_aMessageFilters 做集中处理。
处理完所有的消息响应后,如果消息没有被截断,CPaintManagerUI::MessageHandler 继续处理大多数默认的消息,它会处理在其管理范围中的所有控件的大多数消息和事件等。
然后,消息机制还没有完,这只是CPaintManagerUI::MessageHandler中的消息机制,如果继承的是 WindowImplBase, WindowImplBase实现了DuiLib窗口的大部分功能。WindowImplBase继承了CWindowWnd,重载了 HandleMessage,也就是说,消息发送的窗口过程后,第一个调用的是WindowImplBase::HandleMessage:
LRESULT WindowImplBase::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
LRESULT lRes = 0;
BOOL bHandled = TRUE;
switch (uMsg)
{
case WM_CREATE: lRes = OnCreate(uMsg, wParam, lParam, bHandled); break;
case WM_CLOSE: lRes = OnClose(uMsg, wParam, lParam, bHandled); break;
case WM_DESTROY: lRes = OnDestroy(uMsg, wParam, lParam, bHandled); break;
#if defined(WIN32) && !defined(UNDER_CE)
case WM_NCACTIVATE: lRes = OnNcActivate(uMsg, wParam, lParam, bHandled); break;
case WM_NCCALCSIZE: lRes = OnNcCalcSize(uMsg, wParam, lParam, bHandled); break;
case WM_NCPAINT: lRes = OnNcPaint(uMsg, wParam, lParam, bHandled); break;
case WM_NCHITTEST: lRes = OnNcHitTest(uMsg, wParam, lParam, bHandled); break;
case WM_GETMINMAXINFO: lRes = OnGetMinMaxInfo(uMsg, wParam, lParam, bHandled); break;
case WM_MOUSEWHEEL: lRes = OnMouseWheel(uMsg, wParam, lParam, bHandled); break;
#endif
case WM_SIZE: lRes = OnSize(uMsg, wParam, lParam, bHandled); break;
case WM_CHAR: lRes = OnChar(uMsg, wParam, lParam, bHandled); break;
case WM_SYSCOMMAND: lRes = OnSysCommand(uMsg, wParam, lParam, bHandled); break;
case WM_KEYDOWN: lRes = OnKeyDown(uMsg, wParam, lParam, bHandled); break;
case WM_KILLFOCUS: lRes = OnKillFocus(uMsg, wParam, lParam, bHandled); break;
case WM_SETFOCUS: lRes = OnSetFocus(uMsg, wParam, lParam, bHandled); break;
case WM_LBUTTONUP: lRes = OnLButtonUp(uMsg, wParam, lParam, bHandled); break;
case WM_LBUTTONDOWN: lRes = OnLButtonDown(uMsg, wParam, lParam, bHandled); break;
case WM_MOUSEMOVE: lRes = OnMouseMove(uMsg, wParam, lParam, bHandled); break;
case WM_MOUSEHOVER: lRes = OnMouseHover(uMsg, wParam, lParam, bHandled); break;
default: bHandled = FALSE; break;
}
if (bHandled) return lRes;
lRes = HandleCustomMessage(uMsg, wParam, lParam, bHandled);
if (bHandled) return lRes;
if (m_PaintManager.MessageHandler(uMsg, wParam, lParam, lRes))
return lRes;
return CWindowWnd::HandleMessage(uMsg, wParam, lParam);
}
WindowImplBase处理一些消息,使用成员函数On***来处理消息,所以,可以重载这些函数达到消息过滤的目的。 然后,我们看到,有一个函数:WindowImplBase::HandleCustomMessage,它是虚函数,我们可以重载此函数,进行消息过滤,由于还没有调用m_PaintManager.MessageHandler,所以在收到Notify消息之前进行的过滤。
又多了两种方式:
1、重载父类:WindowImplBase的虚函数。
2、重载父类:WindowImplBase::HandleCustomMessage函数。
最后,继承于WindowImplBase,还有一种过滤消息的方式,和Notify消息平级,实现方式是仿造的MFC消息映射机制: WindowImplBase实现了INotifyUI接口,并且AddNotify了自身,所以,它会收到Notify消息:
void WindowImplBase::Notify(TNotifyUI& msg)
{
return CNotifyPump::NotifyPump(msg);
}
NotifyPump 调用 LoopDispatch,代码片段如下:
... ...
const DUI_MSGMAP_ENTRY* lpEntry = NULL;
const DUI_MSGMAP* pMessageMap = NULL;
#ifndef UILIB_STATIC
for(pMessageMap = GetMessageMap(); pMessageMap!=NULL; pMessageMap = (*pMessageMap->pfnGetBaseMap)())
#else
for(pMessageMap = GetMessageMap(); pMessageMap!=NULL; pMessageMap = pMessageMap->pBaseMap)
#endif
{
#ifndef UILIB_STATIC
ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)());
#else
ASSERT(pMessageMap != pMessageMap->pBaseMap);
#endif
if ((lpEntry = DuiFindMessageEntry(pMessageMap->lpEntries,msg)) != NULL)
{
goto LDispatch;
}
}
... ...
代码量过多,这里进行原理说明,和MFC一样,提供了一些消息宏:
DUI_DECLARE_MESSAGE_MAP()
DUI_BEGIN_MESSAGE_MAP(CKeyBoardDlg, CNotifyPump)
DUI_ON_MSGTYPE(DUI_MSGTYPE_CLICK, OnClick)
DUI_ON_MSGTYPE(DUI_MSGTYPE_WINDOWINIT, OnInitWindow)
DUI_END_MESSAGE_MAP()
和MFC原理一样,声明一些静态变量,存储类的信息,插入一些成员函数,最为回调,最后生成一张静态表。当WindowImplBase::Notify有消息时,遍历表格,进行消息通知。
总结,DuiLib消息响应方式:
duilib还提供了另外一种响应的方法,即消息映射DUI_BEGIN_MESSAGE_MAP,可以将DUI_MSGTYPE_CLICK消息映射到指定的函数(比如OnClick),这和在Notify判断msg.sType是一样的效果,具体请参见duilib的RichListDemo。
先看看下面几段代码:
DUI_BEGIN_MESSAGE_MAP(CPage1, CNotifyPump)
DUI_ON_MSGTYPE(DUI_MSGTYPE_CLICK,OnClick)
DUI_ON_MSGTYPE(DUI_MSGTYPE_SELECTCHANGED,OnSelectChanged)
DUI_ON_MSGTYPE(DUI_MSGTYPE_ITEMCLICK,OnItemClick)
DUI_END_MESSAGE_MAP()
LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
LRESULT lRes = 0;
BOOL bHandled = TRUE;
switch (uMsg)
{
case WM_KEYDOWN: lRes = OnKeyDown(uMsg, wParam, lParam, bHandled); break;
case WM_LBUTTONDOWN: lRes = OnLButtonDown(uMsg, wParam, lParam, bHandled); break;
case WM_MOUSEMOVE: lRes = OnMouseMove(uMsg, wParam, lParam, bHandled); break;
default: bHandled = FALSE; break;
}
if (bHandled) return lRes;
return CWindowWnd::HandleMessage(uMsg, wParam, lParam);
}
void Notify(TNotifyUI& msg)
{
if( msg.sType == _T("windowinit") )
{
}
else if( msg.sType == _T("click") )
{
}
}