心铃讲座之VC篇(5)
vc++6.0中AppWizard
生成的应用程序程序框架分析(上)
l
标准头文件StdAfx.h
在第六讲中我们已经提到了StdAfx.h,它是构成预编译头文件的主体,现在我们就来仔细看看其中的一些语句。在FileView的“Header Files”下面找到StdAfx.h并打开它,我们可以看到几行注释下面的前六行都是编译指令,其中头两条指令用来确保StdAfx.h在同一个模块中只被包含一次,其它头文件中也有类似的语句,接下来三条指令请大家自己在MSDN库中查一下“_MSC_VER”及“#pragma once”的含义,第六条指令定义了一个符号“VC_EXTRALEAN”,有了它之后,VC编译器在处理Windows的头文件时会剔除掉一些极少用到的函数和数据类型,从而节省编译时间,如果我们今后“不幸”用到了这些内容,就会出现编译错误,此时只要把StdAfx.h中的这条指令注释掉就可以了。
接着往下看,StdAfx.h包含了其它一些头文件,根据注释我们知道afxwin.h中定义了MFC类库的核心部件和标准部件,并且包含了Windows头文件,afxext.h中定义了MFC类库的扩展部件,afxcview.h中定义了各种视类,afxdisp.h中定义了MFC类库中的自动化类,afxdtctl.h定义了支持IE4引入的通用控件的MFC类,而afxcmn.h则定义了支持Windows通用控件的MFC类。这些头文件内部的情况又如何呢?我们现在不必知道,如果将来需要的话,可以查看MFC类库的头文件和源代码。
接下来的一条语句引起了我们的注意:
//{{AFX_INSERT_LOCATION}}
它虽然是注释,但格式却很奇怪,再下一句的注释中解释道:VC将会在该句之前插入额外的声明,其实,这种带有两对大括号的特殊格式的注释语句在程序框架的其它源文件中还有很多,是专供VC的ClassWizard(类向导)等工具使用的。这些工具能够自动向程序框架中加入代码(或删除代码),例如添加消息处理函数等,但是面对着程序员已经编辑过的源代码时,它们利用什么方法来确定把代码加入到哪儿呢(即如何在源代码中定位)?VC的设计人员采用了这种特殊注释的方法(利用注释不会对编译连接产生任何影响,心铃认为该方法非常巧妙),即从两个左向大括号开始,加上一句VC能理解的语句,最后由两个右向大括号结束,如果左右向大括号在一行中同时出现(例如StdAfx.h中间的这一条语句),那么在此行之前定义了一个插入点,如果左右向大括号出现在两行注释中(例如ScheduleView.h中的“//{{AFX_VIRTUAL(CScheduleView)”和“//}}AFX_VIRTUAL”),那么在这两行中间定义了一个插入范围,其中的代码都以灰色显示,一般情况下我们都不要去修改这些代码,否则可能会导致ClassWizard的某些功能无法正常工作。
l
应用程序类CScheduleApp
在AppWizard生成的五个类中,CScheduleApp代表的是我们的应用程序,它的基类CWinApp在普遍意义上代表了Windows应用程序。为什么要用一个类来表示应用程序呢?其实这里面包含了面向对象程序设计的思想,我们可以把每一个程序都看做一个对象,把多数Windows应用程序的一些共性提取出来就形成了CWinApp,而Schedule既是普通Windows应用程序,又具有自己的特点,所以我们从CWinApp派生出一个CScheduleApp。
应用程序类(有时也称为App类)将负责完成MFC应用程序的一些例行初始化工作,另外,App类的基类是CWinThread,因此它也要负责管理程序主线程的运行,不过这项工作用不着我们去操心。在Schedule.cpp中,我们发现有下面的一条语句:
CScheduleApp theApp;
这条语句为CScheduleApp定义了一个(也只能有这一个)全局对象theApp,我们知道,Windows应用程序一般从WinMain()函数执行起(心铃:这种说法其实不是非常严格,应该说在编写Windows应用程序时,可由程序员自己控制的部分是由WinMain()函数开始的,在此之前还有一些与编程语言有关的初始化代码),而这个全局对象在WinMain()函数之前就初始化完毕了,在进入MFC版本的WinMain()函数之后,theApp很快就获得了管理权,进行Windows程序的一些例行初始化工作,我们能见到的一部分就是CScheduleApp::InitInstance()函数。
InitInstance()函数首先调用AfxEnableControlContainer()函数,让程序能够使用ActiveX控件,接着又调用Enable3dControls()或Enable3dControlsStatic()函数让程序能够使用具有3D效果的控件。对SetRegistryKey()和LoadStdProfileSettings()的调用将从注册表中读取与本程序有关的信息,这些信息在第一次运行本程序时自动建立,以后在需要时会自动更新。
接下来的几条语句就比较重要了,同时也比较复杂,我们现在只需要知道其功能是为程序定义一种文档模板类型就行了,文档模板把文档类CScheduleDoc、主框架类CMainFrame和视类CScheduleView联系在一起,在下一讲中心铃还要专门介绍它。
ParseCommandLine()和ProcessShellCommand()两个函数分别用于分析和处理命令行参数,如果想让程序支持自定义的命令行参数,我们可以在此添加自己的处理代码。最后两条语句把程序唯一的主窗口显示出来,并进行更新,注释告诉我们主窗口已经创建好了,也就是说m_pMainWnd已经是一个有效的CWnd对象的指针,那么这个对象是在何处创建的呢?有兴趣的朋友可以从ProcessShellCommand()函数的源代码开始追踪下去,看看这到底是怎么一回事(心铃:哈哈,保证大家都会头昏脑胀)。
CScheduleApp类的另外一个成员函数OnAppAbout()实际上是一个消息处理函数,用来处理程序的帮助菜单中的“关于”命令,这个函数简单地创建了一个CAboutDlg对象,并将其作为模态对话框显示出来。CAboutDlg是一个非常简单的对话框类,它对应的对话框资源的ID为ID_ABOUTBOX,这一点可以从CAboutDlg类的定义中看出来,其中有一条枚举语句:
enum { IDD = IDD_ABOUTBOX };
在ResourceView中打开ID为ID_ABOUTBOX的对话框资源(目前也是唯一的一个对话框),就可看到如图7-1所示的关于对话框。我们将在以后详细讨论设计和使用对话框的方法。
我们在源代码中经常会看到“//TODO:”这样的注释,它们很有用,通常用来提示我们为了完成某些工作应该怎样做。
l
主框架类CMainFrame
CMainFrame代表的是程序的主框架窗口,说得简单一点,我们在第六章看到的图6-5中除了白色部分的视图(View)外,程序主窗口的其它部分都归CMainFrame管理。视图实际上是主框架窗口的子窗口,它的大小正好等于主框架窗口的客户区的大小,如果我们想象一下把视图挪走,那么就会留下一个灰色背景的客户区,它也是主框架窗口的一部分,仅观察单文档程序可能不好理解这一点,但观察多文档程序就能发现这一事实。
前面讲过,单文档程序的主框架窗口是在调用ProcessShellCommand()函数时创建的,但是在主框架窗口被创建之前,CMainFrame::PreCreateWindow()函数将被自动调用,我们可以在这个函数中更改主框架窗口的风格,或者对窗口类的一些属性进行修改,此时窗口句柄还不可用。
当Windows通过WIN32函数CreateWindowEx()接收到创建主框架窗口的请求时,它会在系统内部为窗口分配资源,并进行一些设置工作,此时窗口句柄就可以使用了,当Windows从CreateWindowEx()函数返回之前,它向程序发送WM_CREATE消息,让程序完成一些必要的初始化工作,CMainFrame::OnCreate()函数就是WM_CREATE消息的处理函数,这个函数首先调用了基类的处理函数,让基类完成初始化工作,然后为主框架窗口创建工具栏和状态条。
CMainFrame的两个成员变量m_wndToolBar和m_wndStatusBar分别对应着程序主窗口中的工具栏和状态条。工具栏的创建方式很直接,CToolBar::LoadToolBar()函数负责装载工具栏资源,而CToolBar::CreateEx()函数将完成创建工作。状态条的使用方法则比较有意思,它需要一个数组来定义状态条内各个区域的类型,这个数组就是indicators。最后,CMainFrame::OnCreate()把工具栏设置成可停靠类型。
CMainFrame还有两个成员函数AssertValid()和Dump(),它们只在调试版本中存在,我们可以利用它们来验证类的某些成员变量的值是否有效,以及向Output窗口中输出对象的内部状态。很多从CObject派生出来的类都重载了这两个函数,以帮助程序员调试程序,如果有的朋友想了解它们的具体含义及详细用法,请自己查看MSDN库。
l
视类CScheduleView
视类是MFC程序的一项重头戏,因为它负责显示输出信息的工作,用户将直接与它和主框架类进行交互。缺省情况下,程序的视类都从CView直接继承而来,并在重载的OnDraw()函数中完成大部分输出工作,而日程安排程序的视类CScheduleView的上一级基类为CListView,这个类在CView的基础之上又增加了很多功能,以后我们可以看到,用了CListView之后,即使不通过OnDraw()函数也能输出信息。
CScheduleView也有一个PreCreateWindow()函数,其功能与CMainFrame:: PreCreateWindow()很相似,我们可以在这里修改视图的类属性和风格。CScheduleView没有OnCreate()函数,但如果需要的话,我们可以利用ClassWizard来添加。当视窗创建完毕,在第一次显示之前,OnInitialUpdate()函数将被调用,该函数一般用来完成与显示内容有关的一次性初始化工作,以后我们将使用它来初始化列表视图。
当程序接收到WM_PAINT消息时,OnDraw()函数将被调用,以实现绘制视图的功能,由于该函数涉及到设备上下文DC(Device Context)的概念和GDI函数的使用方法,因此有一定的难度,初学者往往对此感到比较头痛,好在由于使用了CListView,日程安排程序可以直接跳过这些概念。
CScheduleView的OnPreparePrinting()、OnBeginPrinting()和OnEndPrinting()三个成员函数用于实现打印功能,日程安排程序暂时不支持打印,因此我们现在不必知道如何使用它们。
CScheduleView还有一个GetDocument()成员函数,其功能是返回与视类对象相关联的文档对象。如果我们在生成Schedule的第一步时不选择“支持文档/视结构”,那么就不会有这个函数。
l
文档类CScheduleDoc
本讲要介绍的最后一个类是CScheduleDoc,其实在支持文档/视结构的MFC程序中,文档才处于中心地位,视类和主框架类都是为文档类服务的。
目前CScheduleDoc的成员函数还不多,OnNewDocument()在新创建一个文档时调用,可以在这里添加与每个文档相关的初始化代码。Serialize()也称为序列化函数,其作用是读取或存储文件的内容,心铃要提醒大家注意一点,我们现在经常挂在嘴边的“文档”并不是指存放在硬盘中的文件,而应把它理解成文件中的数据在内存中的表现形式,因此文档的概念要比文件抽象一些,Serialize()函数就是联系这两种概念的纽带。
对Schedule的单个文件或类的介绍就到这里了,希望大家在今后的编程工作中能够不断地反过头来对程序框架源代码进行分析,搞清楚一些现在暂时还无法理解的东西。在本讲中已经涉及到了很多MFC类,如果大家有时间的话,不妨自己研究一下CWnd、CFrameWnd、CView、CListView、CDC、CWinApp、CDocument、CSingleDocTemplate、CArchive等类,能不能弄懂都没有关系,关键是加深印象。在下一讲中,心铃将主要介绍文档/视结构,也就是讨论一下视类、文档类和主框架类之间的一些内在联系。
名词释疑:
类向导ClassWizard
:与AppWizard一样,ClassWizard也是VC的一个重要工具,它能帮助我们管理和维护程序中的C++类,我们可以利用它来编写新的类和虚拟函数的原型,或者添加新的消息处理函数,ClassWizard的功能异常强大,熟练掌握它后可以大大加快程序开发的速度。
线程(Thread
):所谓线程是指程序的一个指令执行序列,WIN32平台支持多线程程序,允许一个程序中同时存在多条执行序列,也即多个线程,在单CPU系统中,系统把CPU的时间片按照调度算法分配给各个线程,因此各线程实际上是分时执行的,在多CPU的Windows NT系统中,同一个程序的不同线程可以被分配到不同的CPU上去执行。由于一个程序的各线程是在相同的地址空间内运行的,因此涉及到了如何共同使用内存,如何通信等问题,这样便需要处理各线程之间的同步问题,这是多线程编程中的一个难点。