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

  1. #pragma once
  2. #include <vector>
  3. class XqWindow
  4. {
  5. public:
  6. XqWindow(HINSTANCE hInst);
  7. ~XqWindow();
  8. private:
  9. HWND hWnd;    // 对外只读,确保安全
  10. HINSTANCE hInstance;
  11. public:
  12. // 返回窗口对象句柄
  13. HWND GetHandle();
  14. // 消息处理。需要后续默认处理则需要返回0;停止该消息后续处理,则返回1
  15. virtual int HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
  16. private:
  17. // 原始窗口过程
  18. static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
  19. private:
  20. // 已注册过的类集合
  21. static std::vector<void*> registeredClassArray;
  22. public:
  23. // 创建窗口
  24. void Create();
  25. };

实现文件 XqWindow.cpp

  1. #include "stdafx.h"
  2. #include "XqWindow.h"
  3. std::vector<void*> XqWindow::registeredClassArray;
  4. // 创建窗口
  5. void XqWindow::Create()
  6. {
  7. wchar_t szClassName[32];
  8. wchar_t szTitle[128];
  9. void* _vPtr = *((void**)this);
  10. ::wsprintf(szClassName, L"%p", _vPtr);
  11. std::vector<void*>::iterator it;
  12. for (it = registeredClassArray.begin(); it != registeredClassArray.end(); it++)  // 判断对象的类是否注册过
  13. {
  14. if ((*it) == _vPtr)
  15. break;
  16. }
  17. if (it == registeredClassArray.end())                // 如果没注册过,则进行注册
  18. {
  19. //注册窗口类
  20. WNDCLASSEX wcex;
  21. wcex.cbSize = sizeof(WNDCLASSEX);
  22. wcex.style = CS_HREDRAW | CS_VREDRAW;
  23. wcex.lpfnWndProc = XqWindow::WndProc;
  24. wcex.cbClsExtra = 0;
  25. wcex.cbWndExtra = 0;
  26. wcex.hInstance = this->hInstance;
  27. wcex.hIcon = NULL;
  28. wcex.hCursor = ::LoadCursor(NULL, IDC_ARROW);
  29. wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  30. wcex.lpszMenuName = NULL;
  31. wcex.lpszClassName = szClassName;
  32. wcex.hIconSm = NULL;
  33. if (0 != ::RegisterClassEx(&wcex))     // 把注册成功的类加入链表
  34. {
  35. registeredClassArray.push_back(_vPtr);
  36. }
  37. }
  38. // 创建窗口
  39. if (this->hWnd == NULL)
  40. {
  41. ::wsprintf(szTitle, L"窗口类名(C++类虚表指针):%p", _vPtr);
  42. HWND hwnd = ::CreateWindow(szClassName,
  43. szTitle,
  44. WS_OVERLAPPEDWINDOW,
  45. 0, 0, 800, 600,
  46. NULL,
  47. NULL,
  48. hInstance,
  49. (LPVOID)this
  50. );
  51. if (hwnd == NULL)
  52. {
  53. this->hWnd = NULL;
  54. wchar_t msg[100];
  55. ::wsprintf(msg, L"CreateWindow()失败:%ld", ::GetLastError());
  56. ::MessageBox(NULL, msg, L"错误", MB_OK);
  57. return;
  58. }
  59. }
  60. }
  61. XqWindow::XqWindow(HINSTANCE hInst)
  62. {
  63. this->hWnd = NULL;
  64. this->hInstance = hInst;
  65. }
  66. XqWindow::~XqWindow()
  67. {
  68. if ( this->hWnd!=NULL && ::IsWindow(this->hWnd) ) // C++对象被销毁之前,销毁窗口对象
  69. {
  70. ::DestroyWindow(this->hWnd);  // Tell system to destroy hWnd and Send WM_DESTROY to wndproc
  71. }
  72. }
  73. HWND XqWindow::GetHandle()
  74. {
  75. return this->hWnd;
  76. }
  77. // 消息处理。需要后续默认处理则需要返回0;停止该消息后续处理,则返回1
  78. int XqWindow::HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
  79. {
  80. return 0;
  81. }
  82. // 原始窗口过程
  83. LRESULT CALLBACK XqWindow::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
  84. {
  85. XqWindow* pObj = NULL;
  86. if (message == WM_CREATE)   // 在此消息收到时,把窗口对象句柄赋给C++对象成员,同时把C++对象地址赋给窗口对象成员
  87. {
  88. pObj = (XqWindow*)(((LPCREATESTRUCT)lParam)->lpCreateParams);
  89. pObj->hWnd = hWnd;                                            // 在此处获取HWND,此时CreateWindow()尚未返回。
  90. ::SetWindowLong(hWnd, GWL_USERDATA, (LONG)pObj);             // 通过USERDATA把HWND和C++对象关联起来
  91. }
  92. pObj = (XqWindow*)::GetWindowLong(hWnd, GWL_USERDATA);
  93. switch (message)
  94. {
  95. case WM_CREATE:
  96. pObj->HandleMessage(hWnd, message, wParam, lParam);
  97. break;
  98. case WM_DESTROY:
  99. if (pObj != NULL)  // 此时,窗口对象已经销毁,通过设置hWnd=NULL,来通知C++对象
  100. {
  101. pObj->hWnd = NULL;
  102. }
  103. break;
  104. default:
  105. pObj = (XqWindow*)::GetWindowLong(hWnd, GWL_USERDATA);
  106. if (pObj != NULL)
  107. {
  108. if (pObj->HandleMessage(hWnd, message, wParam, lParam) == 0) // 调用子类的消息处理虚函数
  109. {
  110. return DefWindowProc(hWnd, message, wParam, lParam);
  111. }
  112. }
  113. else
  114. {
  115. return DefWindowProc(hWnd, message, wParam, lParam);
  116. }
  117. break;
  118. }
  119. return 0;
  120. }

