duilib图形库事件消息机制

本文迁移于个人博客 https://blog.csdn.net/cjm19960920/article/details/95416652

windows上常用图形库有很多,各自实现的底层机制也是各显神通,由于项目需要,也使用过相应的图形库,为了更深入地了解这些图形库,自己阅读了相关的源码及博客,也希望借以在这篇博客里介绍C++常用图形库duilib的事件消息机制。

windows图形库首要要解决窗体类跟窗体句柄的关系,我们知道,一个窗口对应一个对象,于是设计出了类,类很容易就可以存放窗口的句柄,通过这个类就能一窥window的奥秘,但是怎么通过窗口句柄找到这个对象,是一个较为麻烦的问题,一个容易想到的方法在窗口类里维护一份全局的窗口句柄到窗口类的对应关系,可以使用map集合,如:

#include <map>
class Window
{
public:
    Window();
    ~Window();

public:
    BOOL Create();
protected:
    LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);
protected:
    HWND m_hWnd;
protected:
    static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
    static std::map<HWND, Window *> m_sWindows;
};

当窗口类Create时,指定StaticWndProc 为窗口回调函数,并将 hWnd 与 this 存入 m_sWindows中:​

BOOL Window::Create()
{
    LPCTSTR lpszClassName = _T("ClassName");
    HINSTANCE hInstance = GetModuleHandle(NULL);
    WNDCLASSEX wcex    = { sizeof(WNDCLASSEX) };
    wcex.lpfnWndProc   = StaticWndProc;
    wcex.hInstance     = hInstance;
    wcex.lpszClassName = lpszClassName;
    RegisterClassEx(&wcex);
    m_hWnd = CreateWindow(lpszClassName, NULL, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
    if (m_hWnd == NULL)
    {
        return FALSE;
    }
    m_sWindows.insert(std::make_pair(m_hWnd, this));
    ShowWindow(m_hWnd, SW_SHOW);
    UpdateWindow(m_hWnd);
    return TRUE;
}

​ 在 StaticWindowProc 中,由 hWnd 找到 this,然后转发给成员函数:​

LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    std::map<HWND, Window *>::iterator it = m_sWindows.find(hWnd);
    assert(it != m_sWindows.end() && it->second != NULL);
    return it->second->WndProc(message, wParam, lParam);
}

至此完成了句柄到对象的对应关系,据说MFC采用的就是类似做法,此法的缺点是每次窗口过程函数StaticWndProc回调时都要从map中根据句柄找到对象,再转发成员函数,此时的hWnd相当于索引,当窗口句柄较多时会存在效率问题,那么存不存在一种更高效查找的方法,比如下面代码所示:

LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    return ((Window *)hWnd)->WndProc(message, wParam, lParam);
}

将hWnd用来存this指针,而传说中的WTL所采取的thunk技术就是采用这个思路,其主要实现时在系统调用窗口过程函数时,让他先走到我们的另一处代码,让我们有机会修改堆栈中的hwnd,其代码类似是这样的:​

__asm
{
    //调用 WndProc 时,堆栈结构为:RetAddr, hWnd, message, wParam, lParam, ... 故 [esp+4] 函
//数栈顶指针寄存器+4  WinProc的参数的压栈方法是典型的__stdcall调用方式(右边先压栈)
    mov dword ptr [esp+4], pThis  ;
    jmp WndProc
}

thunk技术的具体实现细节较繁琐,有兴趣的同学可以继续探索。

****Duilib:****

从上面的例子可以看出,封装一个窗体类,与生成的窗体相关联,并且去处理窗体的窗体消息并不是简单,MFC和WTL都有自己一套方法,而duilib作为国内首个开源 的directui 界面库,其解决的思路要简洁明了的多。

当duilib窗口类Create时,代码如下:​

