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
duilib底层机制剖析:窗体类与窗体句柄的关联的更多相关文章
- duilib底层机制剖析:窗口类与窗口句柄的关联
转载请说明原出处.谢谢~~ 看到群里朋友有人讨论WTL中的thunk技术,让我联想到了duilib的类似技术. 这些技术都是为了解决c++封装的窗口类与窗口句柄的关联问题. 这里是三篇关于thunk技 ...
- duilib 使用图片素材或者算法给窗体增加阴影(源码和demo)
转载请说明原出处,谢谢:http://blog.csdn.net/zhuhongshu/article/details/42580877 之前我写的程序使用阴影时,一直是使用codeproject网站 ...
- Flex中利用事件机制进行主程序与子窗体间参数传递
在开发具有子窗体,或者itemrenderer的应用时,常常涉及到子窗体向父窗体传递参数或者从itemrenderer内的控件向外部的主程序传递参数的需求.这些都可以通过事件机制这一统一方法加以解决. ...
- Qt窗体关闭时,如何自动销毁窗体类对象
Qt窗体关闭时,如何自动销毁窗体类对象 要对你的窗口设置WA_DeleteOnClose属性,默认的情况下关闭窗口仅仅意味着隐藏它 ImgWindow1->setAttribute(Qt ...
- Delphi 继承基类的窗体,并显示基类的控件操作。
1. 先建一个普通的窗体,until1 2. 先把类实现基类, 并需要实现基类需要继承的方法, 可以先不用再方法中写实现代码. TForm4 = class(TfrmmtAReportPeriod ...
- 【Demo 0025】注册/反注册窗体类RegisterClassEx/UnregisterClass
所有窗体在创建前都必须注册窗体类,只有注册的窗体类才被系统认知并允许实例化,换句话说通过注册告诉进程窗体管理器此类窗体的属性如: 背景色,窗体上的鼠标样式以及窗体事件处理函数等; 有一些控件类系统自 ...
- JAVA笔记__窗体类/Panel类/Toolkit类
/** * 窗体类 */ public class Main { public static void main(String[] args) { MyFrame m1 = new MyFrame() ...
- C#进阶系列——WebApi 路由机制剖析:你准备好了吗?
前言:从MVC到WebApi,路由机制一直是伴随着这些技术的一个重要组成部分. 它可以很简单:如果你仅仅只需要会用一些简单的路由,如/Home/Index,那么你只需要配置一个默认路由就能简单搞定: ...
- WindowsForm多窗体、多窗体传值、控件数据绑定--2016年12月8日
多窗体 Show Form1 f1 = new Form1(); f1.Show(); ShowDialog--在父窗体之上 Form1 f1 = new Form1(); f1.ShowDialog ...
随机推荐
- cojs 简单的最近公共祖先 解题报告
我曾经自己想过每考试一次就从考试题中找找idea来出题 这次又找到了一个,先不管原来的考试题是什么 考试题中其中的一部分就是今天的这道题目啦 当时考场上自己比较傻,没有注意到有用的性质,套用了之前黑白 ...
- 朴素贝叶斯方法(Naive Bayes Method)
朴素贝叶斯是一种很简单的分类方法,之所以称之为朴素,是因为它有着非常强的前提条件-其所有特征都是相互独立的,是一种典型的生成学习算法.所谓生成学习算法,是指由训练数据学习联合概率分布P(X,Y ...
- 【Linux高频命令专题(2)】awk
简介 awk是一个强大的文本分析工具,相对于grep的查找,sed的编辑,awk在其对数据分析并生成报告时,显得尤为强大.简单来说awk就是把文件逐行的读入,以空格为默认分隔符将每行切片,切开的部分再 ...
- java io异步
1.一般来说,可以通过多线程的方式来实现异步 2.同步和异步着重点在于多个任务的执行过程中,一个任务的执行是否会导致整个流程的暂时等待: 3.而阻塞和非阻塞着重点在于发出一个请求操作时,如果进行操作的 ...
- Linux开机启动流程
开机过程指的是从打开计算机电源直到LINUX显示用户登录画面的全过程: 1)加载BIOS 2)读取MBR 3)Boot Loader 4)加载内核 ...
- 254. Factor Combinations
题目: Numbers can be regarded as product of its factors. For example, 8 = 2 x 2 x 2; = 2 x 4. Write a ...
- Wireshark抓包分析HTTPS与HTTP报文的差异
一.什么是HTTPS: HTTPS(Secure Hypertext Transfer Protocol)安全超文本传输协议 它是一个安全通信通道,它基于HTTP开发,用于在客户计算机和服务器之间交换 ...
- MVC运行原理
Global.asax Global.asax 文件,有时候叫做 ASP.NET 应用程序文件,提供了一种在一个中心位置响应应用程序级或模块级事件的方法.你可以使用这个文件实现应用程序安全性以及其它一 ...
- 想要风投被你的融资 PPT 打动吗?别忘了你其实就是在想方设法卖出自己公司的部分股权
硅谷,一个常常见诸于报端,看着很熟悉,但是又不那么被人所了解的未及之地.它不是一个严格限定的地理位置,一般来说是指旧金山和湾区,其中湾区又分为东湾(East Bay)和南湾(South Bay), ...
- Windows下Java File对象创建文件夹时的一个"坑"
import java.io.File; import java.io.IOException; public class DirCreate { public static void main(St ...