2.3 使用举例

基本用法为,创建一个TestWindow类,继承自XqWindow,然后重新虚函数 HandleMessage()。所有业务处理代码都要在HandleMessage()里调用,由于该函数是成员函数,所有里面可以直接使用this来引用TestWindow类对象的成员。一个例子代码如下:

TestWindow.h

  1. #pragma once
  2. #include "XqWindow.h"
  3. class TestWindow :
  4. public XqWindow
  5. {
  6. public:
  7. TestWindow(HINSTANCE);
  8. ~TestWindow();
  9. protected:
  10. int HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
  11. private:
  12. // 业务数据部分
  13. int rectWidth;
  14. int rectHeight;
  15. };

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;
}

  

调用部分:

  1. pTest = new TestWindow(theApp.m_hInstance);
  2. pTest->Create();
  3. ::ShowWindow(pTest->GetHandle(), SW_SHOW);
  4. ::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. java最简单的知识之创建一个简单的windows窗口,利用Frame类

    作者:程序员小冰,CSDN博客:http://blog.csdn.net/qq_21376985 QQ986945193 微博:http://weibo.com/mcxiaobing 首先给大家看一下 ...

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

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

  3. 简单的windows窗口创建实例

    #include<windows.h> #include<tchar.h> LRESULT CALLBACK WndProc(HWND hwnd,UINT umsg,WPARA ...

  4. 使用Win32 API创建不规则形状&带透明色的窗口

    前一阵突然想起了9月份电面某公司实习时的二面题,大概就是说怎么用Win32 API实现一个透明的窗口,估计当时我的脑残答案肯定让面试官哭笑不得吧.所以本人决定好好研究下这个问题.经过一下午的摸索,基本 ...

  5. 【.Net】从.NET平台调用Win32 API

    小序        Win32 API可以直接控制Microsoft Windows的核心,因为API(Application Programming Interface)本来就是微软留给我们直接控制 ...

  6. C#调用Win32 api学习总结

    从.NET平台调用Win32 API Win32 API可以直接控制Microsoft Windows的核心,因为API(Application Programming Interface)本来就是微 ...

  7. 第一个Windows窗口应用程序

    学习目的 熟悉开发工具Visual C++ 6.0和MSDN 2001的使用. 应用Windows API函数, 手工编写具有最基本构成的Windows窗口应用程序(包含WinMain入口函数, 消息 ...

  8. 控制台——对WIN32 API的使用(user32.dll)

    Win32 API概念:即为Microsoft 32位平台的应用程序编程接口(Application Programming Interface).所有在Win32平台上运行的应用程序都可以调用这些函 ...

  9. win32 api Windows窗口的创建

    windows窗口的创建有以下几个步骤: 1.创建注册窗口类 2.创建窗口句柄 3.显示更新窗口 4.消息循环 1.创建注册窗口类 所谓创建窗口类就是定义一个WNDCLASS类对象,并将该对象进行初始 ...

随机推荐

  1. 一次使用NodeJS实现网页爬虫记

    前言 几个月之前,有同事找我要PHP CI框架写的OA系统.他跟我说,他需要学习PHP CI框架,我建议他学习大牛写的国产优秀框架QeePHP. 我上QeePHP官网,发现官方网站打不开了,GOOGL ...

  2. 解决maven Generating project in Interactive mode

    在idea建一个基于maven结构的web项目时,cmd输出卡死在Generating project in Interactive mode不动了 用命令mvn archetype:generate ...

  3. 从日志文件解决ArcGIS Server性能低下问题的步骤(1)

    日志级别和结构 http://www.cnblogs.com/fortoday/archive/2011/03/30/2000348.html ArcGIS Server日志文件分为几个记录级别: 无 ...

  4. django 简易博客开发 4 comments库使用及ajax支持

    首先还是贴一下源代码地址  https://github.com/goodspeedcheng/sblog 上一篇文章我们介绍了静态文件使用以及如何使用from实现对blog的增删改,这篇将介绍如何给 ...

  5. [RxJS] Create a Reusable Operator from Scratch in RxJS

    With knowledge of extending Subscriber and using source.lift to connect a source to a subscriber, yo ...

  6. C++开发人脸性别识别教程(8)——搭建MFC框架之读取目录信息

    在上一篇博客中我们已经绘制了MFC界面,在这篇博客中我们将加入响应代码,为MFC框架加入一个最主要的功能:打开一个目录. 一.加入相关头文件 这里头文件主要包括三类:opencv头文件.批量读取文件相 ...

  7. 解决UnicodeDecodeError: 'ascii' codec can't decode byte 0xe7 in position 12: ordinal not in range(128)的编码问题

    当我在运行一个基于scrapy的爬虫时出现UnicodeDecodeError: 'ascii' codec can't decode byte 0xe7 in position 12: ordina ...

  8. 解决运行scrapy是报错No module named cryptography,解决cryptography的安装问题,解决libffi的安装问题

    在linux下配置完运行是出现ImportError: No module named cryptography.hazmat.bindings.openssl.binding的错误.原因是crapt ...

  9. 三. 200多万元得到的创业教训--创业并不须要app

    摘要:有个点子,研发app或站点,推广,不断改进,探索盈利模式.这个通用的移动互联网创业流程.但我觉得.在某些特定的商业模式下,"研发app或站点"这步能够砍掉或推迟. 健生干货分 ...

  10. 数据结构-二叉树的遍历(类C语言描写叙述)

    遍历概念     所谓遍历(Traversal)是指沿着某条搜索路线.依次对树中每一个结点均做一次且仅做一次訪问.訪问结点所做的操作依赖于详细的应用问题. 遍历是二叉树上最重要的运算之中的一个,是二叉 ...