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

时间: 2024-08-29 23:48:04

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

Flex中利用事件机制进行主程序与子窗体间参数传递

在开发具有子窗体,或者itemrenderer的应用时,常常涉及到子窗体向父窗体传递参数或者从itemrenderer内的控件向外部的主程序传递参数的需求.这些都可以通过事件机制这一统一方法加以解决.在我的应用中有两个需求: 1.左侧的List控件的itemrenderer中包含CheckBox控件,当其状态改变时需要同时改变主程序中的一个数组变量的内容:2.左下方的"新增届次"按钮会弹出一个窗口,窗口中输入届次信息后需要修改数据库中的表,同时表的更改结果要能够在List控件中体现出来

winfrom 基类窗体与子类窗体load事件详解

今日在写代码时,需要在子窗体运行时调用基类窗体中的load事件,顺带将该部分功能做一个详细的了解. Winform窗体在初始化到呈现在用户眼前会依次经历三个阶段,构造-加载-显示,分别对应.NET 窗体中的 InitializeComponent,onLoad,show三个调用函数,以下: InitializeComponent:初始化窗体及窗体上的控件,加载并分配资源,注册相关事件 onLoad:注册装载窗口事件,是窗体启动时调用该函数,触发formload事件,从而调用From_Load事件

创建新的窗体类

到目前为止,我们看到的程序还只是脚本风格,用现有的窗体和控件,快速把窗体放在一起.这种编程风格可用于快速开发单一窗体的应用程序,但是,对于快速创建有多个窗体组成的应用程序,或者创建窗体库以用于其他 .NET 语言时,就会受到限制:在这些情况下,必须采取一种更面向组件(component-oriented)的方法. 通常,创建一个大型的 Windows  窗体应用程序时,有一些窗体需要重复使用:此外,通常希望这些窗体能够相互通信,通过调整彼此的属性或调用彼此的方法.通常是定义新的窗体类,从 Sys

JAVA笔记__窗体类/Panel类/Toolkit类

/** * 窗体类 */ public class Main { public static void main(String[] args) { MyFrame m1 = new MyFrame(); } } class MyFrame extends Frame{ public MyFrame(){ this.setTitle("My first software"); this.setSize(300,200); this.setBackground(Color.yellow);

Qt窗体关闭时,如何自动销毁窗体类对象

Qt窗体关闭时,如何自动销毁窗体类对象 要对你的窗口设置WA_DeleteOnClose属性,默认的情况下关闭窗口仅仅意味着隐藏它 ImgWindow1->setAttribute(Qt::WA_DeleteOnClose,  true);

关联容器(底层机制) — hashtable

C++ 11已将哈希表纳入了标准之列.hashtable是hash_set.hash_map.hash_multiset.hash_multimap的底层机制,即这四种容器中都包含一个hashtable. 解决碰撞问题的办法有许多,线性探测.二次探测.开链等等.SGI STL的hashtable采用的开链方法,每个hash table中的元素用vector承载,每个元素称为桶(bucket),一个桶指向一个存储了实际元素的链表(list),链表节点(node)结构如下: template <cl

关联容器(底层机制) — 红黑树

set.map.multiset.multimap四种关联式容器的内部都是由红黑树实现的.在STL中红黑树是一个不给外界使用的独立容器.既然是容器,那么就会分配内存空间(节点),内部也会存在迭代器.关于红黑树的一些性质,可以参考"数据结构"中的笔记,这里只记录STL中的红黑树是如何实现的. 和slist一样,红黑树的节点和迭代器均采用了双层结构: 节点:__rb_tree_node继承自__rb_tree_node_base 迭代器:__rb_tree_iterator继承自__rb_

WindowsForm多窗体、多窗体传值、控件数据绑定--2016年12月8日

多窗体 Show Form1 f1 = new Form1(); f1.Show(); ShowDialog--在父窗体之上 Form1 f1 = new Form1(); f1.ShowDialog(); 2者具体区别如下:  1.在调用Form.Show方法后,Show方法后面的代码会立即执行  2.在调用Form.ShowDialog方法后,直到关闭对话框后,才执行此方法后面的代码  3.当窗体显示为模式窗体时,单击“关闭”按钮会隐藏窗体,并将DialogResult属性设置为Dialog

主窗体和继承窗体

程序将第一个创建的窗体作为主窗体.如果创建了一个后,在其他窗体尚未创建的时候便释放调,此时又创建了一个窗体,那么第二次创建的窗体就会变成主窗体了. 关闭主窗体,程序就认为是终止运行,并且调用 Free 释放掉主窗体资源,进而程序结束退出.而关闭其他非主窗体,程序只是将其隐藏起来了. 子窗体会将主窗体的属性方法继承下来,对于主窗体中手动增加的方法(即声明在访问控制级别private.protected.published.public 中的方法),除非显示的增加 virtual 关键字,并在子类中