微软视窗的C++封装

本文详细探讨了微软视窗的C++封装,包括`Window::Create`和窗口过程的处理,`Window::Attach`接管系统控件,以及对话框资源的创建和管理。文中介绍了如何处理窗口过程、全局变量的使用、窗口过程线程同步问题,以及对话框模板资源的解析。同时,还讨论了对话框尺寸的转换,对话框字体设置,以及`Dialog::DoModal`的注意事项。
摘要由CSDN通过智能技术生成

微软视窗的C++封装

                                                                (转自freex64加壳工具)

Window::Create和窗口过程

Window::Create函数是对CreateWindow/ CreateWindowEx的封装。如果Window::Create创建的是系统控件还好,从执行序进入CreateWindowEx的函数体的第一条指令起,直到CreateWindowEx返回了窗口句柄,这期间没有封装代码必须参与的工作。这是因为此类窗口已由系统注册了窗口类和窗口过程。但如果Window::Create创建的是普通窗口,那么在进入CreateWindowEx的函数体的第一条指令之前,封装代码就必须要准备好相关窗口过程。因为CreateWindowEx在返回结果之前会多次调用窗口过程处理一些消息,比如WM_CREATE等等。由于操作系统对窗口过程函数形式的定义,封装代码无法把窗口过程做为Window类的普通成员函数,最多只能把窗口过程当做静态成员函数封装。在CreateWindowEx返回窗口句柄之前,窗口的窗口过程函数其实已经完成了不少的工作。系统控件窗口过程是操作系统代码的一部分,它如何完成这些工作,封装代码可以不关心。但普通窗口的窗口过程则是封装代码的一部分,这就为封装代码的设计带来了一个绕不过去的问题。

虽然窗口过程在处理WM_CREATE这类消息时已经拿到了窗口句柄,但由于此时的CreateWindowEx函数尚未返回,所以Window对象中存放窗口句柄的成员变量还没有被赋于正确的句柄值。如果窗口过程希望把WM_CREATE消息交由Window类的某个成员函数处理,那么,第一,由于Window对象尚未获取窗口句柄,它的任何成员函数都无法正确处理此类消息。第二,由于Window对象尚未获取窗口句柄,也没有办法通过比对句柄值来找到具体该处理此类消息的那个Window对象。

这里的解决思路是,Window::Create函数在调用CreateWindow/ CreateWindowEx之前先把当前的Window对象指针存放在一个全局变量里,窗口过程在处理WM_CREATE这类消息时先从全局变量里获取具体该处理此类消息的那个Window对象,并为对象中的句柄变量正确赋值,然后才把消息交给Window中具体负责的成员函数。显然所有被调用的Window::Create函数必须共用这个全局变量,而且在窗口过程访问此全局变量之前,这个全局变量绝不能被其它被调用的Window::Create函数改变。窗口过程访问此全局变量之后,必须尽快把Window对象的指针存放在别处,让出此全局变量,方便其它被调用的Window::Create函数进行。

关于窗口过程访问此全局变量之后把Window对象的指针另外存放在哪里。一个简单的想法就是利用类似std::map这样的工具,建立从句柄到Window对象的映射关系。除了第一次需要通过全局变量获取Window对象,窗口过程在处理后继消息时只需要使用窗口句柄从std::map中获取Window对象的指针就可以了。当然,这是一个全局的std::map。当前进程中所有由Window对象封装的窗口都必须出现在这个全局std::map中。考虑到每个进程创建的窗口数量并不会非常地多,这个解决方案也许并不像它看起来那么丑陋不堪。下面的代码展示了std::map以及通过句柄获取Window对象的方法:

static std::map<HWND,Window *> g_wndmap;
static rubbish::std_mutex      g_mutex;
Window * FromHandle(HWND const hWnd)
{
        std::map<HWND,Window *>::iterator it = g_wndmap.find(hWnd);

        if(it != g_wndmap.end())
        {
             return it->second;
        }
        return NULL;
}//代码由FreeX64提供

下面的代码展示了临时存放窗口句柄的全局变量,以及在Window::Create函数中使用这个全局变量的方法:

static Window * g_TempWndPtr = NULL;
BOOL Window::Create(CREATESTRUCT & cs)
{
     ……

     g_TempWndPtr   = this;
     this->m_hWnd = ::CreateWindowEx(……);

   ……
}//代码由FreeX64提供

下面的代码展示了窗口过程函数中如何使用临时存放窗口句柄的全局变量:

LRESULT CALLBACK Window::__WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	Window * pWnd = Window::FromHandle(hWnd);	
	if(NULL != pWnd)
	{
		if(NULL == pWnd->m_hWnd)
		{
			pWnd->m_hWnd = hWnd;
		}
		return pWnd->WindowProc(message, wParam, lParam);
	}
	return ::DefWindowProc(hWnd, message, wParam, lParam) ;
}//代码由FreeX64提供

