第13单元 文档读写与打印本单元教学目标介绍在文档/视图结构中文档读写的基本手段——序列化,以及文档打印的编程技术。
学习要求理解序列化的基本思想和MFC的打印工作流程,可编写相应的处理程序。
授课内容
13.1 序列化(Serialize)
文档对象的序列化(Serialize)是指文档对象可以将其当前状态(由其成员变量的值表示)写入到永久性存储体(通常是指磁盘)中,以后还可从永久性存储体中读取对象的状态(载入),从而重建对象。这种对象的保存和恢复的过程称为序列化。保存和载入序列化的数据通过CArchive类的对象作为中介来完成。
文档的序列化在文档类的Serialize()成员函数中进行。当用户选择文件菜单的File Save、Save As或Open选项时,都会自动调用这一成员函数。由于应用程序的数据结构各不相同,所以应重载文档派生类的Serialize()成员函数,使其支持对特定数据的序列化。
AppWizard在生成应用程序时只给出了一个Serialize()函数的框架,程序员要做的工作是为其添加代码,以实现具体数据的序列化。AppWizard生成的Serialize()函数由一个简单的if-else语句组成:
void CMyDoc::Serialze(CArchive& ar)
{
if(ar.IsStoring())
{
// TODO,add storing code here.
}
else
{
// TODO,add loading code here.
}
}
其中参数ar是一个CArchive类型的对象,该对象包含一个CFile类型的文件指针。CArchive对象为读写CFile(文件类)对象中的可序列化数据提供了一种类型安全的缓冲机制。通常CFile类对象代表一个磁盘文件。
CArchive类的成员函数IsStoring()用于通知Serialize()函数是需要写入还是读取序列化数据。如果数据要写入(Save或Save As),IsStoring()返回布尔值TRUE;如果数据是被读取(Open),则返回FALSE。
CArchive类对象使用重载的插入(<<)和提取(>>)操作符执行读和写操作。这种方式与cin和cout中的输入输出流非常相似,只是这里处理的是对象,不象cin和cout那样,处理的是ASCII字符串。
[例13-1] 序列化。如果例12-1的吹泡泡程序使用一般的数组存放泡泡数据(参看例9-1的程序):
CRect m_rectBubble[MAX_BUBBLE];
int m_nBubbleCount;
为其文档类重新设计Serialize()函数。
说 明:按例12-1的方法建立项目和输入源代码,但将文档类中的泡泡数据改为以上两行的形式。修改文档类的Serialize()函数,代码如下。
程 序:
// 序列化函数
void CMyDoc::Serialze(CArchive& ar)
{
if(ar.IsStoring())
{
ar << m_nBubbleCount;
for(int i=0; i<m_nBubbleCount; i++)
ar << m_rectBubble[i];
}
else
{
ar >> m_nBubbleCount;
for(int i=0; i<m_nBubbleCount; i++)
ar >> m_rectBubble[i];
}
}
分 析:在编写序列化函数时,一定要注意写入顺序要和读出顺序一一对应。在本例中,先写入数据成员m_nBubbleCount,再写入泡泡数组m_rectBubble,那么在读出时也要遵循相同的顺序,先读m_nBubbleCount的值,然后再读泡泡数组的各元素值。
在设计Serialize()函数时,还要注意各数据之间的关系。在本例中,一定要先读m_nBubbleCount的值,否则在读泡泡数组时还不知道数组中有几个元素,也就无法确定循环次数。读数据的顺序确定以后,写数据的顺序自然也就定下来了。
因为CObject类支持序列化(CObject类有Serialize()成员函数),所以COjbect派生类也支持序列化。例如,数组类CArray支持序列化,所以如果文档数据存放在CArray类对象中,就可象例12-1中那样,在重载的文档派生类的Serialize()成员函数中直接调用CArray对象的Serialize()成员函数:
void CMyDoc::Serialize(CArchive &ar)
{
m_rectBubble.Serialize(ar);
}
这正是例12-1中的做法。当然,大多数情况下文档类的数据结构比较复杂,不一定都能处理得如此简单。
序列化简化了对象的保存和载入编程。但是,序列化本身还是有一定的局限性的。序列化一次从文件中载入文档中的所有数据,这并不适合于大文件编辑和数据库应用。对于这类应用,应用程序每次只是从文件中读入一部分数据进行处理。此时,就要避开文档的序列化机制直接读取和保存文件,例如直接使用第8单元介绍的CFile类。
13.2 打印和打印预览
在第12单元中已经介绍过,文档/视图结构中的视图类负责程序的输出,包括屏幕显示和打印。也就是说,视图类的OnDraw()函数的输出为显示和打印共用。这种安排大大简化了编程,特别是在使用AppWizard生成应用程序框架的情况下,几乎无需添加任何编码就可实现“所见即所得”式的打印输出功能。
然而,由于打印机和显示屏上的窗口的工资原理完全不同,各种参数之间存在很大差异,在OnDraw()函数中兼顾两者的要求还是有困难的。如果在设计OnDraw()函数时主要考虑显示的需要(前面的文档/视图例题程序均如此),则打印输出的质量不高。这是因为:
1.打印机和窗口(屏幕)显示的分辨率不同。打印机的分辨率用每英寸多少个点来描述,屏幕分辨率用单位面积的像素点来表示。同样是Arial字体的字符,在屏幕上用20个像素表示,而在打印机上则需要50点。因此,如果选用MM_TEXT模式编程(为简单起见,前几单元的示例程序均如此),一个逻辑单位对应于一个像素点,则与屏幕显示相比,打印尺度明显偏小。
2.窗口和打印机对边界的处理不同。窗口可以看作是无边界的,可以在窗口之外绘图而不会引起错误,窗口会自动剪裁超出边界的图形。但打印机却不同,打印机按页打印,输出时必须自己处理分页和换页。
[例13-2] 修改例12-1的程序并观察其打印结果。
程 序:在例12-1程序的视图类CMyView类的成员函数OnDraw()中,添加代码沿窗口客户区轮廓画一矩形:
void CMyView::OnDraw(CDC* pDC)
{
CRect rect;
GetClientRect(&rect);
pDC->Rectangle(rect);
CMyDoc* pDoc = GetDocument(); // 取文档指针
ASSERT_VALID(pDoc);
pDC->SelectStockObject(LTGRAY_BRUSH); // 在视图上显示文档数据
for(int i=0; i<pDoc->GetListSize(); i++)
pDC->Ellipse(pDoc->GetBubble(i));
}
输入输出:用鼠标左键在窗口客户区吹泡泡。打开文件菜单的打印预览选项,可观察打印效果,如图13-1。
分 析:通过打印预览,可观察到打印输出集中在打印纸的左上角,窗口客户区矩形仅占打印纸的很小一部分。在窗口边沿生成的泡泡,在打印时并不受窗口边界的限制。
要正确打印输出屏幕上的内容,就必须解决这些问题。对于第一个问题,解决方法为利用CDC::SetMapMode(int nMode)设置其他映射模式,例如采用MM_LOMETRIC模式。该模式的基本单位不是像素,而是0.1毫米。采用这类映射模式编程,可使窗口显示图象和打印图象的比例相近。
但采用非MM_TEXT模式编程相当麻烦。首先,这些逻辑坐标的y轴方向与MM_TEXT模式不同,下负上正,原点在窗口左上角,所以客户区的y坐标均为负值。第二,由于逻辑坐标和物理坐标不一致,所以在响应鼠标消息时要进行换算(可参看第10单元的有关内容)。
[例13-3] 改进吹泡泡程序,使之打印输出与屏幕显示的比例相近。
程 序:在例12-1基础上修改。首先在CMyView类中重载虚函数OnPrepareDC()。在CMyView类的声明中增加一行:
virtual void OnPrepareDC(CDC *pDC,CPrintInfo *pInfo=NULL);
然后添加该函数的定义,设置映射模式为MM_LOMETRIC:
// 设置映射模式
void CMyView::OnPrepareDC(CDC *pDC,CPrintInfo *pInfo)
{
pDC->SetMapMode(MM_LOMETRIC);
CView::OnPrepareDC(pDC,pInfo);
}
然后修改消息映射函数OnLButtonDown(),将物理坐标转换为逻辑坐标:
// 响应点击鼠标左键消息
void CMyView::OnLButtonDown(UINT nFlags,CPoint point)
{
CMyDoc* pDoc = GetDocument(); // 取文档指针
ASSERT_VALID(pDoc);
CClientDC dc(this); // 设置设备环境
OnPrepareDC(&dc);
int r = rand()%50+5; // 生成泡泡
CRect rect(point.x-r,point.y-r,point.x+r,point.y+r);
InvalidateRect(rect,FALSE); // 更新视图
dc.DPtoLP(rect); // 转换物理坐标为逻辑坐标
pDoc->AddBubble(rect); // 修改文档数据
pDoc->SetModifiedFlag(); // 设置修改标志
}
输入输出:用鼠标左键在窗口客户区吹泡泡。使用文件菜单中的打印选项可打印窗口图象,图象位于打印纸上部,比例恰当,如图13-2所示。
分 析:由于OnDraw()函数输出使用逻辑坐标,所以存储数据(泡泡的包含矩形)也使用逻辑坐标。在OnLButtonDown()函数中鼠标位置参数point为物理坐标,首先据此生成泡泡的包含矩形(物理坐标),更新窗口客户区的相关区域(物理坐标),然后将物理坐标的泡泡包含矩形转换为逻辑坐标并存入文档。
CView类的虚函数OnPrepareDC()用于设置设备环境,其原型为:
virtual void OnPrepareDC( CDC* pDC,CPrintInfo* pInfo = NULL );
其中参数pDC为指向设备环境的指针,pInfo为指向CPrintInfo类对象的指针。CPrintInfo类用来存放与打印有关的信息,其数据成员m_nCurPage为当前打印页的号码;m_rectPage存放着当前打印纸上的可打印区域。常用成员函数有:
设置从第几页开始打印。其原型为:
void SetMinPage( UINT nMinPage );
其中参数nMinPage为开始打印的页号。如果从文档的第1页开始打印,则nMinPage的值应为1。
2.设置打印到第几页结束。其原型为:
void SetMaxPage( UINT nMaxPage );
其中参数nMaxPage为最后一个打印页的页码,其缺省值为1。
3.取关于打印页码的设置。原型为
UINT GetMinPage( ) const;
UINT GetMaxPage( ) const;
如果OnDraw()主要用于显示,打印内容简单(例如只有一页),则OnPrepareDC()的参数pInfo可取空值NULL。
应用程序框架在调用OnDraw()之前会调用OnPrepareDC()函数。在OnDraw()之外使用设备环境时(如在消息响应函数中),应首先声明一个CClientDC对象,然后调用OnPrepareDC()函数。
自学内容
13.3 自定义类的序列化前面已经介绍过,如果文档类的数据是CObject的派生类的对象,则文档类的序列化成员函数Serialize()的编写非常简单。那么,对于程序中的自定义类,能否让其支持序列化呢?回答是肯定的。
要让程序员自定义的类支持序列化,一般要做如下6步工作:
1.从CObject类派生出自定义类;
2.重载自定义类的Serialize()成员函数,加入必要的代码,用以保存自定义类对象的数据成员到CArchive对象以及从CArchive对象载入自定义类对象的数据成员状态;
3.在自定义类的声明中,加入DECLARE_SERIAL()宏,这是序列化对象所必需的;
4.为自定义类定义一个不带参数的构造函数;
5.为自定义类重载赋值运算符“=”;
6.在自定义类的源代码文件中加入IMPLEMENT_SERIAL()宏。
下面以一个自定义的Person类说明自定义类的序列化过程。
[例13-4] 声明一个Person类,并使之支持序列化。
程 序:
class CPerson,public CObject
{
DECLARE_SERIAL( CPerson)
LONG m_IDnumber; // 身份证号码
CString m_strName; // 姓名
CString m_strNation; // 民族
int m_nSex; // 性别
int m_nAge; // 年龄
BOOL m_bMarried; // 婚否
public:
CEmployee(){};
CPerson& operator = (CPerson& person);
void Serialize(CArchive& ar);
};
IMPLEMENT_SERIAL( CPerson,CObject,1 )
CPerson& CPerson::operator = (CPerson& person)
{
m_IDnumber = person.m_IDnumber;
m_strName = person.m_strName;
m_strNation = person.m_strNation;
m_nSex = person.m_nSex;
m_nAge = person.m_nAge;
m_bMarried = person.m_bMarried;
return *this;
}
void CPerson::Serialize(CArchive& ar)
{
CObject::Serialize( ar); // 首先调用基类的Serialize()方法
if(ar.IsStoring())
{
ar << m_IDnumber;
ar << m_strName;
ar << m_strNation;
ar << m_nSex;
ar << m_nAge;
ar << (int)m_bMarried;
}
else
{
ar >> m_IDnumber;
ar >> m_strName;
ar >> m_strNation;
ar >> m_nSex;
ar >> m_nAge;
ar >> (int)m_bMarried;
}
}
分 析:MFC在从磁盘文件载入对象状态并重建对象时,需要有一个缺省的不带任何参数的构造函数以及一个重载的赋值运算符。序列化对象将用该构造函数生成一个对象,然后调用Serialize()函数,用重建对象所需的值来填充所有的数据成员。
重载的赋值运算符也是序列化所必需的。注意最后通过this指针返回CPerson类对象自身的方法。
在序列化成员函数Serialize()中包含对象的保存和载入两部分。注意,CArchive类的“>>”和“<<”操作符并不支持所有的标准数据类型。支持的数据类型有:CObject、BYTE、WORD、int、LONG、DWORD、float和double等。其他的类型的数据要进行序列化输入输出时,需要将该类型的数据转化为上述几种类型之一方可。
另外,在类的实现(类声明)文件开始处,还要加入IMPLEMENT_SERIAL()宏。IMPLEMENT_SERIAL()宏用于定义一个从CObject派生的可序列化类的各种参数。该宏的第1和第2个参数分别代表可序列化的类名和该类的直接基类。第3个参数是对象的版本号,可以是一个大于或等于零的整数。MFC序列化代码在将对象读入内存时检查版本号。如果磁盘文件上的对象的版本号和内存中的对象的版本号不一致,MFC将抛出一个CArchiveException类的异常,阻止程序读入一个不匹配版本的对象。
现在,我们就可以象使用标准MFC类一样使用CPerson类的序列化功能了。
13.4 编写独立的打印处理程序
MFC的打印功能由视图类的OnPrint()成员函数完成。在缺省情况下,OnPrint()调用OnDraw()进行打印,这样就可实现“所见即所得”的打印效果。如果要求打印格式与显示格式不同,那么就要重载OnPrint()函数,自行编写打印代码。
应用程序框架每打印一页调用OnPrint()函数一次,这是为了方便输出页眉、页码等与页面有关的信息。如果要打印的内容不只一页,则要在OnPrint()函数中正确设置要打印的内容。
[例13-5] 修改例13-3的吹泡泡程序,使其打印每个泡泡的数据值。打印格式为每页40行,页眉为文档名,页脚为页号。
说 明:首先为视图类添加一个数据成员m_nLinePerPage,用来存放每页行数,并在视图类CMyView的构造函数中将m_nLinePerPage初始化为40。
修改视图类成员函数OnPrepareDC(),设置映射模式为MM_TWIPS。该模式为每英寸1440点,很适合打印机输出。
程 序:重载视图类的成员函数OnPreparePrinting(),在其中添加计算打印页数的代码:
BOOL CMyView::OnPreparePrinting(CPrintInfo* pInfo)
{
CMyDoc *pDoc = GetDocument();
int nPageCount = pDoc->GetListSize()/m_nLinePerPage;
if(pDoc->GetListSize() % m_nLinePerPage)
nPageCount ++;
pInfo->SetMaxPage(nPageCount);
return DoPreparePrinting(pInfo);
}
最后重载视图类的OnPrint()函数并添加打印代码:
void CMyView::OnPrint( CDC* pDC,CPrintInfo* pInfo )
{
int nPage = pInfo->m_nCurPage; // 当前页号
int nStart = (nPage-1)*m_nLinePerPage; // 本页第一行
int nEnd = nStart+m_nLinePerPage; // 本页最后一行
CFont font; // 设置字体
font.CreateFont(-280,0,0,0,400,FALSE,FALSE,
0,ANSI_CHARSET,OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,
DEFAULT_PITCH|FF_MODERN,"Courier New");
CFont *pOldFont = (CFont *)(pDC->SelectObject(&font));
CRect rectPaper = pInfo->m_rectDraw; // 取页面打印矩形
// 页眉,页面顶端中央打印文档名称
CMyDoc *pDoc = GetDocument();
ASSERT_VALID(pDoc);
CString str;
str.Format("Bubble Report,%s",(LPCSTR)pDoc->GetTitle());
CSize sizeText = pDC->GetTextExtent(str);
CPoint point((rectPaper.Width()-sizeText.cx)/2,0);
pDC->TextOut(point.x,point.y,str);
point.x = rectPaper.left; // 打印页眉下划线
point.y = rectPaper.top-sizeText.cy;
pDC->MoveTo(point);
point.x = rectPaper.right;
pDC->LineTo(point);
// 打印表头
str.Format("%6.6s %6.6s %6.6s %6.6s %6.6s",
"Index","Left","Top","Right","Bottom");
point.x = 720;
point.y -= 720;
pDC->TextOut(point.x,point.y,str);
TEXTMETRIC tm; // 取当前字体有关信息
pDC->GetTextMetrics(&tm);
int nHeight = tm.tmHeight+tm.tmExternalLeading;
point.y -= 360; // 下移 1/4 英寸
for(int i=nStart; i<nEnd; i++) // 打印表体
{
if(i >= pDoc->GetListSize())
break;
str.Format("%6d %6d %6d %6d %6d",i+1,
pDoc->GetBubble(i).left,
pDoc->GetBubble(i).top,
pDoc->GetBubble(i).right,
pDoc->GetBubble(i).bottom);
point.y -= nHeight;
pDC->TextOut(point.x,point.y,str);
}
// 在页面底部中央打印页号
str.Format("- %d -",nPage);
sizeText = pDC->GetTextExtent(str);
point.x = (rectPaper.Width()-sizeText.cx)/2;
point.y = rectPaper.Height()+sizeText.cy;
pDC->TextOut(point.x,point.y,str);
// 释放字体对象
pDC->SelectObject(pOldFont);
}
输入输出:使用鼠标左键在窗口吹泡泡,使用文件菜单的打印选项按表格方式打印泡泡数据,也可通过打印预览选项观察打印格式(图13-3)。
分 析:视图类的OnPrePareDC()、OnPreparePrinting()和OnPrint()都有一个CPrintInfo类型的指针参数pInfo。CPrintInfo为一结构类型,用来存放打印参数。应用程序的CPrintInfo对象的内容可由用户在文件菜单的打印机设置。在本程序中,用到了该对象的两个数据成员m_nCurPage(当前正在打印的页码)和m_rectDraw(当前可用页面区域),及其成员函数 SetMinPage()(指定文档的首页页码,缺省值为1)和 SetMaxPage()(指定文档的最末页码,缺省值为1)。
在程序中,要为打印做的准备包括在视图类的成员函数OnPrepareDC()中的设置映射模式和在OnPreparePrinting()中的设置打印页数。
在重载的视图类成员函数OnPrint()中编写打印代码。在缺省状态下,OnPrint()通过调用OnDraw()实现“所见即所得”式的打印效果。在本例中,由于打印内容与显示内容无关,所以自行编写所有的打印代码。
OnPrint完成打印一页文档的工作。为完成一页打印,应做如下准备工作:确定当前打印页、当前页的第1行数据和最后1行数据对应的pDoc(>m_rectBuble数组的下标等参数;设置打印字体和取打印纸上打印范围矩形。
首先打印页眉。页眉内容为文档名,位置在页面顶端中央,其下有一横线。这一段中用到的方法有:
CDocument::GetTitle ( ) 取当前文档名
CDC::GetTextExtent ( ) 计算字符串在当前映射模式下的宽度和高度然后打印泡泡数据。这里采用表格形式,每行打印一个泡泡的包含矩形的左上角和右下角坐标值。注意,因为采用了MM_TWIPS映射模式,原点在窗口客户区左上角且y轴方向向上,所以y轴数据均为负值。首先打印表头,然后逐行打印本页的泡泡数据。这一段中用到一个结构体类型TEXTMETRIC变量tm,用来存放打印字体的有关信息。存放在TEXTMETRIC中的字体信息很多(参看10.4:“字体”),这里只用到其中的tmHeight(字体高)和tmExternalLeading(两行字体之间的间隔)来计算行间距。要使用字体信息,首先要使用CDC::GetTextMetrics()取得字体信息。
最后打印页脚。页脚信息为页码,打印在页面底部中央。在阅读本程序时,要注意MM_WIPS映射模式的单位为1/1440英寸,因此,语句
point.y (= 360;
的含义是下一打印位置为从当前打印位置下移1/4英寸。
MSDN在线帮助也提供了由“Printing and Print Preview Topics”开始的一系列主题信息。选中SearchTitles Only复选项,并在MSDN的Search(搜索)选项卡中键入该标题,即可找到该主题的有关内容。
13.5 工具条与状态条
AppWizard自动生成的程序框架提供了工具条和状态条。工具条上有一些按钮,每当用户按下一个按钮时,就会发出一条WM_COMMAND消息,其参数为该按钮的标识符。在大多数情况下,工具条中的按钮均有相应的菜单项,它们的标识符相同,即按工具条中的按钮相当于选择相应的菜单项。
状态条的主要用途是显示某个菜单选项或工具条按钮的相关消息(即在建立菜单选项时,Menu Item Properties(菜单选项特征)对话框中最下方的Prompt文本框输入的内容)。除此而外,状态条上有一些指示栏,如大、小写指示灯、滚动指示灯、数字键指示灯、系统时间显示等。
在MFC中,通过CToolBar类和CStatusBar类控制工具条和状态条。检查AppWizard生成的应用程序框架,可在CMainFrame类的声明中发现工具条和状态条的说明:
protected,// control bar embedded members
CStatusBar m_wndStatusBar;
CToolBar m_wndToolBar;
并可在CMainFrame类的OnCreate()函数中发现它们的初始化代码:
if (!m_wndToolBar.CreateEx(this,TBSTYLE_FLAT,
WS_CHILD | WS_VISIBLE | CBRS_TOP|CBRS_GRIPPER |
CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
{
TRACE0("Failed to create toolbar\n");
return -1; // fail to create
}
if( !m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators(indicators,
sizeof(indicators)/sizeof(UINT)))
{
TRACE0("Failed to create status bar\n");
return -1; // fail to create
}
可以看到,工具条的初始化是通过工具条类的OnCreateEx()函数实现的,其参数有父窗口指针、控制风格和工具条风格;状态条的初始化是通过状态条类的OnCreate()函数实现的,同时还调用了SetIndicators()函数以确定状态条中的提示项目。其中的indicators是一全局数组:
static UINT indicators[] =
{
ID_SEPARATOR,// 提示文字的指示栏
ID_INDICATOR_CAPS, // 大小写指示灯
ID_INDICATOR_NUM, // 数字键指示灯
ID_INDICATOR_SCRL, // 滚动指示灯
};
在上述初始化代码之后,还可以看到以下几行代码:
// TODO,Delete these three lines if you don't want the toolbar to
// be dockable
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); // 设置工具条为可停靠的
EnableDocking(CBRS_ALIGN_ANY); // 设置框架窗口允许停靠
DockControlBar(&m_wndToolBar); // 把工具条停靠在窗口中用于将工具条设置成可停靠的并停靠在框架窗口中。
工具条与菜单选项的关系非常密切。因此,通常的做法都是首先确定应用程序的各菜单选项,然后再编辑相应的工具条。如欲使某工具条按钮实现某菜单选项的功能,只需将这两者的标识符设置成同一标识符即可。
13.6 更新命令用户接口(UI)消息一般情况下,菜单选项和工具条按钮都不止一种状态,经常需要根据应用程序的内部状态来对菜单选项和工具条按钮作相应的改变。例如,在没有为编辑菜单的选项“复制”、“剪切”等添加相应的处理代码时,这些菜单选项是无效的(灰色显示)。有时还会发现,在某个菜单选项旁边可能还会有检查标记,表示该选项是否被选中。
在MFC中,应用程序框架通过更新命令用户接口消息来实现对菜单选项状态的改变。更新命令用户接口消息的编程与命令消息的编程类似,包括以下3步:
在窗口类中加入处理更新命令用户接口消息函数的声明;
在窗口类的消息映射宏中加入更新命令用户接口宏;
编写更新命令用户接口消息处理函数。
[例13-6] 修改例11-4的拼图程序,使之在难度菜单的相应选项前打钩。
程 序:首先在框架窗口类的消息响应函数声明处增加以下消息响应函数的声明:
afx_msg void CPuzzleWnd::OnUpdateGrad01(CCmdUI* pCmdUI);
afx_msg void CPuzzleWnd::OnUpdateGrad02(CCmdUI* pCmdUI);
afx_msg void CPuzzleWnd::OnUpdateGrad03(CCmdUI* pCmdUI);
然后在框架窗口类的消息映射宏中加入相应内容:
BEGIN_MESSAGE_MAP(CPuzzleWnd,CFrameWnd)
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_MOUSEMOVE()
ON_WM_PAINT()
ON_COMMAND(ID_SHOWFIG,OnShowFig)
ON_COMMAND(ID_GRAD01,OnGrad01)
ON_COMMAND(ID_GRAD02,OnGrad02)
ON_COMMAND(ID_GRAD03,OnGrad03)
ON_UPDATE_COMMAND_UI(ID_GRAD01,OnUpdateGrad01)
ON_UPDATE_COMMAND_UI(ID_GRAD02,OnUpdateGrad02)
ON_UPDATE_COMMAND_UI(ID_GRAD03,OnUpdateGrad03)
END_MESSAGE_MAP()
注意更新命令用户接口消息映射宏将菜单标识符与相应的消息映射函数联系在一起。最后编写相应的更新命令用户接口消息映射函数:
void CPuzzleWnd::OnUpdateGrad01(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_nColCount == 4);
}
void CPuzzleWnd::OnUpdateGrad02(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_nColCount == 8);
}
void CPuzzleWnd::OnUpdateGrad03(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_nColCount == 16);
}
输入输出:在选择拼图难度时,可在相应选项前打钩(图13-4)。
分 析:SetCheck()函数需要根据某种特征识别出需要打钩的菜单项。由于不同难度直接对应拼图的分块多少,因此我们用检测拼图的每行块数来确定需要打钩的菜单选项。
对于用AppWizard建立的应用程序,可直接使用ClassWizard添加对更新命令用户接口消息的支持。在ClassWizard对话框的Message Map选项卡中,如果选择一个菜单的ID,在Messages列表框中就会出现两项:
COMMAND
UPDATE_COMMAND_UI
其中UPDATE_COMMAND_UI就是更新命令用户接口消息,专门用于处理菜单项和工具条钮的更新。在Object IDs列表中选择相应的菜单消息标识符后在Messages列表中双击ON_UPDATE _COMMAND_UI条目,弹出Add Member Function对话框。最后可直接编辑生成的消息响应函数。
调试技术
13.7 Developer Studio的ClassWizard(类向导)
Developer Studio的MFC ClassWizard用于在AppWizard创建的项目基础上,根据程序员的要求以半自动方式添加程序代码。MFC ClassWizard可为如下目的生成代码:
· 从接收消息或管理控件窗口的MFC类派生新类;
· 为处理消息生成消息映射和消息处理成员函数;
· 处理OLE/ActiveX方法、属性和事件触发;
· 用于输入到对话控件中的数据交换和验证函数。
在使用AppWizard生成应用程序框架之后,随时可以通过View菜单调用MFC ClassWizard。当然,是否使用MFC ClassWizard由程序员决定,如果自己愿意的话,完全不使用MFC ClassWizard也可完成项目的开发。MFC ClassWizard对话框有5个选项卡,本节仅介绍其中的Massage Maps(消息映射)选项卡,如图13-5所示。
Message Maps(消息映射)选项卡用于在应用程序中添加与消息处理有关的代码。Message Maps选项卡左上角的组合框Project指出当前项目;右上方的ClassName组合框用于选择响应消息的类;左方的Object IDs列表框用于选择在当前类中可以响应其消息的对象;中部的Message列表框用于选择对于指定对象,可以响应的消息或重载的成员函数。选项卡下方的Member Functions列表框用于选择需要编辑的成员函数。在该列表框中,条目左边的字母“V”说明该条目为虚函数,字母“W”说明该条目为处理WM_前缀的系统消息的函数。如果选中了一条消息或虚函数,在选项卡的底部Description行会出现选中项目的简要说明。如果想了解有关选中项目的详细信息,可以切换到MSDN在线帮助,然后在索引中搜索。选项卡的右边还有“Add Class”、“Add Function”、“Delete Function”和“Edit Code”等按钮,分别用于为项目添加一个新类、添加一个新函数、删除由MFC ClassWizard添加的函数和编辑由MFC ClassWizard添加的函数代码等。对于添加到类的每个消息处理程序函数,MFC ClassWizard对该类的源文件做三处修改:
· 在头文件中添加函数声明;
· 在CPP源代码文件中添加带有骨干代码的函数定义;
· 将代表该函数的条目添加到类的消息映射中。
例如,要在CMyView类中添加响应鼠标左键按下消息的代码,应进行如下操作:
1.在ClassName组合框中选择类名CMyView;
2.在Object IDs列表框中选择类CMyView(被选中的对象用蓝底白字显示);
3.在Message列表框中选择消息WM_LBUTTONDOWN(可用该列表框右面的滚动条选择);
4.按下选项卡右方的Add Function按钮,此时在Member Functions列表框中会显示出消息响应函数名OnLButtonDown及其对应消息WM_LBUTTONDOWN;
5.按下选项卡右方的Edit Code按钮,自动退出MFC ClassWizard进入文本编辑器,编辑位置为新生成的OnLButtonDown()函数的第1行。
此时即可在新生成的OnLButtonDown()函数中添加有关处理代码了。注意查看CMyView类的头文件和源代码文件,可以发现ClassWizard同时在CMyView类声明的中生成了该消息响应函数的声明(灰色显示):
afx_msg void OnLButtonDown(UINT nFlags,CPoint point);
并在CMyView源代码文件中的消息映射宏中添加了一行
ON_WM_LBUTTONDOWN()
最后,在CMyView的源代码中生成了一个空OnLButtonDown()函数,可在其中添加相应的处理代码:
void CMyView::OnLButtonDown(UINT nFlags,CPoint point)
{
// TODO,Add your message handler code here and/or call default

CView::OnLButtonDown(nFlags,point);
}
程序设计举例
[例13-7] 七巧板是我国民间流传的一种拼图游戏,制作简单但变化多端,具有很高的益智价值。七巧板制作非常简单,使用一张正方形的厚纸板按图13-6所示图样裁开即可。本程序在计算机上模拟使用七巧板进行拼图。
程序的功能为:使用鼠标左键可拖动七巧板的各拼图块在窗口客户区移动;使用鼠标右键点击某拼图块可以使其按顺时针方向转动45°。程序使用SDI程序结构,因此具有存储和打印当前拼图图案的能力。
说 明:用与例12-1相同的方法建立项目并输入源代码程序和资源文件(注意修改资源文件中的字符串使之符合七巧板程序)。
程 序:见附录5:“七巧板程序”。
输入输出:可用鼠标拖曳各拼图块以组成各种图形(图13-7)。
分 析:首先声明一个拼图块类CChip,该类的对象为一拼图块。由于拼图块的形状有三角形、正方形和平行四边形,这些图形均可使用CDC::Polygon()函数绘制。因此,使用坐标点数组m_pointList[]来存放拼图块各顶点的坐标。为了使拼图更漂亮,每种拼图块使用不同的颜色显示,因此使用了一个数据成员m_nType表示拼图块的类别。
在CChip类的成员函数中用到了类CRgn。CRgn类用来表示一个区域,其形状不限于矩形。CRgn类有许多成员函数,其中较重要的有:
1.建立区域:
BOOL CreateRectRgn( int x1,int y1,int x2,int y2 );
BOOL CreateRectRgnIndirect( LPCRECT lpRect );
BOOL CreateEllipticRgn( int x1,int y1,int x2,int y2 );
BOOL CreateEllipticRgnIndirect( LPCRECT lpRect );
BOOL CreatePolygonRgn( LPPOINT lpPoints,int nCount,int nMode );
这几个成员函数分别用于建立矩形、椭圆和多边形区域。对于矩形和椭圆区域要求给出矩形(或椭圆的包含矩形)的参数;对多边形区域,除要求给出多边形的各顶点坐标和顶点个数外,还应说明多边形区域的填充方式。参数nMode可选ALTERNATE 或 WINDING值。如果建立区域成功,返回非零值,否则返回零。
2.取区域的包含矩形
int GetRgnBox ( LPRECT lpRect ) const;
该函数用于得到包含该区域的最小包含矩形,如图13-8所示。
图13-8 各种区域的包含矩形
3.移动区域
int OffsetRgn( int x,int y );
int OffsetRgn( POINT point );
该函数可移动一个区域,参数x,y或point为偏移量。
4.测试给定坐标点是否在区域中
BOOL PtInRegion( int x,int y ) const;
BOOL PtInRegion( POINT point ) const;
该函数用来测试一给定坐标点是否在区域中。若在,则返回非零值,否则返回零。
5.测试给定矩形是否在区域中
BOOL RectInRegion( LPCRECT lpRect ) const;
该函数用来测试给定矩形是否与区域相交。如果给定矩形的任一部分在区域中,返回非零值,否则返回零。
为了方便存储拼图块,将CChip类设计为可序列化的。这包括将CChip类声明为CObject类的派生类,使用宏DECLARE_SERIAL()和IMPLEMENT_SERIAL(),以及重载CObject类的Serialize()函数。
CChip::Rotation()用于旋转拼图块。为了旋转拼图块,需要对拼图块多边形的所有顶点坐标进行变换。其算法为:
· 求出旋转的中心坐标,即该拼板块的中心;
· 将拼板多边形各顶点坐标变换为以旋转中心为原点的相对坐标;
· 利用公式
x’ = xcos((ysin(;
y’ = xsin(+ycos(
· 求出旋转后新的相对坐标;
· 将相对坐标转换为实际坐标。
在文档类中,使用拼图块数组m_chipList存放所有的拼图块。各拼图块的颜色、初始位置和形状在Reset()函数中设置。值得注意的是,初始化工作比较单一,只是在DeleteContents()函数中调用了 Reset()。我们知道,该文档类的该成员函数在关闭文档、建立新文档和打开文档前均要调用。
由于CChip类是可序列化的,所有文档类的序列化函数编程比较简单:只需逐一调用各拼图块对象的序列化成员函数即可。
该程序的视图类用来完成对用户消息的响应。用户可以使用鼠标左键拖动拼图块在视图窗口中移动,其编程原理与例11-4完全相同。用户可以使用鼠标右键旋转某个拼图块,所以在程序中要响应鼠标右键消息。
程序的其他部分与例12-1相同。
单元上机练习题目用AppWizard建立一个SDI应用程序框架,并将其修改为与例13-7功能相同的七巧板程序(应充分利用ClassWizard的各项功能)。
修改上述程序,使其打印输出的比例较为合适。
修改、完善上述程序:
· 选择一幅美观的图片作为背景;
· 为该程序设计一个有特色的图标;
· 为其增加还原菜单选项,还原菜单选项可在任何时候恢复各拼图块原来的位置;
· 为其增加难度菜单,难度可分三级:一级使用1副七巧板;二级使用两副七巧板;三级使用3副七巧板。注意在使用多副七巧板的情况下应适当缩小七巧板的尺寸。
· 在对应当前难度的菜单选项左边添加标记“√”(参考例13-6)。