实验三 图形数据的存盘

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

实验三图形数据的存盘
【实验目的】
1、保存和装入磁盘文件中的文档数据;
2、加入滚动功能;
3、加入分隔功能;
4、更新视图。

【实验要求】
实验前认真预习MFC编程的基本特点和方法,在进行实验时,应注意爱护机器,按照试验指导书的要求的内容和步骤完成实验,尤其应注意认真观察实验结果,做好记录;实验完成后应认真撰写实验报告。

请保存好本次程序代码,下次实验在本次程序基础上继续。

【实验要点】
向前面生成的MiniDraw程序加入支持标准File菜单命令(New、Open、Save和Save As)的代码,在程序中加入拖放特性,使用户可以从拖动文件对象到程序窗口来打开文件。

滚动能力使用户可以编辑大于视图窗口文档的任何部分。

分隔能力使用户可以生成一个文档的多个视图,并独立滚动每个视图。

将这两个特性加入到程序中较容易,几乎所有工作都是用支持这些特性的专用MFC类完成的。

【实验步骤】
一、MiniDraw加入文件I/O
向MiniDraw程序加入Open,Save和Save As命令以及支持这些命令所需的代码。

1、加入File菜单命令
按照实验二的方法,在现有的文件(File),依次加上Open、Save、Save As、分隔符和Recent File命令。

对每个新命令,表3.1列出了Menu Item Properties对话框中标识符、标题和其他特性。

图3.1显示了菜单编辑器窗口显示的完成的File菜单。

表3.1 加入MiniDraw File菜单的项目属性
图3.1 完成的MiniDraw程序的File菜单
2、在新程序中指定缺省文件扩展名
修改MiniDraw资源字串之以指定Open和Save As对话框中显示的缺省文件扩展名,双击ResourceView图中的String Table项目打开Developer Studio字符串编辑器:
当前值为:
MiniDraw\n\nMiniDr\n\n\nMiniDraw.Document\nMiniDr Document
为修改字符串,双击包含字符串的行中任一位置打开String Properties对话框。

在Caption:文本框中修改字符串,使其如下(蓝色标识修改部分):
MiniDraw\n\nMiniDr\n MiniDraw Files(*.drw)\n.drw\nMiniDraw.Document\nMiniDr Document (编辑字符串时不要按Enter,文字到达文本框右边沿时会自动换行)。

插入的第一项(MiniDraw Files(*.drw))是Open或Save As对话框显示在Files of Type:或Save as type:列表框中的缺省文件扩展名。

第二项(*.drw)是缺省文件扩展名本身。

运行MiniDraw,打开或保存文件时如不指定文件扩展名,则Open和Save As对话框显示带缺省文件扩展名的所有文件,Save As对话框将缺省扩展名加到输入的文件名上。

3、支持文件菜单命令
New命令由CWinApp类的成员函数OnFileNew处理。

OnFileNew调用DeleteContents 虚拟函数删除当前文档内容,然后初始化新文档。

Open命令由CWinApp的成员函数OnFileOpen处理。

OnFileOpen显示图3.2所示的标准Open对话框。

用户选择文件并单击Open按钮时,OnFileOpen打开文件,然后调用文档类的Serialize成员函数(CMiniDrawDoc::Serialize)。

函数Serialize完成实际读取操作。

OnFileOpen还存放装入文件的完整文件路径并在主帧窗口标题条中显示文件名。

图3.2 打开文件的标准Open对话框
Save命令由CDocument类的成员函数OnFileSave处理,Save AS命令由CDocument 的成员函数OnFileSaveAs处理。

OnFileSaveAs和首次存放文档时的OnFileSave显示图3.3所示的标准Save As对话框,用户指定文件名。

OnFileSave和OnFi1eSaveAs都打开写入的文件,然后调用CMiniDrawDoc::Serialize完成实际写入操作。

图3.3 存放文件的标准Save As对话框
4、文档数据序列化
应用程序向导生成MiniDraw程序时,定义最小Serialize函数作为文档类的成员(在MiniDrawDoc.cpp文件中):
/////////////////////////////////////////////////////////////////////////////
// CMiniDrawDoc serialization
void CMiniDrawDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: add storing code here
}
else
{
// TODO: add loading code here
}
}
必须在这个最小定义中加入自己的读取和写入文档数据的代码。

为此,MFC向Serialize 传递一个MFC类CArchive实例的引用。

CArchive对象与打开文件有关,它提供—组成员函数,可以方便地从这个文件读取数据或向其写入数据。

