NO IMAGE

轉載請說明原出處,謝謝~~

        看到群裡朋友有人討論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;

引數

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函式直接從窗體佈局獲取到窗體類指標,這可能會在處理某些事情的時候有妙用!

       如果文章中有什麼錯誤,可以聯絡我或者留言

    Redrain  QQ:491646717    2014.9.19