Windows界面编程:MFC
前言
大家之前在学C/C++语言时,基本都是通过控制台实现“人机交流”的。但大家每次在写控制台程序时,看到黑框框应该都会有些许不爽吧:“输入输出为什么不能基于图形用户界面而非要使用文本用户界面呢?”事实上,在各个环境下均可用C/C++实现图形用户界面。下文的MFC技术,就是在Windows环境下实现对图形用户界面的编程的。
MFC即微软基础类库(Microsoft Foundation Classes),是微软公司以C++的形式对Windows API进行封装,从而使开发者较为方便的对Windows进行图形用户界面(GUI)开发。在下文中笔者将和大家聊聊MFC的发展和原理以及简单的应用——计算机绘图。
从C语言开始!
MFC最初的思想是面向过程的,即通过C语言和Windows API函数,实现对图形用户界面的开发。Windows API 函数主要包括三大类型:窗口管理函数(实现窗口的创建、移动和修改等功能)、图形设备函数(实现图形的绘制及操作功能)和系统服务函数(实现与操作系统有关的一些功能)。API函数所操作的基本数据类型是句柄——一种复杂的程序对象和实例(如滚轮、按钮、滚动条等)的标识,类似于用于标识整形变量的int。常见的句柄类型如下:
句柄类型 | 说明 | 句柄类型 | 说明 |
HWND | 窗口句柄 | HDC | 图形设备环境句柄 |
HINSTANCE | 当前程序应用实例句柄 | HBITMAP | 位图句柄 |
HCURSOR | 光标句柄 | HICON | 图标句柄 |
HFONT | 字体句柄 | HMENU | 菜单句柄 |
HPEN | 画笔句柄 | HFELE | 文件句柄 |
HBRUSH | 画刷句柄 |
|
|
接下来介绍“事件”和“消息”的概念。
大家用的操作系统基本都是Windows,Windows的一个特点是在它会弹出一个个窗口,而每个窗口弹出后会进入一个等待状态(while循环),直到接收到了某种刺激(如鼠标点击、键盘输入等)后,系统和程序才会脱离等待状态并对这个刺激进行处理。我们把这个可能触发计算机程序做出相应反应的刺激称作“事件”。
为了描述“事件”的各种信息(即何时、何地发生了何种事件),Windows定义了一个结构,即“消息”。
部分常见消息的标识 | 说明 |
WM_LBUTTONDOWN | 按下鼠标左键时产生的消息 |
WM_LBUTTONBLICK | 双击鼠标左键时产生的消息 |
WM_CLOSE | 关闭窗口时产生的消息 |
WM_DESTROY | 消除窗口时产生的消息 |
WM_PAINT | 需要窗口重画时产生的消息 |
当产生某种消息后,Windows把这种消息送入消息队列中等待使用,此时应用程序调用API函数从消息队列中不断的获取消息并发送给系统,从而构成了“消息循环”。消息循环的代码如下:
此后,系统根据消息中的标识message调用相应的窗口函数处理消息。待处理结束后,返回消息循环等待获取下一个消息直到应用程序结束。
通过上面的分析,大家可以发现应用程序的关键在于消息循环的建立。事实上,建立消息循环和创建应用程序窗口(可以理解为程序提供“舞台”)正是主函数的两个任务。在Windows应用程序中,主函数写作WinMain而不是main。
将上述的API函数、句柄、消息循环、创建的窗口和主函数相结合,就可以实现图形用户界面的设计了。
进化——函数封装、消息映射表和宏定义
上一部分为大家分析和介绍了早期MFC的实现过程,即通过调用API函数来创建窗口和消息循环,之后再利用窗口函数对消息进行处理。这一过程很容易理解,但实际上在写代码时由于要做的任务很多(如完成依次对各个窗口的类型进行初始化和注册、依次初始化窗口函数等),很容易把代码写的很乱且不容易理解,此时可以利用C语言模块化的特点将程序的各个部分用函数封装好,及分别使用不同的函数完成上述的任务,从而使得整个程序由主函数和多个窗口函数组成。
例外,窗口函数(用于处理消息队列中的消息)通常使用了switch语句(因为要根据消息的不同而进行不同的处理),我们可以将各个消息处理程序段(即switch语句中各个case后面的部分)封装成函数,并用函数指针去分别指向这些函数。这种有消息标识和函数指针之间的关系被称作消息映射表,常用数组或链表实现。
某个消息映射表 | |
WM_LBUTTONDOWN | On_LButtonDown |
WM_PAINT | On_Paint |
WM_RESTROY | On_Restroy |
大家肯定有疑惑:“好好的函数不是挺好嘛,为啥要多此一举呢?”就笔者个人的理解,使用函数封装表一是为了使程序更加模块化、更便于修改和理解,二是随着窗口函数的复杂化,比如有多个子类继承了父类,而每个子类又有各自的switch语句(如CcmdTarget类),在这种情况下,建立一个多维的消息映射表就更便于理解和维护程序了。
更进一步,我们还可以把消息映射表的各项定义为宏使代码更规整漂亮。
至此,我们已经把用C语言建立的最初的“MFC”程序通过函数封装、消息映射表和宏定义的方法很好的封装起来了——而此时我们的程序已经很舒服、很接近MFC程序了。
面向对象化
我们在上一部分末尾得到的MFC仍然是面向过程的,虽然它已经封装的很好了,但在设计开发图形用户界面时仍十分困难——代码的重复量太多了。在此基础上,结合面向对象的思想,便可大大简化开发难度得到现在的MFC。
具体来讲,我们可以把主函数(用于创建并显示窗体和实现消息循环)看作是一个对象——应用程序类对象,窗体则是嵌入应用程序对象的另一个对象。在设计具体程序时,其他类派生于窗口类或应用程序类(派生时会用到虚函数和在类中声明消息映射表)。
至此,早期的MFC程序——窗口类+应用程序类便实现了。
MFC的应用实例
上面的三个部分主要介绍了MFC的思想和发展,但现在我们要写一个MFC程序远没有这么麻烦。借助VS中的MFC应用程序向导、类视图、资源试图我们可以相对容易的设计MFC程序。接下来笔者将具体介绍一个用MFC进行绘图的例子。
Windows环境下依靠GDI(图形用户接口)和DC(设备描述环境)对图形进行支持,两者被封装到一起形成CDC类。调用CDC类的派生类的部分函数并结合一些数学知识便可以绘出很棒的图形。
CDC类部分函数 | 说明 |
Arc() | 画圆弧 |
Ellipse() | 画椭圆 |
Lineto() | 把指定画笔画直线到参数指定的位置 |
Textout() | 绘制字符串 |
CDC类部分派生类 | 说明 |
CClientDC | 窗口客户区的设备描述环境,在WM_PAINT消息之外的消息处理函数中 |
CMetaFileDC | 图元文件的设备描述环境,在创建可以放回的图像时使用 |
CpaintDC | 窗口用户区的设备描述环境,在OnDraw()函数中处理WM_PAINT消息 |
CwindowDC | 在整个窗口内(不只是用户区)绘图的设备描述环境 |
下面介绍一个具体的例子。在VS中新建工程,选择VC++中的MFC,打开MFC应用程序,依次选择“下一步”“单个文档”“MFC标准”“完成”。
在“解决方案管理器”中找到myMFCView.cpp,并修改其中的OnDraw函数,添加如下代码:
执行程序结果如下:
结语
随着技术的不断发展,基于对话框的图形用户程序多使用C#编程,绘图则更多地使用opencv、opengl等,但MFC中所蕴涵的早期的图形化界面思想和处理问题的方法仍值得我们学习(笔者觉得“函数映射表”的处理方法对于模块化和封装程序很实用)。由于笔者和大家一样都是初学者,上文中对MFC的阐释难免会有一些不太恰当的地方,望大家见谅。
更多关于MFC的内容,大家可以参考任哲的《MFC Windows应用程序设计》、孙鑫的《VC++深入详解》以及CSDN博客(笔者参考的博客网址:https://blog.csdn.net/Eastmount/article/details/53180524)。