文件打开可写时(选择Save和Save As),CArchive成员函数IsStoring返回TRUE;文件打开可读时(选择Open命令),IsStoring返回FALSE。

因此,输出代码应放在if块中,输入代码应放在else块中。

MiniDraw中,文档类中只存放一个数据成员m_LineArray,管理一组CLine对象。

好在m_LineArray对象有自己的Serialize成员函数,可以读写m_LineArray存放的所有CLine 对象。

结果,可加入CObArray::Seria1ize的两个调用来完成CMiniDrawDoc::Serialize://///////////////////////////////////////////////////////////////////////////
// CMiniDrawDoc serialization
void CMiniDrawDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: add storing code here
m_LineArray.Serialize (ar);
}
else
{
// TODO: add loading code here
m_LineArray.Serialize (ar);
}
}
数据写入文件,CObArray::Serialize对文件中存放的每个CLine对象完成两个主要步骤:
1.将对象的类信息写入文件。

2.调用对象的Serialize成员函数,其将对象的数据写入文件。

读取文件数据,CObArray::Serialize对每个CLine对象完成下列两个步骤:
1.从文件读取类信息,动态生成相应类(即Cline)的对象,并存放对象的指针。

2.调用对象的Serialize成员函数,从文件读取对象数据到新生成的对象。

必须提供支持CLine对象序列化的代码。

为此,必须首先包括两个MFC宏功能DECLARE_SERIAL和IMPLEMENT_SERIAL到CLine类定义中,必须定义缺省类构造器。

宏和缺省构造器使CObArray::Serialize首先将类消息保存在文件,然后用这个类信息动态生成相应类对象(即宏和构造器使CObArray::Serialize能完成上述两个清单中的第—步)。

将DECLARE_SERIAL宏和缺省构造器加入MiniDrawDoc.h中的CLine类定义:
class CLine : public CObject
{
protected:
int m_X1,m_Y1,m_X2,m_Y2;
CLine () {}
DECLARE_SERIAL (CLine)
public:
传递到DECLARE_SERIAL的参数只是类名。

在MiniDrawDoc.cpp的CLine::Draw函数定义上方插入IMPLEMENT_SERIAL宏://///////////////////////////////////////////////////////////////////////////
// CMiniDrawDoc commands
IMPLEMENT_SERIAL (CLine,CObject,1)
void CLine::Draw(CDC *pDC)
传递到IMPLEMENT_SERIAL的第一个参数为类名,第二个参数为其基础类名,第三个参数为程序特定版本的标识符(称为版本号schema number)。

这个代码保存在写入的数据文件中,只有指定同一版本号的程序能读取这个文件。

版本号防止一个版本的程序读取用另一版本存放的数据,其数据格式可能不同。

CLine对象序列化的第二步是,向CLine加入Serialize成员函数,CObArray::Serialize 调用它读取和写入每条直线的数据。

将Serialize声明加入MiniDrawDoc.h中CLine类定义的公用部分:
public:
CLine (int x1,int y1,int x2,int y2)
{
m_X1 = x1;
m_Y1 = y1;
m_X2 = x2;
m_Y2 = y2;
}
void Draw (CDC *pDC);
virtual void Serialize (CArchive &ar);
然后,在MiniDrawDoc.cpp文件的CLine::Draw定义后加入下列Serialize定义:
void CLine::Serialize (CArchive &ar)
{
if (ar.IsStoring ())
ar << m_X1 << m_Y1 << m_X2 << m_Y2;
ar >> m_X1 >> m_Y1 >> m_X2 >> m_Y2;
}
说明:象CLine这样要序列化的类必须直接或间接从MFC CObject类派生。

Cline::Serialize函数完成实际的读取和写入操作,而不只是调用另一个类的Serialize成员函数。

Serialize用重载<<操作符将CLine数据成员写入文件,并用重载>>操作符读取数据成员的值。

这两个重载操作符都由CArchive类定义,可并用于读取和写入各种数据类型。

结论从加入MiniDraw的文件I/O代码可以看出,存放数据的类对象通常负责数据的磁盘读写;特别地,它需提供Serialize成员函数将数据成员从磁盘文件的固定存储中读取或向其写入(面向对象编程的一个一般原则是对象操作自己的数据,例如,对象应能读取自己、写入自己、绘制自己或对自己的数据完成其他相应的操作)。

对于不是对象的数据成员,Serialize函数可用CArchive提供的重载<<和>>操作符直接写入或读取数据成员。

5、设置修改标志
CDocument类维护一个修改标志,指示文档当前是否包含未存盘数据。

MFC调用程序文档类的DeleteContents成员函数删除文档数据之前,要检查这个标志(生成新文档、打开现有文档或退出程序前要调用DeleteContents)。

如果修改标志为TRUE,表示文档包含未存盘数据,它显示一个消息,使用户可以将数据存盘。

文档首次打开和文档存盘时,CDocument将修改标志设置为FALSE。

我们的工作是,文档数据改变时调用CDocument::SetModifiedFlag函数将标志设置为TRUE。

首先在MiniDrawDoc.Cpp程序的AddLine函数中加入SetModifiedFlag的调用:
void CMiniDrawDoc::AddLine (int x1,int y1,int x2,int y2)
{
CLine *pLine = new CLine (x1,y1,x2,y2);
m_LineArray.Add(pLine);
SetModifiedFlag ();
}
在MiniDrawDoc.Cpp的OnEditClearAll函数中加入SetModifiedFlag的调用:
void CMiniDrawDoc::OnEditClearAll()
{
// TODO: Add your command handler code here
DeleteContents ();
UpdateAllViews (0);
SetModifiedFlag ();
}
最后,在MiniDrawDoc.Cpp的OnEditUndo中加入对SetModifiedFlag的调用:
void CMiniDrawDoc::OnEditUndo()
{
// TODO: Add your command handler code here
int Index = m_LineArray.GetUpperBound ();
if (Index > -1)
delete m_LineArray.GetAt (Index);
m_LineArray.RemoveAt (Index);
}
UpdateAllViews (0);
SetModifiedFlag ();
}
SetModifiedFlag参数的缺省值为TRUE,可以不传递参数值来调用函数将修改标志设置为TRUE。

