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技术的具体实现细节较繁琐,有兴趣的同学可以继续探索。
从上面的例子可以看出,封装一个窗体类,与生成的窗体相关联,并且去处理窗体的窗体消息并不是简单,MFC和WTL都有自己一套方法,而duilib作为国内首个开源 的directui 界面库,其解决的思路要简洁明了的多。
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窗体过程回调函数最后一个参数的介绍:
wParam
A pointer to the CREATESTRUCT