duilib底层机制剖析:窗体类与窗体句柄的关联

简介: 转载请说明原出处,谢谢~~         看到群里朋友有人讨论WTL中的thunk技术,让我联想到了duilib的类似技术。这些技术都是为了解决c++封装的窗体类与窗体句柄的关联问题。

转载请说明原出处,谢谢~~

        看到群里朋友有人讨论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


目录
相关文章
|
1月前
|
设计模式 程序员 C#
C# 使用 WinForm MDI 模式管理多个子窗体程序的详细步骤
WinForm MDI 模式就像是有超能力一般,让多个子窗体井然有序地排列在一个主窗体之下,既美观又实用。不过,也要小心管理好子窗体们的生命周期哦,否则一不小心就会出现一些意想不到的小bug
|
6月前
MFC窗口创建机制
MFC窗口创建机制
33 0
|
C#
WPF中窗口控件的跨线程调用
原文:WPF中窗口控件的跨线程调用 在WinForm中,我们要跨线程访问窗口控件,只需要设置属性CheckForIllegalCrossThreadCalls = false;即可。 在WPF中要麻烦一下,同样的不允许跨线程访问,因为没有权限,访问了会抛异常; 没有CheckForIllegalCrossThreadCalls 属性,怎么办? 在WPF中的窗口控件都有一个Dispatcher属性,允许访问控件的线程;既然不允许直接访问,就告诉控件我们要干什么就好了。
1989 0
|
C# 前端开发 Windows
在WPF应用程序中利用IEditableObject接口实现可撤销编辑的对象
原文:在WPF应用程序中利用IEditableObject接口实现可撤销编辑的对象 这是我辅导的一个项目开发中的例子,他们是用WPF做界面开发,在学习了如何使用MVVM来实现界面与逻辑的分离,并且很好的数据更新之后,有一个疑问就是,这种双向的数据更新确实很不错,但如果我们希望用户可以撤销修改怎么办呢?其实这个功能,很早就有,甚至在原先的Windows Forms里面也可以实现。
966 0
|
C# C++
获取当前进程(程序)主窗体句柄并设置wpf的父窗体为此句柄
原文:获取当前进程(程序)主窗体句柄并设置wpf的父窗体为此句柄 有时候在c++调用wpf控件的时候,wpf控件想自己显示窗体,但需要设置owner属性。迂回解决办法是设置wpf的window窗体的父窗体为进程的句柄。
3107 0
|
C# Windows
WPF获得全局窗体句柄,并响应全局键盘事件
场景 wpf窗体运行后,只能捕获当前Active窗体的按键事件,如果要监听windows全局事件,并对当前窗口事件响应. 第一步:导入Winows API public class Win32 { [DllImport("User32.Dll")] public static extern void SetWindowText(int h, String s); /// /// 如果函数执行成功,返回值不为0。
1993 0
MFC 对话框中动态创建N级菜单以及响应事件
创建一个基于对话框的工程,工程名为CreateMenu 为该对话框增加一个文件菜单项和测试菜单项,如下图所示   测试菜单项至少要有一个子菜单项 在对话框属性中关联该菜单 在resource.
1288 0
|
索引 前端开发
winform程序中将控件置于最顶层或最底层的方法
有时,我们可能动态的添加控件,并准备将其置于对顶层或最底层。实现的方法有两个: 一种方法是在WinForm窗体中使用Controls控件集的SetChildIndex方法,该方法将子控件设定为指定的索引值,其方法原型如下: void SetChildIndex(Control child, int newIndex) 假设窗体中有一个按钮Button控件,名为button1,如果将其的索引设置为10,源代码如下: this.Controls.SetChildIndex(button1, 10); 索引越大,控件位置越靠上。
1617 0
mfc控件与其对应的对象的关联方法
对话框的控件与其对应类的对象相关联:(两种方法) (1)      通过CWnd::DoDataExchange函数进行关联;   用VC++6.0的MFC ClassWizard中的Member Variables页面的Add Variable关联一个变量   然后在程序的DoDataExc...
782 0