为将修改标志设置为FALSE,必须明确传递数值FALSE。

6、支持拖放特性
如果程序提供Windows拖放特性的支持,则用户可以从Windows Explorer拖动文件放在程序窗口来打开该文件。

为支持MiniDraw程序的拖放特性,只需主帧窗口对象调用CWnd::DragAcceptFiles。

将调用放在MiniDraw.cpp中的InitInstance成员函数中,在调用UpdateWindow之后:BOOL CMiniDrawApp::InitInstance()
{
// other statements
// The one and only window has been initialized, so show and update it.
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
m_pMainWnd->DragAcceptFiles ();
return TRUE;
}
调用DragAcceptFiles的结果,用户将文件放到程序窗口时,自动打开文件,生成CArchive 对象,并调用文档对象Serialize函数,就象用户选择Open…菜单命令并选择该文件一样。

7、登记MiniDraw文件类型
加入消息到Windows注册表,可使用户双击文件对象打开MiniDraw文件(即文件扩展名为.drw的文件)。

为此,只需在MiniDraw.cpp的InitInstance函数定义中调用CWinApp成员函数EnableShellOpen和RegisterShellFileTypes如下:
BOOL CMiniDrawApp::InitInstance()
{
// other statements…
AddDocTemplate(pDocTemplate);
EnableShellOpen ();
RegisterShellFileTypes ();
// Parse command line for standard shell commands, DDE, file open
// other Statements…
}
第二个函数调用在Windows注册表中生成了MiniDraw缺省文件扩展名(.drw)和MiniDraw程序之间的联系。

此后,任何有这个扩展名的文件对象都显示小版本的MiniDraw 程序图标,双击该对象会运行MiniDraw程序(如果尚未运行)并使MiniDraw打开文件。

联系永久保持在注册表中,除非用Explorer或其他方法明确改变。

注意,EnableShellOpen和RegisterShellFileTypes的调用应放在加入文档模板到应用程序对象的AddDocTemplate的调用之后,使应用程序对象可用缺省文件扩展名和文档类型的消息(因为缺省扩展名在标识符为IDR_MAINFRAME的资源字符串中,而这个标识符是在生成时由模板提供的)。

这就完成了MiniDraw的修改,可以建立并运行程序了。

二、滚动和分隔视图
1、加入滚动功能
在上一版MiniDraw中,如果图形大于视图窗口的当前尺寸,则用户只能编辑视图窗口内的图形部分。

这里要向视图窗口加入垂直和水平滚动
条以及支持滚动的代码,用户可以编辑大于窗口的图形
部分。

要加入的MiniDraw滚动条如图3.4所示。

要修改MiniDraw视图类,使它从CScrollView而
不是从CView派生。

CScrollView是从CView类派生的
专用视图类。