最后是关于窗口过程在哪个线程中被调用的问题。并非所有的消息都是通过界面线程的消息循环分发给窗口过程的。例如,在CreateWindowEx函数的执行过程中,WM_CREATE、WM_SIZE这些消息就是由CreateWindowEx函数所在的线程直接分发到窗口过程的。因此,在WM_CREATE消息中,调用窗口过程的线程与调用CreateWindowEx的线程是同一线程。也就是说,如果程序员从两个线程中同时调用CreateWindowEx,而且被创建的两个实例又基于同一个C++窗口类的话,那么窗口过程就有可能存在线程同步的问题。使用互斥量或临界区这样的工具保护全局变量,当然是不错的方案。另一方面,因为WM_CREATE消息的处理总是与CreateWindowEx函数位于相同的线程中,也许线程本地存储会更高效一点。VS2005支持thread关键字,如果上面的代码可以改进成下面的样子。

__declspec(thread) static Window * g_TempWndPtr = NULL;

也许保护g_TempWndPtr变量的互斥工具就可以省了。

最后还可以有一点小小的改进。因为每个窗口的窗口过程只需要在第一次收到消息时访问存放Window指针的全局变量,所以可以为每个窗口准备两个窗口过程。RegisterClass或RegisterClassEx设定的窗口过程只负责第一次消息时获取Window指针并建立从句柄到Window对象的std::map映射关系,它完成这些工作以后就调用SetWindowLong或者SetWindowLongPtr重新设定另一个窗口过程。另一个窗口过程真正负责处理消息,包括第一次消息也可以由RegisterClass或RegisterClassEx设定的窗口过程委托过来。

LRESULT CALLBACK Window::InitializeProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	WNDPROC const procPtr = reinterpret_cast<WNDPROC>(::GetWindowLongPtr(hWnd,GWLP_WNDPROC));
	if(procPtr == Window::InitializeProc)
	{
		::SetWindowLongPtr(hWnd,GWLP_WNDPROC,reinterpret_cast<LONG_PTR>(Window::__WindowProc));
	}
	Window * pWnd = g_TempWndPtr;
	g_TempWndPtr = NULL;
	if(NULL != pWnd)
	{
		if(0 == pWnd->Handle())
		{
			rubbish::CAutoLock lock(&g_mutex);
			g_wndmap[hWnd] = pWnd;
		}
	}
	return Window::__WindowProc(hWnd, message, wParam, lParam) ;
}//代码由FreeX64提供

Window::Attach

Window::Attach函数的目的是为了接管一些“来源不明”的窗口,比如操作系统内建的各种子控件。之所以说这类窗口“来源不明”,是因为普通程序员不能控制这些窗口过程的代码,这些代码是操作系统的一部分,这些窗口的行为和外观也是操作系统早已定义完毕的。接管这类窗口,本质上就是用程序员自主可控的代码替换掉这些窗口原本由操作系统提供的窗口过程。

使用SetWindowLong或者SetWindowLongPtr,可以很容易地为一个已经存在的窗口指定新的窗口过程。“已经存在的窗口”是指,这些窗口的原生窗口过程已经完成了窗口创建时该做的一些基本初始工作(比如对WM_CREATE消息的响应等)。Window::Attach函数需要做的就是先使用GetWindowLong或者GetWindowLongPtr获取其原生窗口过程,再为这些窗口指定新的窗口过程,如果新窗口过程对某些消息不关心,那就使用CallWindowProc函数把不关心的消息再传回给原生窗口过程处理。粗一看,这个步骤不存在什么问题,事实上可能也真的没什么问题,但是如果不清楚其中的一些细节,这里的问题就不能算是真的解决了。

首先应该想到的就是GetMessage函数,像很多其它的窗口函数一样,其实有两个版本,GetMessageW和GetMessageA。进一步该想到的就是,窗口过程总是收到同一种版本的消息格式,或是ANSI版本,或是Unicode版本,而不是两种都有可能。为了实现这一机制,操作系统在这背后其实做了大量的工作。

原来,每一个窗口都有拥有一个原生字符集,或是ANSI,或是Unicode。这是在注册窗口类的时候就确定下来的。注册窗口类的时候,如果使用的是RegisterClassA或RegisterClassExA,那么由这个类创建出的窗口就拥有原生ANSI字符集。注册窗口类的时候,如果使用的是RegisterClassW或RegisterClassExW,那么由这个类创建出的窗口就拥有原生Unicode字符集。通过操作系统提供的IsWindowUnicode函数,可以很方地便甄别出,一个窗口到底是原生ANSI字符集,还是原生Unicode字符集。

无论窗口拥有哪种原生字符集,都可以使用Unicode或ANSI任意版本的函数对窗口进行操作。操作系统在把消息分发给窗口过程之前,总会检查窗口拥有的原生字符集,如果窗口原生字符集与程序员使用的函数版本不一致,那么操作系统必须把ANSI转成Unicode,或是把Unicode转成ANSI,最终以窗口原生字符集版本的消息格式调用原生窗口过程。这种机制也确定了原生窗口过程的代码版本必须与窗口原生字符集相一致。

难道封装代码需要准备两个版本的窗口过程,先使用IsWindowUnicode函数确定被接管窗口的原生字符集,然后再通过SetWindowLong为窗口指定正确版本的窗口过程吗?这个思路会带来无尽的麻烦,而真正的解决方案却巧到不可思议。不可思议之处就在于SetWindowLong和SetWindowLongPtr也拥有两个版。无论窗口原生哪种字符集,如果

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值