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窗口封装类相关推荐

  1. 按键精灵通过句柄获取窗口坐标_使用visual studio 2019 创建简单的Windows窗口「Win32」...

    今天Bili发现网上好多MFC代码都不能用,给大家分享一个简单的MFC窗口语言: 学的不仅仅是技术,更是梦想 学的不仅仅是技术,更是梦想 学的不仅仅是技术,更是梦想 学的不仅仅是技术,更是梦想 复制代 ...

  2. 【转】win32 的DLL中创建windows窗口

    转自:https://blog.51cto.com/wangyw/1071967 因工作需求,需要写一个DLL,并在其中创建windows窗口,网上有很多关于DLL创建窗口的文章,不过都是基于MFC的 ...

  3. 重温WIN32 API ------ 窗口上绘制位图文件

    1 基本思路 做界面仿真时,经常需要在窗口上贴图,随着图片数量的增多,通过资源文件中添加位图的方式会变得越来越不可控.所以本着"资源与程序分离"的原则,还是使用外部位图文件更加清晰 ...

  4. C# 调Win32 API SendMessage简单用法及wMsg常量

    函数功能:该函数将指定的消息发送到一个或多个窗口.此函数为指定的窗口调用窗口程序,直到窗口程序处理完消息再返回.该函数是应用程序和应用程序之间进行消息传递的主要手段之一.     函数原型:LRESU ...

  5. Win32 API 封装类总结

    以下是一些封装的Win32 API类:备用: 重温WIN32 API ------ 最简单的Windows窗口封装类 http://blog.csdn.net/smstong/article/deta ...

  6. 在windows下对android进行实时快速录屏和模拟点击(二)——win32 API操控模拟器

    从上一节在windows下对android进行实时快速录屏和模拟点击(一)--使用adb命令行当中,描述了适合于所有情况下的通用方法--adb.而通用方法的最大缺点就是:速度非常慢,在有些时候是远远不 ...

  7. Win32 API 打印

    重温 Win32 API ----- 截屏指定窗口并打印 http://blog.csdn.net/smstong/article/details/42082039 朋友说在一个VC++6.0开发的项 ...

  8. 揭示Win32 API拦截细节/API hooking revealed (1)

    原文出处:http://www.codeproject.com/system/hooksys.asp 简要介绍 拦截win32 API 调用对于多数windows开发人员来说都一直是很有挑战性的课题, ...

  9. 揭示win32 api拦截细节

    转自啊D 原文出处:http://www.codeproject.com/system/hooksys.asp     拦截win32 API 调用对于多数windows开发人员来说都一直是很有挑战性 ...

最新文章

  1. 2017 06 11 小分队二期两周总结
  2. Docker:搭建RabbitMQ集群
  3. 工匠精神,缔造美国净水传奇
  4. 存储型xss_web安全测试--XSS(跨站脚本)与CSRF
  5. Android Framework系统服务详解
  6. java数据类型及表示范围_Java数据类型及对应取值范围
  7. 赛门铁克运维注意事项
  8. MongoDB使用经验总结
  9. 【Linux】【Shell】【Basic】字符串操作
  10. 移动光猫获取管理员密码过程记录(吉比特TEWA-272G)
  11. YUV的原始数据文件转rgb使用cv2显示
  12. java导出文件放到压缩包里
  13. chrome浏览器打开base64图片
  14. 电脑怎么压缩jpg图片?如何压缩照片并保持清晰?
  15. 逆流而上的你,送给现在的你
  16. Android 模拟器 Root 和 SuperSU 安装
  17. OTP一次性动态密码工具实现
  18. m基于matlab的光通信误码率仿真,分别对比了OFDM+BPSK和OFDM+4QAM的误码率和星座图
  19. OLE、ActiceX、COM、DLL
  20. 达人评测 i5 12500h和锐龙r5 5600h选哪个好

热门文章

  1. 【深度首发】2018年的小目标是“1个亿”,试玩互动能否成为全球首个游戏试玩移动广告平台的独角兽?丨Xtecher 封面
  2. 湖南c语言高考试卷及答案,高考试题库
  3. 使用CallableStatment存储过程方法
  4. 云邦互联 1GB免费空间(免备案,无广告)
  5. 云呐医院行业固定资产管理系统软件
  6. 会议OA项目之会议发布(一)
  7. 银行、证券行业云计算身份识别服务器深度开发
  8. Flash 与课件制作:Mp3播放
  9. MYSQL经典SQL之生日问题
  10. Hive之自定义函数