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也真的挺不容易的。
关于这个类,大家有什么好的想法,欢迎交流探讨。
WIN32 API ------ 最简单的Windows窗口封装类的更多相关文章
- java最简单的知识之创建一个简单的windows窗口,利用Frame类
作者:程序员小冰,CSDN博客:http://blog.csdn.net/qq_21376985 QQ986945193 微博:http://weibo.com/mcxiaobing 首先给大家看一下 ...
- C# 调Win32 API SendMessage简单用法及wMsg常量
函数功能:该函数将指定的消息发送到一个或多个窗口.此函数为指定的窗口调用窗口程序,直到窗口程序处理完消息再返回.该函数是应用程序和应用程序之间进行消息传递的主要手段之一. 函数原型:LRESU ...
- 简单的windows窗口创建实例
#include<windows.h> #include<tchar.h> LRESULT CALLBACK WndProc(HWND hwnd,UINT umsg,WPARA ...
- 使用Win32 API创建不规则形状&带透明色的窗口
前一阵突然想起了9月份电面某公司实习时的二面题,大概就是说怎么用Win32 API实现一个透明的窗口,估计当时我的脑残答案肯定让面试官哭笑不得吧.所以本人决定好好研究下这个问题.经过一下午的摸索,基本 ...
- 【.Net】从.NET平台调用Win32 API
小序 Win32 API可以直接控制Microsoft Windows的核心,因为API(Application Programming Interface)本来就是微软留给我们直接控制 ...
- C#调用Win32 api学习总结
从.NET平台调用Win32 API Win32 API可以直接控制Microsoft Windows的核心,因为API(Application Programming Interface)本来就是微 ...
- 第一个Windows窗口应用程序
学习目的 熟悉开发工具Visual C++ 6.0和MSDN 2001的使用. 应用Windows API函数, 手工编写具有最基本构成的Windows窗口应用程序(包含WinMain入口函数, 消息 ...
- 控制台——对WIN32 API的使用(user32.dll)
Win32 API概念:即为Microsoft 32位平台的应用程序编程接口(Application Programming Interface).所有在Win32平台上运行的应用程序都可以调用这些函 ...
- win32 api Windows窗口的创建
windows窗口的创建有以下几个步骤: 1.创建注册窗口类 2.创建窗口句柄 3.显示更新窗口 4.消息循环 1.创建注册窗口类 所谓创建窗口类就是定义一个WNDCLASS类对象,并将该对象进行初始 ...
随机推荐
- pip命令自动补全功能;设置代理;使用国内源
这是pip自带的功能 执行的脚本 把脚本写入.zshrc或者profile等里面,执行source立即生效 设置代理: pip --proxy=http://username:password@pro ...
- ArcGIS Engine 10.2 如何发布服务
http://blog.csdn.net/arcgis_all/article/details/17376397 1 ArcGIS Engine 10.2 如何发布服务 ArcGIS Engine的代 ...
- c++之函数对象、bind函数
函数对象实质上是一个实现了operator()--括号操作符--的类. class Add { public: int operator()(int a, int b) { return a + b; ...
- Angular2.x-显示heroes列表
在此页面中,您将展开Tour of Heroes应用程序以显示heroes列表,并允许用户选择heroes并显示heroes的详细信息. 6.X 你需要一些heroes来展示. 最终你会从远程数据服务 ...
- 剑指Offer面试题15(Java版):链表中倒数第K个结点
题目: 输入一个链表.输出该链表中倒数第k哥结点. 为了符合大多数人的习惯,本题从1開始计数.即链表的尾结点是倒数第1个结点. 比如一个链表有6个结点.从头结点開始它们的值依次是1.2.3,4,5, ...
- Asp.net MVC 简单分页 自做简单分页
Asp.net MVC 简单分页: public static string Pager(int page,int pageSize,int total) { ...
- 工作总结 js 选择器选择多条元素 支持一起设置他们属性 $("#edumes input[type='radio']").prop("checked", false);
$("#edumes input[type='radio']").prop("checked", false); $("#edumes input[t ...
- 动态标绘演示系统1.4.3(for ArcGIS Flex)
标绘有API文档啦! 在线浏览 ------------------------------------------------------------------------------------ ...
- What to do about Eclipse's “No repository found containing: …” error messages?
As Mauro said: "you have to remove and re-add the Eclipse Project Update site, so that its meta ...
- C++ 虚函数与纯虚函数 浅析
[摘要] 在虚函数与纯虚函数的学习中.要求理解虚函数与纯虚函数的定义,了解虚函数与纯虚函数在实例化上的差异.掌握两者在实现上的必要性.熟悉纯虚函数在子类与孙类的函数类型.本文即针对上述问题展开阐述. ...