从CScrollView派生视图类就自动将滚动
条加入了视图窗口,并提供支持滚动操作的大多数代码,
但还要自己加入一些支持代码。

图3.4 MiniDraw滚动条为修改视图类,使它从CScrollView派生,只需将MiniDrawView.h和MiniDrawView.cpp 源文件中所有出现的CView类名换成CScrollView即可。

2、坐标换算
修改程序前,要对CScrollView支持滚动的方式有一些了解。

文档首次打开时,图形左上角位于窗口左上角,但用户用滚动条滚动文档,则MFC调整一个称为视图原点的属性。

视图原点确定所画的图形或文本相对于窗口的位置。

通常,如果在坐标(0,0)上画点,则它出现在视图窗口的左上角;如果在坐标(50,100)上画点,则它会出现在视图窗口中离左边沿50象素、离顶边100象素的地方。

如果用户用垂直滚动条将文档向下滚动75个象素,则MFC调整视图原点,使两个点都出现在相对于窗口的较高位置。

这时点(0,0)已经看不到(窗口外的图形被剪去),而点(50,100)显示在离窗口顶边25像素处。

图3.5显示了滚动前后的视图窗口。

MFC响应滚动动作调整视图原点之后,MiniDraw的OnDraw 函数在视图窗口重绘直线,每条线的坐标保持原值。

但由于视图原点变了,直线自动出现在滚动后的相应位置。

OnDraw函数不需要改变就可以支持滚动,滚动逻辑是由MFC悄悄处理的。

图3.5滚动前后的视图窗口。

绘制对象时指定的坐标称为逻辑坐标,对象在窗口中的实际坐标为设备坐标。

图3.6显示了两者的差别。

传入MFC绘图函数(如MoveTo和LineTo)的坐标都是逻辑坐标,但MFC使用的某些其它坐标(如传入鼠标消息处理器的光标位置)则是设备坐标。

程序中加入滚动功能之前,逻辑坐标与设备坐标完全相等。

程序加入滚动功能之后,必须对代码进行一些修改,以便两种坐标进行换算。

图3.6 逻辑坐标与设备坐标
具体来说,需将传入鼠标消息处理函数的鼠标位置(point)从设备坐标换算为逻辑坐标,使直线在图中相应的位置画出。

为此,首先在MiniDrawView.cpp文件的OnLButtonDown函数中加入下列标着黑体的代码:
void CMiniDrawView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
CClientDC ClientDC (this);
OnPrepareDC (&ClientDC);
ClientDC.DPtoLP (&point);
// other Statements …
}
为将特定设备的设备坐标换算为逻辑坐标,必须使用与设备相关的设备描述表。

加入的第一条语句生成与视图相关的设备描述表。

第二条语句调用CScrollView成员函数OnPrepareDC,它根据图形的当前滚动位置调整设备描述表的视图原点。

第三条语句调用CDC成员函数DPtoLP,它将point中存放的光标位置用刚刚设置的新视图原点从设备坐标换算为设备描述表的逻辑坐标。

同样,需修改OnMouseMove和OnLButtonUp鼠标处理函数。

它们已生成设备描述表,只要调用OnPrepareDC和DPtoLP。

这些语句在OnMouseMove中的位置如下:void CMiniDrawView::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
::SetCursor (m_HCross);
if (m_Dragging)
{
CClientDC ClientDC (this);
OnPrepareDC (&ClientDC);
ClientDC.DPtoLP (&point);
// Other Statements
}
语句在OnLButtonUp中的位置如下:
void CMiniDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
if (m_Dragging)
{
m_Dragging = 0;
::ReleaseCapture ();
::ClipCursor (NULL);
CClientDC ClientDC (this);
OnPrepareDC (&ClientDC);
ClientDC.DPtoLP (&point);
// Other Statements
}
3、限制图形大小
将滚动框移到滚动条的特定位置时,CScrollView中的MFC代码必须滚动到图形的对应位置。

例如,拖动滚动框到垂直滚动条的底部,则MFC需滚动滚动框到图形的底部,因此,MFC必须知道图形的总体尺寸。

向MiniDraw加入向MFC报告图形尺寸的代码。

首先在视图类中定义OnInitialUpdate 虚拟函数的覆盖版本。

用C1assWizard生成函数声明和最小函数定义。

在ClassWizard对话框中,单击Message Maps标签,在Class name;和Object IDs:列表框中选择CMiniDrawView 类,在Messages:列表框中选择虚拟函数OnInitialUpdate。

