30
励志照亮人生 编程改变命运零基础学 Visual C++
第2 章Windows编程与MFC基础要想熟练掌握Windows应用程序的开发,首先需要理解Windows平台下程序运行的内部机制。本章首先将剖析Windows程序的内部运行机制,为读者扫清VC++学习路途中的第一个障碍,而后简单介绍一下MFC的基础知识,为进一步学习MFC程序开发打下基础。
2.1 Windows编程基础
Windows操作系统采用了图形用户界面,借助于它提供的API(Application Programming
Interface)函数,用户可以编出具有漂亮图形界面的程序。本节将主要介绍一下涉及Windows编程中用到的一些概念。
2.1.1 Windows API函数为方便用户开发Windows应用程序,Windows操作系统提供了各种各样的函数。这些函数是
Windows操作系统提供给应用程序编程的接口(Application Programming Interface),简称为API函数。
用户在编写Windows程序时所说的API函数,就是指系统提供的函数,所有主要的Windows函数都在
“Windows.h”头文件中进行了声明。
Windows API也是Windows操作系统自带的在Windows环境下运行的软件开发包(SDK)。程序员总是直接或间接引用API进行应用程序的开发,所以Windows应用程序就有大致相同的用户界面。
说明
SDK的全称是Software Development Kit,中文译为软件开发包。假如现在需要开发视频会议系统,在购买视频数据采集卡时,厂商就会提供频数据采集卡的SDK开发包,以方便对频数据采集卡的编程操作。这个开发包通常都会包含频数据采集卡的API函数库、帮助文档、使用手册和辅助工具等资源。也就是说,SDK实际上就是开发所需资源的一个集合。
2.1.2 窗口与句柄窗口是Windows应用程序中一个非常重要的元素,它是Windows应用程序与用户进行交互的接口。
一个Windows应用程序至少要有一个窗口,称为主窗口。通过窗口,可以接收用户的输入,并显示输出。
一个应用程序窗口通常都包含标题栏、菜单栏、系统菜单、最小(大)化按钮、边框和滚动条等。
窗口可以分为客户区和非客户区。客户区是窗口的一部分,应用程序通常在客户区中显示文字或者绘制图形。标题栏、菜单栏、系统菜单、最小(大)化按钮和边框统称为窗口的非客户区,它们由
Windows系统来管理,而应用程序则主要管理客户区的外观及操作。
在Windows应用程序中,窗口是通过窗口句柄(HWND)来标识的。要对某个窗口进行操作,首先就要得到这个窗口的句柄。句柄(HANDLE)是Windows程序中一个重要的概念。在Windows程序中,有各种各样的资源(窗口、图标和光标等),系统在创建这些资源时会为它们分配内存,并返回
31
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础标识这些资源的标识号,即句柄。
Windows中,常用句柄类型及其说明如表2.1所示。
表2.1 常用句柄类型及其说明句柄说明句柄说明
HWND窗口句柄HDC设备环境句柄
HBITMAP位图句柄HCURSOR光标句柄
HICON图标句柄HFONT字体句柄
HMENU菜单句柄HPEN画笔句柄
HFILE文件句柄HBRUSH画刷句柄
HINSTANCE当前实例句柄HLOCAL局部内存对象句柄
HGLOBAL全局内存对象句柄
2.1.3 事件与消息
Windows程序采用的是事件驱动方式的程序设计模式,其操作主要是基于消息的。在应用程序启动后,系统等待用户在图形用户界面内的输入选择,
如鼠标按键、键盘按键、窗口被创建、关闭、改变大小和移动等,对系统而言,这些都是事件。
只要有事件发生,系统即产生特定的消息。消息描述了事件的类别,包含了相关信息,Windows应用程序利用消息与系统及其他应用程序进行信息交换。
由于Windows事件的发生是随机的,程序的执行先后顺序也无法预测,系统采用消息队列来存放事件发生的消息,然后从消息队列中依次取出消息进行相应的处理,可表示为如图2.1所示。
2.1.4 常用的Windows数据类型
Windows应用程序中常用的数据类型如表2.2所示。
表2.2 Windows应用程序常用的数据类型数据类型说明
BYTE 8位无符号字符
PSTR 32位字符指针
COLORREF 32位整数,表示一个颜色
WORD 16位无符号整数
LONG 32位有符号整数
DWORD 32位无符号整数,是WORD的两倍长度
UINT 32位无符号整数
BOOL布尔值,值为TRUE或FALSE
HANDLE句柄
图2.1 事件与消息处理事件键盘消息应用程序提取消息处理消息鼠标消息其他消息消息队列
Windows
系统
32
励志照亮人生 编程改变命运零基础学 Visual C++
(续)
数据类型说明
LPSTR 32位指针,指向字符
LPCSTR 32位指针,指向字符串常量
LPTSTR 32位指针,指向字符串,此字符串可移植到Unicode和DBCS双字符集
LPCTSTR 32位指针,指向字符串常量,此串可移植到Unicode和DBCS双字符集
LPVOID 32位指针,可指向任何类型数据
LPRESULT 32位数值,作为窗口函数或CALLBACK函数的返回类型
WNDPROC 32位指针,指向一个窗口函数
LPARAM 32位数值,作为窗口函数和CALLBACK函数的参数
WPARAM作为窗口函数和CALLBACK函数的参数,在win 16中是16位,在win 32中是32位
2.2 Windows应用程序分析
WinMain和WinProc函数构成了Windows应用程序的主体。WinMain函数负责建立窗口和建立消息循环,WndProc函数负责消息的处理。典型的Windows窗口的创建与处理过程可表示为图2.2所示。
图2.2 Windows窗口创建及处理过程
2.2.1 WinMain函数传统的DOS程序以main函数作为进入程序的初始入口点,在Windows应用程序中,main函数被
WinMain函数取代。当Windows操作系统启动一个程序时,它调用的就是该程序的WinMain函数。
WinMain函数是Windows程序的入口点函数,当WinMain函数结束或返回时,Windows应用程序结束。
WinMain函数的原型如下:
int WINAPI WinMain
( HINSTANCE hThisInst,//应用程序当前实例句柄
HINSTANCe hPrevInst,//应用程序其他实例句柄
LPSTR lpszCmdLine,//指向程序命令行参数的指针
Int nCmdShow,//应用程序开始执行时窗口显示方式的整数值标识
)
程序开始执行程序打开窗口否是否应用程序处理消息是处理消息程序结束,
关闭窗口检测发向窗口的消息
WM_QUIT
WinMain()
函数负责
windows
默认处理
WndProc()
函数负责
参数hInstance表示该程序当前运行的实例的句柄,这是一个数值。当程序在Windows下运行时,
它唯一标识运行中的实例。一个应用程序可以运行多个实例,每运行一个实例,系统都会给该实例分配一个句柄值,并通过hInstance参数传递给WinMain函数。
参数hPrevInstance表示当前实例的前一个实例的句柄。在Win32环境下,这个参数不再起作用,
为NULL。
参数lpCmdLine是一个字符串指针,指定传递给应用程序的命令行参数。
参数nCmdShow指定程序的窗口应该如何显示,例如最大化、最小化、隐藏等。这个参数的值由该程序的调用者所指定,应用程序通常不需要去理会这个参数的值。
WinMain函数接收4个参数,这些参数都是在系统调用WinMain函数时,传递给应用程序的。
2.2.2 创建窗口创建一个完整的窗口,需要经过下面4个操作步骤:定义窗口类、注册窗口类、创建窗口实例、
显示及更新窗口。
1,定义窗口类在创建一个窗口前,必须对该类型的窗口进行设计,指定窗口的特征。窗口的特征是由
WNDCLASS结构体来定义的。WNDCLASS结构体的定义如下:
typedef struct tagWNDCLASS {
UINT style; //窗口风格
WNDPROC lpfnWndProc; //指向窗口处理函数的函数指针
int cbClsExtra; //窗口结构中的预留字节数
int cbWndExtra; //为其他创建窗口预留字节数
HINSTANCE hInstance; //注册该窗口类的实例句柄
HICON hIcon; //代表该窗口类的图标句柄
HCURSOR hCursor; //该窗口客户区鼠标光标句柄
HBRUSH hbrBackGround; //该窗口背景颜色句柄
LPCSTR lpszMenuName; //指向窗口菜单名的字符指针
LPCSTR lpszClassName; //指向窗口名的字符指针
} WNDCLASS,*PWNDCLASS,NEAR *NPWNDCLASS,
FAR *LPWNDCLASS;
2,注册窗口类窗口类(WNDCLASS)设计完成后,需要调用RegisterClass()函数对其进行注册,注册成功后,
才可以创建该类型的窗口。注册函数的原型声明如下:
BOOL RegisterClass(CONST WNDCLASS *lpWndClass);
该函数只有一个参数,即上一步骤中所设计的窗口类对象的指针。
3,创建窗口实例设计好窗口类并且将其成功注册之后,就可以用CreateWindow()函数产生这种类型的窗口了。函数Create Window()原型如下:
HWND CreateWindow
(LPCTSTR lpszClassName,//窗口类名
33
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础
LPCTSTR lpszTitle,//窗口标题名
DWORD dwStyle,//创建窗口的样式
int x,y,//窗口左上角坐标
int nWidth,nHeight,//窗口宽度和度高
HWND hwndParent,//该窗口的父窗口句柄
HWENU hMenu,//窗口主菜单句柄
HINSTANCE hInstance,//创建窗口的应用程序当前句柄
LPVOID lpParam,//指向一个传递给窗口的参数值的指针
)
注意区分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消息直接发送给窗口过程函数进行处理,而没有放到消息队列里。到此,一个窗口就算创建完成了。
2.2.3 消息循环在创建窗口、显示窗口和更新窗口后,就需要编写一个消息循环,不断地从消息队列中取出消息,
并进行响应。要从消息队列中取出消息,需要调用GetMessage()函数,其原型如下:
GetMessage
(lpMSG,//指向MSG结构的指针
hwnd,//窗口句柄
nMsgFilteMin,//用于消息过滤的最小消息号值
nMsgFilterMax //用于消息过滤的最大消息号值
)
只要从消息队列中取出消息不为WM_QUIT,GetMessage()函数就返回一个非零值,否则程序就结束循环并退出。
通常编写的消息循环代码如下:
MSG Msg;
while (GetMessage (&Msg,NULL,0,0))
{ TranslateMessage(&Msg); //将消息的虚拟键转换为字符信息
DispatchMessage(&Msg); //将消息传送到指定窗口函数
}
34
励志照亮人生 编程改变命运零基础学 Visual C++
35
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础
GetMessage()函数只有在接收到WM_QUIT消息时,才返回0。此时while语句判断的条件为假,
循环退出,程序才有可能结束运行。在没有接收到WM_QUIT消息时,Windows应用程序就通过这个
while循环来保证程序始终处于运行状态。
TranslateMessage()函数用于将虚拟键消息转换为字符消息。DispatchMessage()函数分派一个消息到指定窗口,由窗口函数WndProc()对消息进行处理。
说明
DispachMessage实际上是将消息回传给操作系统,由操作系统调用窗口函数对消息进行处理。
Windows应用程序的消息处理机制可表示为图
2.3所示。
2.2.4 WinProc窗口函数在完成上述步骤后,剩下的工作就是编写一个窗口函数,用于处理发送给窗口的消息。WinProc函数由一个或多个switch语句组成。每一条case语句对应一种消息,当应用程序接收到一个消息时,相应的case语句被激活。窗口函数的一般形式如下:
LRESULT CALLBACK WndProc(HWND hwnd,UINT messgae,WPARAM wParam,LPARAM lParam )
{,..
switch(message) //message为标识的消息
{ case,..
...
break;
...
case WM_DESTROY,//退出
PostQuitMessage(0);
default:
return DefWindowProc(hwnd,message,wParam,lParam);
}
return(0);
}
2.2.5 Windows编程实例本节将通过一个实例讲解Windows窗口的创建。该Windows应用程序将创建并显示一个窗口,在客户区中输出文本。
在Visual C++6.0中,可以使用AppWizard创建一个空的“Win32Application”工程,在其中创建源文件,利用Windows API函数实现基本的Windows窗口程序编程。实例的具体实现过程如下:
(1)启动Visual C++ 6.0,利用AppWizard来建立一个“Win32Application”类型的工程
“WindowsDemo”,向导默认选项就是创建一个空工程。
(2)通过执行“File”→“New”菜单命令,向工程添加源文件“Apidemo.cpp”,具体方法参见
2.2.2节。
图2.3 Windows应用程序的消息处理机制
②应用程序调用
GetMessage函数从消息队列中取出消息,
并进行预处理
④利用WNDCLASS
的lpfnWndProc成员保存的窗口函数的指针调用窗口函数,处理消息
①操作系统将接收到应用程序的窗口消息投递到其消息队列
③应用程序调用
DispatchMessage,
将消息回传给操作系统操作系统窗口函数应用程序消息队列
(3)在“Apidemo.cpp”文件中,编辑代码如下:
#include<windows.h> //包含windows.h头文件
LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM ); //窗口函数声明
/*入口函数WinMain()*/
int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,
int nCmdShow){
WNDCLASS wndclass; //定义窗口类结构变量
HWND hwnd; //定义窗口句柄
MSG msg; //定义消息结构变量
/*定义窗口类的各属性*/
wndclass.style = CS_HREDRAW|CS_VREDRAW; //改变窗口大小则重画
wndclass.lpfnWndProc = WndProc; //窗口函数为WndProc
wndclass.cbClsExtra = 0; //窗口类无扩展
wndclass.cbWndExtra = 0; //窗口实例无扩展
wndclass.hInstance = hInstance; //注册窗口类实例句柄
wndclass.hIcon = LoadIcon(NULL,IDI_APPLICATION); //用箭头光标
wndclass.hCursor = LoadCursor(NULL,IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); //背景为白色
wndclass.lpszMenuName = NULL; //窗口默认无菜单
wndclass.lpszClassName = "window窗口创建"; //窗口类名为window窗口创建
/*注册窗口类*/
if(! RegisterClass(&wndclass)) return FALSE;
/*创建窗口*/
hwnd = CreateWindow("window窗口创建",//窗口类名window窗口创建
"window窗口创建",//窗口名window窗口创建
WS_OVERLAPPEDWINDOW,//重叠式窗口
CW_USEDEFAULT,CW_USEDEFAULT,//左上角屏幕坐标默认值
CW_USEDEFAULT,CW_USEDEFAULT,//窗口宽度和高度默认值
NULL,//此窗口无父窗口
NULL,//此窗口无主菜单
hInstance,//创建此窗口的实例句柄
NULL); //此窗口无创建参数
/*显示并更新窗口*/
ShowWindow(hwnd,nCmdShow); //显示窗口
UpdateWindow (hwnd); //更新窗口的客户区
/*消息循环*/
while(GetMessage (&msg,NULL,0,0)) {
TranslateMessage (&msg); //键盘消息转换
DispatchMessage (&msg); //派送消息给窗口函数
}
return msg.wParam; //返回退出值
}
/*窗口函数*/
LRESULT CALLBACK WndProc(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam){
//根据消息值转相应的消息处理
switch (message){
case WM_PAINT,//重画窗口客户区消息处理
HDC hdc; //定义设备描述表句柄
36
励志照亮人生 编程改变命运零基础学 Visual C++
PAINTSTRUCT ps; //定义绘图信息结构变量
hdc = BeginPaint (hwnd,&ps); //获取要重画的窗口的设备描述表句柄
TextOut(hdc,10,20,"哈哈,Windows编程创建的窗口!",28); //输出文本
EndPaint (hwnd,&ps); //结束要重画的窗口
return 0;
case WM_DESTROY,//撤销窗口消息处理
PostQuitMessage (0); //产生退出程序消息WM_QUIT
return 0;
}
return DefWindowProc (hwnd,message,wParam,lParam);
//其他转默认窗口函数
}
程序将创建并显示一个Windows窗口,并在客户窗口中(10,20)的位置处输出一行文字。要在窗口中输出文字或者显示图形,需要用到设备描述表(Device
Context),简称DC。DC是一个包含设备(物理输出设备,如显示器,以及设备驱动程序)信息的结构体,
在Windows平台下,所有的图形操作都是利用DC来完成的。
编译、运行程序后,得到窗口结果如图2.4所示。
2.3 MFC基础前面已经多次提到,使用Viusal C++6.0进行应用程序的开发,其最大的便利就是可以使用其提供的MFC类库,通过MFC AppWizard自动生成的MFC应用程序框架,可以方便地开发自己想要实现的功能。本节将介绍有关MFC的基础知识。
2.3.1 MFC概述
Visual C++的微软基础类库(Microsoft Foundation Class Library,MFC)封装了大部分API函数,
并提供了一个应用程序框架,简化和标准了Windows程序设计,所以用MFC编写Windows应用程序也称为标准Windows程序设计。
说明MFC实际上可以理解为是用来编写Windows应用程序的C++类集。
MFC约有200个类,提供了Windows应用程序框架和创建应用程序的组件。它提供了大量的基类供程序员根据不同的应用环境进行扩充,同时允许在编程过程中自定义和扩展应用程序中的类,它还具有较好的移植性,可移植于众多的平台。
MFC库可以分为三个主要部分:MFC类、宏以及变量(或函数)。如果某个函数或者变量不是类的成员,那么它就是一个全局函数或者全局变量。
2.3.2 MFC基础类及其层次结构
MFC类库采用单一继承结构,从根类CObject层层派生出绝大多数MFC中的类,如图2.5所示。
37
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础图2.4 程序运行结果
38
励志照亮人生 编程改变命运零基础学 Visual C++
图2.5 CObject派生类层次示意图基类CObject的最基本功能包括支持序列化(serialization)、运行时(Run-time)类的信息获取、
提供特定操作符,完成对象的建立与删除。
读者可能会发现,在图2.5的类库结构中,并没有发现MFC应用程序框架所需要的类:应用类
CWinApp、框架类CFrameWnd、文档类
CDocument和视图类CView。实际上,它们都是由CCmdTarget派生出来的,如图2.6所示。
CCmdTarget类是CObject的子类,它是MFC
库中所有具有消息映射属性的基类。消息映射规定了当一对象接收到消息命令时,应调用哪一个函数对该消息进行处理。
2.3.3 MFC中的全局函数
MFC库中还包含有一些全局函数,这些函数不输入任何一个类,即可以直接使用。这些全局函数一般都以“Afx”为前缀,MFC中主要的全局函数及其作用如表2.3所示。
另外,在MFC库中还含有一些宏,在具体用到时,再详细介绍。
表2.3 MFC主要的全局函数及其作用函数名功能
AfxBeginThread开始一个新的线程
AfxEndThread结束一个旧的线程
AfxFormatString类似printf,将字符串格式化
AfxMessageBox类似Windows API函数MessageBox
AfxOuputDebugString将字符串输往除错装置
AfxGetApp
获得application object (CwinApp
派生对象)的指针
AfxGetMainWnd获得程序主窗口的指针
AfxGetInstance获得程序的instance handle
CObject根类
CCmdTarget命令相关类
CDC设备环境类
CGdiObject绘画工具类
CMenu菜单
CArray、CList、CMap、?群(集合)类
CDatabase、CRecordset、?ODBC数据库支持类
CDatabase、CDataRecordset、?DAO数据库支持类
CFile文件类
CException异常类
CSyncObject同步对象类
CInternetSession因特网会话类
CInternetConnection因特网连接类
CFtpConnection
CGopherConnection
CHelpConnection
CClientDC、CWindowDC
CPaintDC、?
CPen、CBrush、CFont
CBitmap、CPalette
CMemFile、COleStreamFile、
CSocketFile、?
图2.6 应用程序框架相关类的层次关系
2.4 MFC应用程序框架分析通过2.2节的介绍,相信读者对Windows应用程序的创建及其运行机制已经有了一定的了解,本节将对MFC应用程序框架作一简单剖析,使读者了解MFC应用程序框架是如何组织与工作的。
2.4.1 入口函数前面已经介绍过,WinMain函数是Windows程序的入口点函数。然而打开2.2.2节利用AppWizard
创建的MFC应用程序“SDIDemo”,却找不到WinMain函数。
39
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础
2O
CObject根类
CCmdTarget命令处理类
CWinThread线程类
CDocument文档类
CWnd窗口类
CFrameWnd框架窗口类
CControlBar控制条类
CSplitterWnd窗口分割类
CPropertSheet属性表类
CDialog对话框类
CProperty属性表
CView视图类
CCtrlView
CFormView
CRecordView
CButton、CEdit、CListBox、CScrollBar
CStatic、CComboBox控制类
CEditView、CListView
CTreeView、CRichEditView
CCommonDialog公用对话框类
CFileDialog、CColorDialog、
CMDIFrameWnd
CMDIChildWnd、CMiniFrameWnd
CDialogBar、CToolBar、CStatusBar
CDocTemplate文档模板类
CSingleDocTemplate单文档模板类
CMultiDocTemplate多文档模板类
CWinApp Windows应用程序类
这是因为MFC考虑到典型的Windows程序需要的大部分初始化工作都是标准化的,因此把
WinMain函数隐藏在应用程序的框架中,编译时会自动将该函数链接到可执行文件中。
在Visual C++6.0安装目录下的“Microsoft Visual Studio\VC98\MFC\SRC”路径中,会发现有一个源文件“WinMain.cpp”,其中定义了入口函数AfxWinMain,文件清单如下:
#include "stdafx.h"
#ifdef AFX_CORE1_SEG
#pragma code_seg(AFX_CORE1_SEG)
#endif
/////////////////////////////////////////////////////////////////////////////
// Standard WinMain implementation
// Can be replaced as long as 'AfxWinInit' is called first
int AFXAPI AfxWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,int nCmdShow)
{
ASSERT(hPrevInstance == NULL);
int nReturnCode = -1;
CWinThread* pThread = AfxGetThread();
CWinApp* pApp = AfxGetApp();
// AFX internal initialization
if (!AfxWinInit(hInstance,hPrevInstance,lpCmdLine,nCmdShow))
goto InitFailure;
// App global initializations (rare)
if (pApp != NULL && !pApp->InitApplication())
goto InitFailure;
// Perform specific initializations
if (!pThread->InitInstance())
{
if (pThread->m_pMainWnd != NULL)
{
TRACE0("Warning,Destroying non-NULL m_pMainWnd\n");
pThread->m_pMainWnd->DestroyWindow();
}
nReturnCode = pThread->ExitInstance();
goto InitFailure;
}
nReturnCode = pThread->Run();
InitFailure:
#ifdef _DEBUG
// Check for missing AfxLockTempMap calls
if (AfxGetModuleThreadState()->m_nTempMapLock != 0)
{
TRACE1("Warning,Temp map lock count non-zero (%ld).\n",
AfxGetModuleThreadState()->m_nTempMapLock);
}
AfxLockTempMaps();
AfxUnlockTempMaps(-1);
#endif
40
励志照亮人生 编程改变命运零基础学 Visual C++
AfxWinTerm();
return nReturnCode;
}
应用程序执行时,Windows自动调用应用程序框架内部的WinMain函数。从上面的代码清单中可见,WinMain函数会查找该应用程序的一个全局构造对象,这个对象是由CWinApp派生类构造的,并且只有一个。它是一个全局对象,因此在程序启动时,它就已经被构造好了。
随后,WinMain将调用这个对象的InitApplication()和InitInstance()成员函数,完成应用程序实例的初始化工作。随后,WinMain调用Run()成员函数,运行应用程序的消息循环。在程序结束时,
WinMain调用AfxWinTerm()函数,做一些清理工作。
2.4.2 应用程序对象每个应用程序必须从CWinApp派生出自己的应用程序类,并定义一个全局的对象。该应用程序类包含了Windows下应用程序的初始化、运行和结束过程。基于框架建立的应用程序必须有一个(且只能有一个)从CWinApp派生的类的对象。
在工程“SDIDemo”的CSDIDemoApp类的源文件中,可以发现框架自动生成了应用程序对象,如下:
// The one and only CSDIDemoApp object
CSDIDemoApp theApp;
2.4.3 InitInstance()函数
CWinApp类中的InitInstance()函数用于初始化实例。每次启动应用程序的一个实例时,WinMain
函数都要调用InitInstance()函数。
在工程“SDIDemo”的CSDIDemoApp类中,自动对InitInstance()函数进行了重载,代码如下:
BOOL CSDIDemoApp::InitInstance()
{
AfxEnableControlContainer();
#ifdef_AFXDLL
Enable3dControls(); // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif
// Change the registry key under which our settings are stored.
// TODO,You should modify this string to be something appropriate
// such as the name of your company or organization.
SetRegistryKey(_T("Local AppWizard-Generated Applications"));
LoadStdProfileSettings(); // Load standard INI file options (including MRU)
// Register the application's document templates,Document templates
// serve as the connection between documents,frame windows and views.
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CSDIDemoDoc),
RUNTIME_CLASS(CMainFrame),// main SDI frame window
41
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础
42
励志照亮人生 编程改变命运零基础学 Visual C++
RUNTIME_CLASS(CSDIDemoView));
AddDocTemplate(pDocTemplate);
// Parse command line for standard shell commands,DDE,file open
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
// Dispatch commands specified on the command line
if (!ProcessShellCommand(cmdInfo))
return FALSE;
// The one and only window has been initialized,so show and update it.
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
return TRUE;
}
从代码中可以看出,InitInstance()函数主要完成了下面几方面功能。
通过LoadStdProfileSettings()函数从注册表中获取一些标准的文件选项,包括最近打开的文件名称,在程序的“文件”菜单中列出。
构造文档模板类对象pDocTemplat,指明文档模板的文档类、框架窗口类和视图类。
调用ParseCommandLine函数进行程序窗口启动方式的分析处理。如果没有提供命令行参数
(打开文档的文件名),则新建一个新文档。
调用ShowWindow()和UpdateWindow()函数显示、更新窗口。其中,m_pMainWnd成员变量是一个CWnd类型的指针,它保存了应用程序框架窗口对象的指针。也就是说,是指向
CMainFrame对象的指针。
注意在CWinApp的派生类中,必须重载InitInstance()函数,因为CWinApp并不知道应用程序需要什么样的窗口,它可以是多文档窗口和单文档窗口,也可以是基于对话框的。
2.4.4 Run()函数
WinMain在初始化应用程序实例后,就调用CWinThread类的Run()函数来处理消息循环。在
Visual C++6.0安装目录下的“Microsoft Visual Studio\VC98\MFC\SRC”路径中的源文件
“THRDCORE.CPP”中会找到Run()函数的实现代码,如下:
int CWinThread::Run()
{
ASSERT_VALID(this);
// for tracking the idle time state
BOOL bIdle = TRUE;
LONG lIdleCount = 0;
// acquire and dispatch messages until a WM_QUIT message is received.
for (;;)
{
// phase1,check to see if we can do idle work
while (bIdle &&
!::PeekMessage(&m_msgCur,NULL,NULL,NULL,PM_NOREMOVE))
{
// call OnIdle while in bIdle state
if (!OnIdle(lIdleCount++))
bIdle = FALSE; // assume "no idle" state
}
// phase2,pump messages while available
do
{
// pump message,but quit on WM_QUIT
if (!PumpMessage())
return ExitInstance();
// reset "no idle" state after pumping "normal" message
if (IsIdleMessage(&m_msgCur))
{
bIdle = TRUE;
lIdleCount = 0;
}
} while (::PeekMessage(&m_msgCur,NULL,NULL,NULL,PM_NOREMOVE));
}
ASSERT(FALSE); // not reachable
}
该函数的主要结构是一个for循环,该循环在接收到一个WM_QUIT消息时退出。
Run()成员函数不断执行消息循环,检查消息队列中有没有消息。如果有消息,Run通过
PumpMessage()函数将其派遣,交由框架去处理,然后返回消息循环。如果没有消息,Run()将调用
OnIdle来做用户或框架可能需要在空闲时才做的工作。
如果既没有消息要处理,也没有空闲时的处理工作要做,
则应用程序将一直等待,直到有事件发生。当应用程序结束时,Run()将调用ExitInstance,结束消息循环。
Run()函数的消息循环可表示为如图2.7所示。
至此,就了解了MFC程序的整个运行机制,实际上与Win32 SDK程序是一致的。它同样也需要经过设计窗口类(MFC程序中已经预定义了一些窗口类,可以直接使用)、注册窗口类、创建窗口、显示并更新窗口和消息循环几个过程。
首先利用全局应用程序对象theApp启动应用程序。正是产生了这个全局对象,基类CWinApp中的this指针才能指向这个对象。
调用全局应用程序对象的构造函数,从而就会先调用其基类CWinApp的构造函数。后者完成应用程序的一些初始化工作,并将应用程序对象的指针保存起来。
进入WinMain函数。在AfxWinMain函数中可以获取子类CSDIDemoApp的指针,利用此指针调用虚函数InitInstance。由该函数完成应用程序的一些初始化工作,包括窗口类的注册、创建、
窗口的显示和更新。
43
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础图2.7 Run成员函数的消息循环有可用的消息吗?
有可用的消息吗?
还有空闲任务吗?
等待消息获取、分发消息
Yes
No
Yes
Yes
No
No
空闲处理
OnIdle()
44
励志照亮人生 编程改变命运零基础学 Visual C++
进入消息循环。MFC应用程序实际上是采用消息映射机制来处理各种消息的。当收到
WM_QUIT消息时,退出消息循环,程序结束。
2.4.5 MFC的消息映射
Windows程序中的消息处理是在WinProc函数中,通过Switch结构实现的。但当处理的消息比较多时,Switch-Case结构将变得分支很多,影响程序的可读性。而在MFC中,则采用消息映射的结构进行结构化消息处理。
进行MFC消息处理,程序员要做的就是为每一个要处理的消息提供一个消息处理函数,然后系统通过MFC提供的一套消息映射系统来调用相应的消息处理函数。
说明消息映射就是消息与消息处理函数一对一的联系。
MFC的消息映射采用消息映射宏的方式,把消息和消息处理函数一一对应起来。在MFC的框架结构下,可以进行消息处理的类的头文件里面都会含有DECLARE_MESSAGE_MAP()宏,这里主要进行消息映射和消息处理函数的声明。可以进行消息处理的类的实现文件里一般都含有如下的结构:
BEGIN_MESSAGE_MAP(CInheritClass,CBaseClass)
//{{AFX_MSG_MAP(CInheritClass)
ON_MESSAGE(message1,memberFxn1)
ON_MESSAGE(message2,memberFxn2)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
其中,CInheritClass为具有消息循环的类的名字,CBaseClass是CInheritClass的父类。在中间的
ON_MESSAGE宏就是消息映射宏。
为简化消息处理,MFC采用默认的消息映射和消息处理函数。如对消息标识符
WM_LBUTTONDOWN完整的消息映射应该如下:
ON_MESSAGE(WM_LBUTTONDOWN,Function1)
而MFC采用的更简捷的映射方式是:
ON_WM_LBUTTONDO()
MFC还为默认的消息映射预定义了消息处理函数:
OnLButtonDown(UINT nFlags,CPoint point)
假如要处理消息完成自己的任务,则要在派生类中重写这些函数。
2.4.6 MFC消息分类
MFC把消息分为3大类:窗口消息、控件通知消息和命令消息。
1,窗口消息当创建窗口、绘制窗口、移动窗口、销毁窗口,以及使用键盘、鼠标等与操作窗口有关的动作时,
产生的消息均属于窗口消息。
45
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础窗口消息由MFC的窗口类(CWnd)对象来处理,即这类消息处理函数一般是CWnd类的成员函数,有默认的窗口处理函数。
典型的窗口消息、消息映射宏和默认的消息处理函数如表2.4所示。
说明若CWnd派生类没有重载窗口消息处理函数,则消息映射机制会转由其基类处理
(最终是CWnd类)。
2,控件通知消息控件是一个Windows的一个子窗口(如对话框中的按钮、编辑框等),控件通知消息是指在事件发生时,由控件或其他类型的子窗口发送到父窗口的消息。它通知父窗口,该控件接受了某操作,为父窗口进一步控制子窗口提供了条件。
控件消息一般由按钮(BN_)、编辑框(EN_)、组合框(CBN_)和列表框(LBN_)等产生,其消息映射宏为在消息名前加上ON_即可。举例如下:
ON_BN_CLICKED (按钮ID,响应函数)
ON_CBN_DBCLK (组合框ID,响应函数)
ON_EN_SETFOCUS (组合框ID,响应函数)
ON_LBN_DBCLK (列表框ID,响应函数)
分别表示选择各个控件后,产生的消息由其后面定义的函数进行处理。
3,命令消息命令消息一般与处理用户的请求有关,主要是来自菜单、工具栏和加速键的通知消息。从
CCmdTarget派生的类(如文档、文档模板、应用程序对象、窗口和视图等)都能处理命令消息。
可以使用MFC ClassWizard(类向导),建立消息映射和消息处理函数的框架,消息和函数都由
MFC默认的命名方式命名。
命令消息使用WM_COMMAND宏定义对其进行映射响应,格式如下:
ON_COMMAND(命令ID,响应函数)
举例如下:
ON_COMMAND (IDM_FILENEW,OnFileNew) //"新建"菜单命令
ON_COMMAND (IDM_FILEOPEN,OnFileOpen) //"打开"菜单命令对于命令消息,MFC应用程序框架会通过消息映射机制,按一定的搜索顺序在各个CCmdTarget
类(命令处理类)的派生类中查找对应消息处理函数。
说明所有由用户定义的命令消息也由ON_COMMAND定义消息映射关系。
表2.4 窗口消息、消息映射宏及其处理函数窗口消息消息映射宏默认处理函数
WM_CHAR ON_WM_CHAR OnChar
WM_ CLOSE ON_WM_ CLOSE OnClose
WM_CREATE ON_WM_CREATE OnCreate
WM_LBUTTONDOWN ON_WM_LBUTTONDOWN OnLButtonDown
WM_ MOUSEMOVE ON_ WM_MOUSEMOVE OnMouseMove
励志照亮人生 编程改变命运零基础学 Visual C++
第2 章Windows编程与MFC基础要想熟练掌握Windows应用程序的开发,首先需要理解Windows平台下程序运行的内部机制。本章首先将剖析Windows程序的内部运行机制,为读者扫清VC++学习路途中的第一个障碍,而后简单介绍一下MFC的基础知识,为进一步学习MFC程序开发打下基础。
2.1 Windows编程基础
Windows操作系统采用了图形用户界面,借助于它提供的API(Application Programming
Interface)函数,用户可以编出具有漂亮图形界面的程序。本节将主要介绍一下涉及Windows编程中用到的一些概念。
2.1.1 Windows API函数为方便用户开发Windows应用程序,Windows操作系统提供了各种各样的函数。这些函数是
Windows操作系统提供给应用程序编程的接口(Application Programming Interface),简称为API函数。
用户在编写Windows程序时所说的API函数,就是指系统提供的函数,所有主要的Windows函数都在
“Windows.h”头文件中进行了声明。
Windows API也是Windows操作系统自带的在Windows环境下运行的软件开发包(SDK)。程序员总是直接或间接引用API进行应用程序的开发,所以Windows应用程序就有大致相同的用户界面。
说明
SDK的全称是Software Development Kit,中文译为软件开发包。假如现在需要开发视频会议系统,在购买视频数据采集卡时,厂商就会提供频数据采集卡的SDK开发包,以方便对频数据采集卡的编程操作。这个开发包通常都会包含频数据采集卡的API函数库、帮助文档、使用手册和辅助工具等资源。也就是说,SDK实际上就是开发所需资源的一个集合。
2.1.2 窗口与句柄窗口是Windows应用程序中一个非常重要的元素,它是Windows应用程序与用户进行交互的接口。
一个Windows应用程序至少要有一个窗口,称为主窗口。通过窗口,可以接收用户的输入,并显示输出。
一个应用程序窗口通常都包含标题栏、菜单栏、系统菜单、最小(大)化按钮、边框和滚动条等。
窗口可以分为客户区和非客户区。客户区是窗口的一部分,应用程序通常在客户区中显示文字或者绘制图形。标题栏、菜单栏、系统菜单、最小(大)化按钮和边框统称为窗口的非客户区,它们由
Windows系统来管理,而应用程序则主要管理客户区的外观及操作。
在Windows应用程序中,窗口是通过窗口句柄(HWND)来标识的。要对某个窗口进行操作,首先就要得到这个窗口的句柄。句柄(HANDLE)是Windows程序中一个重要的概念。在Windows程序中,有各种各样的资源(窗口、图标和光标等),系统在创建这些资源时会为它们分配内存,并返回
31
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础标识这些资源的标识号,即句柄。
Windows中,常用句柄类型及其说明如表2.1所示。
表2.1 常用句柄类型及其说明句柄说明句柄说明
HWND窗口句柄HDC设备环境句柄
HBITMAP位图句柄HCURSOR光标句柄
HICON图标句柄HFONT字体句柄
HMENU菜单句柄HPEN画笔句柄
HFILE文件句柄HBRUSH画刷句柄
HINSTANCE当前实例句柄HLOCAL局部内存对象句柄
HGLOBAL全局内存对象句柄
2.1.3 事件与消息
Windows程序采用的是事件驱动方式的程序设计模式,其操作主要是基于消息的。在应用程序启动后,系统等待用户在图形用户界面内的输入选择,
如鼠标按键、键盘按键、窗口被创建、关闭、改变大小和移动等,对系统而言,这些都是事件。
只要有事件发生,系统即产生特定的消息。消息描述了事件的类别,包含了相关信息,Windows应用程序利用消息与系统及其他应用程序进行信息交换。
由于Windows事件的发生是随机的,程序的执行先后顺序也无法预测,系统采用消息队列来存放事件发生的消息,然后从消息队列中依次取出消息进行相应的处理,可表示为如图2.1所示。
2.1.4 常用的Windows数据类型
Windows应用程序中常用的数据类型如表2.2所示。
表2.2 Windows应用程序常用的数据类型数据类型说明
BYTE 8位无符号字符
PSTR 32位字符指针
COLORREF 32位整数,表示一个颜色
WORD 16位无符号整数
LONG 32位有符号整数
DWORD 32位无符号整数,是WORD的两倍长度
UINT 32位无符号整数
BOOL布尔值,值为TRUE或FALSE
HANDLE句柄
图2.1 事件与消息处理事件键盘消息应用程序提取消息处理消息鼠标消息其他消息消息队列
Windows
系统
32
励志照亮人生 编程改变命运零基础学 Visual C++
(续)
数据类型说明
LPSTR 32位指针,指向字符
LPCSTR 32位指针,指向字符串常量
LPTSTR 32位指针,指向字符串,此字符串可移植到Unicode和DBCS双字符集
LPCTSTR 32位指针,指向字符串常量,此串可移植到Unicode和DBCS双字符集
LPVOID 32位指针,可指向任何类型数据
LPRESULT 32位数值,作为窗口函数或CALLBACK函数的返回类型
WNDPROC 32位指针,指向一个窗口函数
LPARAM 32位数值,作为窗口函数和CALLBACK函数的参数
WPARAM作为窗口函数和CALLBACK函数的参数,在win 16中是16位,在win 32中是32位
2.2 Windows应用程序分析
WinMain和WinProc函数构成了Windows应用程序的主体。WinMain函数负责建立窗口和建立消息循环,WndProc函数负责消息的处理。典型的Windows窗口的创建与处理过程可表示为图2.2所示。
图2.2 Windows窗口创建及处理过程
2.2.1 WinMain函数传统的DOS程序以main函数作为进入程序的初始入口点,在Windows应用程序中,main函数被
WinMain函数取代。当Windows操作系统启动一个程序时,它调用的就是该程序的WinMain函数。
WinMain函数是Windows程序的入口点函数,当WinMain函数结束或返回时,Windows应用程序结束。
WinMain函数的原型如下:
int WINAPI WinMain
( HINSTANCE hThisInst,//应用程序当前实例句柄
HINSTANCe hPrevInst,//应用程序其他实例句柄
LPSTR lpszCmdLine,//指向程序命令行参数的指针
Int nCmdShow,//应用程序开始执行时窗口显示方式的整数值标识
)
程序开始执行程序打开窗口否是否应用程序处理消息是处理消息程序结束,
关闭窗口检测发向窗口的消息
WM_QUIT
WinMain()
函数负责
windows
默认处理
WndProc()
函数负责
参数hInstance表示该程序当前运行的实例的句柄,这是一个数值。当程序在Windows下运行时,
它唯一标识运行中的实例。一个应用程序可以运行多个实例,每运行一个实例,系统都会给该实例分配一个句柄值,并通过hInstance参数传递给WinMain函数。
参数hPrevInstance表示当前实例的前一个实例的句柄。在Win32环境下,这个参数不再起作用,
为NULL。
参数lpCmdLine是一个字符串指针,指定传递给应用程序的命令行参数。
参数nCmdShow指定程序的窗口应该如何显示,例如最大化、最小化、隐藏等。这个参数的值由该程序的调用者所指定,应用程序通常不需要去理会这个参数的值。
WinMain函数接收4个参数,这些参数都是在系统调用WinMain函数时,传递给应用程序的。
2.2.2 创建窗口创建一个完整的窗口,需要经过下面4个操作步骤:定义窗口类、注册窗口类、创建窗口实例、
显示及更新窗口。
1,定义窗口类在创建一个窗口前,必须对该类型的窗口进行设计,指定窗口的特征。窗口的特征是由
WNDCLASS结构体来定义的。WNDCLASS结构体的定义如下:
typedef struct tagWNDCLASS {
UINT style; //窗口风格
WNDPROC lpfnWndProc; //指向窗口处理函数的函数指针
int cbClsExtra; //窗口结构中的预留字节数
int cbWndExtra; //为其他创建窗口预留字节数
HINSTANCE hInstance; //注册该窗口类的实例句柄
HICON hIcon; //代表该窗口类的图标句柄
HCURSOR hCursor; //该窗口客户区鼠标光标句柄
HBRUSH hbrBackGround; //该窗口背景颜色句柄
LPCSTR lpszMenuName; //指向窗口菜单名的字符指针
LPCSTR lpszClassName; //指向窗口名的字符指针
} WNDCLASS,*PWNDCLASS,NEAR *NPWNDCLASS,
FAR *LPWNDCLASS;
2,注册窗口类窗口类(WNDCLASS)设计完成后,需要调用RegisterClass()函数对其进行注册,注册成功后,
才可以创建该类型的窗口。注册函数的原型声明如下:
BOOL RegisterClass(CONST WNDCLASS *lpWndClass);
该函数只有一个参数,即上一步骤中所设计的窗口类对象的指针。
3,创建窗口实例设计好窗口类并且将其成功注册之后,就可以用CreateWindow()函数产生这种类型的窗口了。函数Create Window()原型如下:
HWND CreateWindow
(LPCTSTR lpszClassName,//窗口类名
33
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础
LPCTSTR lpszTitle,//窗口标题名
DWORD dwStyle,//创建窗口的样式
int x,y,//窗口左上角坐标
int nWidth,nHeight,//窗口宽度和度高
HWND hwndParent,//该窗口的父窗口句柄
HWENU hMenu,//窗口主菜单句柄
HINSTANCE hInstance,//创建窗口的应用程序当前句柄
LPVOID lpParam,//指向一个传递给窗口的参数值的指针
)
注意区分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消息直接发送给窗口过程函数进行处理,而没有放到消息队列里。到此,一个窗口就算创建完成了。
2.2.3 消息循环在创建窗口、显示窗口和更新窗口后,就需要编写一个消息循环,不断地从消息队列中取出消息,
并进行响应。要从消息队列中取出消息,需要调用GetMessage()函数,其原型如下:
GetMessage
(lpMSG,//指向MSG结构的指针
hwnd,//窗口句柄
nMsgFilteMin,//用于消息过滤的最小消息号值
nMsgFilterMax //用于消息过滤的最大消息号值
)
只要从消息队列中取出消息不为WM_QUIT,GetMessage()函数就返回一个非零值,否则程序就结束循环并退出。
通常编写的消息循环代码如下:
MSG Msg;
while (GetMessage (&Msg,NULL,0,0))
{ TranslateMessage(&Msg); //将消息的虚拟键转换为字符信息
DispatchMessage(&Msg); //将消息传送到指定窗口函数
}
34
励志照亮人生 编程改变命运零基础学 Visual C++
35
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础
GetMessage()函数只有在接收到WM_QUIT消息时,才返回0。此时while语句判断的条件为假,
循环退出,程序才有可能结束运行。在没有接收到WM_QUIT消息时,Windows应用程序就通过这个
while循环来保证程序始终处于运行状态。
TranslateMessage()函数用于将虚拟键消息转换为字符消息。DispatchMessage()函数分派一个消息到指定窗口,由窗口函数WndProc()对消息进行处理。
说明
DispachMessage实际上是将消息回传给操作系统,由操作系统调用窗口函数对消息进行处理。
Windows应用程序的消息处理机制可表示为图
2.3所示。
2.2.4 WinProc窗口函数在完成上述步骤后,剩下的工作就是编写一个窗口函数,用于处理发送给窗口的消息。WinProc函数由一个或多个switch语句组成。每一条case语句对应一种消息,当应用程序接收到一个消息时,相应的case语句被激活。窗口函数的一般形式如下:
LRESULT CALLBACK WndProc(HWND hwnd,UINT messgae,WPARAM wParam,LPARAM lParam )
{,..
switch(message) //message为标识的消息
{ case,..
...
break;
...
case WM_DESTROY,//退出
PostQuitMessage(0);
default:
return DefWindowProc(hwnd,message,wParam,lParam);
}
return(0);
}
2.2.5 Windows编程实例本节将通过一个实例讲解Windows窗口的创建。该Windows应用程序将创建并显示一个窗口,在客户区中输出文本。
在Visual C++6.0中,可以使用AppWizard创建一个空的“Win32Application”工程,在其中创建源文件,利用Windows API函数实现基本的Windows窗口程序编程。实例的具体实现过程如下:
(1)启动Visual C++ 6.0,利用AppWizard来建立一个“Win32Application”类型的工程
“WindowsDemo”,向导默认选项就是创建一个空工程。
(2)通过执行“File”→“New”菜单命令,向工程添加源文件“Apidemo.cpp”,具体方法参见
2.2.2节。
图2.3 Windows应用程序的消息处理机制
②应用程序调用
GetMessage函数从消息队列中取出消息,
并进行预处理
④利用WNDCLASS
的lpfnWndProc成员保存的窗口函数的指针调用窗口函数,处理消息
①操作系统将接收到应用程序的窗口消息投递到其消息队列
③应用程序调用
DispatchMessage,
将消息回传给操作系统操作系统窗口函数应用程序消息队列
(3)在“Apidemo.cpp”文件中,编辑代码如下:
#include<windows.h> //包含windows.h头文件
LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM ); //窗口函数声明
/*入口函数WinMain()*/
int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,
int nCmdShow){
WNDCLASS wndclass; //定义窗口类结构变量
HWND hwnd; //定义窗口句柄
MSG msg; //定义消息结构变量
/*定义窗口类的各属性*/
wndclass.style = CS_HREDRAW|CS_VREDRAW; //改变窗口大小则重画
wndclass.lpfnWndProc = WndProc; //窗口函数为WndProc
wndclass.cbClsExtra = 0; //窗口类无扩展
wndclass.cbWndExtra = 0; //窗口实例无扩展
wndclass.hInstance = hInstance; //注册窗口类实例句柄
wndclass.hIcon = LoadIcon(NULL,IDI_APPLICATION); //用箭头光标
wndclass.hCursor = LoadCursor(NULL,IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); //背景为白色
wndclass.lpszMenuName = NULL; //窗口默认无菜单
wndclass.lpszClassName = "window窗口创建"; //窗口类名为window窗口创建
/*注册窗口类*/
if(! RegisterClass(&wndclass)) return FALSE;
/*创建窗口*/
hwnd = CreateWindow("window窗口创建",//窗口类名window窗口创建
"window窗口创建",//窗口名window窗口创建
WS_OVERLAPPEDWINDOW,//重叠式窗口
CW_USEDEFAULT,CW_USEDEFAULT,//左上角屏幕坐标默认值
CW_USEDEFAULT,CW_USEDEFAULT,//窗口宽度和高度默认值
NULL,//此窗口无父窗口
NULL,//此窗口无主菜单
hInstance,//创建此窗口的实例句柄
NULL); //此窗口无创建参数
/*显示并更新窗口*/
ShowWindow(hwnd,nCmdShow); //显示窗口
UpdateWindow (hwnd); //更新窗口的客户区
/*消息循环*/
while(GetMessage (&msg,NULL,0,0)) {
TranslateMessage (&msg); //键盘消息转换
DispatchMessage (&msg); //派送消息给窗口函数
}
return msg.wParam; //返回退出值
}
/*窗口函数*/
LRESULT CALLBACK WndProc(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam){
//根据消息值转相应的消息处理
switch (message){
case WM_PAINT,//重画窗口客户区消息处理
HDC hdc; //定义设备描述表句柄
36
励志照亮人生 编程改变命运零基础学 Visual C++
PAINTSTRUCT ps; //定义绘图信息结构变量
hdc = BeginPaint (hwnd,&ps); //获取要重画的窗口的设备描述表句柄
TextOut(hdc,10,20,"哈哈,Windows编程创建的窗口!",28); //输出文本
EndPaint (hwnd,&ps); //结束要重画的窗口
return 0;
case WM_DESTROY,//撤销窗口消息处理
PostQuitMessage (0); //产生退出程序消息WM_QUIT
return 0;
}
return DefWindowProc (hwnd,message,wParam,lParam);
//其他转默认窗口函数
}
程序将创建并显示一个Windows窗口,并在客户窗口中(10,20)的位置处输出一行文字。要在窗口中输出文字或者显示图形,需要用到设备描述表(Device
Context),简称DC。DC是一个包含设备(物理输出设备,如显示器,以及设备驱动程序)信息的结构体,
在Windows平台下,所有的图形操作都是利用DC来完成的。
编译、运行程序后,得到窗口结果如图2.4所示。
2.3 MFC基础前面已经多次提到,使用Viusal C++6.0进行应用程序的开发,其最大的便利就是可以使用其提供的MFC类库,通过MFC AppWizard自动生成的MFC应用程序框架,可以方便地开发自己想要实现的功能。本节将介绍有关MFC的基础知识。
2.3.1 MFC概述
Visual C++的微软基础类库(Microsoft Foundation Class Library,MFC)封装了大部分API函数,
并提供了一个应用程序框架,简化和标准了Windows程序设计,所以用MFC编写Windows应用程序也称为标准Windows程序设计。
说明MFC实际上可以理解为是用来编写Windows应用程序的C++类集。
MFC约有200个类,提供了Windows应用程序框架和创建应用程序的组件。它提供了大量的基类供程序员根据不同的应用环境进行扩充,同时允许在编程过程中自定义和扩展应用程序中的类,它还具有较好的移植性,可移植于众多的平台。
MFC库可以分为三个主要部分:MFC类、宏以及变量(或函数)。如果某个函数或者变量不是类的成员,那么它就是一个全局函数或者全局变量。
2.3.2 MFC基础类及其层次结构
MFC类库采用单一继承结构,从根类CObject层层派生出绝大多数MFC中的类,如图2.5所示。
37
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础图2.4 程序运行结果
38
励志照亮人生 编程改变命运零基础学 Visual C++
图2.5 CObject派生类层次示意图基类CObject的最基本功能包括支持序列化(serialization)、运行时(Run-time)类的信息获取、
提供特定操作符,完成对象的建立与删除。
读者可能会发现,在图2.5的类库结构中,并没有发现MFC应用程序框架所需要的类:应用类
CWinApp、框架类CFrameWnd、文档类
CDocument和视图类CView。实际上,它们都是由CCmdTarget派生出来的,如图2.6所示。
CCmdTarget类是CObject的子类,它是MFC
库中所有具有消息映射属性的基类。消息映射规定了当一对象接收到消息命令时,应调用哪一个函数对该消息进行处理。
2.3.3 MFC中的全局函数
MFC库中还包含有一些全局函数,这些函数不输入任何一个类,即可以直接使用。这些全局函数一般都以“Afx”为前缀,MFC中主要的全局函数及其作用如表2.3所示。
另外,在MFC库中还含有一些宏,在具体用到时,再详细介绍。
表2.3 MFC主要的全局函数及其作用函数名功能
AfxBeginThread开始一个新的线程
AfxEndThread结束一个旧的线程
AfxFormatString类似printf,将字符串格式化
AfxMessageBox类似Windows API函数MessageBox
AfxOuputDebugString将字符串输往除错装置
AfxGetApp
获得application object (CwinApp
派生对象)的指针
AfxGetMainWnd获得程序主窗口的指针
AfxGetInstance获得程序的instance handle
CObject根类
CCmdTarget命令相关类
CDC设备环境类
CGdiObject绘画工具类
CMenu菜单
CArray、CList、CMap、?群(集合)类
CDatabase、CRecordset、?ODBC数据库支持类
CDatabase、CDataRecordset、?DAO数据库支持类
CFile文件类
CException异常类
CSyncObject同步对象类
CInternetSession因特网会话类
CInternetConnection因特网连接类
CFtpConnection
CGopherConnection
CHelpConnection
CClientDC、CWindowDC
CPaintDC、?
CPen、CBrush、CFont
CBitmap、CPalette
CMemFile、COleStreamFile、
CSocketFile、?
图2.6 应用程序框架相关类的层次关系
2.4 MFC应用程序框架分析通过2.2节的介绍,相信读者对Windows应用程序的创建及其运行机制已经有了一定的了解,本节将对MFC应用程序框架作一简单剖析,使读者了解MFC应用程序框架是如何组织与工作的。
2.4.1 入口函数前面已经介绍过,WinMain函数是Windows程序的入口点函数。然而打开2.2.2节利用AppWizard
创建的MFC应用程序“SDIDemo”,却找不到WinMain函数。
39
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础
2O
CObject根类
CCmdTarget命令处理类
CWinThread线程类
CDocument文档类
CWnd窗口类
CFrameWnd框架窗口类
CControlBar控制条类
CSplitterWnd窗口分割类
CPropertSheet属性表类
CDialog对话框类
CProperty属性表
CView视图类
CCtrlView
CFormView
CRecordView
CButton、CEdit、CListBox、CScrollBar
CStatic、CComboBox控制类
CEditView、CListView
CTreeView、CRichEditView
CCommonDialog公用对话框类
CFileDialog、CColorDialog、
CMDIFrameWnd
CMDIChildWnd、CMiniFrameWnd
CDialogBar、CToolBar、CStatusBar
CDocTemplate文档模板类
CSingleDocTemplate单文档模板类
CMultiDocTemplate多文档模板类
CWinApp Windows应用程序类
这是因为MFC考虑到典型的Windows程序需要的大部分初始化工作都是标准化的,因此把
WinMain函数隐藏在应用程序的框架中,编译时会自动将该函数链接到可执行文件中。
在Visual C++6.0安装目录下的“Microsoft Visual Studio\VC98\MFC\SRC”路径中,会发现有一个源文件“WinMain.cpp”,其中定义了入口函数AfxWinMain,文件清单如下:
#include "stdafx.h"
#ifdef AFX_CORE1_SEG
#pragma code_seg(AFX_CORE1_SEG)
#endif
/////////////////////////////////////////////////////////////////////////////
// Standard WinMain implementation
// Can be replaced as long as 'AfxWinInit' is called first
int AFXAPI AfxWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,int nCmdShow)
{
ASSERT(hPrevInstance == NULL);
int nReturnCode = -1;
CWinThread* pThread = AfxGetThread();
CWinApp* pApp = AfxGetApp();
// AFX internal initialization
if (!AfxWinInit(hInstance,hPrevInstance,lpCmdLine,nCmdShow))
goto InitFailure;
// App global initializations (rare)
if (pApp != NULL && !pApp->InitApplication())
goto InitFailure;
// Perform specific initializations
if (!pThread->InitInstance())
{
if (pThread->m_pMainWnd != NULL)
{
TRACE0("Warning,Destroying non-NULL m_pMainWnd\n");
pThread->m_pMainWnd->DestroyWindow();
}
nReturnCode = pThread->ExitInstance();
goto InitFailure;
}
nReturnCode = pThread->Run();
InitFailure:
#ifdef _DEBUG
// Check for missing AfxLockTempMap calls
if (AfxGetModuleThreadState()->m_nTempMapLock != 0)
{
TRACE1("Warning,Temp map lock count non-zero (%ld).\n",
AfxGetModuleThreadState()->m_nTempMapLock);
}
AfxLockTempMaps();
AfxUnlockTempMaps(-1);
#endif
40
励志照亮人生 编程改变命运零基础学 Visual C++
AfxWinTerm();
return nReturnCode;
}
应用程序执行时,Windows自动调用应用程序框架内部的WinMain函数。从上面的代码清单中可见,WinMain函数会查找该应用程序的一个全局构造对象,这个对象是由CWinApp派生类构造的,并且只有一个。它是一个全局对象,因此在程序启动时,它就已经被构造好了。
随后,WinMain将调用这个对象的InitApplication()和InitInstance()成员函数,完成应用程序实例的初始化工作。随后,WinMain调用Run()成员函数,运行应用程序的消息循环。在程序结束时,
WinMain调用AfxWinTerm()函数,做一些清理工作。
2.4.2 应用程序对象每个应用程序必须从CWinApp派生出自己的应用程序类,并定义一个全局的对象。该应用程序类包含了Windows下应用程序的初始化、运行和结束过程。基于框架建立的应用程序必须有一个(且只能有一个)从CWinApp派生的类的对象。
在工程“SDIDemo”的CSDIDemoApp类的源文件中,可以发现框架自动生成了应用程序对象,如下:
// The one and only CSDIDemoApp object
CSDIDemoApp theApp;
2.4.3 InitInstance()函数
CWinApp类中的InitInstance()函数用于初始化实例。每次启动应用程序的一个实例时,WinMain
函数都要调用InitInstance()函数。
在工程“SDIDemo”的CSDIDemoApp类中,自动对InitInstance()函数进行了重载,代码如下:
BOOL CSDIDemoApp::InitInstance()
{
AfxEnableControlContainer();
#ifdef_AFXDLL
Enable3dControls(); // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif
// Change the registry key under which our settings are stored.
// TODO,You should modify this string to be something appropriate
// such as the name of your company or organization.
SetRegistryKey(_T("Local AppWizard-Generated Applications"));
LoadStdProfileSettings(); // Load standard INI file options (including MRU)
// Register the application's document templates,Document templates
// serve as the connection between documents,frame windows and views.
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CSDIDemoDoc),
RUNTIME_CLASS(CMainFrame),// main SDI frame window
41
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础
42
励志照亮人生 编程改变命运零基础学 Visual C++
RUNTIME_CLASS(CSDIDemoView));
AddDocTemplate(pDocTemplate);
// Parse command line for standard shell commands,DDE,file open
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
// Dispatch commands specified on the command line
if (!ProcessShellCommand(cmdInfo))
return FALSE;
// The one and only window has been initialized,so show and update it.
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
return TRUE;
}
从代码中可以看出,InitInstance()函数主要完成了下面几方面功能。
通过LoadStdProfileSettings()函数从注册表中获取一些标准的文件选项,包括最近打开的文件名称,在程序的“文件”菜单中列出。
构造文档模板类对象pDocTemplat,指明文档模板的文档类、框架窗口类和视图类。
调用ParseCommandLine函数进行程序窗口启动方式的分析处理。如果没有提供命令行参数
(打开文档的文件名),则新建一个新文档。
调用ShowWindow()和UpdateWindow()函数显示、更新窗口。其中,m_pMainWnd成员变量是一个CWnd类型的指针,它保存了应用程序框架窗口对象的指针。也就是说,是指向
CMainFrame对象的指针。
注意在CWinApp的派生类中,必须重载InitInstance()函数,因为CWinApp并不知道应用程序需要什么样的窗口,它可以是多文档窗口和单文档窗口,也可以是基于对话框的。
2.4.4 Run()函数
WinMain在初始化应用程序实例后,就调用CWinThread类的Run()函数来处理消息循环。在
Visual C++6.0安装目录下的“Microsoft Visual Studio\VC98\MFC\SRC”路径中的源文件
“THRDCORE.CPP”中会找到Run()函数的实现代码,如下:
int CWinThread::Run()
{
ASSERT_VALID(this);
// for tracking the idle time state
BOOL bIdle = TRUE;
LONG lIdleCount = 0;
// acquire and dispatch messages until a WM_QUIT message is received.
for (;;)
{
// phase1,check to see if we can do idle work
while (bIdle &&
!::PeekMessage(&m_msgCur,NULL,NULL,NULL,PM_NOREMOVE))
{
// call OnIdle while in bIdle state
if (!OnIdle(lIdleCount++))
bIdle = FALSE; // assume "no idle" state
}
// phase2,pump messages while available
do
{
// pump message,but quit on WM_QUIT
if (!PumpMessage())
return ExitInstance();
// reset "no idle" state after pumping "normal" message
if (IsIdleMessage(&m_msgCur))
{
bIdle = TRUE;
lIdleCount = 0;
}
} while (::PeekMessage(&m_msgCur,NULL,NULL,NULL,PM_NOREMOVE));
}
ASSERT(FALSE); // not reachable
}
该函数的主要结构是一个for循环,该循环在接收到一个WM_QUIT消息时退出。
Run()成员函数不断执行消息循环,检查消息队列中有没有消息。如果有消息,Run通过
PumpMessage()函数将其派遣,交由框架去处理,然后返回消息循环。如果没有消息,Run()将调用
OnIdle来做用户或框架可能需要在空闲时才做的工作。
如果既没有消息要处理,也没有空闲时的处理工作要做,
则应用程序将一直等待,直到有事件发生。当应用程序结束时,Run()将调用ExitInstance,结束消息循环。
Run()函数的消息循环可表示为如图2.7所示。
至此,就了解了MFC程序的整个运行机制,实际上与Win32 SDK程序是一致的。它同样也需要经过设计窗口类(MFC程序中已经预定义了一些窗口类,可以直接使用)、注册窗口类、创建窗口、显示并更新窗口和消息循环几个过程。
首先利用全局应用程序对象theApp启动应用程序。正是产生了这个全局对象,基类CWinApp中的this指针才能指向这个对象。
调用全局应用程序对象的构造函数,从而就会先调用其基类CWinApp的构造函数。后者完成应用程序的一些初始化工作,并将应用程序对象的指针保存起来。
进入WinMain函数。在AfxWinMain函数中可以获取子类CSDIDemoApp的指针,利用此指针调用虚函数InitInstance。由该函数完成应用程序的一些初始化工作,包括窗口类的注册、创建、
窗口的显示和更新。
43
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础图2.7 Run成员函数的消息循环有可用的消息吗?
有可用的消息吗?
还有空闲任务吗?
等待消息获取、分发消息
Yes
No
Yes
Yes
No
No
空闲处理
OnIdle()
44
励志照亮人生 编程改变命运零基础学 Visual C++
进入消息循环。MFC应用程序实际上是采用消息映射机制来处理各种消息的。当收到
WM_QUIT消息时,退出消息循环,程序结束。
2.4.5 MFC的消息映射
Windows程序中的消息处理是在WinProc函数中,通过Switch结构实现的。但当处理的消息比较多时,Switch-Case结构将变得分支很多,影响程序的可读性。而在MFC中,则采用消息映射的结构进行结构化消息处理。
进行MFC消息处理,程序员要做的就是为每一个要处理的消息提供一个消息处理函数,然后系统通过MFC提供的一套消息映射系统来调用相应的消息处理函数。
说明消息映射就是消息与消息处理函数一对一的联系。
MFC的消息映射采用消息映射宏的方式,把消息和消息处理函数一一对应起来。在MFC的框架结构下,可以进行消息处理的类的头文件里面都会含有DECLARE_MESSAGE_MAP()宏,这里主要进行消息映射和消息处理函数的声明。可以进行消息处理的类的实现文件里一般都含有如下的结构:
BEGIN_MESSAGE_MAP(CInheritClass,CBaseClass)
//{{AFX_MSG_MAP(CInheritClass)
ON_MESSAGE(message1,memberFxn1)
ON_MESSAGE(message2,memberFxn2)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
其中,CInheritClass为具有消息循环的类的名字,CBaseClass是CInheritClass的父类。在中间的
ON_MESSAGE宏就是消息映射宏。
为简化消息处理,MFC采用默认的消息映射和消息处理函数。如对消息标识符
WM_LBUTTONDOWN完整的消息映射应该如下:
ON_MESSAGE(WM_LBUTTONDOWN,Function1)
而MFC采用的更简捷的映射方式是:
ON_WM_LBUTTONDO()
MFC还为默认的消息映射预定义了消息处理函数:
OnLButtonDown(UINT nFlags,CPoint point)
假如要处理消息完成自己的任务,则要在派生类中重写这些函数。
2.4.6 MFC消息分类
MFC把消息分为3大类:窗口消息、控件通知消息和命令消息。
1,窗口消息当创建窗口、绘制窗口、移动窗口、销毁窗口,以及使用键盘、鼠标等与操作窗口有关的动作时,
产生的消息均属于窗口消息。
45
励志照亮人生 编程改变命运第3 章Windows编程与MFC基础窗口消息由MFC的窗口类(CWnd)对象来处理,即这类消息处理函数一般是CWnd类的成员函数,有默认的窗口处理函数。
典型的窗口消息、消息映射宏和默认的消息处理函数如表2.4所示。
说明若CWnd派生类没有重载窗口消息处理函数,则消息映射机制会转由其基类处理
(最终是CWnd类)。
2,控件通知消息控件是一个Windows的一个子窗口(如对话框中的按钮、编辑框等),控件通知消息是指在事件发生时,由控件或其他类型的子窗口发送到父窗口的消息。它通知父窗口,该控件接受了某操作,为父窗口进一步控制子窗口提供了条件。
控件消息一般由按钮(BN_)、编辑框(EN_)、组合框(CBN_)和列表框(LBN_)等产生,其消息映射宏为在消息名前加上ON_即可。举例如下:
ON_BN_CLICKED (按钮ID,响应函数)
ON_CBN_DBCLK (组合框ID,响应函数)
ON_EN_SETFOCUS (组合框ID,响应函数)
ON_LBN_DBCLK (列表框ID,响应函数)
分别表示选择各个控件后,产生的消息由其后面定义的函数进行处理。
3,命令消息命令消息一般与处理用户的请求有关,主要是来自菜单、工具栏和加速键的通知消息。从
CCmdTarget派生的类(如文档、文档模板、应用程序对象、窗口和视图等)都能处理命令消息。
可以使用MFC ClassWizard(类向导),建立消息映射和消息处理函数的框架,消息和函数都由
MFC默认的命名方式命名。
命令消息使用WM_COMMAND宏定义对其进行映射响应,格式如下:
ON_COMMAND(命令ID,响应函数)
举例如下:
ON_COMMAND (IDM_FILENEW,OnFileNew) //"新建"菜单命令
ON_COMMAND (IDM_FILEOPEN,OnFileOpen) //"打开"菜单命令对于命令消息,MFC应用程序框架会通过消息映射机制,按一定的搜索顺序在各个CCmdTarget
类(命令处理类)的派生类中查找对应消息处理函数。
说明所有由用户定义的命令消息也由ON_COMMAND定义消息映射关系。
表2.4 窗口消息、消息映射宏及其处理函数窗口消息消息映射宏默认处理函数
WM_CHAR ON_WM_CHAR OnChar
WM_ CLOSE ON_WM_ CLOSE OnClose
WM_CREATE ON_WM_CREATE OnCreate
WM_LBUTTONDOWN ON_WM_LBUTTONDOWN OnLButtonDown
WM_ MOUSEMOVE ON_ WM_MOUSEMOVE OnMouseMove