文章篇幅可能会比较长,从入门到基本能上项目的全部内容。建议观看的过程中,用电脑跟着学习案例。
持续输出优质文章是作者的追求,因为热爱,所以热爱。
最近看动漫被一句鸡汤感动到了,也送给各位朋友:
只要有一息尚存,战斗便永不停止...
windows消息机制
如果想要更好的学习掌握MFC,必须要先了解WIndows程序的内部运行机制。
windows编程基础 -- win32程序
在windows平台下,提供了很多函数可以调用,这些函数是由windows操作系统本身提供的。windows应用程序API函数是通过C语言实现的,所有主要的windows函数都在windows.h头文件中进行了声明。Windows操作系统提供了1000多种API函数。
窗口和句柄
窗口是windows应用程序中一个非常重要的元素,一个windows应用程序至少要有一个窗口,称为主窗口。窗口是屏幕上的一块矩形区域,是windows应用程序与用户进行交互的接口。利用窗口可以接收用户的输入以及显示输出。
窗口又分为客户区(中间空白的部分)和非客户区(蓝色那部分)。
客户区是窗口的一部分,应用程序通常在客户区中显示文字或者绘制图形。 应用程序则主要管理客户区的外观及操作
非客户区也是窗口的一部分,通常将标题栏、菜单栏、系统菜单、最小化框和最大化框、可调边框统称为窗口的非客户区,它们由windows系统来管理,
总结来说:我们写代码主要控制客户区部分、非客户区部分通常由操作系统控制。
窗口是windows中一个很重要的概念,是一个几部组件部分。窗口有父窗口和子窗口,除了上图所示类型的窗口外,对话框和消息框也是一种窗口。在对话框上通常还包含很多子窗口,这些窗口的形式也按钮、单选按钮、复选按钮、组框、文本编辑等。
操作窗口是通过一个指针来控制的(可以将窗口近似的理解为一个结构体的一个变量),有一个指针指向这个对象,通过这个指针就可以对这个结构体变量中的成员进行操作,比如大小、背景颜色、按钮的响应等。所以想要操作某一个窗口,就必须先拿到这个窗口的指针,在windows中习惯称为句柄(HWDN)。
句柄(HANDLE)是一个非常重要的概念,在windows程序中,定义了很多结构体,操作这些结构体变量(也是各种各样的资源)通常情况需要使用句柄。系统在创建这些资源时会为它们分配内存,并返回标识这些资源的标识号。比如:图标句柄、光标句柄、画刷句柄等。
消息与消息队列
通常情况下,我们写的程序是一行一行执行的,也就是DOS方式的程序设计方式。而windows程序并不是这样的,它是一种事件驱动模式,这中框架模式思想很值得我们了解。
每一个windows应用程序开始执行后,系统都会为该程序创建一个消息队列,这个消息队列用来存放程序创建的窗口的消息。(这些消息比如:鼠标左键按下)
一个应用程序可以有多个窗口,这些窗口会有一个窗口标记,也就是上面说的窗口句柄,通过这个窗口句柄,操作系统就知道在哪个窗口上进行相应的处理。
整个框架的流程:
1.用户在客户区按下了鼠标左键,此时操作系统会感知到用户按下了鼠标左键这个事件。
2.操作系统将这个事件包装成一个消息,并将这个消息放到消息队列中,等待应用程序从队列中取出消息并进行处理。
3.应用程序从消息队列中取出消息,然后进行响应
4.这个响应过程,通常应用程序交给操作系统进行处理,操作系统会调用消息处理函数(回调函数),这个函数被称为窗口过程。
步骤4:整个响应过程就是一个回调
Windows编程模型
一个完整的win32程序实现的功能就是创建一个窗口,然后在这个窗口中响应键盘、鼠标消息。这个窗口可以有一些子窗口...本质上就是这样的,并不复杂。
大致步骤:
1.windows函数定义
2.创建一个窗口
3.进行消息循环
4.编写窗口过程函数
创建一个win32的空项目
编写一个简单的WIN32程序
1.主函数定义(WinMain)
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR IpCmdLine, int nShowCmd) { return 0; }
WINAPI:是一个宏,表示参数从右往左入栈,同时在函数返回前自动清空堆栈
hInstance:(当前应用程序的句柄)一个应用程序的的实例句柄,一个应用程序可以运行多个实例,每一个运行的应用程序都会有一个句柄,通过hInstance传递给WinMain函数
hPrevInstance:当前实例的前一个实例句柄,通常传NULL
IpCmdLine:一个字符串数组,传给给应用程序的命令行参数,相当于 char* argv[]
nShowCmd:窗口显示的方式,最大化/最小化/正常显示/隐藏显示
2.创建一个窗口
创建一个WIN32窗口,通常有4个步骤: 设计一个窗口类,注册窗口类,创建窗口,显示和更新窗口(Windows.h中有现成的API可以调用)
步骤1:设计一个窗口类
这一步可以用来指定在我们这个窗口中的光标的样子、背景色、图标等这些属性。
也就是说,在创建一个窗口前,必须对给类型的窗口进行设计,指定窗口的特征。在windows中,窗口的特征由WNDCLASS结构体来定义的,我们只需要给WNDCLASS结构体对应的成员进行赋值就可以完成窗口类的设计。
WNDCLASS结构体的定义:
style:指定窗口的样式
CS_HREDRAW ==> 当窗口水平方向上的宽度发生变化时, 将重新绘制整个窗口。如果没有指定这一样式,那么在水平方向上调整窗口宽度时,将不会重绘窗口。
CS_VREDRAW ==> 当窗口垂直方向上的高度发生变化时,将重新绘制整个窗口。如果没有指定这一样式,那么在垂直方向上调整窗口高度时,将不会重绘窗口。
CS_NOCLOSE ==> 禁用系统菜单的 Close 命令,这将导致窗口没有关闭按钮。
CS_DBLCLKS ==> 当用户在窗口中双击鼠标时,向窗口过程发送鼠标双击消息。
lpfnWndProc:指定一个窗口回调函数,是一个函数的指针
当应用程序收到给某一窗口的消息时,就应该调用某一函数来处理这条消息。这一调用过程不用应用程序自己完成,由操作系统来完成。上面说的回调过程。每一个窗口都有自己专用的回调函数,该函数就是通过lpfnWndProc成员指定的。
cbClsExtra:类的附加内存,通常数情况下为0
cbWndExtra:窗口附加内存,通常情况下为0
hInstance:当前实例句柄,用WinMain中的形参hInstance为其赋值
hIcon:指定窗口类的图标句柄,设置为NULL,则使用默认图标
hCursor:指定窗口类的光标句柄,设置为NULL,则使用默认图标,也可用如下函数进行赋值:
hbrBackground:指示窗口的背景颜色,可用如下函数进行赋值:
lpszMenuName:指定菜单资源的名字。如果设置为NULL,那么基于这个窗口类创建的窗口将没有默认菜单
lpszClassName:指定窗口类的名字
步骤2:注册窗口类
设计完窗口类(WNDCLASS)后, 需要调用RegisterClass函数对其进行注册,注册成功后,才可以创建该类型的窗口。
步骤3:创建窗口
设计好窗口类并且将其成功注册之后, 即可用CreateWindow函数产生这种类型的窗口了。
lpClassName:指定窗口类的名称,此名字必须和WNDCLASS的lpszClassName成员指定的名称一样。
lpWindowName:指定窗口的名字,即窗口的标题。
dwStyle:指定创建的窗口的样式,常指定为指WS_OVERLAPPEDWINDOW类型,这是一种多种窗口类型的组合类型。
x, y:指定窗口左上角的x,y坐标。如果参数x被设为CW_USEDEFAULT,那么系统为窗口选择默认的左上角坐标并忽略y参数。
nWidth,nHeight:指定窗口窗口的宽度,高度。如果参数nWidth被设为 CW_USEDEFAULT,那么系统为窗口选择默认的宽度和高度,参数nHeight被忽略。
hWndParent:指定被创建窗口的父窗口句柄,没有父窗口,则设置NULL。
hMenu:指定窗口菜单的句柄,没有,则设置为NULL。
hInstance:窗口所属的应用程序实例的句柄,用WinMain中的形参hInstance为其赋值。
lpParam:作为WM_CREATE消息的附加参数lParam传入的数据指针。通常设置为NULL。
返回值:如果窗口创建成功,CreateWindow函数将返回系统为该窗口分配的句柄,否则,返回NULL。
步骤4:显示和更新窗口
显示窗口函数原型:
更新窗口函数原型:
示例:
3.消息循环
我们需要编写一个消息循环,不断地从消息队列中取出消息,并进行响应。在Windows程序中,消息是由MSG结构体来表示的。
MSG结构体的定义如下:
hWnd:消息所属的窗口。我们通常开发的程序都是窗口应用程序,一个消息一般都是与某个窗口相关联的。例如,在某个活动窗口中按下鼠标左键,产生的按键消息就是发给该窗口的。
message:消息的标识符,是由一个数值来表示的,不同的消息对应不同的数值。例如,鼠标左键按下消息是WM_LBUTTONDOWN,键盘按下消息是WM_KEYDOWN,字符消息是 WM_CHAR。
wParam: 指定消息的附加信息,如键盘按下会触发WM_KEYDOWN消息,但是,具体按下哪个按键需要wParam区分。
time:标识一个消息产生时的时间。
pt:表示产生这个消息时光标或鼠标的坐标。
取消息:
要从消息队列中取出消息,我们需要调用GetMessage()函数。
lpMsg:指向一个消息结构体(MSG)
hWnd:指定接收属于哪一个窗口的消息。通常我们将其设置为NULL,用于接收属于调用线程的所有窗口的窗口消息。
wMsgFilterMin:指定消息的最小值。
wMsgFilterMax:指定消息的最大值。如果wMsgFilterMin和wMsgFilterMax都设置为0, 则接收所有消息。
返回值:GetMessage函数接收到除 WM_QUIT 外的消息均返回非零值。对于WM_QUIT消息,该函数返回零。如果出现了错误,该函数返回-1,例如,当参数hWnd是无效的窗口句柄或lpMsg是无效的指针时。
建立消息循环
TranslateMessage:用于翻译、处理和转换消息并把新消息投放到消息队列中,并且此过程不会影响原来的消息队列。
DispatechMessage:用于把收到的消息传到窗口回调函数进行分析和处理。
4.窗口过程处理函数
在完成上述步骤后,剩下的工作就是编写一个窗口过程函数,用于处理发送给窗口的消息。窗口过程函数的名字可以随便取, 如WinProc, 但函数定义的形式必须和下面声明的形式相同:
示例:
LRESULT CALLBACK WinProc( HWND hWnd, //信息所属的窗口句柄 UINT uMsg, //消息类型 WPARAM wParam, //附加信息(如键盘按键) LPARAM lParam //附加信息(如鼠标点击坐标) ) { switch (uMsg) { case WM_KEYDOWN: //键盘按下 //…… break; case WM_LBUTTONDOWN: //鼠标右键按下 //…… break; case WM_PAINT: //绘图事件 //…… break; case WM_DESTROY: PostQuitMessage(0); break; case WM_CLOSE: DestroyWindow(hWnd); break; default: //以windows默认方式处理 return DefWindowProc(hWnd, uMsg, wParam, lParam); break; } return 0; }
DefWindowProc函数:DefWindowProc函数调用默认的窗口过程,对应用程序没有处理的其他消息提供默认处理
WM_CLOSE:对WM_CLOSE消息的响应并不是必须的,如果应用程序没有对该消息进行响应,系统将把这条消息传给DefWindowProc函数而 DefWindowProc函数则调用DestroyWindow函数来响应这条WM_CLOSE消息
WM_DESTROY:DestroyWindow函数在销毁窗口后,会给窗口过程发送 WM_DESTROY消息,我们在该消息的响应代码中调用PostQuitMessage函数。PostQuitMessage函数向应用程序的消息队列中投递一条WM_QUIT消息并返回。WinMain函数中,GetMessage 函数只有在收到WM_QUIT消息时才返回0,此时消息循环才结束,程序退出。传递给 PostQuitMessage函数的参数值将作为WM_QUIT消息的wParam参数,这个值通常用做WinMain函数的返回值。
完整代码:
#include <Windows.h> LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR IpCmdLine, int nShowCmd) { // 1.设计一个窗口类 WNDCLASS wc; // 窗口类变量 wc.cbClsExtra = 0; // 类附加内存 wc.cbWndExtra = 0; // 窗口附加对象 wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); // 设置背景色 wc.hCursor = (HCURSOR)LoadCursor(NULL, IDC_HELP); // 帮助光标 wc.hIcon = (HICON)LoadIcon(NULL, IDI_ERROR); // 警告图标 wc.hInstance = hInstance; // 应用程序实例 wc.lpfnWndProc = WindowProc; // 窗口过程函数名字 wc.lpszClassName = TEXT("MyWin"); // 类的名字 wc.lpszMenuName = NULL; // 没有菜单 wc.style = 0; // 默认风格 // 2.注册窗口类 RegisterClass(&wc); // 3.创建窗口 HWND hWnd = CreateWindow(TEXT("MyWin"), TEXT("测试"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); // 4.显示和更新窗口 ShowWindow(hWnd, SW_SHOWNORMAL); UpdateWindow(hWnd); // 5,建立消息循环 MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.lParam; } LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_KEYDOWN: // 键盘按下 MessageBox(hWnd, TEXT("键盘按下"), TEXT("键盘"), MB_OK); break; case WM_LBUTTONDOWN: // 鼠标左键按下 MessageBox(hWnd, TEXT("鼠标左键按下"), TEXT("鼠标"), MB_OK); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, uMsg, wParam, lParam); } }
MFC入门
MFC(Microsoft Foundation Classese)是微软公司提供一个类库,以C++形式(上面使用的WIN32是C风格的接口)封装了Windows API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量。其中包含的类包括大量Windows句柄封装类和很多Windows的内建控件和组件的封装类。
MFC把Windows SDK API函数包装成了几百个类,MFC给Windows操作系统提供了面向对象的接口,支持可重用性、自包含性以及其他OPP原则。MFC通过编写类来封装窗口、对话框以及其他对象,引入某些关键的虚函数(覆盖这些虚函数可以改变派生类的功能)来完成,并且MFC设计者使类库带来的总开销降到了最低。
第一个MFC程序
编写MFC程序需要包含#include<afxwin.h>头文件:
程序的执行流程:
1.程序开始时,先实例化应用程序对象(有且只有一个)
2.执行程序入口函数InitInstance()
3.给框架类MyFrame对象动态分配空间(自动调用它的构造函数)
4.管家类对象显示窗口 CWnd::ShowWindows
5.框架类对象更新窗口 CWnd::UpdateWindow
6.保存框架类对象指针 CWinTread::m_pMainWnd
代码分析:
1.CFrameWnd框架窗口类
CFrameWnd是从CWnd(窗口基类)派生出来的。CFrameWnd模仿框架窗口行为,我们
可以把框架窗口作为顶层窗口看待,它是应用程序与外部世界的主要接口。
如果想创建一个窗口,可以在此类中调用CWnd::Create或CWnd::CreateEX函数
一般情况下,前6个参数使用默认值。lpszClassName指定了窗口基于WNDCLASS类的名称,为此将其设定为NULL将创建一个基于已注册的WNDCLASS类的默认框架窗口。lpszWindowName参数指定将在窗口的标题栏出现的文字。
2.CWinApp应用程序类
MFC应用程序的核心就是基于CWinApp类的应用程序对象。CWinApp提供了消息循环来检索消息并将消息调度给应用程序窗口。它还包括可被覆盖的、用来自定义应用程序行为的主要虚函数。
一个MFC程序可以有且仅有一个应用程序对象,此对象必须声明为在全局范围内有效,以便它在程序开始时即在内存中被实例化。
3.InitInstance函数
CWinApp::InitInstance函数是一个虚函数,其默认操作仅有一条语句:
InitInstance的目的是给应用程序提供一个自身初始化的机会,其返回值决定了框架接下来要执行的内容,如果返回FALSE将关闭应用程序,如果初始化正常返回TRUE以便允许程序继续进行。此函数是MFC应用程序的入口。
4.m_pMainWnd成员变量
在CWinApp中有一个名为CWinTread::m_pMainWnd的成员变量。该变量是一个CWnd类型的指针,它保存了应用程序框架窗口对象的指针。也就是说,是指向CFramWnd对象(框架窗口类对象)的指针。
消息映射机制:
消息映射是一张将消息和成员函数相互关联的表。比如,框架窗口接收到了一个鼠标左击消息,MFC将搜索该窗口的消息映射,如果存在一个处理WM_LBUTTONDOWN消息的处理程序,就会去调用。
消息映射的步骤:
1.所操作类中,声明消息映射宏
2.通过放置标识的宏来执行消息映射,相应的类将在对BEGIN_MESSAGE_MAP和END_MESSAGE_MAP的调用之间处理消息。
3.对应消息处理函数分别在类中声明,类外定义
Widnows字符集
多字节字符集(8位的ANSI字符集)
在Windows98以及以前的版本使用8位ANSI字符集,它类似于我们程序员熟悉的ASCII字符集。
宽字符集(16位的Unicode字符集)
在WindowsNT中和Windows2000后开始使用16位的Unicode字符集,它是ANSI字符集的一个超集。Unicode适用于国际市场销售应用程序,因为它包含了各种各样来自非U.S.字母表的字符,比如:中文、日文、韩文、西欧等
TEXT(_T)宏
MFC中的TEXT宏可以自动适应字符类型,如果定义了预处理器程序符号_UNICODE,那么编译器将使用Unicode字符,如果没有定义该预处理程序符号,那么编译器将使用ANSI字符。
TCHAR类型
如果定义了_UNICODE符号TCHAR将变成wchar_t类型。如果没有定义_UNICODE符号,TCHAR将变成普通古老的char类型。
用向导生成一个MFC应用程序
类视图
文档/视图结构体系
MFC应用程序框架结构的基础是文档/视图体系结构,这种结构依靠文档对象保存应用程序的数据,并依靠视图对象控制视图中显示的数据,把数据本身与它的显示分离开。
数据的存储和加载由文档类来完成,数据的显示和修改则由视图类来完成。MFC在类CDocument和CView中为视图提供了基础结构。
CWinApp、CFrameWnd和其他类与CDocument和CView合作,把所有的片段连在一起。CView类也派生于CWnd类,框架窗口是视图窗口的一个父窗口。
主框架窗口(CFrameWnd)是整个应用程序外框架所包含的部分,而视图类窗口只是主框架中空白的地方。
消息处理添加
在主框架类中添加WM_LBUTTONDOWN消息的响应函数:
从类视图中找到CMainFrame(继承自CFrameWnd),选择此类然后从属性面板中找到消息按钮,在消息列表中找到WM_LBUTTONDOWN消息,添加。
工程文件增加的改变:
1.在框架类头文件中添加了鼠标左键消息函数的函数声明
2.在框架类cpp文件中添加了消息映射宏
3.在框架类cpp文件中添加了处理鼠标左键消息的函数定义
我们再此OnLButtonDown函数中添加一个MessageBox消息,鼠标左键按下弹出一个提示框,然后执行程序。我们会惊奇的发现程序并未如我们所愿弹出消息框。
因为,框架窗口是视窗口的父窗口,那么视类窗口就应该始终覆盖在框架类窗口之上。就好比框架窗口是一面墙,视类窗口就是墙纸,它始终挡在这面墙前边。也就是说,所有操作,包括鼠标单击、鼠标移动等操作都只能有视类窗口捕获。
我们通过上述同样的操作在视图类中添加鼠标响应消息,看程序运行的结果