加入函数并单击Edit Code按钮后,在MiniDrawView.cpp的函数定义中加入下列代码:
void CMiniDrawView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
// TODO: Add your specialized code here and/or call the base class
SIZE Size = {640,480};
SetScrollSizes (MM_TEXT,Size);
}
MFC在新的或现有文档首次在视图窗口显示前调用视图类的OnInitialUpdate函数。

调用CScrollView成员函数SetScrollSizes,告诉MFC图形尺寸。

图形的水平尺寸和垂直尺寸赋予SIZE结构,作为第二个参数传递。

在当前版本的MiniDraw中,图形设固定尺寸:640象素宽和480象素高。

说明:在MiniDraw程序中,文档是定长的,所以尺寸只需指定一次。

如果用户输入或删除数据时应用程序(如字处理程序)改变文档长度,则每次长度改变时都应调用SetScrol1Sizes,这可以在视图类的OnUpdate成员函数中完成,因为每次改变文档数据时都要调用这个函数。

传入SetScrollSizes的第一个参数指示映象方式。

映象方式指定用于绘制文本和图形的单位及坐标系统。

MiniDraw采用MM_TEXT映象方式。

在MM_TEXT映象方式中,单位都用象素,水平坐标从左向右增大,垂直坐标从上向下增大。

MiniDraw图形有特定的尺寸,所以要防止在图形区外画直线(根据当前程序,如果视图窗口大于图形,则可以在图形区外画直线,滚动无法正确处理这些直线)。

第一步要在OnDraw函数中加入绘制图形区右端和下端边框的代码,告诉程序图形的边界。

为此,在MiniDrawView.cpp文件的OnDraw函数中加入下列黑体行:
void CMiniDrawView::OnDraw(CDC* pDC)
{
……
// TODO: add draw code for native data here
CSize ScrollSize = GetTotalSize ();
pDC->MoveTo (ScrollSize.cx,0);
pDC->LineTo (ScrollSize.cx,ScrollSize.cy);
pDC->LineTo (0,ScrollSize.cy);
……
}
首先调用CScrollView成员函数GetTotalSize,它返回包含图形尺寸的CSize对象。

然后,程序用CSize对象中包含的尺寸在图形右端和下端画直线。

接着,在OnLButtonDown消息处理器中加入程序,防止用户将直线放在图形区外。

改写MiniDrawView.cpp中的OnLButtonDown:
void CMiniDrawView::OnLButtonDown(UINT nFlags, CPoint point)
{
……
// test whether cursor is within drawing area of view window
CSize ScrollSize = GetTotalSize ();
CRect ScrollRect (0,0,ScrollSize.cx,ScrollSize.cy);
if (!ScrollRect.PtInRect (point))
return;
// save cursor position,capture mouse,and set dragging flag
m_PointOrigin = point;
m_PointOld = point;
SetCapture ();
m_Dragging = 1;
// clip mouse cursor
ClientDC.LPtoDP (&ScrollRect);
CRect ViewRect;
GetClientRect (&ViewRect);
CRect IntRect;
IntRect.IntersectRect (&ScrollRect,&ViewRect);
ClientToScreen (&IntRect);
::ClipCursor (&IntRect);
CScrollView::OnLButtonDown(nFlags, point);
}
视图类接受鼠标消息,鼠标光标必须在视图窗口内,但光标可能在绘图区外。

为测定光标是否在图形内,加入的代码定义ScrollRect对象,它是MFC类CRect的实例,尺寸用图形的尺寸(调用GetTotalSize取得)。

然后,程序调用ScrollRect对象的CRect::PtinRect函数,传入光标的坐标。

只有光标坐标在ScrollRect中存放的矩形区内时,PtinRect才返回TRUE。

如果光标在图形区外,函数立即返回,所以无法在图形区外画图。

除了防止从图形区外开始画线外,OnLButtonDown还需防止将直线延伸出图形区域外。

因此,加入的代码将光标限制在视图窗口的图形区内,即将光标限制在图形区和视图窗口的相交部分。

为此,加入的代码首先调用ClientDC对象的CDC::LPtoDP函数,将存放在ScrollRect 中的图形坐标从逻辑坐标换算为设备坐标。

然后,定义CRect对象ViewRect,赋予它视图窗口的设备坐标(调用GetClientRect取得)。

接着定义另一个CRect对象IntRect并调用这个对象的CRect::InterSectRect赋予它图形区和视图窗口区相交部分的坐标。

最后,它调用CWnd::C1ientToScreen函数以将IntRect换算为屏幕坐标,并将IntRect 传递给::ClipCursor,将光标限制在相交区。

