重温WIN32 API ------ 最简单的Windows窗口封装类
1 开发语言抉择
1.1 关于开发Win32 程序的语言选择 C还是C++
在决定抛弃MFC,而使用纯Win32 API 开发Window桌面程序之后,还存在一个语言的选择,这就是是否使用C++。C++作为C的超集,能实现所有C能实现的功能。其实反之亦然,C本身也能完成C++超出的那部分功能,只是可能需要更多行的代码。就本人理解而言,
- 对于巨大型项目,还是使用纯C来架构更加稳妥;
- 对于中小型项目来说,C++可能更方便快捷。由于目前做的是中小项目,所以决定把C++作为主要开发语言。
1.2 关于C++特性集合的选择
在决定使用C++之后,还有一个至关重要的抉择,那就是C++特性集合的选择。C++实在是太复杂了,除了支持它的老祖先C的所有开发模式,还支持基于对象开发(OB)、面向对象开发(OO)、模板技术。可以说,C++是个真正全能型语言,这同时也造成了C++的高度复杂性。使用不同的开发模式,就相当于使用不同的编程语言。就本人而言,对C++的模板编程也根本没有任何经验。综合过去的经验教训和本人对C++的掌握程度,决定:
- 使用基于对象和面向对象两种开发模式,如果一个功能两种都可以实现,则优先选择基于对象。倾向于OB的技术观点来自对苹果Object-C开发经验。
- 尽量避免多继承,此观点来自Java和.net开发经验。
- 数据结构和容器,使用C++标准模板库(STL),模板编程本身复杂,但是使用STL却非常容易。
2 Windows窗口对象的封装类
对Windows桌面程序而言,Window和Message的概念是核心。首先需要封装的就是窗口,例如MFC就是用CWnd类封装了窗口对象。我们当初抛弃MFC的原因,就是因为它太复杂不容易理解,所以对基本窗口对象的封装一定要做到最简单化。
2.1 封装原则
首要的原则就是“简单”。能用一个Win32API直接实现的功能,绝不进行二次包装,如移动窗口可以使用 MoveWindow()一个函数实现,类中就不要出现同样功能的MoveWindow()函数。MFC里有很多这种重复的功能,其实只是可以少写一个hwnd参数而已,却多加了一层调用。我就是要让HWND句柄到处出现,绝不对其隐藏,因为这个概念对于Windows来说太重要了,开发者使用任何封装类都不应该对其视而不见。
其次,同样功能多种技术可以实现时,优先选择容易理解的技术,“可理解性”比“运行效率”更重要。
2.2 源码
头文件 XqWindow.h
#pragma once #include <vector> class XqWindow { public: XqWindow(HINSTANCE hInst); ~XqWindow(); private: HWND hWnd; // 对外只读,确保安全 HINSTANCE hInstance; public: // 返回窗口对象句柄 HWND GetHandle(); // 消息处理。需要后续默认处理则需要返回0;停止该消息后续处理,则返回1 virtual int HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); private: // 原始窗口过程 static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); private: // 已注册过的类集合 static std::vector<void*> registeredClassArray; public: // 创建窗口 void Create(); };
实现文件 XqWindow.cpp
#include "stdafx.h" #include "XqWindow.h" std::vector<void*> XqWindow::registeredClassArray; // 创建窗口 void XqWindow::Create() { wchar_t szClassName[32]; wchar_t szTitle[128]; void* _vPtr = *((void**)this); ::wsprintf(szClassName, L"%p", _vPtr); std::vector<void*>::iterator it; for (it = registeredClassArray.begin(); it != registeredClassArray.end(); it++) // 判断对象的类是否注册过 { if ((*it) == _vPtr) break; } if (it == registeredClassArray.end()) // 如果没注册过,则进行注册 { //注册窗口类 WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = XqWindow::WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = this->hInstance; wcex.hIcon = NULL; wcex.hCursor = ::LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wcex.lpszMenuName = NULL; wcex.lpszClassName = szClassName; wcex.hIconSm = NULL; if (0 != ::RegisterClassEx(&wcex)) // 把注册成功的类加入链表 { registeredClassArray.push_back(_vPtr); } } // 创建窗口 if (this->hWnd == NULL) { ::wsprintf(szTitle, L"窗口类名(C++类虚表指针):%p", _vPtr); HWND hwnd = ::CreateWindow(szClassName, szTitle, WS_OVERLAPPEDWINDOW, 0, 0, 800, 600, NULL, NULL, hInstance, (LPVOID)this ); if (hwnd == NULL) { this->hWnd = NULL; wchar_t msg[100]; ::wsprintf(msg, L"CreateWindow()失败:%ld", ::GetLastError()); ::MessageBox(NULL, msg, L"错误", MB_OK); return; } } } XqWindow::XqWindow(HINSTANCE hInst) { this->hWnd = NULL; this->hInstance = hInst; } XqWindow::~XqWindow() { if ( this->hWnd!=NULL && ::IsWindow(this->hWnd) ) // C++对象被销毁之前,销毁窗口对象 { ::DestroyWindow(this->hWnd); // Tell system to destroy hWnd and Send WM_DESTROY to wndproc } } HWND XqWindow::GetHandle() { return this->hWnd; } // 消息处理。需要后续默认处理则需要返回0;停止该消息后续处理,则返回1 int XqWindow::HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { return 0; } // 原始窗口过程 LRESULT CALLBACK XqWindow::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { XqWindow* pObj = NULL; if (message == WM_CREATE) // 在此消息收到时,把窗口对象句柄赋给C++对象成员,同时把C++对象地址赋给窗口对象成员 { pObj = (XqWindow*)(((LPCREATESTRUCT)lParam)->lpCreateParams); pObj->hWnd = hWnd; // 在此处获取HWND,此时CreateWindow()尚未返回。 ::SetWindowLong(hWnd, GWL_USERDATA, (LONG)pObj); // 通过USERDATA把HWND和C++对象关联起来 } pObj = (XqWindow*)::GetWindowLong(hWnd, GWL_USERDATA); switch (message) { case WM_CREATE: pObj->HandleMessage(hWnd, message, wParam, lParam); break; case WM_DESTROY: if (pObj != NULL) // 此时,窗口对象已经销毁,通过设置hWnd=NULL,来通知C++对象 { pObj->hWnd = NULL; } break; default: pObj = (XqWindow*)::GetWindowLong(hWnd, GWL_USERDATA); if (pObj != NULL) { if (pObj->HandleMessage(hWnd, message, wParam, lParam) == 0) // 调用子类的消息处理虚函数 { return DefWindowProc(hWnd, message, wParam, lParam); } } else { return DefWindowProc(hWnd, message, wParam, lParam); } break; } return 0; }
2.3 使用举例
基本用法为,创建一个TestWindow类,继承自XqWindow,然后重新虚函数 HandleMessage()。所有业务处理代码都要在HandleMessage()里调用,由于该函数是成员函数,所有里面可以直接使用this来引用TestWindow类对象的成员。一个例子代码如下:
TestWindow.h
#pragma once #include "XqWindow.h" class TestWindow : public XqWindow { public: TestWindow(HINSTANCE); ~TestWindow(); protected: int HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); private: // 业务数据部分 int rectWidth; int rectHeight; };
TestWindow.cpp
#include "stdafx.h" #include "TestWindow.h"
TestWindow::TestWindow(HINSTANCE hInst) :XqWindow(hInst) { rectWidth = 300; rectHeight = 200; }
TestWindow::~TestWindow() { } int TestWindow::HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hdc; switch (message) { case WM_PAINT: hdc = ::BeginPaint(hWnd, &ps); ::Rectangle(hdc, 0, 0, this->rectWidth, this->rectHeight); ::EndPaint(hWnd, &ps); return 1; default: break; } return 0; }
调用部分:
pTest = new TestWindow(theApp.m_hInstance); pTest->Create(); ::ShowWindow(pTest->GetHandle(), SW_SHOW); ::UpdateWindow(pTest->GetHandle());
运行效果:
2.4 技术要点
这个XqWindow类对窗口对象做了最小的封装,主要实现了消息处理函数和C++对象的关联。内存布局如下:
需要说明的几点:
(1)C++类和窗口类的一一对应。由于VC++默认不启用RTTI,同时考虑到代码兼容性和运行效率,也不提倡启用RTTI,在没有RTTI支持的情况下,如何才能在运行时把同一个类的所有实例与其他类的实例进行区分呢?这里我们采用了C++的虚表指针,每一个有虚函数的类都拥有自己独立的虚表,而这个虚表指针又在每个实例中存储。同一个类的不同实例共享一个虚表,所以这给了我们区分对象所属C++类的机会。当然这种技术只能用到有虚函数的类中,对于没有虚函数的类的对象,不存在虚表。对于我们的情况,XqWindow类有一个HandleMessage虚函数,从而其他所有继承此类的子类孙类也就都有自己的虚表了。
在RegisterClass()之前,首先判断当前C++对象所属类的虚表指针是否存在vptrAraay链表中。如果没有,则注册窗口类,并把虚表指针存放到vptrArray链表中;如果存在,则直接使用该虚表指针对应的窗口类。
需要注意的是,获取对象虚表指针值的操作不能在XqWindow::XqWindow()构造函数里进行,因为在执行此函数时,C++对象的虚表指针成员尚未被设置到指向派生类的虚表地址(因为尚未调用子类的构造函数)。所以必须在对象构造完成之后才能获取虚表指针值,这也是为什么Create()不能在XqWindow()构造函数里调用的原因。(我曾经为了简化调用把Create()放到XqWindow()里,导致了所有对象的虚表指针都相同的后果!)
(2)C++对象与窗口对象的关系。C++对象创建以后,调用Create()是唯一可以和窗口对象绑定到一起的途径。在旧窗口销毁之前,C++对象不能再创建新窗口,调用Create()多次也没用。
C++对象生存寿命也大于对应的窗口寿命,否则窗口过程中使用C++对象就会出现非法访问内存问题。这两种对象的生命序列为: C++ 对象出生 -- 调用Create()产生窗口对象--某种原因窗口对象销毁--C++对象销毁。
为防止C++对象在窗口对象之前销毁,在XqWindow类的析构函数中,先通过DestroyWindow()销毁窗口对象。窗口对象销毁时,也会设置C++对象的hWnd为NULL,来通知C++对象窗口的销毁。
形象一点的说法:C++对象和窗口对象则是一夫一妻制、且只能丧偶不能离异条件下的夫妻关系,而且C++对象是寿命长的一方,窗口对象则是寿命短的一方。只有一个窗口对象死掉后,C++对象才能重新生成新窗口。而且C++对象死掉之前,需要先把窗口对象杀死陪葬。
(3)C++对象和窗口对象的彼此引用。C++对象通过成员变量hWnd引用窗口对象,窗口对象则通过GWL_USERDATA附加数据块指向C++对象。另外为了及时捕获WM_CRATE消息并在HandleMessage里处理,C++成员hWnd的赋值并没有在CreateWindow()之后,而是在原始窗口过程函数处理WM_CREAT消息时。这主要与CreateWindow()原理有关。
CreateWindow()
{
HWND hwnd = malloc(..);
初始化窗口对象;
WndProc(hwnd, WM_CRATE, ..); // 此时已经创建了窗口
其他操作;
return hwnd;
}
同理,DestroyWindow()的原理为.
DestroyWindow(hwnd)
{
窗口对象清理工作;
WndProc(hwnd, WM_DESTROY, ..); // 此时窗口已经不可见了
其他操作;
free(hwnd);
}
2.5 存在问题
虽然XqWindow类可以很好的工作,但也存在一些问题:
(1)由于Window对象靠USERDATA引用C++对象,所以如果其他代码通过SetWindowLong(hwnd, GWL_USERDATA, xxx)修改了这个数据块,那么程序将会崩溃。如何防止这种破坏,需要进一步研究。
(2)使用C++对象的虚表指针,而这个指针的具体内存布局并没有明确的规范标准,一旦将来VC++编译器修改虚表指针的存放位置,程序将会出问题。不过由于考虑到二进制的兼容性,VC++作出这种改变的可能性不大。
3 一点感受
XqWindow类的源码一共不到150行,却花了我2天的业余时间来完成。这里涉及到对C++对象内存布局,窗口创建、销毁、消息处理过程的深入理解。写一个小小类就如此不易,写一个健壮的类库真是难上加难,想想MFC也真的挺不容易的。
关于这个类,大家有什么好的想法,欢迎交流探讨。