轉載請說明原出處,謝謝~~
看到羣裏朋友有人討論WTL中的thunk技術,讓我聯想到了duilib的類似技術。這些技術都是爲了解決c++封裝的窗體類與窗體句柄的關聯問題。
這裏是三篇關於thunk技術的博客,不懂的朋友可以先看一下:
WTL學習之旅(三)WTL中 Thunk技術本質(含代碼)
深入剖析WTL—WTL框架窗口分析 (5)
學習下 WTL 的 thunk
我這裏直接引用其他博客的一部分文字來說明窗體類與窗體句柄關聯的重要性和相關的問題,然後說明一下duilib中的解決方法:
-----------------------------------------------------引用開始------------------------------------------------------------------
由於 C++ 成員函數的調用機制問題,對C語言回調函數的 C++ 封裝是件比較棘手的事。爲了保持C++對象的獨立性,理想情況是將回調函數設置到成員函數,而一般的回調函數格式通常是普通的C函數,尤其是 Windows API 中的。好在有些回調函數中留出了一個額外參數,這樣便可以由這個通道將 this 指針傳入。比如線程函數的定義爲:
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(
LPVOID lpThreadParameter
);
typedef PTHREAD_START_ROUTINE LPTHREAD_START_ROUTINE;
這樣,當我們實現線程類的時候,就可以:
class Thread
{
private:
HANDLE m_hThread;
public:
BOOL Create()
{
m_hThread = CreateThread(NULL, 0, StaticThreadProc, (LPVOID)this, 0, NULL);
return m_hThread != NULL;
}
private:
DWORD WINAPI ThreadProc()
{
// TODO
return 0;
}
private:
static DWORD WINAPI StaticThreadProc(LPVOID lpThreadParameter)
{
((Thread *)lpThreadParameter)->ThreadProc();
}
};
不過,這樣,成員函數 ThreadProc() 便喪失了一個參數,這通常無傷大雅,任何原本需要從參數傳入的信息都可以作爲成員變量讓 ThreadProc 來讀寫。如果一定有些什麼是非從參數傳入不可的,那也可以,一種做法,創建線程的時候傳入一個包含 this 指針信息的結構。第二種做法,對該 class 作單例限制——如果現實情況允許的話。
所以,有額外參數的回調函數都好處理。不幸的是,Windows 的窗口回調函數沒有這樣一個額外參數:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
這使得對窗口的 C++ 封裝變得困難。爲了解決這個問題,一個很自然的想法是,維護一份全局的窗口句柄到窗口類的對應關係,如:
#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);
}
(m_sWindows 的多線程保護略過,下同)
據說 MFC 採用的就是類似的做法。缺點是,每次 StaticWndProc 都要從 m_sWindows 中去找 this。由於窗口類一般會保存窗口句柄,回調函數裏的 hWnd 就沒多大作用了,如果這個 hWnd 能夠被用來存 this 指針就好了,那麼就能寫成這樣:
LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
return ((Window *)hWnd)->WndProc(message, wParam, lParam);
}
這樣看上去就爽多了。傳說中 WTL 所採取的 thunk 技術就是這麼幹的。
-----------------------------------------------------引用結束------------------------------------------------------------------
可以看到,封裝一個窗體類,讓這個類與他生成的窗體關聯,並且去處理這個窗體的窗體消息並不是簡單的事,MFC和WTL都有自己的方法來解決。而duilib庫的最初作者更是對MFC、WTL等庫相當熟悉,我這裏說明一下duilib解決這個問題的辦法,個人覺得duilib的這個辦法要比thunk簡單好用很多。
我們使用duilib創建一個窗體,會調用窗體基類CWindowWnd類的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解決窗體類與窗體句柄關聯的起點了。
接着當窗體開始建立時就會發送消息到相關的消息處理回調函數,duilib中對應的是__WndProc函數,函數代碼如下:
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);
}
}
我們通常會理解在窗口創建時發出消息WM_CREATE,但是在WM_CREATE消息之前還有一個消息是被髮出的,那就是WM_NCCREATE消息,可以看到在duilib處理函數中圍繞這個消息做了文章。先看看這個消息的介紹:
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結構,這個結構體介紹如下:
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;
如果文章中有什麼錯誤,可以聯繫我或者留言
Redrain QQ:491646717 2014.9.19