这些矩形区如图3.7所示。

图3.7 图形小于视图窗口时光标限于图形区,图形大于视图窗口时光标限于视图窗口
4、改变鼠标光标
最后可以修改程序,使鼠标光标位于视图窗口的绘图区时呈现十字形(表示可以画直线)而位于绘图区外时呈现标准箭头形状(表示不能画直线)。

为此,在MiniDrawView.h的CMiniDrawView类定义数据成员m_HArrow:
class CMiniDrawView : public CScrollView
{
protected:
CString m_ClassName;
int m_Dragging;
HCURSOR m_HArrow;
HCURSOR m_HCross;
……
}
并在MiniDrawView.cpp的CMiniDrawView构造器中初始化m_HArrow如下:
CMiniDrawView::CMiniDrawView()
{
// TODO: add construction code here
m_Dragging = 0;
m_HArrow = AfxGetApp ()->LoadStandardCursor (IDC_ARROW);
m_HCross = AfxGetApp ()->LoadStandardCursor (IDC_CROSS);
}
接着,在MiniDrawView.cpp的OnMouseMove函数中,将ClientDC的声明和OnPrepareDC 与DPtoLP的调用移到函数的开头,并加入黑体行,使函数定义如下:
void CMiniDrawView::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
CClientDC ClientDC (this);
OnPrepareDC (&ClientDC);
ClientDC.DPtoLP (&point);
CSize ScrollSize = GetTotalSize ();
CRect ScrollRect (0,0,ScrollSize.cx,ScrollSize.cy);
if (ScrollRect.PtInRect (point))
::SetCursor (m_HCross);
else
::SetCursor (m_HArrow);
if (m_Dragging)
{
ClientDC.SetROP2 (R2_NOT);
ClientDC.MoveTo (m_PointOrigin);
ClientDC.LineTo (m_PointOld);
ClientDC.MoveTo (m_PointOrigin);
ClientDC.LineTo (point);
m_PointOld = point;
}
CScrollView::OnMouseMove(nFlags, point);
}
新版0nMouseMove不是总显示十字形光标,而是光标在绘图区(ScrollRect)内时显示十字形光标,光标在绘图区外时显示箭头形光标。

可以建立并运行MiniDraw程序,来测试新加入的功能。

5、加入分隔功能
在MiniDraw程序窗口加入分隔框,使用户能将窗口分成两个独立的视窗(称板面)。

两个板面间的边框称为分隔条。

两个视图窗口显示同一图形,但每个窗口可以单独滚动,以显示图形的不同部分。

为分隔窗口,用户可以双击分隔框(将窗口分成相同的两个视图)或拖动分隔框到所需的位置。

窗口分隔时,单个垂直滚动条同时滚动两个视图,但每个视图有自己的水平滚动条,所以视图在水平方向可以单独滚动。

为给程序加入分隔功能,必须修改主帧窗口类。

首先在MainFrm.h的CMainFrame类定义开头加入m_SplitterWnd的声明如下:
class CMainFrame : public CFrameWnd
{
protected:
CSplitterWnd m_SplitterWnd;
……
}
新成员对象m_SplitterWnd是MFC类CSplitterWnd(从CWnd派生)的实例。

这个对象用于生成和管理分隔窗口。

定义OnCreateClient虚拟函数的覆盖版作为CMainFrame类成员。

用ClassWizard生成函数声明和最小函数定义。

在ClassWizard对话框中单击Message Maps标签,在C1ass name:和Object IDs:列表框中选择CMainFrame类,在Message:列表框中选择OnCreateClient 虚拟函数。

加入函数并单击Edit Code按钮后,删除CFrameWnd::OnCreateClient调用并加入黑体行,使函数变成如下形式:
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext*
pContext)
{
// TODO: Add your specialized code here and/or call the base class
return m_SplitterWnd.Create
(this, // parent of splitter window
1, // maxium rows
2, // maxium colums
CSize (15,15), // miniumum view window size
pContext); // pass on context information
}
主帧窗口首次生成时,MFC调用OnCreateClient虚拟函数。

这个函数的缺省版本(在CFrameWnd中定义)生成单个视图窗口,填满主帧窗口的客户区。

刚刚生成的这个函数的覆盖版本调用m_SplitterWnd对象的CSplitterWnd::Create函数生成分隔窗口。

分隔窗口起初生成一个视图窗口,后面用户单击分隔框时,分隔窗口就生成另一个视图窗口,提供图形的第二个视图。

