2023年11月30日发(作者:)
第6章 创建和使用对话框
第8章 文档和视图
精讲
MFC应用程序的核心是文档/视图结构。在前面章节的学习中,已经接触了不少文档/视图结构的应
用程序,本章将详细分析其结构和原理,并进一步学习使用复杂的文档结构、构造更加丰富的视图。
8.1 文档/视图概述
使用MFC的AppWizard可以创建三种类型的应用程序:
(1) 单文档界面的应用程序(SDI:Single Document Interface)
(2) 多文档界面的应用程序(MDI:Multiple Documents Interface)
(3) 基于对话框的应用程序(Dialog based)
基于对话框的应用程序框架非常简单,由应用程序类、对话框类构成。通过应用程序类的
InitInstance()函数,构造一个模式对话框对象;调用DoModal()函数,让Windows对话框处理程序象通
常情况一样接受和分配消息;用户退出对话框后,程序也就结束了。
我们已经知道SDI应用程序由应用程序类(CWinApp)、框架窗口类(CFrameWnd)、文档类
(CDocument)、视图类(CView)和文档模板类(CSingleDocTemplate)共同作用。MDI应用程序与SDI
应用程序的主要差别在于:MDI有CMDIFrameWnd和CMDIChildWnd两个框架窗口类,前一个派生
CMainFrame类,负责菜单等界面元素的主框架窗口管理;后一个派生CChildFrame类,负责相应的文
档及其视图的子框架维护。而SDI由框架窗口类CFrameWnd 派生CMainFrame类。
一个文档可以有多个视图,但一个视图只能对应一个确定的文档。因此,MDI应用程序需要解决的
问题是多个文档的数据管理方法。在MDI应用程序中,文档模板只支持主窗口。每打开一个新文档时,
都调用文档类的成员函数OnNewDocument(),建立一个由CMDIChildWnd派生的新的MDI子窗口,在
子窗口中保存已打开的文档,所有这些细节都由MFC库来处理。
8.1.1 文档和视图的关系
文档/视图结构的最大特点就是:把数据操作和数据表示分离开来,与数据库管理系统提供的数据库
与视图的关系一致。图8-1说明了文档及其视图之间的关系。所有对数据的修改由文档对象来完成,用
视图调用这个对象的方法来访问和更新数据。
1
VC++6简明教程
文档
数据
视图
1556558798
22323373109
34542265143
4674478187
52421190203
ABCD
600
A
400
200
0
12345
B
C
D
A
1
2
3
4
5
图8-1文档和视图的关系
在MFC应用程序框架中,文档和视图的关系主要体现在:文档类和视图类对象的相互作用和相互
访问上。如前面章节所述,关系图示如下:
CView::OnInitialUpdate()
CView::GetDocument()
文档类 视图类
CDocument::UpdateAllVie
ws()
CView::Invalidate()
CView::InvalidateRect()
图8-2 文档和视图的相互访问
对图示中的函数介绍如下:
1. CView::GetDocument( )
返回文档类的指针,通过该指针在视图类中访问并更新文档数据。
2. CDocument::UpdateAllViews(CView *pSender,LPARAM lHint=0,Cobject *pHint=NULL)
该函数通知与文档相连的所有或部分视图,更新窗口内容。在MFC应用程序框架中,由于文档和
视图的一对多关系,当用户在一个视图中修改文档后,本视图将发生改变,相应地,与文档相连的其他
视图也应与更新后的文档内容保持一致。这时,本视图便可以调用该函数向其他视图窗口发出
WM_PAINT消息,通知它们更新。
pSender为修改文档并发出更新通知的视图的指针,当pSender为NULL时通知与文档相连的所有
视图更新,当pSender不为NULL时,通知除pSender代表的视图以外的与文档相连的所有视图更新,
得到更新的视图类将调用该类的OnUpdate()函数。
lHint和pHint是关于视图更新内容的提示。lHint可自定义含义,pHint是一个CObject指针,能够
表示MFC所有的对象,它规定了视图需要更新的区域。经常使用来规定部分刷新区域,从而避免全部
区域刷新带来的屏幕闪动。
3. CView::OnUpdate( )
2
第6章 创建和使用对话框
该函数是一个虚函数,当应用程序调用CDocument::UpdateAllViews()函数时,应用程序框架会相应
地调用它。还可以在应用程序视图类的派生类中,直接调用OnUpdate()函数,OnUpdate()函数访问文档,
得到文档的数据,然后更新视图的数据成员或控制来反应这些变化。另外,OnUpdate()函数可以使视图
的一部分无效,导致视图的OnDraw()使用文档数据来在窗口中重画。
4. CView::OnInitialUpdate( )
该函数也是一个虚函数,当应用程序启动时,或者用户执行菜单命令File->New或File->Open时,
就会调用这个虚函数。如果要初始化视图对象,可在视图类的派生类中重载该函数,添加初始化代码。
当应用程序启动时,先调用OnCreate()函数,接着就调用OnInitialUpdate()函数。
在通常情况下,视图通过GetDocument()函数获得指向文档对象的指针,并通过该指针访问文档类
的成员函数或数据成员获取数据。视图把数据显示于计算机屏幕上,用户通过与视图的交互来查看数据
并对数据进行修改,然后视图通过相关联的文档类的成员函数将经过修改的数据传递给文档对象。文档
对象获得修改过的数据之后,对其进行必要的修改,最后保存到永久介质(如磁盘文件)中。
开发一个简单的文档/视图应用程序(不需要多个视图支持),只需要按下面的步骤操作:
(1) 在派生的文档类添加公共类型的数据成员,这些数据成员是基本数据存储器。
(2) 在派生的视图类中重载OnInitialUpdate()函数,更新视图,反映当前的文档的数据。在文档数据
初始化之后或从磁盘上读入之后,应用程序框架会自动调用该函数。
(3) 在派生的视图类中,使用GetDocument()来访问文档对象,使窗口处理程序、命令消息处理程序
和OnDraw()函数直接读取和更新文档的数据成员。
程序运行时事件发生的次序如图8-3所示,假设工程名为My。
应用程序启动
构造CMyDocument对象
创建View窗口
调用CMyView::OnInitialUpdate()
初始化View对象
使View窗口无效
调用CMyView::OnDraw()
用户编辑数据
CMyView中函数更新CMyDocument的数据成员
退出应用程序
删除CMyView对象
删除CMyDocument对象
图8-3 简单文档/视图应用程序事件发生次序
构造CMyView对象
调用CMyView::OnCreate(如映射)
调用CMyDocument::OnNewDocument()
8.1.2文档模板类的功能
在MFC中,文档类、与文档类相关联的视图类以及为视图类提供显示的框架窗口都是由文档模板
3
VC++6简明教程
类创建的。每一种文档类型都有一种文档模板与之相对应,文档模板负责创建和管理该文档类型的所有
文档。由AppWizard创建的SDI应用程序的5个基类之间的关系如图8-4所示。
应用程序对象
CWinApp
创建
文档模板
CSingleDocTemplate
创建 创建
创建
文档对象 视图对象 框架窗口
CDocument CView CFrameWnd
图8-4 SDI应用程序的5个基类之间的关系模型
打开SDI应用程序中应用程序类的InitInstance()成员函数,可以看到如程序清单8-1所示的一段代
码,其作用是为程序定义一种文档模板类型。
程序清单8-1:动态分配SDI文档模板对象
BOOL CEXSDIApp::InitInstance()
{
。。。。。。
// 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(CEXSDIDoc ),
RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CEXSDIView);
AddDocTemplate(pDocTemplate);
。。。 。。。
}
这段代码首先创建了CSingleDocTemplate的一个对象实例,该对象是适用于单文档程序的文档模
板,它的构造函数带有四个参数:第一个参数是一个资源ID,标识了用于给框架提供加速键表、菜单和
图标的资源。这个ID应用程序使用的框架窗口对它使用的每种资源类型采用同一个资源ID,通常是
IDR_MAINFRAME;构造函数的后三个参数都使用了RUNTIME_CLASS宏, RUNTIME_CLASS接受
一个类名作为参数,返回指向一个CRuntimeClass结构的指针,从MSDN库中了解到,该结构可以用来
在运行过程中获取一个对象的类及其基类的信息,并可以动态创建类的对象实例。CRuntimeClass结构
实际上是RUNTIME_CLASS的参数中指定的类的一个静态成员。
CSingleDocTemplate对象被AddDocTemplate()函数加入到应用程序对象内部的一个文档模板链表
中,多数单文档程序都只支持一种文档类型,因此链表中一般只存有一个文档模板对象。写字板程序是
一个能处理多种文档类型的单文档程序,在这种情况下链表中就不止一个文档模板对象了。
4
第6章 创建和使用对话框
如果没有在命令行中指定要打开的文件名,应用程序类的ProcessShellCommand()函数就会自动生成
一个新文档。在生成新文档时,应用程序类将检查文档模板链表,如果链表中只有一个文档模板对象,
那么就直接调用该对象的OpenDocumentFile()函数,如果链表中有多个文档模板对象,那么它会弹出一
个对话框,让用户选择一种类型,然后调用所选文档模板对象的OpenDocumentFile()函数,这个函数将
负责创建文档对象和主框架对象。
MDI应用程序的布局要比SDI应用程序的布局稍微复杂一些,图8-5展示了MDI应用程序结构的
基本布局。
应用程序对象
CWinApp
创建
文档模板
创建
创建
CMultiDocument
文档对象 框架窗口
CDocument CMDIChildWnd
创建
视图对象
CView
图8-5 MDI应用程序的5个基类之间的关系模型
如图中所示,MDI应用程序仍使用一个主框架来容纳菜单、工具栏和状态栏。然而,在MDI应用
程序中,CMainFrame类是从MFC的CMDIFrameWnd类的,而不是从CMainFrame类派生的。
CMDIFrameWnd类有着与CMainFrame类同样的可视特征,但它还实现了Windows对MDI应用程序所
要求的MDI框架协议。
图中示例框架窗口是CMDIChildWnd的实例,提供了Windows MDI应用程序客户区中的子窗口,
用以存放视图,被包装的视图可以是任何类型,并且可以用于应用程序当前管理的任何打开的文档。
同样打开一个MDI应用程序中应用程序类的InitInstance()函数,都可以找到类似程序清单8-2所示
的源代码。与SDI应用程序有所区别的是MDI应用程序生成的一个CMultiDocTemplate类的文档模板对
象实例,第一个参数资源ID为IDR_EXMDITYPE。
程序清单8-2:动态分配MDI文档模板对象
BOOL CEXMDIApp::InitInstance()
{
。。。。。。
// Register the application's document templates. Document templates
// serve as the connection between documents, frame windows and views.
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_EXMDITYPE,
RUNTIME_CLASS(CEXMDIDoc),
RUNTIME_CLASS(CChildFrame), // custom MDI child frame
RUNTIME_CLASS(CEXMDIView));
AddDocTemplate(pDocTemplate);。。。 。。。}
5
VC++6简明教程
8.1.3编程实例
【例8-1】本例将设计一个简单的文本编辑器(不使用CEditView类),用以说明文档/视图的原理及应用。
在这个编辑器中,用户只能逐行输入字符,按回车键结束一行并换行,不支持字符的删除和插入,也没
有光标指示当前编辑位置。但是用户可以选择编辑器显示文本所使用的字体。
实验步骤:
1. 生成项目
使用AppWizard生成编辑器程序的框架,项目名为Exam8_1。在MFC AppWizard-step 1选择Single
Document;在MFC AppWizard 4 of 6的对话框中,单击Advanced按钮,弹出Advanced Option对话框,
如图8-6所示,该对话框设置文档/视图结构和主框架窗口的一些属性。
图8-6 Advanced Option 对话框
该对话框有两个标签页,一页是Document Template Strings,用于设置文档/视图结构的一些属性,
该项的值将与应用程序类中定义文档模板类对象的第一个参数IDR_MAINFRAME对应,它包括以下几
个文本框:
(1) File extersion
指定应用程序创建的文档所用的文件名的后缀。输入后缀名txt。
(2) File type ID
用于在Windows的注册数据库中标识应用程序的文档类型。
(3) Main frame caption
主框架窗口标题,默认情况下与项目名相一致。
(4) Doc type name
该文档类型名,指定与一个从CDocument派生的文档类相关的文档类型名。
(5) Filter name
用作“打开文件”、“保存文件”对话框中的过滤器。Visual Studio会自动根据输入的后缀名生成一
个过滤器:Exam8_1 文件(*.txt)。这样当在Open File对话框中选择Exam8_1 文件(*.txt)时,只有以txt
为后缀名的文件名显示在文件名列表中。
(6) File new name(short name)
用于指定在new对话框中使用的文档名。当应用程序支持多种文档类型时,选择File->New菜单项
会弹出一个对话框,应用程序所支持的所有文档类型供用户选择。选择一种文档类型后,自动创建相应
6
第6章 创建和使用对话框
类型的文档。
(7) File type name(long name)
用于指定当应用程序作为OLE Automation 服务器进使用的文档类型名,使用默认值。
另一标签页是Windows Styles,用于设置主框架窗口的一些属性,包括窗口是否使用最大化按钮,
窗口启动时是否最大化或最小化等。这里使用默认值。
单击Close按钮,关闭Advanced Options对话框。单击Finish按钮,生成应用程序框架。
2. 定义文档类的数据成员
对于一个文本编辑器,增加和删除一行字符是动态的,因此将使用MFC提供的集合类CStringList
来保存文本行信息。CStringList中每一个元素是CString类型的对象,代表一行字符。
另外还需要增加一个数据成员nLineNum用于指示当前编辑行行号。在文档类的头文件
CExam8_1Doc.h中加入以下粗体部分代码:
程序清单8-3:定义文档类的数据成员
class CExam8_1Doc : public CDocument
{
protected: // create from serialization only
CExam8_1Doc();
DECLARE_DYNCREATE(CEX8_1Doc)
// Attributes
public:
CStringList lines;
int nLineNum;
。。。。。。
}
3. 初始化文档类的数据成员
由于文档对象创建后,需要反复刷新而不是反复创建,因此初始化工作放在OnNewDocument()函数
中进行,而不是构造函数中进行,OnFileNew()函数会自动调用OnNewDocument()函数,代码如程序清
单8-4粗体部分所示。
程序清单8-4:初台化文档类的数据成员
BOOL CExam8_1Doc::OnNewDocument()
{ if (!CDocument::OnNewDocument())
return FALSE;
// TODO: add reinitialization code here
// (SDI documents will reuse this document)
nLineNum=0;
POSITION pos;
pos=dPosition();
while(pos!=NULL)
{
((CString)t(pos)).Empty();
}
All();
return TRUE;
7
VC++6简明教程
}
其中:pos指向链表当前元素。CStringList的成员函数GetHeadPosition()返回链表头指针。链表的
GetNext()函数以当前指针为参数,返回下一个元素指针,同时修改pos,使它指向下一个元素。使用类
型转换将GetNext()函数返回的元素指针转化为CString类型,然后调用CString::Empty()方法清除该行中
的所有字符。通过一个while循环,清除所有文本行的数据。CStringList的RemoveAll()函数清除链表中
的所有指针。
4. 重载DeleteContents()函数
在用File->Open菜单命令打开一个文档或关闭应用程序时,都需要清理文档对象中的数据。文档的
清理是在文档类的DeleteContents()虚函数中完成的。DeleteContents()函数的功能是删除文档数据,并确
信一个文档在使用前为空。通过ClassWizard重载DeleteContents()函数,然后添加代码如程序清单8-5
所示。
程序清单8-5:DeleteContents()函数删除文档
void CExam8_1Doc::DeleteContents()
{
// TODO: Add your specialized code here and/or call the base class
nLineNum=0;
POSITION pos;
pos=dPosition();
while(pos!=NULL)
{
((CString)t(pos)).Empty();
}
All();
}
5. 编写Serialize()函数代码
文档的序列化在Serialize()函数中进行。当用户执行File 菜单中的Save、Save As或Open命令时,
都会自动执行这一成员函数。AppWizard给出了该函数的框架,用户需要添加代码,如程序清单8-6粗
体部分所示。
程序清单8-6:重载Serialize()函数
void CExam8_1Doc::Serialize(CArchive& ar)
{
CString s("");
int nCount=0;
CString item("");
if (ing())
{
// 将文档对象中的数据存入文件
POSITION pos;
pos=dPosition();//pos指向字符串链表的头部
8
第6章 创建和使用对话框
if(pos==NULL)
return;
while(pos!=NULL)
{
item=t(pos);//读取一行,并使pos指向下一行
ar< (); } } else { // 将文件中的数据读入到文档对象 while(1) { try{ ar>>item;//读取文本行到item l(item);//将文本串加在字符串链表的尾部 nCount++;}//记录行数 catch(CArchiveException*e)//捕捉文件读取完毕异常 { e->Delete(); break; } } nLineNum=nCount;//修改字符串总行数 } } 6. 添加视图类数据成员 在CExam8_1View视图类添加三个数据成员:CFont类型指针pFont,表示当前所用的字体;int型 变量lHeight表示字体的高度,int型变量cWidth表示字体的宽度。添加后视图类定义如程序清单8-7粗 体部分所示。 程序清单8-7:添加视图类数据成员 class CExam8_1View : public CView { protected: // create from serialization only CExam8_1View(); DECLARE_DYNCREATE(CExam8_1View) 。。。。。。 // Implementation CFont *pFont; int lHeight; int cWidth; 。。。。。。 } 7. 视图类数据成员初始化 9 VC++6简明教程 视图类数据成员初始化一般在CView::OnInitialUpdate()函数中进行,这是由于当调用 CDocument::OnNewDocument()和CDocument::OnOpenDocument()函数时,应用程序都会自动执行 OnInitialUpdate()函数。 本例视图类的初始化操作主要是对编辑器所使用的字体初始化。OnInitialUpdate()函数是虚函数,要 通过ClassWizard重载,重载后添加初始化代码如程序清单8-8所示。 程序清单8-8:初始化视图类数据成员 void CExam8_1View::OnInitialUpdate() { CView::OnInitialUpdate(); // TODO: Add your specialized code here and/or call the base class CDC *pDC=GetDC(); pFont=new CFont(); if(!(pFont->CreateFont (0,0,0,0,FW_NORMAL,FALSE,FALSE,FALSE, ANSI_CHARSET,OUT_TT_PRECIS,CLIP_TT_ALWAYS, DEFAULT_QUALITY,DEFAULT_PITCH,"courier New"))) { pFont->CreateStockObject (SYSTEM_FONT); } CFont* oldFont=pDC->SelectObject(pFont); TEXTMETRIC tm; pDC->GetTextMetrics(&tm); lHeight=ht +rnalLeading; cWidth=harWidth ; pDC->SelectObject(oldFont); } OnInitialUpdate()首先调用GetDC()函数取得当前窗口的设备场境指针并存放在pDC中。然后创建视 图所用的字体,并调用SelectObject()函数将字体选入到设备场境中,同时保留原来字体到oldFont指针。 最后调用GetTextMetrics()函数获取TEXTMETRIC信息,该数据结构包括字体的宽度、高度、字的前后 空白等字段,初始化lHeight和cWidth变量。 8. 修改析构函数 在关闭视图时,需要修改视图的析构函数,删除所创建的字体对象pFont,实现代码如程序清单8-9 所示。 程序清单8-9:删除字体对象pFont CExam8_1View::~CExam8_1View() { if(pFont) delete pFont; } 9. 修改OnDraw()函数 在OnDraw()函数中实现使用pFont指针所表示的字体,显示编辑器中的文本。在OnDraw()函数中 需要首先访问文档对象,将字符串链表中的文本行逐行读出显示,添加的代码如程序清单8-10所示。 程序清单8-10:显示编辑器中的文本的OnDraw()函数 10 第6章 创建和使用对话框 void CExam8_1View::OnDraw(CDC* pDC) { CExam8_1Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: add draw code for native data here //选择字体 CFont *oldFont; oldFont=pDC->SelectObject(pFont); //计算字的高度和宽度 TEXTMETRIC tm; pDC->GetTextMetrics(&tm); lHeight=ht +rnalLeading; cWidth=harWidth ; int y=0;//设定纵坐标为0 POSITION pos; CString line; if(!(pos=pDoc->dPosition() )) return ; //循环输出各文本行 while(pos!=NULL) {line=pDoc->t(pos); pDC->TextOut(0,y,line,gth()); //更新y坐标,下一行文本行的位置 y+=lHeight; } //恢复原来DC所用的字体 pDC->SelectObject(pFont); } 10. 响应键盘输入 编辑器要不断接收用户的键盘输入,就必须处理键盘消息。每按一下字符,窗口就会接收到一个 WM_CHAR消息,该消息是在视图类中处理的,窗口接受到该消息后,把消息所包含的字符加入到当 前行的末尾,并马上把输入的内容在屏幕上显示出来。 首先用ClassWizard 在CExam8_1View视图类中生成WM_CHAR消息的函数OnChar(),然后打开 该函数进行编辑。修改后的OnChar()函数如程序清单8-11所示。 程序清单8-11:响应键盘输入消息处理函数OnChar() void CExam8_1View::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: Add your message handler code here and/or call default CExam8_1Doc* pDoc = GetDocument(); CClientDC dc(this); //选择当前字体 CFont *oldFont; oldFont=Object(pFont); 11 VC++6简明教程 CString line("");// 存放编辑器当前字符串 POSITION pos=NULL;//字符串链表位置指示 if(nChar=='r')//若是回车增加一行 { pDoc->nLineNum++; } else { //按行号返回字符串链表中的位置值 pos=pDoc->dex(pDoc->nLineNum); if(!pos) //没有找到该行号对应的行,因此是一个空行,加入到字符串链表中 {line+=(char)nChar; pDoc->l(CString(line)); } else { //当前文本还没有换行结束,将字符加入到行末 line=pDoc->(pos); line+=(char)nChar; pDoc->(pos,line); } TEXTMETRIC tm; tMetrics(&tm); // 将修改后的文本行显示到屏幕 t(0,(int)pDoc->nLineNum*ht,line,gth()); } pDoc->SetModifiedFlag(TRUE);// 设置文本修改标志 Object(oldFont); } 11. 实现字体选择 使用通用字体对话框实现字体的选择,分两步进行: (1) 在“查看”菜单中增加一个调用字体选择对话框的菜单。菜单名为Select&Font,菜单ID为 ID_SELECT_FONT,提示文字为select a font for current view。 (2) 使用ClassWizard为该菜单项在CExam8_1View视图类生成消息处理函数OnSelectFont(),并添 加代码实现字体选择,代码见程序清单8-12所示: 程序清单8-12:实现字体选择的菜单处理函数 void CExam8_1View::OnSelectFont() { // TODO: Add your command handler code here CFontDialog dlg; if(l()==IDOK) { LOGFONT LF; rentFont(&LF);//获取当前字体信息 12 第6章 创建和使用对话框 pFont->DeleteObject();//建立新的字体 pFont->CreateFontIndirect(&LF); this->Invalidate(); UpdateWindow(); } } 在OnSelectFont()消息处理函数中,首先定义一个选择字体通用对话框,然后创建该对话框,并返 回所选的字体。字体对话框通过GetCurrentFont()函数返回字体信息。字体对象首先通过DeleteObject() 删除原来的字体对象,然后通过CreateFontIndirect()函数创建新字体。调用Invalidate()函数向视图发送 WM_PAINT消息,由于WM_PAINT消息级别比较低,不会立即被处理,因此调用UpdateWindow()强 制更新。 12. 编译运行程序 编译运行程序,弹出文本编辑器窗口。试着输入几行文本并存盘,建立新文件,打开存盘的文件, 执行“查看”菜单下的命令Select&Font,修改文本字体。运行效果如图8-7所示。 图8-7 Exam8_1运行效果 8.2 创建用户自定义类 观察MFC库的层次结构,绝大多数类都是从CObject根类派生出来。当一个类从CObject类派生时, 它继承了许多重要的特性。通过学习这些特性,可以构建用户自己的用于组织文档类数据的类。 8.2.1 使用CObject类 CObject类提供了3个重要的服务: 1. 持续性 从CObject类直接或间接继承下来的类能维护一个对象持续性。即将内存中的对象数据保存到持久 介质,或反过来,从持久介质中读取数据,然后重建对象。例如,CStudent类是从CObject类的派生类, 13 VC++6简明教程 那么CStudent从CObject自然地继承得到了一个担任数据持续化的虚函数Serialize(CArchive &ar),只要 调用该函数,CStudent对象便可完成持续化,即完成CStudent对象的文件存取。 CArchive对象ar保存了以读或写的方式打开的文件的相关信息,如文件句柄等。Serialize()函数的 主要逻辑已封闭在CObject对象中,但是对什么数据进行操作,将依赖于各类对该虚函数的重载。 2. 动态性 前面已介绍过文档类、视图类、窗口框架类的动态创建。但从CObject类派生的类只具有一般意 义上的动态性,例如,CStudent是CObject的派生类,那么CStudent自然从CObject得到一个成员函数 IsKindOf(CRunTimeClass*ptr),只要调用该函数,CStudent对象就可以判断该类指针所指是不是CStudent 类对象。 3. 诊断性 CObject类提供了把对象状态转储给调试机制(如Debug输出窗口)的能力。CObject类有两种存 储方式,通过CDumpContext类或者AssertValid()成员函数。Dump()成员函数能够把类的内部数据输出 到CDumpContext类对象afxDump中,而afxDump是与调试输出窗口绑定的;AssertValid()成员函数能 够自动维护数据的有效性。 例如,在任一个SDI应用程序中,文档类都是CObject的派生类,自然继承了Dump()和AssertValid() 函数,如程序清单8-13所示。 程序清单8-13:CObject派生类继承的诊断性成员函数 //////////////////////////////////////////////////////////////////////////// // CEXSDIDoc diagnostics #ifdef _DEBUG void CEXSDIDoc::AssertValid() const { CDocument::AssertValid(); } void CEXSDIDoc::Dump(CDumpContext& dc) const { CDocument::Dump(dc); } #endif //_DEBUG 如果该类CEXSDIDoc具有数据成员: CString m_Name; int m_Age 那么,上述的两个成员函数可以修改如程序清单8-14粗体部分所示。 程序清单8-14:修改后的诊断性成员函数 //////////////////////////////////////////////////////////////////////////// // CEXSDIDoc diagnostics #ifdef _DEBUG void CEXSDIDoc::AssertValid() const 14 第6章 创建和使用对话框 { ASSERT(!m_y());//不能为空 ASSERT(!(m_Age<0));//不能小于0 CDocument::AssertValid(); } void CEXSDIDoc::Dump(CDumpContext& dc) const { dc<<"m_Name:"< dc<<"m_Age:"< CDocument::Dump(dc); } #endif //_DEBUG CEXSDIDoc::CEXSDIDoc() { m_Name=”Mary”; m_Age=17 } CEXSDIDoc::~CEXSDIDoc() { #ifdef _DEBUG Dump(afxDump); #endif //_DEBUG } 在调试状态下,AssertValid()函数将判断上述两个成员是否有效,如果无效,将终止程序运行。Dump() 函数能将上述两个成员的当前值输出到Debug窗口。使用析构函数来调用文档的Dump()函数,在Output 窗口的Debug标签中观看运行结果,如图8-8所示。 图8-8 Dump函数运行结果 8.2.2 支持持续性和动态性的宏 从CObject类直接或间接派生的类,虽然支持持续性和动态性,但是这种支持是有限的。另外还有 三对宏用来弥补上述缺陷。以CStudent类为例。 1. DECLARE_DYNAMIC和IMPLMENT_DYNAMIC 第一对宏支持动态性服务,主要支持IsKindOf服务,告知该类以及基类的类名及大小,得到该类及 15 VC++6简明教程 其基类的CRunTimeClass结构信息。它们在类中的位置: (1) 在类声明文件Student.h中。 Class CStudent : public CObject {。。。。。。 protected : CStudent(); DECLARE_DYNAMIC(CStudent) 。。。。。。 }; (2) 在类的实现文件中: IMPLEMENT_DYNAMIC (CStudent , CObject) 2. DECLARE_DYNCREATE和INPLMENT_DYNCREATE 第二对宏是在第一对宏的动态性服务的基础上,提供了对对象的动态创建的支持。在类中又增加了 一个CreateObject()函数,返回动态创建的该类对象。以窗口框架类CMainFrame为例,它们的位置为: (1) 在CMainFrame的类的定义文件(MainFrame.h)中 class CMainFrame : public CFrameWnd {。。。。。。 protected: // create from serialization only CMainFrame(); DECLARE_DYNCREATE(CMainFrame) 。。。。。。 }; (2) 在类的实现文件()中 IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd) 3. DECLARE_SERIAL和IMPLMENT_SERIAL 第三对宏的作用是提供对象的持续性,它在第二对宏的基础上,增加了支持使用>>操作符以重建该 类对象的能力,下面以CStudent类为例,在该类加入一对DECLARE_SERIAL和IMPLMENT_SERIAL 宏,使CStudent具有对象持续能力。 (1) 在类的定义文件Student.h中添加宏,如程序清单8-15粗体部分所示。 程序清单8-15:使用DECLARE_SERIAL宏 Class CStudent : public CObject { 。。。。。。 protected : CStudent(); DECLARE_SERIAL(&CStudent) 。。。。。。 public: //Overrides //ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CStudent) public: 16 第6章 创建和使用对话框 Virtual void Serialize(CArchive& ar); //}}AFX_VIRTUAL 。。。。。。 private: int m_Grade; CString m_Name; }; (2) 在类的实现文件中添加宏如程序清单8-16所示。 程序清单8-16:使用IMPLEMENT_SERIAL宏 IMPLEMENT_SERIAL(CStudent , CObject,1) void CStudent::Serialize(CArchive& ar) { if (ing()) { ar< } else { ar>>m_Name>>m_Grade; } } 综上所述,三对宏对CObject派生类的支持程序表示为不同的层次,在类的定义中使用不同的宏, 可以使类获得不同层次的动态性和持续性的支持。我们通常需要使用一个或多个从CObject类派生的类 对象来组成一个文档对象内的复杂的数据,这样的派生类需要第三对类的支持,即获得对象的持续性支 持。 8.2.3 编程实例 【例8-2】在应用程序的文档类使用一个嵌入的CStudent类对象,CStudent类保存学生记录,包括CString 型的姓名和int型的成绩组成,视图类的基类使用了CFormView,CFromView视图使用编辑控件显示学 生记录。运行效果如图8-9所示。 17 VC++6简明教程 图8-9 Exam8_2运行效果图 实验步骤: 1. 生成项目 使用AppWizard生成编辑器程序的框架,项目名为Exam8_2。在MFC AppWizard-step 1选择Single Document,使用的语言选择中文(中国);在MFC AppWizard 6 of 6的对话框中选择视图类的基类为 CFormView;单击Finish按钮,生成应用程序框架。 2. 设计CStudent类 学生类设计要达到以下要求: (1) 从CObject类公有继承。 (2) 有支持动态性的宏的支持。 (3) 有默认构造函数和用于初始化的构造函数。 (4) 重载运算符=,==,!=,实现对象赋值,比较的功能。 (5) 重载Dump()函数调试时输出对象的成员变量。 3. 在Exam8_2工程中创建类CStudent (1) 单击工具栏按钮“New Text File”,编辑生成CStudent类的头文件student.h,保存文件到工程文 件夹中。 (2) 再次单击工具栏按钮“New Text File”,编辑生成CStudent类的实现文件,保存文件 到工程文件夹中。 (3) 选择菜单命令Project->Add to Project->Files,在弹出的文件对话框中将student.h和 两个文件添加到工程Exam8_2。 CStudent类的student.h和文件实现代码如程序清单8-17所示。 程序清单8-17:CStudent类清单 //student.h #ifndef _INSIDE_VISUAL_CPP_STUDENT #define _INSIDE_VISUAL_CPP_STUDENT class CStudent:public CObject { DECLARE_DYNAMIC(CStudent)//支持动态性的宏 public:CString m_Name; 18 第6章 创建和使用对话框 int m_Grade; CStudent() //默认构造函数 {m_Grade=0;} //支持初始化的构造函数 CStudent(const char*Name,int Grade):m_Name(Name) {m_Grade=Grade; } //支持对象拷贝的构造函数 CStudent(const CStudent&s):m_Name(s.m_Name) {//copy constructor m_Grade=s.m_Grade; } const CStudent&operator=(const CStudent& s)//对象赋值运算 {m_Name=s.m_Name; m_Grade=s.m_Grade; return *this; } BOOL operator==(const CStudent&s)const//等于运算 {if ((m_Name==s.m_Name)&&(m_Grade==s.m_Grade)) return true; else return false; } BOOL operator!=(const CStudent&s)const//不等于运算 {return !(*this==s); } #ifdef _DEBUG void Dump(CDumpContext&dc)const; #endif //_DEBUG }; #endif //_INSIDE_VISUAL_CPP_STUDENT // #include "stdafx.h" VC++6简明教程 4. 设计对话框模板 (1) 打开IDD_Exam8_2_FORM对话框编辑器,按图8-10所示增加控件,属性设置如表8-1所示。 (2) 打开ClassWizard,按表8-1为编辑控件IDC_NAME和IDC_GRADE设置连接变量。m_Grade 变量值的有效范围在0-100。 图8-10 IDD_ Exam8_2_FORM对话框模板 表8-1 IDD_ Exam8_2_FORM对话框控件的属性和连接的变量 控件 ID 连接变量 类别 数据类型 姓名编辑控件 IDC_NAME m_Name Value CString 分数编辑控件 IDC_GRADE m_Grade Value int 录入按钮 IDC_ENTER 5. 使用菜单编辑器修改菜单项 删除“编辑”菜单下的所有菜单项,增加菜单项Clear All,ID使用应用框架分配的默认常量 ID_EDIT_CLEAR_ALL,菜单提示会自动出现。 6. 使用ClassWizard在CExam8_2View视图类添加消息处理程序 表8-2在视图类映射消息处理 对象ID 消 息 IDC_ENTER BN_CLICKED ID_EDIT_CLEAR_ALL COMMAND ID_EDIT_CLEAR_ALL UPDATE_COMMAND_UI 7. 为文档类添加数据成员 在ClassView中,使用文档类的快捷菜单命令Add Member Variable在CExam8_2Doc文档类添加一 个CStudent类的对象m_Student,访问类型为Public,ClassView会自动在文档类的开始处加入相应的 #include语句。 8. 修改CExam8_2Doc文档类构造函数来初始化学生对象 程序清单8-18:文档类构造函数初始化学生对象 CExam8_2Doc::CExam8_2Doc():m_Student("default Value",0) { // TODO: add one-time construction code here TRACE("documet object constructedn"); } 20 第6章 创建和使用对话框 9. 修改文档的Dump()函数,并使用析构函数调用文档的Dump()函数 程序清单8-19:文档类的Dump()函数和析构函数 void CExam8_2Doc::Dump(CDumpContext& dc) const { CDocument::Dump(dc); dc<<"n"< } CExam8_2Doc::~CExam8_2Doc() { #ifdef _DEBUG Dump(afxDump); #endif //_DEBUG } 10. 编辑视图类,初始化视图 在ClassView中,执行文档类快捷菜单命令Add Member Function,在CExam8_2View视图类中添加 成员函数UpdateControlsFromDoc(),访问类型为Protected。该函数的作用是把文档中的数据传输到视图 类的数据成员m_Name和m_Grade,然后传送到窗体中的编辑控件中去。在InitialUpdate()函数中调用 该函数。应用程序启动的时候会自动调用InitialUpdate()函数,实现视图的初始化。 函数代码如程序清单8-20粗体部分所示。 程序清单8-20: 视图类中实现视图初始化的函数 void CExam8_2View::OnInitialUpdate() { CFormView::OnInitialUpdate(); GetParentFrame()->RecalcLayout(); ResizeParentToFit(); UpdateControlsFromDoc(); } void CExam8_2View::UpdateControlsFromDoc() {//called from OnInitialUpdate and OnEditClearAll CExam8_2Doc*pDoc=GetDocument(); //把文档中的数据传输到视图类的数据成员 m_Grade=pDoc->m_Student.m_Grade; m_Name=pDoc->m_Student.m_Name; UpdateData(false);// 传送到窗体中的编辑控件 } 11. 实现消息处理函数 21 VC++6简明教程 OnEnter()函数的作用是按照视图中控件编辑框中的数据修改文档数据;OnEditClearAll()函数的作用 是清空文档数据,并更新视图;OnUpdateEditClearAll()函数的作用使学生记录为空时,使ClearAll菜单 项无效。 各函数实现代码如程序清单8-21所示。 程序清单8-21: 实现消息处理函数 void CExam8_2View::OnEnter() { // TODO: Add your control notification handler code here CExam8_2Doc*pDoc=GetDocument(); UpdateData(true); pDoc->m_Student.m_Grade=m_Grade; pDoc->m_Student.m_Name=m_Name; } void CExam8_2View::OnEditClearAll() { // TODO: Add your command handler code here GetDocument()->m_Student=CStudent(); UpdateControlsFromDoc(); } void CExam8_2View::OnUpdateEditClearAll(CCmdUI* pCmdUI) { // TODO: Add your command update UI handler code here pCmdUI->Enable(GetDocument()->m_Student!=CStudent()); } 12. 编译并测试程序 从调试程序Go(F5)运行程序,键入一个姓名和成绩,然后单击Enter按钮,退出应用程序时,在 Output窗口的Debug标签页上可以找到与图8-11所示类似的内容。 图8-11 Debug窗口显示Dump()函数输出信息 8.3 文档读写 使用AppWizard生成的每一个SDI或MDI应用程序框架中都有一个File菜单,其中包含New、Open、 22 第6章 创建和使用对话框 Save和Save As命令。前面已经学会使用这些命令进行简单结构的数据的存储操作,在本节将详细介绍 其工作原理,并组织复杂结构的文档数据进行存储操作。 8.3.1 文档序列化原理 已知文档类的数据的保存和读取,是通过文档类的虚函数Serialize()函数来进行的,其参数CArchive 对象ar是指向要操作的文件的档案类对象。但是对于应用程序框架来说,CArchive对象ar是怎么得到 的?什么时候调用Serialize()函数?文档类中什么类型的数据可以借助Searialize机制完成文档的序列 化?这是本节要讨论的问题。 从程序清单8-16中给出了一个Serialize()函数的基本形式。由于CArchive对象ar保存了打开文件 的信息以及读或写等方面的信息,当往文件中写数据时,执行ing()分支,操作运算符<<能把数 据存到ar指定的文件中去;当从文件读取数据时,则执行另一分支,操作运算符>>能从文件读数据, 并初始化相关成员变量。 Serialize函数的参数ar是一个CArchive类型的对象,它包含一个CFile类型的文件指针。CArchive 对象为读写CFile对象中的可串行化数据提供了一种类型安全的缓冲机制。通常CFile代表一个磁盘文 件;但它也可以是一个内存文件(CMemFile对象)或剪贴板。一个给定的CArchive对象只能读数据或 写数据,而不能同时读写数据。当保存数据到CArchive对象中时,CArchive把它放在一个缓冲区中。 直至缓冲区满,才把数据写入它所包含的文件指针指向的CFile对象中。同样的,当从CArchive对象读 数据时,CArchive对象从文件中读取到缓冲区,然后再从缓冲区读入到可串行化的对象中。这种缓冲机 制减少了访问物理大生产的次数,从而提高了应用程序的性能。在应用程序框架中,ar对象是由应用程 序框架来完成初始化的。 当选择菜单命令File->New时,应用程序框架调用OnNewDocument()函数建立新的文档。 OnNewDocument()函数将首先调用DeleteContents()清空文档类数据成员,然后,再调用 SetModifiedFlag(FALSE)将文档修改标志清除。 当选择菜单命令File->Open时,应用程序框架调用OnOpenDocument()函数打开已有的文档。 OnOpenDocument()函数将首先调用GetFile()函数获得给定文件的CFile指针,再调用DeleteContents()函 数清空文档类的数据成员,然后把CFile指针构造 CArchive对象交给Serialize()函数完成读文件重建文 档对象的工作,将调用SetModifiedFlag(FALSE)将文档修改标志清除。 当选择菜单命令File->Save或者File->Save AS时,应用程序框架将调用OnSaveDocument()函数保 存指定文件名的文档。OnSaveDocument()将首先询问文件的名字,调用GetFile()函数获得给定文件的 CFile指针,然后把CFile指针构造的CArchive对象交给Serialize()函数完成写文件的工作,将调用 SetModifiedFlag(FALSE)将文档修改标志清除。 8.3.2 序列化与数据类型 Serialize()函数支持哪些数据类型序列化取决于CArchive类的定义。程序清单8-22定义了能重载运 算符<<和>>的简单数据类型和结构变量。 程序清单8-22:<<和>>运算符可序列化的类型说明 //简单数据类型 CArchive& operator<<(BYTE by); CArchive& operator<<(WORD w); 23 VC++6简明教程 CArchive& operator<<(LONG l); CArchive& operator<<(DWORD dw); CArchive& operator<<(float f); CArchive& operator<<(double d); CArchive& operator<<(int i); CArchive& operator<<(short w); CArchive& operator<<(char ch); CArchive& operator<<(unsigned u); CArchive& operator>>(BYTE by); CArchive& operator>> (WORD w); CArchive& operator>> (LONG l); CArchive& operator>> (DWORD dw); CArchive& operator>> (float f); CArchive& operator>> (double d); CArchive& operator>> (int i); CArchive& operator>> (short w); CArchive& operator>> (char ch); CArchive& operator>> (unsigned u); //结构变量 CArchive& AFXAPI operator<< (CArchive& ar, SIZE size); CArchive& AFXAPI operator<< (CArchive& ar, POINT point); CArchive& AFXAPI operator<< (CArchive& ar, const RECT&rect); CArchive& AFXAPI operator>> (CArchive& ar, SIZE size); CArchive& AFXAPI operator>> (CArchive& ar, POINT point); CArchive& AFXAPI operator>> (CArchive& ar, const RECT&rect); 假如一个不在上述列表的类对象需要序列化,那么只需要把该类的数据成员分解成上述列表中的基 本类型,一个一个地序列化。 8.3.3使用复杂的文档数据 在开发的大多数应用程序中,需要处理的文档数据显然要比用简单数据类型的几个变量表示要复杂 的多。可以使用许多不同的方法来管理一个文档对象内复杂的数据类型,一般来说,可以使用一系列从 CObject类派生的类,每个派生类都可以存储复杂的数据对象,而文档则使用一个标准的或自定义的集 合类在文档类嵌入对象。 MFC提供的集合类较多,主要分为三种形式: 第6章 创建和使用对话框 CDWordArray,CPtrArray,CStringArray,CTypedPtrArray。 3. Map 映射又称为字典。是一个把关键字与对象值关联起来的集合。例如:CMap,CTypedPtrMap。 在8.1.3节的编程实例中,我们使用了集合类CStringList定义了一个字符串列表对象来存放CString 类型的文本串,完成对文本编辑器中文本行的操作。该类常用的成员函数如表8-3所示。 表8-3 CStringList类的常用成员函数 方 法 说 明 GetHeadPostion() 返回链表头指针 GetNext() 返回下一元素指针 GetAt() 返回位置指针所指向的位置的指针 RemoveAll() 清除链表中的所有指针(这些指针所指向的元素已经被清除) RemoveAt() 清除位置指针所指向的位置的指针 AddTail() 在尾部增加元素 AddHead() 在头部增加元素 在一个链表的元素是通过指针进行连接的,所以在元素之间进行移动,不是通过索引值,则是通过 GetNext()和GetPrev()函数进行的,返回指向上一个或下一个元素的位置,这就需要POSITION类型的配 合,POSITION变量是元素在列表中的位置的内部表示方式,例如,下面代码段实现了链表lines的遍历 置空。 POSITION pos; pos=dPosition(); while(pos!=NULL) { ((CString)t(pos)).Empty(); } All(); 在复杂的情况下,在文档内使用集合类对象存放由CObject类派生的类对象。这一集合类对象通常 被声明为protected类型,文档类还需要提供方法的成员函数,以让必须访问文档的数据的视图以及其他 对象能够达到目的。例如,提供Get类成员函数获取集合类对象中存放的元素,提供Add类成员函数, 向集合类对象中插入元素等等。 在应用程序内使用MFC集合类的另一个优势是,集合类支持序列化。例如,为读取和保存存储在 CObject对象内并通过集合类对象引用的文档数据,所需做的工作就是构造文档的Serializ()e函数,如下 所示,m_ObArray是一个CObArray类的对象。 void CStuDoc::Serialize(CArchive& ar) { if (ing()) { // TODO: add storing code here m_ize(ar); } else { // TODO: add loading code here 25 VC++6简明教程 m_ize(ar); } } 存储在m_ObArray中的CObject派生类需要重载实现自己的Serialize()成员函数。下一节的编程实 例学生记录录入的应用程序说明了如何使用集合类对象与Cobject派生类对象共同作用表示文档数据。 8.3.4 编程实例 【例8-3】本例实现一个简单的学生记录录入的应用程序。文档类使用CObArray类管理若干个CStudent 类的学生记录,每一个学生记录由CString型的姓名和int型的成绩组成。视图类由CFormView派生的。 运行效果如图8-12所示。工具栏上提供了学生记录的基本操作按钮,工具栏按钮只有在适当的时候才 能启用。Clear按钮清除视图中文本框的内容。 图8-12 Exam8_3的运行效果 实验步骤: 1. 生成项目 使用AppWizard生成编辑器程序的框架,项目名为Exam8_3。在MFC AppWizard-step 1选择Single Document。使用的语言选择英文(美国);在MFC AppWizard 4 of 6的对话框中,单击Advanced按钮, 弹出Advanced Option对话框,该对话框设置File extension为dat;MFC AppWizard 6 of 6的对话框中选 择视图类的基类为CFormView。单击Finish按钮,生成应用程序框架。 2. 在Exam8_3工程中创建CStudent类 CStudent学生类设计在8.2.3的例8-2基础上作修改要达到以下要求: (1) 有支持序列化的宏的支持。 (2) 重载Serialize()函数实现序列化。 CStudent类的student.h和文件实现代码如程序清单8-23所示,将生成的student.h和 文件加入到工程Exam8_3。 26 第6章 创建和使用对话框 程序清单8-23: CStudent类清单(需使用IMPLEMENT_SERIAL宏) //student.h #ifndef _INSIDE_VISUAL_CPP_STUDENT #define _INSIDE_VISUAL_CPP_STUDENT class CStudent:public CObject { DECLARE_SERIAL(CStudent)//支持序列化的宏声明 public: CString m_Name; //数据成员 int m_Grade; CStudent() //默认构造函数 {m_Grade=0;} //支持初始化的构造函数 CStudent(const char*Name,int Grade):m_Name(Name) {m_Grade=Grade; } //支持对象拷贝的构造函数 CStudent(const CStudent&s):m_Name(s.m_Name) {//copy constructor m_Grade=s.m_Grade; } const CStudent&operator=(const CStudent& s)//对象赋值运算 {m_Name=s.m_Name; m_Grade=s.m_Grade; return *this; } BOOL operator==(const CStudent&s)const//等于运算 {if ((m_Name==s.m_Name)&&(m_Grade==s.m_Grade)) return true; else return false; } BOOL operator!=(const CStudent&s)const//不等于运算 {return !(*this==s); } virtual void Serialize(CArchive &ar);//重载序列化函数 #ifdef _DEBUG void Dump(CDumpContext&dc)const; #endif //_DEBUG }; VC++6简明教程 // #include "stdafx.h" #include "student.h" IMPLEMENT_SERIAL(CStudent,CObject,1)//支持序列化的宏声明 #ifdef _DEBUG void CStudent::Dump(CDumpContext& dc)const {CObject::Dump(dc); dc<<"m_Name="< } #endif //_DEBUG void CStudent::Serialize(CArchive &ar)//序列化函数实现 { if (ing()) { // TODO: add storing code here ar< } else { // TODO: add loading code here ar>>m_Grade>>m_Name; } } 3. 设计对话框模板 (1) 打开IDD_Exam8_3_FORM对话框编辑器,按图8-13所示增加控件,属性设置如表8-4所示。 (2) 打开ClassWizard,按表8-4为编辑控件IDC_NAME和IDC_GRADE设置连接变量。m_Grade 变量值的有效范围在0-100。 图8-13 IDD_Exam8_3_FORM对话框模板 表8-4 IDD_Exam8_3_FORM对话框控件的属性和连接的变量 控件 连接变量 类别 数据类型 Name编辑控件 IDC_NAME m_Name Value CString ID 28 第6章 创建和使用对话框 Grade编辑控件 IDC_GRADE m_Grade Value int Clear按钮 IDC_CLEAR 4. 设计文档类 (1) 在ClassView中,执行文档类快捷菜单命令Add Member Variable添加集合类数据成员CObArray 类对象m_ObArray,访问类型为Protected。 (2) 在ClassView中,执行文档类快捷菜单命令Add Member Function添加供视图类访问m_ObArray 类的成员函数: void delStuAll():删除所有的学生记录。 void DelStu(int index):删除index所指向的学生记录。 int GetStuNumber():获取学生记录总数。 CStudent* GetStu(int index):返回index所指向的学生记录的指针。 void AddStu(CString Name,int Grade):在数组的最后增加一条学生记录。 (3) 在文档类Exam8_3Doc.h文件中加入语句:#include “student.h” (4) 实现添加的函数,代码如程序清单8-24所示。这一组函数的实现是使用CObArray类的成员函 数实现对对象的数组的访问, 程序清单8-24:供视图访问对象数组的方法 void CExam8_3Doc::delStuAll() { int index; CStudent*ps; index=this->GetStuNumber();//获取数据中元素个数 while(index--)//循环删除学生记录 { ps=GetStu(index-1); delete ps; } m_All(); //清空对象数据 SetModifiedFlag(); //设置修改标志 this->UpdateAllViews(NULL);//更新视图 } void CExam8_3Doc::DelStu(int index) { CStudent*ps=GetStu(index);//获取要删除的学生对象指针 delete ps; //删除学生对象 m_At(index);//在对象数组中删除元素(指针) SetModifiedFlag(); //设置修改标志 //更新视图,将调用视图类的OnUpdate函数 this->UpdateAllViews(NULL); } int CExam8_3Doc::GetStuNumber() { 29 VC++6简明教程 //调用GetSize函数返回数组中的元素个数 return m_e(); } CStudent* CExam8_3Doc::GetStu(int index) { //GetUpperBound返回最大索引值 if(index<0||index>m_erBound() ) return 0; //调用GetAt函数返回一个学生对象指针 return (CStudent*)m_(index); } void CExam8_3Doc::AddStu(CString Name, int Grade) { //动态创建一个学生对象,并使用构造函数赋值 CStudent*pStu=new CStudent(Name,Grade); //调用Add函数增加一个学生对象到对象数组 m_(pStu); SetModifiedFlag(); //设置修改标志 } (5) 使用ClassWizard重载CExam8_3Doc文档类成员函数DeleteContent()函数。 执行菜单命令File->New、File->Open都会自动调用DeleteContent()函数,需要在这个函数中清空当 前的文档数据,实现代码如程序清单8-25所示。 程序清单8-25:重载DeleteContents()函数 void CExam8_3Doc::DeleteContents() { // TODO: Add your specialized code here and/or call the base class int index; index=m_e(); while(index--) delete m_(index); m_All(); CDocument::DeleteContents(); } 定义了DeleteContents()函数后,还可以修改delStuAll()函数如下,调用DeleteContents()函数以简化 代码。 程序清单8-26:修改delStuAll()函数 void CExam8_3Doc::delStuAll() 30 第6章 创建和使用对话框 { DeleteContents(); SetModifiedFlag(); this->UpdateAllViews(NULL); } (6) 修改Serialize()函数,直接调用CObArray的成员函数Serialize()函数即可,该函数会处理其数组 元素的序列化操作。 程序清单8-27:重载Serialize()函数 void CExam8_3Doc::Serialize(CArchive& ar) { m_ize(ar); } 5. 添加菜单项和工具栏 按表8-5所示完成以下操作: (1) 增加菜单项student及子菜单。 (2) 清除菜单项Edit下的所有菜单项,添加菜单项Clear all。 (3) 编辑工具栏按钮。 (4) 使用ClassWizard为菜单项在CExam8_3View视图类添加消息处理函数。 表8-5 菜单栏和工具栏设计 菜单项 ID prompt 按钮 映射消息 HomeIDC_STUDENT_HOME COMMAND the first student entrynhome End the last student entrynend Prev the prev student entrynprev Next the next student entrynnext Insert insert a studentninsert Del delete a student entryndelete Clear all clear all the student entrys IDC_STUDENT_END COMMAND UPDATE_COMMAND_UI IDC_STUDENT_PREV COMMAND UPDATE_COMMAND_UI IDC_STUDENT_NEXT COMMAND UPDATE_COMMAND_UI IDC_STUDENT_INSERT COMMAND UPDATE_COMMAND_UI IDC_STUDENT_DEL COMMAND IDC_EDIT_CLEAR COMMAND UPDATE_COMMAND_UI 6. 设计视图类 视图类需要实现功能为,读取文档数据,显示到控件中的文本框,实现上一步设计的菜单功能。操 作步骤如下: (1) 添加两个公有数据成员,一个是CStudent类对象指针m_pStu,指向当前显示的学生记录,另 一个是int型变量m_StuIndex,为当前显示的学生记录在对象数组中的索引值。 (2) 使用ClassWizard重载视图类虚函数OnUpdate()。OnUpdate()函数为OnInitUpdate()和 31 VC++6简明教程 UpdateAllViews()自动调用,在该函数中实现视图的初始化,若文档类对象数组为空,视图显 示一个空的记录,索引值为-1;若文档类对象数组不为空(如执行打开文件操作时),视图显 示第一个记录,索引值为0。 OnUpdate()代码如程序清单8-28所示。 程序清单8-28:重载OnUpdate()函数,初始化视图 void CExam8_3View::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint) { // TODO: Add your specialized code here and/or call the base class if(GetDocument()->GetStuNumber()>0)//文档数据不为空 { m_StuIndex=0; //索引值为0 m_pStu=GetDocument()->GetStu(0);//取第一个学生记录 } else//文档数据为空 { m_StuIndex=-1;//索引值为-1 m_pStu=new CStudent("",0);//创建一个为空的学生记录 } //为控件变量赋值,初始化视图 m_Grade=m_pStu->m_Grade ; m_Name=m_pStu->m_Name ; UpdateData(false); } (3) 在Exam8_3View.h文件的开头加入语句:#include “student.h” (4) 添加代码菜单命令消息处理函数,第一个记录,上一个记录,下一个记录,最后一个记录,删 除当前记录,添加一个记录,删除所有记录。 程序清单8-29:实现菜单命令消息处理函数 void CExam8_3View::OnStudentHome() //第一个记录 { if(GetDocument()->GetStuNumber()>0) //文档数据不空 { m_StuIndex=0;//第一个记录索引值为0 m_pStu=GetDocument()->GetStu(m_StuIndex);//获取第一个记录 //为控件变量赋值 m_Grade=m_pStu->m_Grade; m_Name=m_pStu->m_Name; //更新视图 UpdateData(false); UpdateWindow(); } } void CExam8_3View::OnStudentPrev() { 32 第6章 创建和使用对话框 m_StuIndex--;//索引值向前移动1位 m_pStu=GetDocument()->GetStu(m_StuIndex); m_Grade=m_pStu->m_Grade; m_Name=m_pStu->m_Name; UpdateData(false); UpdateWindow(); } void CExam8_3View::OnStudentNext() { m_StuIndex++;//索引值向后移动1位 m_pStu=GetDocument()->GetStu(m_StuIndex); m_Grade=m_pStu->m_Grade; m_Name=m_pStu->m_Name; UpdateData(false); UpdateWindow(); } void CExam8_3View::OnStudentEnd() { //最后一学生记录的索引值为记录总数-1 m_StuIndex=GetDocument()->GetStuNumber()-1 ; if(m_StuIndex>0) {m_pStu=GetDocument()->GetStu(m_StuIndex); m_Grade=m_pStu->m_Grade; m_Name=m_pStu->m_Name; UpdateData(false); UpdateWindow();} } void CExam8_3View::OnStudentDel() { //调用文档类DelStu函数删除当前记录 GetDocument()->DelStu(m_StuIndex); } void CExam8_3View::OnStudentInsert() { UpdateData(true);//更新控件变量 //调用文档类AddStu函数增加一条记录 GetDocument()->AddStu(m_Name,m_Grade); //索引值指向最后一条记录(新增记录) m_StuIndex=GetDocument()->GetStuNumber()-1; } void CExam8_3View::OnEditClear() { m_StuIndex=-1;//索引值设为1,表示文档数据为空 //调用文档delStuAll删除所有记录,更新视图 33 VC++6简明教程 GetDocument()->delStuAll(); } (5) 添加代码菜单更新消息处理函数,使用指针pCmdUI调用Enable()函数,根据当前记录的索引 值,控制按钮在合理的情况下有效。例如,在文档数据不为空时才能做删除操作。当索引值>0 时,才能做Home和Prev操作。当索引值小于最大值时,才能做End和Next操作。 程序清单8-30:实现菜单更新消息处理函数 void CExam8_3View::OnUpdateStudentDel(CCmdUI* pCmdUI) { pCmdUI->Enable(m_StuIndex>=0); } void CExam8_3View::OnUpdateStudentEnd(CCmdUI* pCmdUI) { pCmdUI->Enable(m_StuIndex } void CExam8_3View::OnUpdateStudentHome(CCmdUI* pCmdUI) { pCmdUI->Enable(m_StuIndex>0); } void CExam8_3View::OnUpdateStudentNext(CCmdUI* pCmdUI) { pCmdUI->Enable(m_StuIndex } void C Exam8_3View::OnUpdateStudentPrev(CCmdUI* pCmdUI) { pCmdUI->Enable(m_StuIndex>0); } 7. 控件Clear按钮的处理 使用ClassWizard为控件IDC_CLEAR在视图类映射BN_CLICKED消息处理函数。该按钮的作用是 在插入一个新记录前,将Name和Grade编辑框清空,供用户重新输入。实现代码如程序清单8-31所示。 程序清单8-31: 控件Clear按钮的处理函数 void CExam8_3View::OnClear() { // TODO: Add your control notification handler code here m_Name=""; m_Grade=0; UpdateData(false); } 8. 编译运行程序 34 第6章 创建和使用对话框 运行程序,测试以下操作是否正确: (1) 观察除insert按钮,其它自定义按钮都不可用。 (2) 输入一个学生记录后,单击Insert按钮。 (3) 单击Clear按钮清除编辑框。 (4) 重复(2)(3)输入2个学生记录。 (5) 测试Home按钮,单击后显示第一个学生记录,并且Home和Prev按键钮不可用。 (6) 测试End按钮,单击后显示最后一个学生记录,并且End和Next按键钮不可用。 (7) 测试next 和Prev按钮,显示前一个或后一个学生记录。 (8) 执行菜单命令File->Save保存学生记录到一个文件。 (9) 单击工具栏按钮New,建立新文档,视图更新为空文档状态。 (10) 打开刚刚保存过的文件。 (11) 单击Del按钮,删除某一条记录。 (12) 执行菜单命令Edit->ClearAll,清空所有记录。 实验 实验1: 8.1.3节例8-1的编辑器是不支持滚动,当文本行超过窗口大小时,窗口不能自动向上滚动以显示输 入的字符。当打开一个文本,如果文件大小超过窗口大小,也通过滚动视图来查看文档的全部内容。实 验1要求例8-1的基础上增加滚动功能。如图8-14所示。 图8-14支持滚动的文本编辑器 MFC提供了CScrollView类,简化了滚动视图需要处理的大量工作。除了管理文档中的滚动操作处, MFC还通过调用Windows API函数画出滚动条、箭头和滚动光标,它还负责处理: (1) 用户初始化滚动条的范围(通过滚动视图的SetScrollRange的方法); (2) 处理滚动条消息,并滚动文档到相应位置; (3) 管理窗口和视图的尺寸大小。 35 VC++6简明教程 调整滚动条上滑块(或称拇指框)的位置,使之与文档当前的位置相匹配。如果要让应用程序支持 滚动视图,只需程序作下列工作: (1) 从CScrollView类派生自己的视图类; (2) 提供文档大小,确定滚动范围和设置初始位置; (3) 协调文档位置和屏幕坐标。 实验步骤: 1. 将CVeiw改为CScrollView 将所有出现的CView都用CScrollView替代,最方便的方法是使用Edit->Repalce,进行全部替换。 2. 设置文档大小 (1) 在文档类添加CSize类的保护类数据成员m_DocSize,表示文档的大小。 (2) 在文档类添加成员函数GetDocSize()来访问m_DocSize。在文档类的头文件CEx8_1Doc.h中加 入以下粗体部分代码。 程序清单8-32:文档类添加数据成员和成员函数支持滚动功能 class CEx8_1Doc : public CDocument { protected: // create from serialization only CExa8_1Doc(); DECLARE_DYNCREATE(CEX8_1Doc) CSize m_DocSize; public: CSize GetDocSize(){return m_DocSize} // Attributes public: CStringList lines; int nLineNum; // Operations 。。。。。。}; (3) 在文档类的构造函数中,给m_DocSize设置初值。 程序清单8-33:设置文档大小 CEx8_1Doc::CEx8_1Doc() { // TODO: add one-time construction code here m_DocSize=CSize(700,800); } 3. 设置滚动范围 视图类的成员函数SetScrollSizes()用于设置文档滚动范围,一般在重载OnInitialUpdate()函数或 OnUpdate()函数时调用该函数。 修改OnInitialUpdate()函数,调用SetScrollSizes()函数设置滚动范围,其中第一个参数为映射模式, 可以使用除MM_ISOTROPIC和MM_ANISOTROPIC的其他任何映射模式。代码如程序清单8-34所示。 36 第6章 创建和使用对话框 程序清单8-34:设置滚动范围 void CEx8_1View::OnInitialUpdate() { // TODO: Add your specialized code here and/or call the base class CDC *pDC=GetDC(); pFont=new CFont(); if(!(pFont->CreateFont (0,0,0,0,FW_NORMAL,FALSE,FALSE,FALSE, ANSI_CHARSET,OUT_TT_PRECIS,CLIP_TT_ALWAYS, DEFAULT_QUALITY,DEFAULT_PITCH,"courier New"))) { pFont->CreateStockObject (SYSTEM_FONT); } CFont* oldFont=pDC->SelectObject(pFont); TEXTMETRIC tm; pDC->GetTextMetrics(&tm); lHeight=ht +rnalLeading; cWidth=harWidth ; pDC->SelectObject(oldFont); SetScrollSizes(MM_TEXT,GetDocument()->GetDocSize() ); CView::OnInitialUpdate(); } 4. 修改OnChar()函数 程序清单8-35:支持滚动的OnChar()函数 void CEx8_1View::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: Add your message handler code here and/or call default CEx8_1Doc* pDoc = GetDocument(); CClientDC dc(this); OnPrepareDC(&dc); CFont *oldFont; oldFont=Object(pFont); CString line(""); POSITION pos=NULL; if(nChar=='r') { pDoc->nLineNum++; VC++6简明教程 } else {line=pDoc->(pos); line+=(char)nChar; pDoc->(pos,line); } TEXTMETRIC tm; tMetrics(&tm); t(0,(int)pDoc->nLineNum*ht,line,gth()); } pDoc->SetModifiedFlag(TRUE); Object(oldFont); SetScrollSizes(MM_TEXT,GetDocument()->GetDocSize() ); CView::OnChar(nChar, nRepCnt, nFlags); } 5. 编译运行程序 实验2:(独立练习) 完善8.3.4 节的例8-3学生记录录入应用程序,要求浏览时,编辑框为只读,并且增加学生记录的 更新功能。 提示: (1) 删除Clear按钮,在视图中增加Save和Cancel按钮。在工具栏和菜单栏增加update操作。 (2) 初始状态为浏览状态,编辑框为只读,Save和Cancel按钮隐藏。 (3) 当单击Insert按钮或Update按钮,进入编辑状态,编辑框可写,浏览记录的菜单项不可用。在 Insert状态下,编辑框清空,在Update状态下编辑框显示当前记录。 (4) 当单击Save按钮,在Update状态下,更新记录,在Insert状态下,在数组的最后增加新记录, 并恢复到浏览状态。 (5) 当单击Cancel按钮,恢复到浏览状态。 自测题: 1、使用MFC的AppWizard可以创建三种类型的应用程序: 、 、 。 2、MFC应用程序的核心是 ,其最大特点就是: 。 3、在通常情况下,视图通过 函数获得指向文档对象的指针,并通过该指针访 问 。 4、在MFC中, 类、 类以及 都是由文档模板类创建的。 每一种 都有一种文档模板之相对应 ,文档模板负责 。 5、MDI应用程序与SDI应用程序的主要差别是什么? 38 第6章 创建和使用对话框 小结 1. 文档/视图结构是MFC的核心,它最大的特点就是数据操作和数据表示分离开,一个 文档可以对应多个视图,一个视图只能对应一个确定的文档。单文档应用程序处理单个文档的程序, 多文档应用程序处理多个文档的程序。 2. 视图对象可以通过成员函数GetDocument()返回指向其所对应的文档对象的指针,访问 或更新文档对象的内容。文档内容改变后可以调用文档类成员函数UpdateAllViews()或视图类成员函 数Invalidate()更新视图显示。 3. 更新显示的窗口消息为WM_PAINT,视图类的OnDraw函数处理视图的绘制。 4. 文档模板类创建一个应用程序的文档对象,视图对象和框架窗口对象,对应两种不同 的应用程序,文档模板类分为CSingleDocTemplate和CMultiDocTemplate。 5. CObject类是MFC的根类,大多类都是由CObject类派生得到的,CObject类提供了 持续性,动态性和诊断性三个重要的服务。 6. 持续性支持数据在持久性存贮介质(如磁盘)与内存(文档对象)之间的流动。 7. 动态性支持对象的动态创建,应用程序的文档类,视图类和窗口框架类都是由文档模 板动态创建的。 8. 诊断性提供在Debug开发模式下的调试机制,每个类中的AssertValid函数是自动维护 数据的有效性,Dump函数能够将调试窗口作为中间过程数据的输出窗口。 9. 支持动态的宏 (1)DECLARE_DYNAMIC和IMPLMENT_DYNAMIC (2)DECLARE_DYNCREATE和IMPLENT_DYNCREATE 10. 支持持续性的宏 (1)DECLARE_SERIAL和IMPLEMENT_SERIAL 11. 文档类的Serialize()函数处理文档对象数据的保存和读取,流操作符<<负责将文档数据 存入到ar所表示的文件,流操作符>>负责从ar所表示的文件中读取数据到文档对象的数据成员。 12. 文档类提供了与文件操作相关的一组成员函数 (1)OnOpenDocument() (2)OnNewDocument() (3)DeleteContents() (4)SetModifiedFlag() (5)OnSaveDocument() 13. MFC提供的集合类分为三种类型:List类、Array类、Map。本章介绍了使用CStringList 类和CObArray类操作复杂文档数据的方法。 39


发布评论