![此博文包含图片](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
从程序员角度看,每个MDI 应用程序必须有一个CMDIFrameWnd 或其派生类的实例,这个窗口称作MDI 框架窗口。CMDIFrameWnd 是CFrameWnd 的派生类,它除了拥有CFrameWnd 框架窗口类的全部特性外,还具有以下与MDI 相关的特性:
与SDI 不同,主框架窗口并不直接与一个文档和视图相关联。MDI 框架窗口拥有MDICLIENT (MDI 客户窗口),在显示或隐藏控制条(包括工具条、状态栏、对话条)时,重新定位该子窗口。
MDI 客户窗口是MDI 子窗口的直接父窗口,它负责管理主框架窗口的客户区以及创建子窗口。每个MDI 主框架窗口都有且只有一个MDI 客户窗口。
MDI 主框架窗口、客户窗口和子窗口的关系如下图所示:
MDI 子窗口是CMDIChildWnd 或其派生类的实例,CMDIChildWnd 是CFrameWnd 的派生类,用于容纳视图和文档,它相当于SDI 下的主框架窗口。每打开一个文档,框架就自动为文档创建一个MDI 子窗口。一个MDI 应用程序负责动态的创建和删除MDI 子窗口。在任何时刻,最多只有一个子窗口是活动的( 窗口标题栏颜色呈高亮显示) 。MDI 框架窗口始终与当前活动子窗口相关联,命令消息在传给MDI 框架窗口之前首先分派给当前活动子窗口。
在没有任何活动的MDI 子窗口时,MDI 框架窗口可以拥有自己的缺省菜单。当有活动子窗口时,MDI框架窗口的菜单条会自动被子窗口的菜单所替代。框架会自动监视当前活动的子窗口类型,并相应的改变主窗口的菜单。比如,在Visual Studio中,当选择对话框资源编辑窗口或源程序窗口时,菜单会有所不同。但是,对于程序员来说,只需要在InitInstance 中注册文档时指定每一类子窗口(严格的讲是文档)所使用的菜单,而不必显式的通过调用函数去改变主框架窗口的菜单,因为框架会自动完成这一任务。
MDI 框架窗口为层叠、平铺、排列子窗口和新建子窗口等一些标准窗口操作提供了缺省的菜单响应。在响应新建子窗口命令时,框架调用CDocTemplate::CreateNewFrame() 为当前活动文档创建一个子窗口。CreateNewFrame() 不仅创建子窗口,还创建与文档相对应的视图。
下面,我们结合一个绘图程序例子,介绍多文档界面技术。在此之前,我们首先要介绍一下如何在Windows 中绘图以及Windows 的图形设备接口(GDI )。
窗口区域重绘
当Windows通知窗口要重绘用户区时,并非整个用户区都需要重绘,需要重绘的区域称为“无效矩形区”。用户区中出现一个无效矩形提示Windows在应用程序队列中放置WM_PAINT消息。由于WM_PAINT消息优先级最低,可调用UpdateWindows直接立即向窗口发送WM_PAINT消息,从而立即重绘。无效矩形区限制程序只能在该区域中绘图,越界的绘图将被裁剪掉。下面三个函数与无效矩形有关:
l
l
l
Windows为每个窗口保留一个PAINTSTRUCT结构,其中包含无效矩形区域的坐标值。
要想在自己的程序高效绘图、只绘制无效矩形,首先需要重载视图的OnUpdate成员函数。
virtual void CView::OnUpdate( CView* pSender, LPARAM lHint, CObject* pHint );
当调用文档的UpdateAllViews时,框架会自动调用OnUpdate函数,也可在视图类中直接调用该函数。OnUpdate函数一般是这样处理的:访问文档,读取文档的数据,然后对视图的数据成员或控制进行更新,以反映文档的改动。可以用OnUpdate函数使视图的某部分无效,以便触发视的OnDraw,利用文档数据重绘窗口。缺省的OnUpdate使窗口整个客户区都无效,在重新设计时,要利用提示信息lHint和pHint定义一个较小的无效矩形。修改后的OnUpdate成员函数如下
void CDrawView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
{
if (pHint != NULL)
{
if (pHint->IsKindOf(RUNTIME_CLASS(CStroke)))
{
CStroke* pStroke = (CStroke*)pHint;
CClientDC dc(this);
OnPrepareDC(&dc);
CRect rectInvalid = pStroke->GetBoundingRect();
dc.LPtoDP(&rectInvalid);
InvalidateRect(&rectInvalid);
return;
}
}
Invalidate(TRUE);
return;
}
这里,传给pHint指针的内容是指向需要绘制的笔画对象的指针。采用强制类型转换将它转换为笔划指针,然后取得包围该笔划的最小矩形。OnPrepareDC用于调整视图坐标原点。由于InvalidateRect需要设备坐标,因此调用LPToDP(&rectInvalid)将逻辑坐标转换为设备坐标。最后,调用InvalidateRect是窗口部分区域“无效”,也就是视图在收到WM_PAINT消息后需要重绘这一区域。
InvalidateRect函数原型为:
void InvalidateRect( LPCRECT lpRect, BOOL bErase = TRUE );
第一个参数是指向要重绘的矩形的指针,第二个参数告诉视图是否要删除区域内的背景。
这样,当需要重画某一笔划时,只需要重画包围笔划的最小矩形部分就可以了,其他部分就不再重绘。这也是为什么在笔划对象中提供最小矩形信息的原因。
如果pHint为空,则表明是一般的重绘,此时需要重绘整个客户区。
现在,在OnDraw中,根据无效矩形绘制图形,而不是重绘全部笔划,见下
void CDrawView::OnDraw(CDC* pDC)
{
CDrawDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CRect rectClip;
CRect rectStroke;
pDC->GetClipBox(&rectClip);
pDC->LPtoDP(&rectClip);
rectClip.InflateRect(1, 1); // avoid rounding to nothing
CTypedPtrList<CObList,CStroke*>& strokeList = pDoc->m_strokeList;
POSITION pos = strokeList.GetHeadPosition();
while (pos != NULL)
{
CStroke* pStroke = strokeList.GetNext(pos);
rectStroke = pStroke->GetBoundingRect();
pDC->LPtoDP(&rectStroke);
rectStroke.InflateRect(1, 1); // avoid rounding to nothing
if (!rectStroke.IntersectRect(&rectStroke, &rectClip))
continue;
pStroke->DrawStroke(pDC);
}
}
OnDraw首先调用GetClipBox取得当前被剪裁区域(无效矩形区域),它把矩形复制导GetClipBox的参数rectClip中。然后将rectClip的坐标由逻辑坐标转换为设备坐标。为了防止该矩形太小而无法包围其他内容,上下各放大一个单位。然后OnDraw遍历笔划链表中的所有笔划,获取它们的最小矩形,用IntersectRect看它是否与无效矩形相交。如果相交,说明笔划的部分或全部落在无效矩形中,此时调用笔划的DrawStroke方法画出该笔划。