第3章 Windows编程与MFC基础
要想熟练掌握Windows应用程序的开发,首先需要理解Windows平台下程序运行的内部机制。本章将首先剖析Windows程序的内部运行机制,为读者扫清Visual C++学习路途中的第一个障碍;而后简单介绍MFC的基础知识,为读者进一步学习MFC程序开发打下基础。本章涉及的知识点如下。
·Windows应用程序。了解Windows应用程序的创建和特点。
·应用程序的类型。了解各种Windows应用程序的类型,如Win32应用程序、MFC应用程序等。
·MFC应用程序向导的功能。学会MFC应用程序向导中的所有功能,了解MFC应用程序向导会带来怎样的应用程序框架。
3.1 Windows编程基础
Windows操作系统采用了图形用户界面,借助于它提供的API(Application Programming Interface)函数,用户可以编出具有漂亮图形界面的程序。本节将主要介绍Windows编程中用到的一些概念。
3.1.1 Windows API函数
为方便用户开发Windows应用程序,Windows操作系统提供了各种各样的函数。这些函数是Windows操作系统提供给应用程序编程的接口,简称为API函数。用户在编写Windows程序时所说的API函数,就是指系统提供的函数,所有主要的Windows函数都在“Windows.h”头文件中进行了声明。
Windows API也是Windows操作系统自带的在Windows环境下运行的软件开发包(SDK)。程序员总是直接或间接引用API进行应用程序的开发,所以Windows应用程序就有大致相同的用户界面。
说明 SDK的全拼是Software Development Kit,中文译为软件开发包。假如现在需要开发视频会议系统,在购买视频数据采集卡时,厂商就会提供视频数据采集卡的SDK开发包,以方便用户对视频数据采集卡的编程操作。这个开发包通常都会包含视频数据采集卡的API函数库、帮助文档、使用手册、辅助工具等资源。也就是说,SDK实际上就是开发所需资源的一个集合。
3.1.2 窗口与句柄
窗口是Windows应用程序中一个非常重要的元素,它是Windows应用程序与用户进行交互的接口。一个Windows应用程序至少要有一个窗口,称为主窗口。通过窗口,应用程序可以接收用户的输入,并显示输出。
应用程序窗口通常包含标题栏、菜单栏、系统菜单、最小(大)化按钮、边框及滚动条等。窗口可以分为客户区和非客户区。客户区是窗口的一部分,应用程序通常在客户区中显示文字或者绘制图形。标题栏、菜单栏、系统菜单、最小(大)化按钮、边框统称为窗口的非客户区,它们由Windows系统来管理,而应用程序则主要管理客户区的外观及操作。
在Windows应用程序中,窗口是通过窗口句柄(HWND)来标识的。要对某个窗口进行操作,首先就要得到这个窗口的句柄。句柄(HANDLE)是Windows程序中一个重要的概念。在Windows程序中,有各种各样的资源(窗口、图标、光标等),系统在创建这些资源时会为它们分配内存,并返回标识这些资源的标识号,即句柄。
Windows中的常用句柄类型及其说明如表3-1所示。
表3-1 常用句柄类型及其说明
3.1.3 事件与消息
Windows程序采用的是事件驱动方式的程序设计模式,其操作主要是基于消息的。在应用程序启动后,系统会等待用户在图形用户界面内进行输入选择,如鼠标按键、键盘按键、窗口创建、关闭、改变大小及移动等,对系统而言,这些都是事件。
只要有事件发生,系统即产生特定的消息。消息描述了事件的类别,包含相关信息,Windows应用程序利用消息与系统及其他应用程序进行信息交换。
由于Windows事件的发生是随机的,程序的执行顺序也无法预测,因此系统采用消息队列来存放事件发生的消息,然后从消息队列中依次取出消息进行相应的处理,如图3-1所示。
图3-1 事件与消息处理
3.1.4 常用的Windows数据类型
Windows应用程序中常用的数据类型如表3-2所示。
表3-2 Windows应用程序中常用的数据类型
3.2 Windows应用程序分析
WinMain()和WndProc()函数构成了Windows应用程序的主体。WinMain()函数负责建立窗口和消息循环,WndProc()函数负责消息的处理。典型的Windows窗口的创建与处理过程如图3-2所示。
图3-2 Windows窗口创建及处理过程
3.2.1 WinMain()函数
传统的DOS程序以main()函数作为进入程序的初始入口点,在Windows应用程序中,main()函数被WinMain()函数取代。WinMain()函数是Windows程序的入口点函数,当Windows操作系统启动一个程序时,它调用的就是该程序的WinMain()函数;当WinMain()函数结束或返回时,Windows应用程序结束。WinMain()函数的原型如下:
01 int WINAPI WinMain 02 ( HINSTANCE hThisInst, ∥应用程序当前实例句柄 03 HINSTANCe hPrevInst, ∥应用程序其他实例句柄 04 LPSTR lpszCmdLine, ∥指向程序命令行参数的指针 05 Int nCmdShow, ∥应用程序开始执行时窗口显示方式的整数值标识 06 )
【参数说明】
·参数hThisInst表示该程序当前运行的实例的句柄,这是一个数值。当程序在Windows环境下运行时,它唯一标识运行中的实例。一个应用程序可以运行多个实例,每运行一个实例,系统都会给该实例分配一个句柄值,并通过HINSTANCE参数传递给WinMain()函数。
·参数hPrevInst表示当前实例的前一个实例的句柄。在Win32环境下,这个参数不再起作用,为NULL。
·参数lpszCmdLine是一个字符串指针,指定传递给应用程序的命令行参数。
·参数nCmdShow指定程序的窗口应该如何显示,例如最大化、最小化、隐藏等。这个参数的值由该程序的调用者所指定,应用程序通常不需要去理会这个参数的值。
WinMain()函数接收4个参数,这些参数都是在系统调用WinMain()函数时传递给应用程序的。
3.2.2 创建窗口
创建一个完整的窗口,需要经过:定义窗口类、注册窗口类、创建窗口实例、显示及更新窗口4个操作步骤。
1.定义窗口类
在创建一个窗口前,必须对该类型的窗口进行设计,指定窗口的特征。窗口的特征是由WNDCLASS结构体来定义的。WNDCLASS结构体的定义代码如下。
01 typedef struct tagWNDCLASS { 02 UINT style; //窗口风格 03 WNDPROC lpfnWndProc; //指向窗口处理函数的函数指针 04 int cbClsExtra; //窗口结构中的预留字节数 05 int cbWndExtra; //为其他创建窗口预留字节数 06 HINSTANCE hInstance; //注册该窗口类的实例句柄 07 HICON hIcon; //代表该窗口类的图标句柄 08 HCURSOR hCursor; //该窗口客户区鼠标光标句柄 09 HBRUSH hbrBackGround; //该窗口背景颜色句柄 10 LPCSTR lpszMenuName; //指向窗口菜单名的字符指针 11 LPCSTR lpszClassName; //指向窗口名的字符指针 12 } WNDCLASS, *PWNDCLASS,NEAR *NPWNDCLASS, 13 FAR *LPWNDCLASS;
2.注册窗口类
窗口类(WNDCLASS)设计完成后,需要调用RegisterClass()函数对其进行注册,注册成功后,才可以创建该类型的窗口。注册函数的原型声明如下。
BOOL RegisterClass(CONST WNDCLASS *lpWndClass);
该函数只有一个参数,即所设计的窗口类对象的指针。
3.创建窗口实例
设计好窗口类并且将其成功注册之后,就可以用CreateWindow()函数产生这种类型的窗口了。函数CreateWindow()原型如下。
01 HWND CreateWindow 02 ( LPCTSTR lpszClassName, //窗口类名 03 LPCTSTR lpszTitle, //窗口标题名 04 DWORD dwStyle, //创建窗口的样式 05 int x,y, //窗口左上角坐标 06 int nWidth,nHeight, //窗口宽度和高度 07 HWND hwndParent, //该窗口的父窗口句柄 08 HWENU hMenu, //窗口主菜单句柄 09 HINSTANCE hInstance, //创建窗口的应用程序当前句柄 10 LPVOID lpParam, //指向一个传递给窗口的参数值的指针 11 )
技巧 注意区分WNDCLASS中的style成员与CreateWindow函数的dwStyle参数,前者是指定窗口类的样式,基于该窗口类创建的窗口都具有这些样式;后者是指定某个具体的窗口的样式。
4.显示及更新窗口
窗口创建之后,就可以调用函数ShowWindow()来显示窗口,该函数的原型如下。
BOOL ShowWindow( HWND hWnd, int nCmdShow );
ShowWindow()函数有两个参数,第一个参数hWnd就是在成功创建窗口后返回的那个窗口句柄;第二个参数nCmdShow指定窗口显示的状态。
在调用ShowWindow()函数之后,紧接着调用UpdateWindow来刷新窗口。UpdateWindow()函数的原型如下。
BOOL UpdateWindow( HWND hWnd);
其中,参数hWnd指的是创建成功后的窗口的句柄。UpdateWindow()函数通过发送一条WM_PAINT消息来刷新窗口,UpdateWindow()函数将WM_PAINT消息直接发送给窗口过程函数进行处理,而没有放到消息队列里。
至此,一个窗口就算创建完成了。
3.2.3 消息循环
在创建窗口、显示窗口、更新窗口后,需要编写一个消息循环,不断地从消息队列中取出消息,并进行响应。要从消息队列中取出消息,需要调用GetMessage()函数,其原型如下。
01 GetMessage 02 (lpMSG, //指向MSG结构的指针 03 hwnd, //窗口句柄 04 nMsgFilteMin, //用于消息过滤的最小消息号值 05 nMsgFilterMax //用于消息过滤的最大消息号值 06 )
只要从消息队列中取出的消息不为WM_QUIT,GetMessage()函数就返回一个非零值,否则程序结束循环并退出。
通常编写的消息循环代码如下。
01 MSG Msg; 02 … 03 while (GetMessage (&Msg,NULL,0,0)) 04 { TranslateMessage(&Msg); //将消息的虚拟键转换为字符信息 05 DispatchMessage(&Msg); //将消息传送到指定窗口函数 06 }
GetMessage()函数只有在接收到WM_QUIT消息时,才返回0。此时while语句判断的条件为假,循环退出,程序才有可能结束运行。在没有接收到WM_QUIT消息时,Windows应用程序就通过这个while循环来保证程序始终处于运行状态。
TranslateMessage()函数用于将虚拟键消息转换为字符消息。DispatchMessage()函数分派一个消息到指定窗口,由窗口函数WndProc对消息进行处理。
说明 DispachMessage实际上是将消息回传给操作系统,由操作系统调用窗口函数对消息进行处理。
Windows应用程序的消息处理机制如图3-3所示。
图3-3 Windows应用程序的消息处理机制
3.2.4 WinProc窗口函数
在完成上述步骤后,剩下的工作就是编写一个窗口函数,用于处理发送给窗口的消息。WinProc()函数由一个或多个switch语句组成,每一条case语句对应一种消息,当应用程序接收到一个消息时,相应的case语句被激活。窗口函数的一般形式如下。
01 LRESULT CALLBACK WndProc(HWND hwnd, UINT messgae,WPARAM wParam,LPARAM lParam ) 02 { … 03 switch(message) // message为标识的消息 04 { case… 05 … 06 break; 07 … 08 case WM_DESTROY: //销毁窗口并退出 09 PostQuitMessage(0); 10 default: 11 return DefWindowProc(hwnd,message,wParam,lParam); 12 } 13 return(0); 14 }
3.2.5 Windows编程实例
本节将通过一个实例讲解Windows窗口的创建方法,创建并显示一个窗口,然后在客户区中输出文本。
【实例3-1】在Visual C++6.0中,使用AppWizard创建一个空的“Win32Application”工程,并在其中创建源文件,然后利用Windows API函数实现基本的Windows窗口程序编程。实例的具体实现过程如下。
(1)启动Visual C++6.0,利用AppWizard来建立一个“Win32Application”类型的工程“WindowsDemo”。向导默认选项就是创建一个空工程。
(2)执行“File”|“New”菜单命令,然后向工程添加源文件“Apidemo.cpp”,具体步骤可参考第2章。
(3)在“Apidemo.cpp”文件中编辑如下代码。
01 #include<windows.h> //包含windows.h头文件 02 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM ); //窗口函数声明 03 /*入口函数 WinMain()*/ 04 int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR 05 lpCmdLine,int nCmdShow){ 06 WNDCLASS wndclass; //定义窗口类结构变量 07 HWND hwnd; //定义窗口句柄 08 MSG msg; //定义消息结构变量 09 /*定义窗口类的各属性*/ 10 wndclass.style = CS_HREDRAW|CS_VREDRAW; //改变窗口大小则重画 11 wndclass.lpfnWndProc = WndProc; //窗口函数为WndProc() 12 wndclass.cbClsExtra = 0; //窗口类无扩展 13 wndclass.cbWndExtra = 0; //窗口实例无扩展 14 wndclass.hInstance = hInstance; //注册窗口类实例句柄 15 wndclass.hIcon = LoadIcon(NULL,IDI_APPLICATION); //用箭头光标 16 wndclass.hCursor = LoadCursor(NULL,IDC_ARROW); 17 wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); //背景为白色 18 wndclass.lpszMenuName = NULL; //窗口默认无菜单 19 wndclass.lpszClassName = "window窗口创建"; //窗口类名为window窗口创建 20 /*注册窗口类*/ 21 if(! RegisterClass(&wndclass)) return FALSE; 22 23 /*创建窗口*/ 24 hwnd = CreateWindow("window窗口创建", //窗口类名为window窗口创建 25 "window窗口创建", //窗口名为window窗口创建 26 WS_OVERLAPPEDWINDOW, //重叠式窗口 27 CW_USEDEFAULT, CW_USEDEFAULT, //左上角屏幕坐标默认值 28 CW_USEDEFAULT, CW_USEDEFAULT, //窗口宽度和高度默认值 29 NULL, //此窗口无父窗口 30 NULL, //此窗口无主菜单 31 hInstance, //创建此窗口的实例句柄 32 NULL); //此窗口无创建参数 33 /*显示并更新窗口*/ 34 ShowWindow(hwnd,nCmdShow); //显示窗口 35 UpdateWindow (hwnd); //更新窗口的客户区 36 /*消息循环*/ 37 while(GetMessage (&msg,NULL,0,0)) { 38 TranslateMessage (&msg); //键盘消息转换 39 DispatchMessage (&msg); //派送消息给窗口函数 40 } 41 return msg.wParam; //返回退出值 42 } 43 /*窗口函数*/ 44 LRESULT CALLBACK WndProc(HWND hwnd,UINT message,WPARAM wParam, LPARAM 45 lParam){ 46 //根据消息值转相应的消息处理 47 switch (message){ 48 case WM_PAINT: //重画窗口客户区消息处理 49 HDC hdc; //定义设备描述表句柄 50 PAINTSTRUCT ps; //定义绘图信息结构变量 51 hdc = BeginPaint (hwnd,&ps); //获取要重画的窗口的设备描述表句柄 52 TextOut(hdc,10,20,"哈哈,Windows编程创建的窗口!", 28); //输出文本 53 EndPaint (hwnd,&ps); //结束要重画的窗口 54 return 0; 55 case WM_DESTROY: //撤销窗口消息处理 56 PostQuitMessage (0); //产生退出程序消息WM_QUIT 57 return 0; 58 } 59 return DefWindowProc (hwnd, message, wParam, lParam); 60 //其他转默认窗口函数 61 }
【代码说明】程序将创建并显示一个Windows窗口,从第47行开始,就是一个接收消息的分类处理代码,第52行表示在客户窗口中的(10,20)位置处输出一行文字。要在窗口中输出文字或者显示图形,需要用到设备描述表(Device Context,DC)。DC是一个包含设备(物理输出设备,如显示器以及设备驱动程序)信息的结构体,在Windows平台下,所有图形操作都是利用DC来完成的。
【运行效果】编译、运行程序得到窗口结果,如图3-4所示。
图3-4 程序运行结果
3.3 MFC基础
使用Viusal C++6.0进行应用程序的开发,其最大的便利就是用户可以使用其提供的MFC类库,通过MFC AppWizard自动生成的MFC应用程序框架,方便地开发自己想要实现的功能。本节将介绍有关MFC的基础知识。
3.3.1 MFC概述
Visual C++的微软基础类库(Microsoft Foundation Class Library,MFC)封装了大部分API函数,并提供了一个应用程序框架,简化和标准了Windows程序设计,所以用MFC编写Windows应用程序也称为标准Windows程序设计。
Visual C++不仅仅是一个编译器,它是一个全面的应用程序开发环境,使用它可以充分利用具有面向对象特性的C++开发出专业级的Windows应用程序。为了能充分利用这些特性,用户必须理解C++程序设计语言。要掌握C++,就必须掌握MFC的层次结构。该层次结构包容了Windows API中的用户界面部分,并使用户能够很容易地以面向对象的方式建立Windows应用程序。这种层次结构适用于所有版本的Windows,并彼此兼容。而且使用MFC所建立的代码是完全可移植的。
好的开端是从设计用户界面开始。首先需要决定什么样的用户能使用该程序并根据需要来设置相应的用户界面对象。Windows用户界面有一些标准的控件,如按钮、菜单、滚动条和列表等,这对那些Windows用户已经是很熟悉了,程序员必须选择一组控件并决定如何把它们安排到屏幕上。传统情况下,程序员需要先做用户界面的草图,直到对各元素感到满意为止。这对于一些比较小的项目以及一些大项目的早期原型阶段是可以的。然后是实现代码。为任何Windows平台建立应用程序时,程序员都有两种选择,即C或C++。使用C,程序员是在Windows应用程序界面(API)的水平上编写代码,该界面由几百个C函数所组成,这些函数在Windows API参考手册中都有介绍。
Microsoft也提供了C++库,它位于任何Windows API之上,能够使程序员的工作更容易,它就是MFC。该库的主要优点是效率高。它减少了大量在建立Windows程序时必须编写的代码,还提供了所有一般C++编程的优点,例如继承和封装。MFC是可移植的,例如,在Windows 3.1下编写的代码可以很容易地移植到Windows NT或Windows 95上。因此,MFC是很值得推荐的开发Windows应用程序的方法。
说明 MFC实际上可以理解为用来编写Windows应用程序的C++类集。
MFC约有200个类,提供了Windows应用程序框架和应用程序的创建组件,提供了大量的基类供程序员根据不同的应用环境进行扩充,同时允许在编程过程中自定义和扩展应用程序中的类。
MFC库可以分为3个主要部分,即MFC类、宏以及变量(或函数)。如果某个函数或者变量不是类的成员,那么它就是一个全局函数或者全局变量。
3.3.2 MFC基础类及其层次结构
MFC类库采用单一继承结构,从根类CObject层层派生出绝大多数MFC中的类,如图3-5所示。
图3-5 CObject派生类层次示意图
基类CObject的基本功能包括支持序列化(serialization)与运行时(Run-time)类的信息获取、提供特定操作符及完成对象的建立与删除。
读者可能会发现,图3-5的类库结构中并没有MFC应用程序框架所需要的类(应用类CWinApp、框架类CFrameWnd、文档类CDocument、视图类CView)。实际上,它们都是由CCmdTarget为基类派生出来的,如图3-6所示。
说明 CCmdTarget类是CObject的子类,是MFC库中所有具有消息映射属性的基类。消息映射规定了当一对象接收到消息命令时应调用哪一个函数对该消息进行处理。
3.3.3 MFC中的全局函数
MFC库中还包含一些全局函数,这些函数不属于任何一个类,可以直接使用。这些全局函数一般都以“Afx”为前缀,MFC中主要的全局函数及作用如表3-3所示。
表3-3 MFC中主要的全局函数及其作用
另外,MFC库中还含有一些宏,本书将在具体用到时再详细介绍。
图3-6 应用程序框架相关类的层次关系
3.4 MFC应用程序框架分析
通过3.2节的介绍,相信读者对Windows应用程序的创建及其运行机制已经有了一定的了解,本节将对MFC应用程序框架进行简单剖析,帮助读者了解MFC应用程序框架是如何组织与工作的。
3.4.1 入口函数
前面已经介绍过,WinMain()函数是Windows程序的入口点函数。然而打开2.2.2节利用AppWizard创建的MFC应用程序“SDIDemo”,却找不到WinMain()函数。
这是因为MFC考虑到典型的Windows程序需要的大部分初始化工作都是标准化的,把WinMain()函数隐藏在应用程序的框架中,编译时会自动将该函数链接到可执行文件中。
【实例3-2】Viusal C++6.0安装目录下的“Microsoft Visual Studio\VC98\MFC\SRC”路径中有一个源文件“WinMain.cpp”,其中定义了入口函数AfxWinMain(),代码如下。
01 #include "stdafx.h" 02 #ifdef AFX_CORE1_SEG 03 #pragma code_seg(AFX_CORE1_SEG) 04 #endif 05 ///////////////////////////////////////////////////////////////////////////// 06 int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 07 LPTSTR lpCmdLine, int nCmdShow) // MFC程序的主函数入口 08 { 09 ASSERT(hPrevInstance == NULL); 10 int nReturnCode = -1; 11 CWinThread* pThread = AfxGetThread(); 12 CWinApp* pApp = AfxGetApp(); 13 if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow)) 14 goto InitFailure; 15 if (pApp != NULL && !pApp->InitApplication()) 16 goto InitFailure; 17 if (!pThread->InitInstance()) //程序启动的线程调用初始化函数,窗口出现了 18 { 19 if (pThread->m_pMainWnd != NULL) 20 { 21 TRACE0("Warning: Destroying non-NULL m_pMainWnd\n"); 22 pThread->m_pMainWnd->DestroyWindow(); 23 } 24 nReturnCode = pThread->ExitInstance(); 25 goto InitFailure; 26 } 27 nReturnCode = pThread->Run(); 28 InitFailure: 29 #ifdef _DEBUG 30 if (AfxGetModuleThreadState()->m_nTempMapLock != 0) 31 { 32 TRACE1("Warning: Temp map lock count non-zero (%ld).\n", 33 AfxGetModuleThreadState()->m_nTempMapLock); 34 } 35 AfxLockTempMaps(); 36 AfxUnlockTempMaps(-1); 37 #endif 38 AfxWinTerm(); 39 return nReturnCode; 40 }
【代码说明】应用程序执行时,代码第06行中Windows自动调用应用程序框架内部的WinMain()函数。从代码中可见,WinMain()函数会查找该应用程序的一个全局构造对象,这个对象是由CWinApp派生类构造的,有且只有一个是一个全局对象,在程序启动时就已经被构造好了。代码第17行即是调用初始化函数将窗口启动并初始化。随后WinMain()将调用这个对象的InitApplication()和InitInstance()成员函数,完成应用程序实例的初始化工作。然后,WinMain()调用Run()成员函数,运行应用程序的消息循环。在程序结束时,WinMain()调用AfxWinTerm()函数做一些清理工作。
3.4.2 应用程序对象
每个应用程序必须从CWinApp派生出自己的应用程序类,并定义一个全局的对象。该应用程序类包含了Windows下应用程序的初始化、运行和结束过程。基于框架建立的应用程序必须有一个(且只能有一个)从CWinApp派生的类的对象。
在工程“SDIDemo”的CSDIDemoApp类的源文件中,可发现框架自动生成了应用程序对象,代码如下。
CSDIDemoApp theApp;
3.4.3 InitInstance()函数
CWinApp类中的InitInstance()函数用于初始化实例。每次启动应用程序的一个实例时,WinMain()函数都要调用InitInstance()函数。
【实例3-3】工程“SDIDemo”的CSDIDemoApp类中会自动对InitInstance()函数进行重载,代码如下。
01 BOOL CSDIDemoApp::InitInstance() 02 { 03 AfxEnableControlContainer(); 04 #ifdef _AFXDLL 05 Enable3dControls(); 06 #else 07 Enable3dControlsStatic(); 08 #endif 09 SetRegistryKey(_T("Local AppWizard-Generated Applications")); 10 LoadStdProfileSettings(); //加载标准的配置文件选项 11 CSingleDocTemplate* pDocTemplate; 12 pDocTemplate = new CSingleDocTemplate( 13 IDR_MAINFRAME, 14 RUNTIME_CLASS(CSDIDemoDoc), //单文档应用程序的文档对象 15 RUNTIME_CLASS(CMainFrame), //主框架窗口 16 RUNTIME_CLASS(CSDIDemoView)); //程序的视图对象 17 AddDocTemplate(pDocTemplate); 18 CCommandLineInfo cmdInfo; 19 ParseCommandLine(cmdInfo); 20 if (!ProcessShellCommand(cmdInfo)) 21 return FALSE; 22 m_pMainWnd->ShowWindow(SW_SHOW); 23 m_pMainWnd->UpdateWindow(); 24 return TRUE; 25 }
【代码说明】代码中英文注释非常多,这个是系统自带的默认注释,因此这里直接写出,以便读者核对,本书中其他英文注释也是如此。从代码中可以看出,InitInstance()函数主要完成了以下几方面功能。
·通过第10行代码的LoadStdProfileSettings()函数从注册表中获取一些标准的文件选项,包括最近打开的文件名称,并在程序的“文件”菜单中列出。
·代码第12~16行用于构造文档模板类对象pDocTemplate,指明文档模板的文档类、框架窗口类和视图类。
·第19行调用ParseCommandLine()函数进行程序窗口启动方式的分析处理,如果没有提供命令行参数(打开文档的文件名),则新建一个新文档。
·代码第22~23行调用ShowWindow()和UpdateWindow()函数显示、更新窗口。其中,m_pMainWnd成员变量是一个CWnd类型的指针,保存了应用程序框架窗口对象的指针,也就是说,其是指向CMainFrame对象的指针。
注意 在CWinApp的派生类中,必须重载InitInstance()函数,因为CWinApp并不知道应用程序需要什么样的窗口,它可以是多文档窗口或单文档窗口,也可以是基于对话框的窗口。
3.4.4 Run()函数
【实例3-4】WinMain()在初始化应用程序实例后,就调用CWinThread类的Run()函数来处理消息循环。Viusal C++6.0安装目录下“Microsoft Visual Studio\VC98\MFC\SRC”路径中的源文件“THRDCORE.CPP”中会找到Run()函数的实现代码,具体如下。
01 int CWinThread::Run() 02 { 03 ASSERT_VALID(this); 04 BOOL bIdle = TRUE; 05 LONG lIdleCount = 0; 06 for (;;) 07 { 08 while (bIdle && 09 !::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE)) 10 { 11 if (!OnIdle(lIdleCount++)) 12 bIdle = FALSE; // assume "no idle" state 13 } 14 do 15 { 16 if (!PumpMessage()) 17 return ExitInstance(); 18 if (IsIdleMessage(&m_msgCur)) 19 { 20 bIdle = TRUE; 21 lIdleCount = 0; 22 } 23 } while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE)); 24 } 25 ASSERT(FALSE); // not reachable 26 }
【代码说明】该函数的主要结构是一个for循环(第06~24行),该循环在接收到一个WM_QUIT消息时退出。Run()成员函数不断执行消息循环,检查消息队列中有没有消息,如果有消息,Run()通过PumpMessage()函数将其派遣,交由框架去处理,然后返回继续消息循环;如果没有消息,Run()将调用OnIdle()函数来做用户或框架可能需要在空闲时才做的工作;如果既没有消息要处理,也没有空闲时的处理工作要做,则应用程序将一直等待,直到有事件发生。当应用程序结束时,Run()将调用ExitInstance()函数结束消息循环。Run()函数的消息循环如图3-7所示。
图3-7 Run()函数的消息循环
至此,就完成了MFC程序的整个运行机制。实际上与Win32 SDK程序是一致的,它同样也需要经过设计窗口类(MFC程序中已经预定义了一些窗口类,可以直接使用)、注册窗口类、创建窗口、显示并更新窗口和消息循环几个过程,具体如下所述。
·首先利用全局应用程序对象theApp启动应用程序。正是由于产生了这个全局对象,基类CWinApp中的this指针才能指向这个对象。
·调用全局应用程序对象的构造函数,从而就会先调用其基类CWinApp的构造函数。后者完成应用程序的一些初始化工作,并将应用程序对象的指针保存起来。
·进入WinMain()函数。在AfxWinMain()函数中可以获取子类CSDIDemoApp的指针,利用此指针调用虚函数InitInstance(),完成应用程序的一些初始化工作,包括窗口类的注册、创建,窗口的显示和更新。
·进入消息循环。MFC应用程序实际上是采用消息映射机制来处理各种消息的。当收到WM_QUIT消息时,退出消息循环,程序结束。
3.4.5 MFC的消息映射
Windows程序中的消息处理是在WinProc()函数中通过switch结构实现的。但当处理的消息比较多时,switch-case结构将变得分支很多,影响程序的可读性。
而在MFC中,则采用了消息映射的结构进行结构化消息处理。进行MFC消息处理时,程序员要做的就是为每一个要处理的消息提供一个消息处理函数,然后系统通过MFC提供的一套消息映射系统来调用相应的消息处理函数。
说明 消息映射就是消息与消息处理函数一对一的联系。
MFC的消息映射采用消息映射宏的方式,把消息和消息处理函数一一对应起来。在MFC的框架结构中,可以进行消息处理的类的头文件里面都会含有DECLARE_MESSAGE_MAP()宏,主要进行消息映射和消息处理函数的声明。
【实例3-5】可以进行消息处理的类的实现文件里一般都含有如下结构。
01 BEGIN_MESSAGE_MAP(CInheritClass, CBaseClass) 02 ON_MESSAGE(message1,memberFxn1) 03 ON_MESSAGE(message2,memberFxn2) 04 … 05 END_MESSAGE_MAP()
【代码说明】代码第01行中的CInheritClass为具有消息循环的类的名字,CBaseClass是CInheritClass的父类。第02~03行的ON_MESSAGE宏就是消息映射宏。
为简化消息处理,MFC采用默认的消息映射和消息处理函数。例如,对消息标识符WM_LBUTTONDOWN,完整的消息映射应该如下。
ON_MESSAGE(WM_LBUTTONDOWN,Function1)
而MFC采用的更简捷的映射方式,如下。
ON_WM_LBUTTONDOWN()
MFC还为默认的消息映射预定义了消息处理函数,如下。
OnLButtonDown(UINT nFlags, CPoint point)
假如要处理消息完成自己的任务,则要在派生类中重写这些函数。
3.4.6 MFC消息分类
MFC把消息分为窗口消息、控件通知消息和命令消息。
1.窗口消息
当创建窗口、绘制窗口、移动窗口、销毁窗口以及使用键盘、鼠标操作等进行与窗口有关的动作时,产生的消息均属于窗口消息。
窗口消息由MFC的窗口类(CWnd)对象来处理,即这类消息处理函数一般是CWnd类的成员函数,有默认的窗口处理函数。
典型的窗口消息、消息映射宏和默认的消息处理函数如表3-4所示。
表3-4 窗口消息、消息映射宏及其默认处理函数
说明 若CWnd派生类没有重载窗口消息处理函数,则消息映射机制会转由其基类处理(最终是CWnd类)。
2.控件通知消息
控件是一个窗口的子窗口(如对话框中的按钮、编辑框等),控件通知消息是指在事件发生时,由控件或其他类型的子窗口发送到父窗口的消息。控件通知消息用来通知父窗口该控件接受了某操作,为父窗口进一步控制子窗口提供条件。
控件通知消息一般由按钮(BN_)、编辑框(EN_)、组合框(CBN_)、列表框(LBN_)等产生,其消息映射宏为在消息名前加上“ON_”。举例如下,选择各个控件后,产生的消息由其后面定义的函数进行处理。
01 ON_BN_CLICKED (按钮ID,响应函数) 02 ON_CBN_DBCLK (组合框ID,响应函数) 03 ON_EN_SETFOCUS (编辑框ID,响应函数) 04 ON_LBN_DBCLK (列表框ID,响应函数)
3.命令消息
命令消息一般与处理用户的请求有关,主要是来自菜单、工具栏和加速键的通知消息。从CCmdTarget派生的类(如文档、文档模板、应用程序对象、窗口和视图等)都能处理命令消息。
用户可以使用MFC ClassWizard(类向导)建立消息映射和消息处理函数的框架,消息和函数都由MFC默认的命名方式命名。
命令消息使用WM_COMMAND宏定义对其进行映射响应,格式如下。
ON_COMMAND(命令ID,响应函数)
举例如下。
01 ON_COMMAND ( IDM_FILENEW, OnFileNew) //“新建”菜单命令 02 ON_COMMAND ( IDM_FILEOPEN, OnFileOpen) //“打开”菜单命令
对于命令消息,MFC应用程序框架会通过消息映射机制,按一定的搜索顺序在各个CCmdTarget类(命令处理类)的派生类中查找对应的消息处理函数。
说明 所有由用户定义的命令消息也由ON_COMMAND定义消息映射关系。
3.4.7 在Visual C++6.0中添加消息映射
在MFC中添加消息映射的方法有两种,即使用类向导创建和手动创建。
1.使用类向导创建
按Ctrl+W快捷键可以打开类向导对话框。在“Message Maps”选项卡下可以添加消息映射,如图3-8所示。
如果用户需要处理窗口关闭时的WM_DESTROY消息,则操作步骤如下。
(1)在“Class name”下拉列表框中选择“CMainFrame”选项。
(2)在“Object IDs”列表框中选择“CMainFrame”选项。
(3)在“Messages”列表框中选择“WM_CLOSE”后双击该选项,将WM_CLOSE的消息处理函数On_Close函数添加到下方的列表中。
(4)在“Object IDs”列表框中选择“OnClose”选项,单击“Edit Code”按钮,编辑OnClose函数,编写代码如下。
01 void CMainFrame::OnClose() 02 { 03 if(::MessageBox(NULL,"是否关闭窗口","提示",MB_YESNO) == IDNO) 04 { 05 return; 06 } 07 CFrameWnd::OnClose(); 08}
图3-8 使用类向导添加消息映射
(5)执行程序,当关闭程序时,弹出提示对话框,如图3-9所示。
图3-9 关闭程序时弹出提示信息框
2.手动创建
使用类向导添加的消息映射完全可以手动完成,手动添加消息映射分为如下3步。
(1)在类声明中添加消息函数声明。消息函数前要有afx_msg宏,用来区分函数是普通函数还是消息函数。
(2)在类的实现文件中的BEGIN_MESSAGE_MAP与END_MESSAGE_MAP之间添加消息映射宏。消息映射宏必须定义为ON_MESSAGE(消息ID,消息函数),表示当消息ID代表的消息产生时,使用消息函数处理。
(3)编写消息函数。继续在前面的示例代码中手动添加WM_DESTROY的消息处理,具体操作如下。
首先在CMainFrame中添加消息函数声明(粗体字表示手动添加代码),代码如下。
01 … 02 //{{AFX_MSG(CMainFrame) 03 afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); //WM_CREATE消息 04 afx_msg void OnClose(); //WM_CLOSE消息 05 //}}AFX_MSG 06 afx_msg LRESULT OnDestroy(WPARAM,LPARAM); //手动添加的WM_DESTROY消息 07 DECLARE_MESSAGE_MAP() 08 …
然后在CMainFrame的实现文件中添加代码,如下。
01 BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) 02 //{{AFX_MSG_MAP(CMainFrame) 03 ON_WM_CREATE() //WM_CREATE消息映射 04 ON_WM_CLOSE() //WM_CLOSE消息映射 05 //}}AFX_MSG_MAP 06 ON_MESSAGE(WM_DESTROY,OnDestroy) //手动添加的WM_DESTROY消息映射 07 END_MESSAGE_MAP()
再编写OnDestroy消息函数,代码如下。
01 LRESULT CMainFrame::OnDestroy(WPARAM,LPARAM) 02 { 03 ::MessageBox(NULL,"关闭窗口","提示",MB_OK); 04 return 0; 05 }
编译程序并运行,当程序关闭时即会提示信息框。通常情况下,应尽量使用类向导生成消息函数,以避免因个人的编程习惯问题造成的程序可读性降低,除非已经熟练掌握消息映射机制。
3.5 小结
本章首先介绍了Windows编程的基础知识,并且分析了Visual C++6.0中默认生成的一些文件。然后介绍了MFC的基础和应用框架分析,帮助读者对MFC有大概的了解。
3.6 习题
一、填空题
1.创建一个完整的窗口,需要经过4个操作步骤,分别是____________、____________、____________和____________。
2.MFC库可以分为3个主要部分,分别是____________、____________和____________。
3.MFC把消息分为3大类,分别是____________、____________和____________。
4.在MFC中添加消息映射有两种方法,分别为____________和____________。
二、上机实践
1.创建一个无任何功能的MFC单文档应用程序,并指出它有哪些类。
【提示】使用应用程序向导创建单文档应用程序。
【关键代码】
01 class CMainFrame : public CframeWnd{}; 02 class CMy3_exampleView : public Cview{}; 03 class CMy3_exampleDoc : public Cdocument{}; 04 class CMy3_exampleApp : public CwinApp{};
2.上题所创建代码的具体入口是哪里?请写出相关代码。
【提示】在应用程序类实现(.cpp文件)中,可以找到应用程序类对象创建的代码,它不属于任何局部函数或类。
CMy3_exampleApp theApp;