HWND CWindowWnd::Create(HWND hwndParent, LPCTSTR pstrName, DWORD dwStyle, DWORD dwExStyle, int x, int y, int cx, int cy, HMENU hMenu)
{
    if( GetSuperClassName() != NULL && !RegisterSuperclass() ) return NULL;
    if( GetSuperClassName() == NULL && !RegisterWindowClass() ) return NULL;
    m_hWnd = ::CreateWindowEx(dwExStyle, GetWindowClassName(), pstrName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this);
    ASSERT(m_hWnd!=NULL);
    return m_hWnd;
}

通过CreateWindowEx函数来创建窗体,CreateWindowEx函数允许用户传递一个自定义数据,因此duilib正好把自己类对象的this指针传了进去。

接着当窗体开始建立时就会发送消息到相关的消息处理回调函数,duilib中对应的是__WndProc函数,函数代码如下:​

RESULT 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 ) {
        return pThis->HandleMessage(uMsg, wParam, lParam);
    } 
    else {
        return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
}

通常认为窗口创建时会发出消息WM_CREATE,但是在WM_CREATE消息之前还有一个消息是被发出的,那就是WM_NCCREATE消息,一个完整的流程是WM_NCCREATE->WM_CREATE->WM_DESTROY->WM_NCDESTROY,可以看到在duilib处理函数中围绕这个消息做了文章。首先我们来看此时__WndProc窗体过程回调函数最后一个参数的介绍:

Parameters

wParam

----This parameter is not used.

lParam

---- A pointer to the CREATESTRUCT structure that contains information about the window being created. The members of CREATESTRUCT are identical to the parameters of the CreateWindowEx function.

其中lParam参数是关键,这个参数此时是传进来CREATESTRUCT结构,这个结构体介绍如下:​

typedef struct tagCREATESTRUCT {
   LPVOID lpCreateParams;
   HANDLE hInstance;
   HMENU hMenu;
   HWND hwndParent;
   int cy;
   int cx;
   int y;
   int x;
   LONG style;
   LPCSTR lpszName;
   LPCSTR lpszClass;
   DWORD dwExStyle;
} CREATESTRUCT;

参数

******lpCreateParams******

将与要使用数据的点创建一个窗口。

******hInstance******

识别模块拥有新窗口模块的实例句柄。

******hMenu******

标识新窗口将使用菜单。 子窗口,如果包含整数 ID.

******hwndParent******

标识拥有新窗口的窗口。 新窗口,如果是顶级窗口,该成员是 ****NULL****。

******cy******

指定窗口的新高度。

******cx******

指定窗口的新宽度。

******y******

指定新窗口左上角的 y 坐标。 如果新窗口是子窗口,坐标系是相对于父窗口;否则是相对于屏幕坐标原点。

******x******

指定新窗口左上角的 x坐标。 如果新窗口是子窗口,坐标系是相对于父窗口;否则是相对于屏幕坐标原点。

******style******

指定新窗口中 style

******lpszName******

为指定新窗口的名称以 NULL 结尾的字符串的位置。

******lpszClass******

为指定新窗口的窗口类名的 null 终止的字符串的结构;WNDCLASS (点有关更多信息,请参见 Windows SDK。)

******dwExStyle******

对于新窗口指定 扩展样式

这个结构体的第一个参数正是在CreateWindowEx函数传入的自定义数据,也就是窗体类的this指针,duilib接下来通过这个结构体获取到窗体类的指针,并使其m_hWnd成员变量赋值为窗体的句柄,接着把这个这个指针通过SetWindowLongPtr函数与窗体句柄关联了起来!然后可以看到如果处理的不是WM_NCCREATE消息,就是用GetWindowLongPtr函数通过窗体句柄获取到窗体类的指针,再去调用相关的消息处理函数。duilib使用这个方法巧妙的将窗体类和窗体句柄关联起来,而没有像WTL的thunk技术那么麻烦。在使用duilib的时候,我们同样可以使用GetWindowLongPtr函数直接从窗体布局获取到窗体类指针,这在某些时候特别有用。

