• 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


  • 相关阅读:
    系统设计的一些原则
    分层开发思想与小笼包
    工作与生活
    Microsoft .NET Pet Shop 4 架构与技术分析
    用人之道(二) 如何管理软件开发团队
    也谈很多开发人员的毛病
    《3S新闻周刊》第10期,本期策划:“超女”营销带来的启示
    浅析ArcIMS
    MapX的坐标问题
    应用ArcIMS构建GMap风格的地图应用
  • 原文地址:https://www.cnblogs.com/lcchuguo/p/5067894.html
Copyright © 2020-2023  润新知