传入Create的第一个参数指定分隔窗口的父窗口,传入this使分隔窗口成为主帧窗口的子窗口。

第二个参数指定垂直方向视图的最大数目,传入1表示用户不能用水平分隔条分隔窗口,因此窗口不出现水平分隔框。

同样,第三个参数指定水平方向的最大视图数,传入2表示用户可将窗口分成左右两个视图。

第四个参数设置视图窗口的最小水平尺寸和最小垂直尺寸,用户不能将分隔条移到生成更小的尺寸或垂直尺寸的视图位置。

第五个参数传递传入OnCreateClient函数的描述表消息。

6、在新应用程序中加入分隔功能
也可以用应用程序向导在生成新程序时加入分隔功能。

在应用程序向导的Step4对话框中执行下列步骤:
1.单击Advanced按钮,打开Advanced Options对话框。

2.在Advanced Options对话框中,单击Window Styles,设置主帧窗口的高级选项
3.选取Use Split Window(使用分隔窗口)选项
使应用程序向导自动生成上节介绍的代码。

可以人工调整传入CSplitterWnd::Create的参数(具体是第二、三和第四个参数)以改变最大视图窗口数和最小视图窗口尺寸。

7、更新视图
分隔窗口生成的每个视窗由单个视图对象(即CMiniDrawView类的单个实例)管理。

某个外部事件使窗口数据失效时(如用户放大窗口或删除重叠窗口),MFC自动调用视图对象的CMiniDrawView::OnDraw以重绘视图窗口。

在视窗画直线时,其他视图窗口必须重绘,使直线出现在两个视图中(假设第二个视图滚动到了包含直线的图形区)。

但MFC并不自动调用第二个视图对象的OnDraw。

在一个视图中画线之后,程序必须明确调用文档对象的CDocument::UpdateAllViews函数使MFC调用其他视图的OnDraw。

因此,需在MiniDrawView.cpp的OnLButtonUp函数中画线程序的末尾加入UpdateAllViews的调用如下:
void CMiniDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
if (m_Dragging)
{
……
pDoc->UpdateAllViews (this);
}
CScrollView::OnLButtonUp(nFlags, point);
}
UpdateAllViews函数强制所有与显示相关的文档调用OnDraw函数。

在MiniDraw中,对UpdateAllViews传入this保证只有其他视图调用OnDraw函数(当前视图不需要重绘,它已经显示新直线)。

说明:如果将0传入UpdateAllViews,则所有视图重绘。

MiniDraw程序最多可有两个视图。

提示:用户分隔MiniDraw窗口时,两个视图对象以同样的方式显示图形。

程序也可以在不同视图中显示不同文档数据。

例如,在字处理程序,一个视图可以显示完整文档文本,另一视图显示文档概要。

8、有效地重绘
有个问题,用户在一个视图中画一条线时,另一个视图的整个图形强制重绘,甚至包括当前不在视图窗口内的部分。

修改MiniDraw程序,使在一个视图中画线时,另一视图只重绘修改的和可见的图形部分,以提高视图更新的效率。

第一步提供CLine类的新成员函数GetDimRect,它提供对象中边框的尺寸,该边框包围着上述直线;这个矩形表示画线时影响的窗口部分。

在MiniDrawDoc.h的CLine定义的公开部分,加人下列GetDimRect声明:
class CLine : public CObject
{ ……
public:
……
CRect GetDimRect ();
virtual void Serialize (CArchive &ar);
};
并在MiniDrawDoc.cpp实现文件的末尾加入下列函数定义:
CRect CLine::GetDimRect ()
{
return CRect
(min (m_X1,m_X2),
min (m_Y1,m_Y2),
max (m_X1,m_X2) + 1,
max (m_Y1,m_Y2) + 1);
}
GetDimRect返回包含边界矩形尺寸的CRect对象;min宏和max宏(在Windows.h中定义)用于确保left字段小于right字段,top字段小于bottom字段(否则CRect要传入的一些函数会把该矩形理解为空的)。

下一步要修改文档类的AddLine成员函数,使它返回存放新加入直线的CLine对象指针。