讲完duilib如何解决窗体类跟窗体句柄的关系后,接下来重点介绍duilib的消息处理剖析,以下为duilib核心的大体结构图:

duilib

在讲之前,我们需要了解Win32的消息机制:

1.消息产生。
2.系统将消息排列到其应该排放的线程消息队列中。
3.线程中的消息循环调用GetMessage(or PeekMessage)获取消息。
4.传送消息TranslateMessage and DispatchMessage to 窗口过程(Windows procedure)。
5.在窗口过程里进行消息处理

我们看到消息经过几个步骤,DuiLib可以在某些步骤间进行消息过滤。首先,第1、2和3步骤,DuiLib并不关心。DuiLib对消息处理集中在第4、5步骤中,即发送消息至窗口过程的前后进行消息过滤,对应CPaintManagerUI类中(也就是上面提到的窗体管理器)。
DuiLib的消息渠,也就是所谓的消息循环在CPaintManagerUI::MessageLoop()或者CWindowWnd::ShowModal()中实现。俩套代码的核心基本一致,以ShowModal为例:​

UINT CWindowWnd::ShowModal()
{
    ASSERT(::IsWindow(m_hWnd));
    UINT nRet = 0;
    HWND hWndParent = GetWindowOwner(m_hWnd);
    ::ShowWindow(m_hWnd, SW_SHOWNORMAL);
    ::EnableWindow(hWndParent, FALSE);
    MSG msg = { 0 };
    while( ::IsWindow(m_hWnd) && ::GetMessage(&msg, NULL, 0, 0) ) {
        if( msg.message == WM_CLOSE && msg.hwnd == m_hWnd ) {
            nRet = msg.wParam;
            ::EnableWindow(hWndParent, TRUE);
            ::SetFocus(hWndParent);
        }
        if( !CPaintManagerUI::TranslateMessage(&msg) ) {
            ::TranslateMessage(&msg);
            ::DispatchMessage(&msg);
        }
        if( msg.message == WM_QUIT ) break;
    }
    ::EnableWindow(hWndParent, TRUE);
    ::SetFocus(hWndParent);
    if( msg.message == WM_QUIT ) ::PostQuitMessage(msg.wParam);
    return nRet;
}

在win32消息路由步骤3和4之间,DuiLib调用CPaintManagerUI::TranslateMessage做了过滤,通过这套消息循环代码,我们能做到在消息发送到窗口过程前进行常规过滤,我们可以看到DuiLib中几乎所有的demo在创建完消息后,都调用了这俩个消息循环函数。下面是TranslateMessage代码:​

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++ ) //m_aPreMessages存储了当前进程
//中所有的CPaintManagerUI实例指针                    
        {
            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;
                    // 这里进行消息过滤
                    if( pT->PreMessageHandler(pMsg->message, pMsg->wParam, pMsg->lParam, lRes) ) 
                        return true;
                    return false;
                }
                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;
}

我们从函数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)
{
    ASSERT(::IsWindow(hWnd));
    // Remember the window context we came from
    m_hWndPaint = hWnd;
    m_hDcPaint = ::GetDC(hWnd);
    // We'll want to filter messages globally too
    m_aPreMessages.Add(this);
}

可以看到,m_aPreMessages存储的类型为CPaintManagerUI* ,也就说,这个静态成员数组里,存储了当前进程中所有的CPaintManagerUI实例指针,所以,如果有多个CPaintManagerUI实例, 也不会存在过滤问题,互不干扰,都能各自过滤。

接着来讲****CPaintManagerUI::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); // 这里调用接口 IMessageFilterUI::MessageHandler 来进行消息过滤
        if( bHandled ) {
            return true;
        }
}
…… ……
return false;
}

