第6章 程序的生死因果
这一部分应该是第一章某些章节的扩展,难怪当时看第一章觉得解释不够详细,原来侯捷把大头放在这一章了。前面看到第一章时,还特意找了份关于SDK程序的材料来看,估计现在看这一章会轻松一点。
需要什么函数库?
1.Windows C Runtime函数库(如LIBC.LIB,MSVCRT.LIB,MSVCRTD.LIB)
2.DLL Import函数库(如GDI32.LIB,USER32.LIB,KERNEL32.LIB)
3.MFC函数库(AFX函数库),即MFC这个appliction framework的本体,可以静态
链接(如MFC42.DLL,MFC42D.DLL),也可以动态链接(用对应的MFC import函数库
代替,如MFC42.LIB,MFC42D.LIB)
以上工作,可以由IDE自动完成。
需要什么头文件
如果是SDK程序,只要载入WINDOWS.H就可以了;
如果是MFC程序,则还需要其他一些头文件:
StdAfx.h:用来作为预编译头文件,其内只是载入其他的MFC头文件。用VC自动生成
的程序就能看到该文件,下面提到的几个头文件就在此文件中included
进来。
AfxWin.h:每个MFC程序必须载入此头文件(出现在上面那个头文件中),该文件声
明了所有的MFC类。AfxWin.h内含Afx.h,后者又载入AfxVer_.h,后者又
载入Windows.h,即Windows.h这个SDK函数必须载入的头文件在MFC中由
AfxWin.h间接载入。
AfxExt.h:凡使用工具栏,状态栏的程序必须载入。
AfxDlgs.h:使用通用对话框的程序必须载入。
AfxColl.h:凡使用Collections classes的程序必须载入。
AfxRes.h :MFC的rc文件必须载入此文件。标准资源的ID都有默认值,都定义于此
如:
#define ID_FILE_NEW 0xE100
#define ID_FILE_OPEN 0xE101
… …
这些菜单项都有文字说明(出现在状态栏),文字说明在定作框架程序
时才把说明文字加到程序的RC文件中。
为什么需要预编译头文件(Precompiled Header)?
一个程序在编写过程中需要经常编译,但Windows程序的标准头文件非常巨大,且内容不变,如果每次都要重新编译要浪费很多时间,而且也没有必要。预编译可以将头文件第一次编译后的结果存起来,以后就可以直接从磁盘取出来用。
简化的MFC程序结构
MFC将传统Win32程序中所调用的WinAPI函数很好地封装进不同的类中,甚至连SDK中几大基本函数都被封装进去。此部分就是通过一个简单的win32程序来看看MFC程序的基本结构。
一个SDK程序,其主体在与WinMain和WndProc函数,而这两部分在所有的程序中其框架都是不变的,于是,MFC把这两个具有相当固定行为的函数封装至两个类中:
WinMain内部操作被封装在CWinApp中,CWinApp代表程序本体;
WndProc内部操作被封装在CFrameWnd中,CFrameWnd代表一个窗口。
当然,每个程序中WinMain和WndProc部分的操作不可能完全相同,所以我们必须以上面两个类为基础,派生出自己的类,并改写一部分成员函数。
下面我们具体的看看这两个类:
CWinApp—-取代WinMain的地位
从CWinApp派生出来的对象被称为application object,即为程序的本体。一个程序的本体包含了很多内容,如如与程序有关而与窗口无关的数据或者动作,命令行参数,三InitApplication 和InitInstance操作等都包含在内,即CWinApp必须有以上的成员变量以及成员函数。
SDK中要做的都由CWinApp中的三个成员函数完成:
virtual BOOL InitApplication();
virtual BOOL InitInstance();
virtual int Run();
这三块由一个类似于WinMain的函数来驱动。
CFrameWnd—-取代WndProc的地位
传统的SDK程序主要是用一个swith-case语句来处理窗口收到的消息;MFC程序有新的做法,如在这里的Hello程序中,CMyFrameWnd处理两个消息,具有如下的声明:
public:
afx_msg void OnPain();
afx_msg void OnAbout();
DECLARE_MESSAGE_MAP() //该宏用来实现消息与处理函数的关联
… …
Application object—-引爆器
在每个MFC程序中,都有且只有一个application object,如在HELLO.CPP中,有如下声明:
CMyWinApp theApp;
当程序运行到此处时,会调用CMyWinApp的构造函数,由于我们没有定义该够函数,所以调用其父类的构造函数,以用来完成一些初始化工作和配置工作,如置CWinApp一些成员变量为NULL,指定m_pCurrentWinApp为this,等等。
隐晦不明的WinMain
在全局变量theApp配置好后,才开始进入WinMain函数,但是该函数并没有出现在我们的自己撰写的代码中。事实上,这部分代码是由连接器直接加到我们的应用程序中去的。
在一个名为_tWinMain的函数中,调用了AfxWinMain函数。AfxWinMain函数定义于WinMain.cpp中,其主要任务是:
int AfxWinMain(….){
CWinApp* pApp=AfxGetApp();
AfxWinInit(….); //CWinApp构造后的第一个操作—-初始化
pApp->InitApplication(); //a
pApp->InitInstance(); //b
nReturnCode=pApp->Run(); //c
AfxWinTerm();
return nReturnCode;
}
注意a,b,c处,继承,虚拟,对象指针等知识的应用。
以下分别对AfxWinMain内几个函数作介绍:
1.AfxWinInit—-AFX内部初始化操作
AfxWinInit是CWinApp构造函数之后的第一个操作,主要步骤:
1.取到当前的全局变量theApp地址;
2.对该全局变量初始化(该全局变量在刚构造出来时,基本上是初始化为NULL了,而
此处赋予了具体的值);
值得注意的是,以前版本的VC中,WinMain一开始就调用AfxWinInit,注册四个窗口类,而现在这四个窗口类不在AfxWinInit中完成。
2.CWinApp::InitApplication
pApp->InitApplication,虽然pApp是CWinApp类,但实际调用的是CMyWinApp::InitApplication,但又因为,CMyWinApp继承自CWinApp时,通常不需要该改写InitApplication函数,故最终仍调用CWinApp::InitApplication()。
InitApplication中所作操作都是为了MFC的内部管理而作的。
3. CMyWinApp::InitInstance
pApp->InitInstance(),最终调用的是CMyWinApp::InitInstance(),因为CWinApp中该函数是个空函数,应用程序一定要改写该函数,即此部分代码要我们写好(如Hello.cpp中)。
通常,CWinApp中的InitApplication和Run都不需要改写,而InitInstance一定要改写。
这一部分有以下几步:创建窗口,显示窗口,更新窗口。
3.1 CFrameWnd::Create产生主窗口,并先注册窗口类
CMyWinApp::InitInstance一开始new了一个CMyFrameWnd对象,从而引发相应的构造函数,其中主要是调用了CFrameWnd的成员函数Create(),来获得一个CMyFrameWnd对象,用作主窗口。
Create函数有八个参数,分别代表WNDCLASS窗口类,窗口标题,窗口风格,区域,父窗口,菜单等等。
Create函数执行时,先看需不需要装载菜单,然后调用CreateEx()函数,此处实际调用的是父类CWnd::CreateEx()
在Wincore.cpp中,有CWnd::CreateEx()的定义,主要是:
1. 设置一些属性值;
2. PreCreateWindow();该函数是虚函数,主要完成注册窗口类操作,其中用到了一些
宏展开,会根据窗口的具体类型(如MDI窗口,SDI窗口等)注册具体的窗口类(如
AfxMDIFrame42d,AfxFrameOrView42等),当然,Application Framework会把窗口类
名自动转换成Afx:x:y:z:w的格式,成为独一无二的类名称,x,y,z,w分别表示窗口风
格的hex值,窗口鼠标的hex值,背景颜色的hex值和窗口图标的hex值。
3. 其他操作(具体情况还不知道,得等到看到后面时才会明白)
3.2 窗口的显示与更新
BOOL CMyWinApp::InitInstance(){
m_pMainWnd = new CMyFrameWnd();
m_pMainWnd -> ShowWindow(m_nCmdShow);
m_pMainWnd -> UpdateWindow();
return TRUE;
}
4. CWinApp::Run—-程序生命的活水源头
pApp->Run();因为Run是虚拟函数,而且用户通常不去重载该函数,所以调用的是CWinApp::Run(),该函数中,主要是调用CWinThread::Run();
CWinThread::Run跟前面的SDK的消息循环不同在于,它除了处理消息循环外,还要处理Idle time:
for (;;;){//该循环只有在碰到WM_QUIT消息时才退出
处理空闲时间;
处理消息循环;
}
消息循环部分是由PumpMessage()来完成,不停地通过TranslateMessage(),和DispatchMessage()来转换,分发消息。
DispatchMessage后,消息被发送到相应的窗口函数,当然,窗口函数已经由MFC提供了—-在我们注册窗口类时,已经指定了窗口函数:
wndcls.lpfnWndProc = DefWindowProc;
消息映射机制
消息映射时,MFC采用默认的规则,即看到一条ON_WM_CLOSE的宏,就会相应的将WM_CLOSE与OnClose处理函数关联起来。故只要声明了消息处理函数:
class CMyFrameWnd :…{
public:
afx_msg void OnPaint();//声明消息处理函数
……
}
并定义了对应的宏,即在BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间加入ON_WM_PAINT即可。
MFC来龙去脉总整理
程序的诞生
1.在进入AfxWinMain函数前,产生全局的application object,内存获得配置,设立初
值;
2.AfxWinMain中执行AfxWinInit,后者又调用AfxInitThread,把消息队列
尽量加大到96
3.AfxWinMain执行InitApplication。这是CWinApp的虚函数,通常我们不改写它;
4.AfxWinMain执行InitInstance。也是CwinApp的虚函数,但我们必须改写它;
5.CMyWinApp::InitInstance ‘new’了一个CMyFrameWnd对象;
6.CMyFrameWnd构造函数调用Create,产生主窗口。我们在Create参数中指定的窗口类
是NULL,于是MFC根据窗口种类,自行为我们注册一个名为“AfxFrameOrView42d”的
窗口类;
7.回到InitInstance中继续执行ShowWindow,显示窗口;
8.执行UpadteWindow,于是发出WM_PAINT;
9.回到AfxWinMain,执行Run,进入消息循环。
程序的运行
1.程序获得WM_PAINT消息(由CWinApp::Run中的::GetMessage循环获得;
2.WM_PAINT经由::DispatchMessage送到窗口函数CWnd::DefWindowProc中;
3.CWnd::DefWindowProc将消息传递到消息映射表格;
4.传递过程中发现有相符项目,于是调用对应的函数。此函数是利用
BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间的宏设立起来的;
5.标准消息的处理程序亦有标准命名,如WM_PAINT必然由OnPaint处理。
程序的死亡
1.用户单击file/close,于是发出WM_CLOSE;
2.CMyFrameWnd通常不设置处理该消息的程序,于是交给默认的处理程序;
3.默认函数对WM_CLOSE的处理方式是调用::DestroyWindow,并因而发出WM_DESTROY;
4.默认的WM_DESTROY处理方式是调用::PostQuitMessage,因而发出WM_QUIT;
5.CWinApp::Run收到WM_QUIT后悔结束内部的消息循汉,。然后调用ExitInstance,这
是CWinApp的一个虚拟函数,用户可以改写;
6.最后回到AfxWinMain,执行AfxWinTerm,结束程序。
Callback函数
Hello中使用了LineDDA(),这是一个Windows API函数:
void WINAPI LineDDA(int,int,int,int,LINEDDAPROC,LPARAM);
前面是个参数指定了起始点的横纵座标,该函数将根据Bresenham算法计算对应直线所通过的每个屏幕像素座标,每计算出一个,就调用第五个参数所指定的callback函数,这个callback函数的类型必须是:
typedef void (CALLBACK* LINEDDAPROC) (int,int,LPARAM);
此处,LineDDA不属于任何一个MFC类,所以调用时必须加上全局操作符(::):
void CMyFrameWnd::OnPaint(){
CPaintDC dc(this);
CRect rect;
GetClientRect(rect);
dc.SetTextAlign(TA_BOTTOM|TA_CENTER);
::LineDDA(rect.right/2,0,rect.right/2,rect.bottom/2,
(LINEDDAPROC) LineDDACallback,(LPARAM) (LPVOID) &dc);
};
其中的LineDDACallback是我们准备的callback函数,必须在类中有声明(按照LineDDA所要求的callback函数类型),并要把它声明为“static”,以把C++编译器加诸于普通类成员函数的一个隐藏参数this去掉。
凡是由你设计而由Windows系统调用的函数,称为callback函数,这些函数都有一定类型,以配合Windows的调用。
一方面,某些Windows API函数会以callback函数作为其参数之一,如LineDDA函数;
另一方面,callback函数带有所要求的类型,以便于上述API函数调用。
但由于C++给类中的成员函数(包括普通的callback函数)都加了一个隐藏的this参数,而Windows callback函数并没有要求有该参数,所以,我们在把某个函数用作callback函数时,必须告诉编译器不要加入this参数,可以:
1.用全局函数作callback函数;(C语言的做法)
2.用static的成员函数作callback函数。(更符合OO)
空闲时间的处理:OnIdle
CwinThread在处理消息循环前,还处理了空闲时间:发现当前状态处于空闲(系统还是程序),就调用OnIdle()函数。该函数做一些MFC本身的维护工作。
如果想在自己的MFC程序中处理空闲时间,可以改写CWinApp派生类的OnIdle函数。
Dialog和Control
当我们点“file/about”菜单项时,命令消息WM_COMMAND(IDM_ABOUT)被送到OnAbout函数。我们现在OnAbout函数中产生一个CDialog对象,about:
CDialog about (“AboutBox”,this);//前者指定资源模板(在RC文件中)
//后者指定about的主人
然后运行对话框:
about.DoModal();
通用对话框
Open对话框,获得文件路径:
char szFilters[]="Text files(*.txt)|*.txt|All files(*.*)|*.*||"
CFileDialog opendlg (
TRUE, //说明对话框是“Open”而非“Save as”
"txt", //默认扩展名
"*.txt", //默认文件名
OFN_FILEMUSTEXIST | OFN_HIDEREADONLY, //指定文件的属性
szFilters,//可以选择的文件类型
this) //父窗口
if (opendlg.DoModal() == IDOK){
filename = opendlg.GetPathName();
}