改变MiniDrawDoc.h的CMiniDrawDoc定义中公用部分的AddLine声明,使它变成如下形式:
CLine *AddLine (int x1,int y1,int x2,int y2);
改变MiniDraw.cpp中的AddLine定义:
CLine *CMiniDrawDoc::AddLine (int x1,int y1,int x2,int y2)
{
CLine *pLine = new CLine (x1,y1,x2,y2);
m_LineArray.Add(pLine);
SetModifiedFlag ();
return pLine;
}
改变MiniDrawView.cpp中OnLButtonUp函数内的AddLine和UpdateAllViews调用:void CMiniDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
……
CMiniDrawDoc *pDoc = GetDocument ();
CLine *pLine;
pLine = pDoc ->AddLine (m_PointOrigin.x,m_PointOrigin.y,point.x,point.y);
pDoc->UpdateAllViews (this,0,pLine);
}
CScrollView::OnLButtonUp(nFlags, point);
}
加入的代码保存AddLine返回的新CLine对象的指针,然后将指针作为第三个参数传入UpdateAllViews。

CDocument的UpdateAllViews成员函数的格式如下:void UpdateAllViews (CView *pSender, LPARAM lHint = 0L, CObject *pHint = NULL);
第二和第三个参数是可选的,提供文档所作修改的消息(暗示)。

这个消息可用于增加视图窗口重绘的效率。

UpdateAllViews函数调用每个视图对象的OnUpdate虚拟成员函数,向OnUpdate传递两个暗示参数(lHint和pHint)的值。

OnUpdate的缺省版本(在CView中定义)忽略暗示值,使整个视图窗口重绘。

为—了提高重绘效率,应定义OnUpdate的覆盖版本,用暗示消息使只有视图窗口中受影响的区(如有)重绘。

一如既往,用ClassWizard生成函数声明和最小函数定义。

打开ClassWizard对话框并单击Message Maps标签。

然后在Class name:和Object IDs:清单中选择CMiniDrawView类,在Message:清单选择OnUpdate虚拟函数。

加入函数并单击Edit Code按钮后,在MiniDrawView.cpp的函数外壳中加入代码:void CMiniDrawView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
{
// TODO: Add your specialized code here and/or call the base class
if (pHint != 0)
{
CRect InvalidRect = ((CLine *)pHint)->GetDimRect ();
CClientDC ClientDC (this);
OnPrepareDC (&ClientDC);
ClientDC.LPtoDP (&InvalidRect);
InvalidateRect (&InvalidRect);
}
else
CScrollView::OnUpdate (pSender,lHint,pHint);
}
如果UpdateAllViews调用了OnUpdate,则pHint参数包含CLine对象的指针。

但OnInitialUpdate函数的缺省实现也调用OnUpdate(其在视图窗口首次显示图形之前接受控制),这时,pHint参数设置为0。

因此OnUpdate首先测试pHint值。

如果pHint参数包含CLine对象的指针(即非零),OnUpdate完成下列步骤:
1.用CLine指针调用Cline::GetDimRect函数,取得新加入直线的边界矩形,并保存
在CRect对象InvalidRect中
2.生成设备描述表ClientDC,调用CScrollView::OnPrepareDC,对图形的当前滚动位
置调整对象
3.调用设备描述表的CDC::LPtoDP将InvalidRect的坐标从逻辑坐标换算成设备坐标
4.将InvalidRect传递给CWnd成员函数InvalidateRect。

InvalidateRect使指定矩形区
作废,标明重绘区。

InvalidateRect还调用视图类的OnDraw函数。

传入InvalidateRect
的坐标必须是设备坐标。

但如果pHint不包含CLine指针(即为零),则OnUpdate调用OnUpdate的缺省版本,作废整个视图窗口并调用OnDraw。

当前版本OnDraw函数总想重绘整个图形,即使只有部分图形落入视图窗口的作废区。

现在改写OnDraw以便只重绘作废区,提高效率。

新版OnDraw如下:
void CMiniDrawView::OnDraw(CDC* pDC)
{
pDC->LineTo (0,ScrollSize.cy);
CRect ClipRect;
CRect DimRect;
CRect IntRect;
CLine *pLine;
pDC->GetClipBox (&ClipRect);
int Index = pDoc->GetNumLines ();
while (Index --)
{
pLine = pDoc->GetLine (Index);
DimRect = pLine->GetDimRect ();
if(IntRect.IntersectRect (DimRect,ClipRect))
pLine->Draw (pDC);
}
}
新版OnDraw调用设备描述表的CDC::GetClipBOX函数,取得作废区的尺寸。

画每条直线前,它调用存放的直线对象的GetDimRect取得直线边界矩形,然后调用IntersectRect 确定该直线的边界矩形是否落在作废区内(IntersectRect只在传入的两个矩形有重叠时才返回TRUE)。

OnDraw只在线边界矩形在作废区内时才画线。

相关文档
最新文档