duilib底層機制剖析:窗體類與窗體句柄的關聯

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

        看到羣裏朋友有人討論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


發佈了84 篇原創文章 · 獲贊 208 · 訪問量 61萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章