前面所做的案例程序只能显示一个文本串,如果重新输入一个新的文本串,旧的文本串就会被替代,如何才能在视图中按照所给定的位置和内容同时显示更多的文本串?
首先,需要将文本的相关数据都保存在一个数据结构中,包括:文本内容、显示位置坐标、字体特征属性(字体、字形、字号、颜色等)。
为此设计一个CMyText类,记录每个文本的信息。
(1)在应用程序中增加一个新类CMyText类,并且公有继承根类CObject。
(2)添加成员变量和修改构造函数,MyText.h代码如下:
class CMyText:public CObject
{ public:
CMyText();
CMyText(CString text, CPoint pos, LOGFONT font, COLORREF color);
virtual ~CMyText();
public:
CString m_strText; //文本内容
CPoint m_textPos; //显示位置坐标
LOGFONT m_textFont; //用户所选字体属性参数
COLORREF m_textColor; //用户所选择的字体颜色
};
……
CMyText::CMyText()
{
m_strText = ""; //文本内容
m_textPos = CPoint(0,0); //显示位置坐标
m_textColor = RGB(0, 0, 0); //用户所选择的字体颜色
}
CMyText::CMyText(CString text, CPoint pos, LOGFONT font, COLORREF color)
{
m_strText = text; //文本内容
m_textPos = pos; //显示位置坐标
m_textFont = font; //字体参数
m_textColor = color; //用户所选择的字体颜色
}
(3)修改操作流程,为“文本”菜单添加一个新菜单项“添加文本”,点击后,先设置字体和颜色,之后利用对话框输入文本内容和显示位置坐标,点击OK后,在视图区完成文本输出。
添加一个新菜单项“输出文本”,ID为ID_ADD_TEXT,Caption为“添加文本”。添加其响应函数。
(4)在App15Doc.cpp中定义“添加文本”命令响应函数OnAddText ()。代码如下。
同时在该文件的前面加入:
#include “TextDlg.h”
#include “MyText.h”
void CApp15Doc::OnAddText()
{
CFontDialog dlgFont; //公用字体对话框
CTextDlg tdlg; //定义一个对话框对象
LOGFONT font; COLORREF color;
int x = 0; int y = 0;
CString msg;
if(dlgFont.DoModal() == IDOK)
{
dlgFont.GetCurrentFont(&font);
//获得用户所选择的字体
color = dlgFont.GetColor();
if(tdlg.DoModal()==IDOK) //文本对话框
{
x=tdlg.m_nX;
y=tdlg.m_nY;
msg=tdlg.m_strText; //保存编辑框数据
//将新增文本对象添加到文本链表中
CMyText *pText = new CMyText(msg,CPoint(x,y),font,color);
//将多个文本对象按照创建的顺序依次加到对象链表中
UpdateAllViews(NULL);
}
}}
(5)在App15Doc.h中定义一个对象链表变量。
class CApp15Doc : public CDocument
{
protected: // create from serialization only
…
CObList m_textList; //对象链表
…
}
我们使用了CObList类的对象m_textList作为保存CMyText类对象的链表。
CObList类属于集合类。集合类维护并支持数组、链表和数据对象映射。集合类适用于各种对象和预定义类型,而且大小是动态变化的。集合类最广泛的用途是定义文档类的数据结构。CObList类能够支持指向CObject派生类对象的指针列表。
if(tdlg.DoModal()==IDOK) //文本对话框
{
x=tdlg.m_nX;
y=tdlg.m_nY;
msg=tdlg.m_strText; //保存编辑框数据
//将新增文本对象添加到文本链表中
CMyText *pText = new CMyText(msg,CPoint(x,y),font,color);
//将多个文本对象按照创建的顺序依次加到对象链表中
m_textList.AddTail(pText);
UpdateAllViews(NULL);
}
}}
AddTail()是CObList类的成员函数:
POSITION CObList::AddTail(CObject* newElement)
该函数的功能是在对象链表的尾部添加新元素。输入参数为CObject类型的指针。函数返回值是新插入元素的位置。
POSITION是一个指向无成员结构的指针,主要用于描述collection对象(如数组或链表)中的元素位置,常用于MFC集合类。
(6)修改OnDraw()函数,将前面案例的代码删除,编写新的代码,显示对象链表m_textList中所有的CMyText类对象。
void CApp15View::OnDraw(CDC* pDC)
{
CApp15Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
//从文本链表中取出各个文本对象进行绘制
POSITION pos = pDoc->m_textList.GetHeadPosition();
while(pos != NULL) //链表不为空
{
CMyText* pText =(CMyText*)pDoc->m_textList.GetNext(pos);
CFont font;
font.CreateFontIndirect(&pText->m_textFont);
CFont *pfntOld = pDC->SelectObject(&font);
//设置字体
pDC->SetTextColor(pText->m_textColor); //设置字体颜色
pDC->TextOut(pText->m_textPos.x,pText->m_textPos.y, pText->m_strText);
pDC->SelectObject(pfntOld); //恢复以前的字体
}
}
POSITION GetHeadPosition()
该函数的功能是提取链表的头元素位置。
CObject* GetNext( POSITION& rPosition )
该函数的功能是从链表当前位置提取元素,并将位置变量更新为指向下一元素。
(7)当关闭程序窗口时应该删除new出来的文本,下面设计RemoveTextList()函数用于删除链表中的所有CMyText类对象,之后再删除对象链表本身。
在App15Doc.h头文件中添加函数声明:
void RemoveTextList();
在App15Doc.cpp文件中实现该函数。
void CApp15Doc::RemoveTextList()
{
POSITION pos = m_textList.GetHeadPosition();
while(pos != NULL)
{
CMyText* pText = (CMyText*)m_textList.GetNext(pos);
delete pText;
}
m_TextList.RemoveAll();
}
(8)为CMainFrame类添加窗口关闭响应函数OnClose()。如图所示。
void CMainFrame::OnClose()
{
// TODO: Add your message handler code here and/or call default
CApp15Doc* pDoc = (CApp15Doc*)this->GetActiveDocument();
pDoc->RemoveTextList();
CFrameWnd::OnClose();
}
在文件的前面添加:
#include “App15Doc.h"
(9)当执行File|New命令时,应该将客户区窗口中所绘制的文本清除。为此需要进一步完善程序。
执行File|New命令时,将调用文档类成员函数OnNewDocument(),该函数又要调用另一个成员函数DeleteContents()。
另外执行File|Open命令时也会调用DeleteContents()函数。该函数是一个虚函数。
在文档派生类CApp15Doc中重载该函数,完成清除客户区已有显示内容的功能。
void CApp15Doc::DeleteContents()
{
// TODO: Add your specialized code here and/or call the base class
RemoveTextList();
CDocument::DeleteContents();
}
编译连接运行程序,点击File|New命令,可以看到客户区内容被清除。
在哪些情况下需要清除对象链表呢?在本例中主要有这样两种情况。
(1) 文档对象被销毁(如关闭了应用程序)
当文档对象消失时,其包含的链表应当被清除。
(2)创建了一个新的文档或打开了(另外)一个已存储的文件。
此时文档类对象的内容已经改变:或者为一个新的空文档,或者将要加载已存储的文件,这时应该将链表清除。
应用程序通常需要在程序运行结束前,将对象当前的状态数据写入永久性存储器,以便程序再次运行时进行数据的读取以恢复对象状态,这种对象的保存与恢复的过程在MFC中称为序列化(serialization)。
一个类若要具备序列化的能力,则需要直接或间接地派生自MFC的根类CObject,因为负责序列化这项工作的是CObject类中的一个虚拟成员函数Serialize(),文件的“读”和“写”的操作都通过它来进行。
CObject类的成员函数Serialize()的函数声明如下。
virtual void Serialize(CArchive& ar);
其中函数参数ar是一个CArchive类对象的引用,文档数据的序列化操作通过CArchive对象作为中介来完成,CArchive对象由应用程序框架创建。
大多数MFC应用程序通过CArchive对象间接使用CFile类的功能对磁盘文件进行读写。CArchive类的构造函数有一个CFile指针参数,当创建一个CArchive对象时该对象与一个CFile类或其派生类的对象关联,即与一个打开的文件相关联。
在上例基础上,进一步完善单文档应用程序,完成自定义类CMyText的序列化。
可序列化的类必须直接或间接地从CObject派生,为此前面已将CMyText类定义为CObject类的派生类。另外还需要完成以下操作:
在类声明中包含宏 DECLARE_SERIAL;
在实现类成员函数的C++源文件中,包含宏IMPLEMENT_SERIAL;
在类中添加序列化函数;
自定义类具有一个无参的构造函数。
(1)重载CObject的Serialize函数:保存或读取CMyText的数据成员。在MyText.h文件中添加代码如下。
class CMyText:public CObject
{
DECLARE_SERIAL(CMyText)
public:
……
virtual void Serialize(CArchive &ar);
……
}
其使用形式为:
DECLARE_SERIAL( class_name )
DECLARE_SERIAL宏包括了DECLARE_DYNAMIC宏 和 DECLARE_DYNCREATE宏所具有的功能。
(2)在MyText.cpp文件中添加代码如下。
IMPLEMENT_SERIAL(CMyText, CObject, 0)
IMPLEMENT_SERIAL 宏的使用形式如下:
IMPLEMENT_SERIAL( class_name,
base_class_name, wSchema )
其中参数wSchema为类对象版本号,使用大于或等于零的整数。如果要使Serialize成员函数能够读取多个版本
(3)在MyText.cpp文件中添加代码如下。
void CMyText::Serialize(CArchive &ar)
{
if(ar.IsStoring())
ar << m_strText << m_textPos << m_textFon << m_textColor ;
else
ar >> m_strtext >> m_TextPos >> m_textFnnt >> m_textColor ;
}
CArchive类针对C++的基本数据类型、Windows数据类型以及CObject类的派生类,重载了流插入操作符“<<”和流提取操作符“>>”,这些重载运算符均定义于AFX.INL文件中,其函数声明则在AFX.h中。
CArchive对象允许对文档数据使用重载的流插入操作符“<<”和流提取操作符“>>”,用于向文件中写入各种类型的数据以及从文件中读取它们。
(4)由于自定义类CMyText中有LOGFONT类型的成员变量m_textFont,为了对LOGFONT类型的变量进行序列化,需要重载<<和>>运算符。首先将下面的代码添加到MyText.cpp中,注意这两个函数是普通函数。
CArchive& operator<<(CArchive& ar, LOGFONT& logfont)
{
ar.Write(&logfont, sizeof(logfont));
return ar;
}
CArchive& operator>>(CArchive& ar, LOGFONT& logfont)
{
ar.Read(&logfont, sizeof(logfont));
return ar;
}
CMyText::CMyText()
…
在上面的<<和>>运算符重载的代码中调用了CArchive类的成员函数Read()和Write(),这两个成员函数的原型及其用法如下。
UINT Read(void* lpBuf, UINT nMax);
void Write(const void* lpBuf, UINT nMax);
函数Read()从文件对象中读取长度为nMax的数据,放到缓冲区中,缓冲区的首地址由该函数的第一个参数lpBuf指定。函数Write() 从缓冲区中取出指定长度nMax的数据写入文件对象中,缓冲区的首地址由该函数的第一个参数lpBuf指定。
(5)CObList属于集合类,所有的集合类都是从CObject类派生出来的,并且集合类声明中都包含有DECLARE_SERIAL宏调用,因此可以通过调用集合类的Serialize成员函数,方便地完成集合的序列化。
通过调用由CMyText对象指针组成的CObList集合的Serialize函数,每个CMyText*指针所指向的对象的Serialize函数就会被自动调用。
在文档类的序列化函数中完成文本对象集合的文件保存和文件读取。在App15Doc.cpp中添加代码如下。
void CApp15Doc::Serialize(CArchive& ar)
{
m_textList.Serialize(ar); //文本链表的序列化和反序列化
if (ar.IsStoring())
{
// TODO: add storing code here
}
else
{
// TODO: add loading code here
}
}
(6)为程序添加提示保存功能。当打开一个新的文档或退出程序时一般需要提示用户是否保存当前文档。
由于MFC应用程序框架已经做了主要的工作,因此实现这个功能较为简单。
我们只要在修改了文档之后调用文档类的成员函数SetModifiedFlag()设置修改标志即可。
本案例程序中在CApp15Doc类的成员函数OnAddText ()中设置修改标志。
void CApp15Doc::OnAddText()
{
……
m_TextList.AddTail(pText); //加入文本对象链表
SetModifiedFlag(); //设置文档修改标志
UpdateAllViews(NULL);
}
编译、连接并运行程序,可以看到程序已经具有了序列化功能。