可以看到在PreMessageHandler函数中主要是依次在m_aPreMessageFilters数组中调用接口 IMessageFilterUI::MessageHandler 来进行消息过滤,IMessageFilterUI,此接口只有一个成员:MessageHandler纯虚函数,我们的窗口类要提前过滤消息,只要实现这个IMessageFilterUI,调用CPaintManagerUI::AddPreMessageFilter,将我们的窗口类实例指针添加到CPaintManagerUI::m_aPreMessageFilters 数组中。当消息到达窗口过程之前,就会会先调用我们的窗口类的成员函数:MessageHandler。下面是AddPreMessageFilter代码:​

bool CPaintManagerUI::AddPreMessageFilter(IMessageFilterUI* pFilter)
{
    // 将实现好的接口实例,保存到数组 m_aPreMessageFilters 中。
    ASSERT(m_aPreMessageFilters.Find(pFilter)<0);
    return m_aPreMessageFilters.Add(pFilter);
}

至此duilib在窗口过程前的消息过滤暂且告一段落,接着,消息抵达窗口过程后,如何处理。首先,要清楚,窗口过程在哪儿?使用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 ) {
        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。
两者分别在窗口过程函数前后进行过滤,没有特殊需求,一定要调用MessageHandler函数,此函数处理了绝大部分常用的消息响应。而且如果要响应Notify事件(后面会介绍),不调用此函数将无法响应。
好现在我们已经知道,俩个地方可以截获消息:

  • IMessageFilterUI接口,调用CPaintManagerUI:: AddPreMessageFilter,进行消息发送到窗口过程前的过滤。
  • 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<INotifyUI*>
        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;
        }
    }
    … …
}

其中m_aNotifiers定义为CStdPtrArray数组,目前还看不出其指向的实际类型。看看,什么时候给该数组添加成员:​

bool CPaintManagerUI::AddNotifier(INotifyUI* pNotifier)
{
    ASSERT(m_aNotifiers.Find(pNotifier)<0);
    return m_aNotifiers.Add(pNotifier);
}

不错,正是AddNotifier,类型也有了:即上面提到INotifyUI接口。所以,duilib教程里会在响应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);
    }
    for( int j = 0; j < m_aNotifiers.GetSize(); j++ ) {
        static_cast<INotifyUI*>(m_aNotifiers[j])->Notify(*pMsg);
    }
    delete pMsg;
}

可以看到MessageHandler首先从异步队列中一个消息并调用OnNotify。OnNotify和上面的Notify也不一样。OnNotify是响应消息的另外一种方式。它的定义为:CEventSource 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);

至于代理的代码实现,就不展示了,这里简单说明,就是将类成员函数,作为回调函数,加入到OnNotify中,然后调用 pMsg→pSender→OnNotify(pMsg)的时候,循环调用所有的类函数,实现通知的效果。代理代码处理的很巧妙,结合多态和模板,能将任何类成员函数作为回调函数。

查阅CSliderUI代码,发现他在自身的DoEvent函数内调用了诸如:m_pManager->SendNotify(this, DUI_MSGTYPE_VALUECHANGED);类似的代码,调用它,我们就会得到通知。现在,又多了两种消息处理的方式:

  • INotifyUI,调用CPaintManagerUI::AddNotifier,将自身加入Notifier队列。
  • 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消息之前进行的过滤。

于是又多了两种方式:

  • WindowImplBase的虚函数
  • WindowImplBase::HandleCustomMessage函数

最后可以得出总结,DuiLib消息响应方式:

  • IMessageFilterUI接口,调用CPaintManagerUI::AddPreMessageFilter,进行消息发送到窗口过程前的过滤。
  • HandleMessage函数,当消息发送到窗口过程中时,最先进行过滤。
  • INotifyUI,调用CPaintManagerUI::AddNotifier,将自身加入Notifier队列。
  • MakeDelegate(this, &CFrameWindowWnd::OnAlphaChanged);,当程序某个地方调用了 CPaintManagerUI::SendNotify,并且Msg.pSender正好是this,我们的类成员回调函数将被调用。
  • WindowImplBase的虚函数
  • WindowImplBase::HandleCustomMessage函数

以上即duilib从对象如何关联窗体句柄到实现窗体消息机制的大致过程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容