当前位置: 首页 > 文档资料 > VC 经典教程 >

8.3 绘图程序

优质
小牛编辑
124浏览
2023-12-01

在了解GDI的一些基本知识之后,我们就可以着手编写绘图程序了。这个绘图程序可以让读者用鼠标器在窗口内任意涂写,并可以保存所画的内容。这里我们参考了Visual C++的例子Scribble,并作了一些修改和简化。

8.3.1 MDI应用程序框架

首先用AppWizard生成绘图程序的基本框架:

选择File->New,弹出New对话框,选择MFC AppWizard(exe),并指定项目文件名为Draw。

在MFC AppWizard-Step1对话框中指定框架类型为Multiple Document(多文档,这是缺省设置)。

Step2,3按缺省值。在MFC AppWizard Step 4 of 6对话框中,点“Advanced...”按钮,弹出Advanced Options对话框。在File Extension编辑框中指定文件名后缀为.drw,按OK关闭Advanced Options对话框。

Step5按缺省设置。在MFC AppWizard Step 6 of 6中,在应用程序所包含的类列表中选择CDrawView,并为其指定基类为CScrollView,因为绘图程序需要卷滚文档。现在点Finish按钮生成绘图所需的应用程序框架。

在往框架里添加代码实现绘图程序之前,先看看多文档框架与单文档框架的差别。

AppWizard为多文档框架创建了以下类:

CAboutDlg:“关于”对话框

CChildFrame:子框架窗口,用于容纳视图

CDrawApp:应用程序类

CDrawDoc:绘图程序视图类

CDrawView:绘图视图类

CMainFrame:主框架窗口,用来容纳子窗口,它是多文档应用程序的主窗口。

在生成的类上,MDI比SDI多了一个CChildFrame子框架窗口类,而且CMainFrame的职责也不同了。

另外,MDI和SDI在初始化应用程序实例上也有所不同。MDI应用程序InitInstance函数如清单8.2定义。

清单8.2 多文档程序的InitInstance成员函数定义

BOOL CDrawApp::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_DRAWTYPE,

RUNTIME_CLASS(CDrawDoc),

RUNTIME_CLASS(CChildFrame), // custom MDI child frame

RUNTIME_CLASS(CDrawView));

AddDocTemplate(pDocTemplate);

// create main MDI Frame window

CMainFrame* pMainFrame = new CMainFrame;

if (!pMainFrame->LoadFrame(IDR_MAINFRAME))

return FALSE;

m_pMainWnd = pMainFrame;

// Enable drag/drop open

m_pMainWnd->DragAcceptFiles();

// Enable DDE Execute open

EnableShellOpen();

RegisterShellFileTypes(TRUE);

// Parse command line for standard shell commands, DDE, file open

CCommandLineInfo cmdInfo;

ParseCommandLine(cmdInfo);

// Dispatch commands specified on the command line

if (!ProcessShellCommand(cmdInfo))

return FALSE;

// The main window has been initialized, so show and update it.

pMainFrame->ShowWindow(m_nCmdShow);

pMainFrame->UpdateWindow();

return TRUE;

}

在注册文档模板时,首先创建一个CMultiDocTemplate类型(在SDI下是CSingleDocTemplate)的模板对象,然后用AddDocTemplate()把它加入到文档模板链表中去。

CMultiDocTemplate构造函数带四个参数,第一个参数是文档使用的资源ID定义。第二个是文档类型,第三个是子窗口类型,第四个是视图类型。

与SDI不同,由于MDI的主框架窗口并不直接与文档相对应,因此无法通过创建文档来创建主框架窗口,而需要自己去创建。

//定义一个主窗口类指针,并创建一个窗口的空的实例

CMainFrame* pMainFrame = new CMainFrame;

//从资源文件中载入菜单、图标等信息,并创建窗口

if (!pMainFrame->LoadFrame(IDR_MAINFRAME))

return FALSE;

//将应用程序对象的主窗口指针数据成员设为当前创建的窗口

m_pMainWnd = pMainFrame;

 

8.3.2 设计绘图程序的文档类

Draw需要保存用户在屏幕上涂写的每一个笔划。一副画由许多笔划组成,可以把它看作是笔划组成的链表。每一个笔划可以看作一个对象,它由许多点组成。这样,我们可以把绘图文档的数据看作是笔划对象CStroke组成的链表。另外,我们还需要一些数据成员表示当前画图所使用的画笔和画笔的宽度。

修改后的文档类声明文件如清单8-1:

清单8.3文档类声明

// DrawDoc.h : interface of the CDrawDoc class

//

/////////////////////////////////////////////////////////////////////////////

#if !defined(AFX_DRAWDOC_H__143330AE_85BC_11D1_9304_444553540000__INCLUDED_)

#define AFX_DRAWDOC_H__143330AE_85BC_11D1_9304_444553540000__INCLUDED_

#if _MSC_VER >= 1000

#pragma once

#endif // _MSC_VER >= 1000

 

class CDrawDoc : public CDocument

{

protected: // create from serialization only

CDrawDoc();

DECLARE_DYNCREATE(CDrawDoc)

// Attributes

 

public:

UINT m_nPenWidth; // current user-selected pen width

CPen m_penCur; // pen created according to

// user-selected pen style (width)

public:

CTypedPtrList<CObList,CStroke*> m_strokeList;

//获取当前使用的画笔,为视图所使用

CPen* GetCurrentPen() { return &m_penCur; }

protected:

CSize m_sizeDoc;

public:

CSize GetDocSize() { return m_sizeDoc; }

// Operations

public:

//往链表里增加一个笔划

CStroke* NewStroke();

// Operations

//用于初始化文档

protected:

void InitDocument();

// Overrides

// ClassWizard generated virtual function overrides

//{{AFX_VIRTUAL(CDrawDoc)

public:

virtual BOOL OnNewDocument();

virtual void Serialize(CArchive& ar);

//}}AFX_VIRTUAL

// Implementation

public:

virtual ~CDrawDoc();

#ifdef _DEBUG

virtual void AssertValid() const;

virtual void Dump(CDumpContext& dc) const;

#endif

protected:

// Generated message map functions

protected:

//{{AFX_MSG(CDrawDoc)

// NOTE - the ClassWizard will add and remove member functions here.

// DO NOT EDIT what you see in these blocks of generated code !

//}}AFX_MSG

DECLARE_MESSAGE_MAP()

};

 

这里我们使用 指针链表模板来保存指向每个笔划的指针:

CTypedPtrList<CObList,CStroke*> m_strokeList;

其中“<>”第一个参数表示链表基本类型,第二个参数代表链表中所存放的元素的类型。

为了使用模板,还要修改stdafx.h,在其中加入afxtempl..h头文件,它包含了使用模板时所需的类型定义和宏:

//.........

#define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers

#include <afxwin.h> // MFC core and standard components

#include <afxext.h> // MFC extensions

 

#include <afxtempl.h> // MFC templates

#include <afxdisp.h> // MFC OLE automation classes

#ifndef _AFX_NO_AFXCMN_SUPPORT

#include <afxcmn.h> // MFC support for Windows Common Controls

#endif // _AFX_NO_AFXCMN_SUPPORT

//......

由于绘图程序需要卷滚文档,因此象前面的编辑那样,增加一个m_sizeDoc数据成员存放文档的大小。另外,还需要提供一个GetDocSize()来访问它。NewStroke()用于往链表里增加一个笔划。

现在,开始设计CStroke类。笔划可以看作由一系列点组成,这样CStroke可以用一个点的数组来表示。另外,还需要一些成员函数来访问这个数组。我们还希望笔划能够自己绘制自己,并用串行化机制保存自己的数据。

CStroke类定义清单如8.4,我们把它在CDrawDoc类定义之前。

清单8.4 CStroke类定义

class CStroke : public CObject

{

public:

CStroke(UINT nPenWidth);//用笔的宽度构造一个画笔

//用于串行化笔划对象

protected:

CStroke(); //串行化对象所需的不带参数的构造函数

DECLARE_SERIAL(CStroke)

// Attributes

protected:

UINT m_nPenWidth; // one pen width applies to entire stroke

public:

//用数组模板类保存笔划的所有点

CArray<CPoint,CPoint> m_pointArray; // series of connected points

//包围笔划所有的点的一个最小矩形,关于它的作用以后会提到

CRect m_rectBounding; // smallest rect that surrounds all

// of the points in the stroke

// measured in MM_LOENGLISH units

// (0.01 inches, with Y-axis inverted)

public:

CRect& GetBoundingRect() { return m_rectBounding; }

//结束笔划,计算最小矩形

void FinishStroke();

// Operations

public:

//绘制笔划

BOOL DrawStroke(CDC* pDC);

public:

virtual void Serialize(CArchive& ar);

};

文档的初始化

文档的初始化在OnNewDocument()和OnOpenDocument()中完成。对于Draw程序来说,两者的初始化相同,因此设计一个InitDocument()函数用于文档初始化:

void CDrawDoc::InitDocument()

{

m_nPenWidth=2;

m_nPenCur.CreatePen(PS_SOLID,m_nPenWidth,RGB(0,0,0));

//缺省文档大小设置为800X900个逻辑单位

m_sizeDoc = CSize(800,900);

}

InitDocument()函数将笔的宽度初值设为2,然后创建一个画笔对象。该对象在以后绘图是要用到。最后将文档尺寸大小设置为800X900个逻辑单位。

然后在OnNewDocument()和OnOpenDocument()中调用它:

void CDrawDoc::OnNewDocument()

{

if (!CDocument::OnNewDocument())

return FALSE;

// TODO: add reinitialization code here

// (SDI documents will reuse this document)

 

InitDocument();

return TRUE;

}

AppWizard并没有生成OnOpenDocument()的代码,因此要用ClassWizard来生成OnOpenDocument()的框架。生成框架后,在其中加入代码:

BOOL CDrawDoc::OnOpenDocument(LPCTSTR lpszPathName)

{

if (!CDocument::OnOpenDocument(lpszPathName))

return FALSE;

 

// TODO: Add your specialized creation code here

 

InitDocument();

return TRUE;

}

文档的清理

在关闭文档的最后一个子窗口时,框架要求文档清理数据。文档清理在文档类的DeleteContents()中完成。同样需要用ClassWizard生成DeleteContents的框架。

void CDrawDoc::DeleteContents()

{

// TODO: Add your specialized code here and/or call the base class

 

while (!m_strokeList.IsEmpty())

{

delete m_strokeList.RemoveHead();

}

CDocument::DeleteContents();

}

DeleteContents()从头到尾遍里链表中的所有对象指针,并通过指针删除对象,然后用RemoveHead()删除该指针。

文档的串行化

现在设计文档的Serialize函数,实现文档数据的保存和载入:

void CDrawDoc::Serialize(CArchive& ar)

{

if (ar.IsStoring())

{

 

ar << m_sizeDoc;

}

else

{

 

ar >> m_sizeDoc;

}

 

m_strokeList.Serialize(ar);

}

文档的Serialize()函数首先分别保存和载入文档大小,然后调用m_strokeList的Serialize()方法。m_strokeList.Serialize()又会自动调用存放在m_strokeList中的每一个元素CStroke的串行化方法CStroke.Serialize()最终实现文档的串行化即文档所包含的对象的存储和载入。

在DrawDoc.cpp的末尾加上CStroke::Serialize()函数的定义:

void CStroke::Serialize(CArchive& ar)

{

if (ar.IsStoring())

{

ar << m_rectBounding;

ar << (WORD)m_nPenWidth;

m_pointArray.Serialize(ar);

}

else

{

ar >> m_rectBounding;

WORD w;

ar >> w;

m_nPenWidth = w;

m_pointArray.Serialize(ar);

}

}

CStroke的Serialize()依次保存(载入)笔划的矩形边界、线宽度以及点数组。注意m_nPenWidth是UINT类型的,>>和<<操作符并不支持UINT类型但却支持WORD,因此要作UINT和DWORD之间的类型转换。点数组的串行化通过调用数组的每个CPoint类元素的Serialize()完成,CPoint类是MFC类,它本身支持串行化。

8.3.3 设计绘图程序的视图类

视图类数据成员

现在着手设计绘图程序的视图类。首先,需要在视图中增加两个数据成员:

class CDrawView : public CScrollView

{

protected: // create from serialization only

CDrawView();

DECLARE_DYNCREATE(CDrawView)

// Attributes

public:

CDrawDoc* GetDocument();

 

protected:

CStroke* m_pStrokeCur; // the stroke in progress

CPoint m_ptPrev; // the last mouse pt in the stroke in progress

// 其它数据成员和成员函数......

};

m_pStrokeCur代表正在画的那一个笔划。m_ptPrev保存鼠标上次移动位置。画图时,LineTo从这个点到当前鼠标位置画一条直线。

视图初始化

接下去,要初始化视图。由于是卷滚视图,因此要在OnInitialUpdate()中设置卷滚范围。在用户选择File->New菜单或File->Open菜单时,框架调用OnInitialUpdate函数。

void CDrawView::OnInitialUpdate()

{

 

SetScrollSizes(MM_LOENGLISH, GetDocument()->GetDocSize());

CScrollView::OnInitialUpdate();

}

注意我们这里将映射模式设置为MM_LOENGLISH,MM_LOENGLISH以0.01英寸为逻辑单位,y轴方向向上递增,同MM_TEXT的y轴递增方向相反。

视图绘制

在CDrawView::OnDraw()内完成视图绘制工作。在以前的文档视结构程序中,在需要绘图的时侯都是绘制整个窗口。如果窗口只有很小的一部分被覆盖,是否可以只绘制那些需要重画的部分?

回答是肯定的,而且大部分程序都这么做了。

比如,象下图这种情况:

图8-5 窗口的重绘

当窗口2从窗口1上移开后,只需要重画阴影线所包围的区域就够了。

当Windows通知窗口要重绘用户区时,并非整个用户区都需要重绘,需要重绘的区域称为“无效矩形区”,如上图中的阴影区域。用户区中出现一个无效矩形提示Windows在应用程序队列中放置WM_PAINT消息。由于WM_PAINT消息优先级最低,可调用UpdateWindows直接立即向窗口发送WM_PAINT消息,从而立即重绘。无效矩形区限制程序只能在该区域中绘图,越界的绘图将被裁剪掉。下面三个函数与无效矩形有关:

InvalidateRect 产生一个无效矩形,并生成WM_PAINT消息

ValidateRect 使无效矩形区有效

GetUpdateRect 获得无效矩形坐标(逻辑)

Windows为每个窗口保留一个PAINTSTRUCT结构,其中包含无效矩形区域的坐标值。

要想在自己的程序高效绘图、只绘制无效矩形,首先需要重载视图的OnUpdate成员函数。

virtual void CView::OnUpdate( CView* pSender, LPARAM lHint, CObject* pHint );

当调用文档的UpdateAllViews时,框架会自动调用OnUpdate函数,也可在视图类中直接调用该函数。OnUpdate函数一般是这样处理的:访问文档,读取文档的数据,然后对视图的数据成员或控制进行更新,以反映文档的改动。可以用OnUpdate函数使视图的某部分无效。以便触发视的OnDraw,利用文档数据重绘窗口。缺省的OnUpdate使窗口整个客户区都无效,在重新设计时,要利用提示信息lHint和pHint定义一个较小的无效矩形。修改后的OnUpdate成员函数如清单8.5。

清单8.5 修改后的OnUpdate成员函数

void CDrawView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)

{

// TODO: Add your specialized code here and/or call the base class

// The document has informed this view that some data has changed.

 

if (pHint != NULL)

{

if (pHint->IsKindOf(RUNTIME_CLASS(CStroke)))

{

// The hint is that a stroke as been added (or changed).

// So, invalidate its rectangle.

CStroke* pStroke = (CStroke*)pHint;

CClientDC dc(this);

OnPrepareDC(&dc);

CRect rectInvalid = pStroke->GetBoundingRect();

dc.LPtoDP(&rectInvalid);

InvalidateRect(&rectInvalid);

return;

}

}

// We can't interpret the hint, so assume that anything might

// have been updated.

Invalidate(TRUE);

return;

}

这里,传给pHint指针的内容是指向需要绘制的笔画对象的指针。采用强制类型转换将它转换为笔划指针,然后取得包围该笔划的最小矩形。OnPrepareDC用于调整视图坐标原点。由于InvalidateRect需要设备坐标,因此调用LPToDP(&rectInvalid)将逻辑坐标转换为设备坐标。最后,调用InvalidateRect是窗口部分区域“无效”,也就是视图在收到WM_PAINT消息后需要重绘这一区域。

InvalidateRect函数原型为:

void InvalidateRect( LPCRECT lpRect, BOOL bErase = TRUE );

第一个参数是指向要重绘的矩形的指针,第二个参数告诉视图是否要删除区域内的背景。

这样,当需要重画某一笔划时,只需要重画包围笔划的最小矩形部分就可以了,其他部分就不再重绘。这也是为什么在笔划对象中提供最小矩形信息的原因。

如果pHint为空,则表明是一般的重绘,此时需要重绘整个客户区。

现在,在OnDraw中,根据无效矩形绘制图形,而不是重绘全部笔划,见清单8.6。

清单8.6 根据无效矩形绘制图形的OnDraw成员函数

void CDrawView::OnDraw(CDC* pDC)

{

CDrawDoc* pDoc = GetDocument();

ASSERT_VALID(pDoc);

 

// Get the invalidated rectangle of the view, or in the case

// of printing, the clipping region of the printer dc.

CRect rectClip;

CRect rectStroke;

pDC->GetClipBox(&rectClip);

pDC->LPtoDP(&rectClip);

rectClip.InflateRect(1, 1); // avoid rounding to nothing

// Note: CScrollView::OnPaint() will have already adjusted the

// viewport origin before calling OnDraw(), to reflect the

// currently scrolled position.

// The view delegates the drawing of individual strokes to

// CStroke::DrawStroke().

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);

}

// TODO: add draw code for native data here

}

OnDraw首先调用GetClipBox取得当前被剪裁区域(无效矩形区域),它把矩形复制导GetClipBox的参数rectClip中。然后将rectClip的坐标由逻辑坐标转换为设备坐标。为了防止该矩形太小而无法包围其他内容,上下各放大一个单位。然后OnDraw遍历笔划链表中的所有笔划,获取它们的最小矩形,用IntersectRect看它是否与无效矩形相交。如果相交,说明笔划的部分或全部落在无效矩形中,此时调用笔划的DrawStroke方法画出该笔划。

图8-6 根据包围笔划 的矩形是否与无效

矩形相交 ,判断笔划是否落入无效矩形中

为了获得笔划的最小包围矩形,需要在结束笔划时计算出包围笔划的最小矩形。因此为笔划提供两个方法:一个是FinishStroke(),用于在笔划结束时计算最小矩形,见清单8.7。

清单8.7 CStroke::FinishStroke()成员函数

void CStroke::FinishStroke()

{

// Calculate the bounding rectangle. It's needed for smart

// repainting.

if (m_pointArray.GetSize()==0)

{

m_rectBounding.SetRectEmpty();

return;

}

CPoint pt = m_pointArray[0];

m_rectBounding = CRect(pt.x, pt.y, pt.x, pt.y);

for (int i=1; i < m_pointArray.GetSize(); i++)

{

// If the point lies outside of the accumulated bounding

// rectangle, then inflate the bounding rect to include it.

pt = m_pointArray[i];

m_rectBounding.left = min(m_rectBounding.left, pt.x);

m_rectBounding.right = max(m_rectBounding.right, pt.x);

m_rectBounding.top = max(m_rectBounding.top, pt.y);

m_rectBounding.bottom = min(m_rectBounding.bottom, pt.y);

}

// Add the pen width to the bounding rectangle. This is necessary

// to account for the width of the stroke when invalidating

// the screen.

m_rectBounding.InflateRect(CSize(m_nPenWidth, -(int)m_nPenWidth));

return;

}

另一个是DrawStroke(),用于绘制笔划:

BOOL CStroke::DrawStroke(CDC* pDC)

{

CPen penStroke;

if (!penStroke.CreatePen(PS_SOLID, m_nPenWidth, RGB(0,0,0)))

return FALSE;

CPen* pOldPen = pDC->SelectObject(&penStroke);

pDC->MoveTo(m_pointArray[0]);

for (int i=1; i < m_pointArray.GetSize(); i++)

{

pDC->LineTo(m_pointArray[i]);

}

pDC->SelectObject(pOldPen);

return TRUE;

}

鼠标绘图

鼠标绘图基本过程是:用户按下鼠标左键时开始绘图,在鼠标左键按下且移动过程中不断画线跟踪鼠标位置,当松开鼠标左键结束绘图。因此,需要处理三个消息:WM_LBUTTONDOWN、WM_MOUSEMOVE、WM_LBUTTONUP。用ClassWizard为上述三个消息生成消息处理函数,并在其中手工加入代码,修改后的成员函数如下:

清单8.8 鼠标消息处理函数OnLButtonDown()

void CDrawView::OnLButtonDown(UINT nFlags, CPoint point)

{

// TODO: Add your message handler code here and/or call default

// Pressing the mouse button in the view window starts a new stroke

// CScrollView changes the viewport origin and mapping mode.

// It's necessary to convert the point from device coordinates

// to logical coordinates, such as are stored in the document.

CClientDC dc(this);

OnPrepareDC(&dc);

dc.DPtoLP(&point);

m_pStrokeCur = GetDocument()->NewStroke();

// Add first point to the new stroke

m_pStrokeCur->m_pointArray.Add(point);

SetCapture(); // Capture the mouse until button up.

m_ptPrev = point; // Serves as the MoveTo() anchor point for the

// LineTo() the next point, as the user drags the

// mouse.

return;

}

 

在鼠标左键按下,首先获得鼠标按下的位置坐标。由于它是设备坐标,因此先用DPToLP将它转换为逻辑坐标。在此之前,要用OnPrepareDC()对视图坐标原点进行调整。然后用CDrawDoc的NewStroke()成员函数创建一个笔划对象,并将笔划对象加入到笔划链表中。然后,将当前点坐标加入道笔划对象内部的点数组中。以后,当鼠标移动时,OnMouseMove就不断修改该笔划对象的内部数据成员(加入新的点到笔划对象的数组中)。另外,为了用LineTo画出线条,需要将当前鼠标位置保存到m_ptPrev中,以便出现一个新的点时,画一条从m_ptPrev到新的点的直线。

但是,由于用户的鼠标可以在屏幕上任意移动。当鼠标移出窗口外时,窗口无法收到鼠标消息。此时,如果松开了鼠标左键,应用程序由于无法接受到该条消息而不会终止当前笔划,这样就造成了错误。如何避免这种情况发生呢?解决的办法是要让窗口在鼠标移出窗口外时仍然能接受到鼠标消息。幸好,Windows提供了一个API函数SetCapture()解决了这一问题。

CWnd::SetCapture()用于捕获鼠标:无论鼠标光标位置在何处,都会将鼠标消息送给调用它的那一个窗口。在用完后,需要用ReleaseCapture()释放窗口对鼠标的控制,否则其他窗口将无法接收到鼠标消息。这一工作当然最好在鼠标左键松开OnLButtonUp()时来做。

清单8.9 OnLButtonUp消息处理函数

void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)

{

// TODO: Add your message handler code here and/or call default

 

// Mouse button up is interesting in the draw application

// only if the user is currently drawing a new stroke by dragging

// the captured mouse.

if (GetCapture() != this)

return; // If this window (view) didn't capture the mouse,

// then the user isn't drawing in this window.

CDrawDoc* pDoc = GetDocument();

CClientDC dc(this);

// CScrollView changes the viewport origin and mapping mode.

// It's necessary to convert the point from device coordinates

// to logical coordinates, such as are stored in the document.

OnPrepareDC(&dc); // set up mapping mode and viewport origin

dc.DPtoLP(&point);

CPen* pOldPen = dc.SelectObject(pDoc->GetCurrentPen());

dc.MoveTo(m_ptPrev);

dc.LineTo(point);

dc.SelectObject(pOldPen);

m_pStrokeCur->m_pointArray.Add(point);

// Tell the stroke item that we're done adding points to it.

// This is so it can finish computing its bounding rectangle.

m_pStrokeCur->FinishStroke();

// Tell the other views that this stroke has been added

// so that they can invalidate this stroke's area in their

// client area.

pDoc->UpdateAllViews(this, 0L, m_pStrokeCur);

ReleaseCapture(); // Release the mouse capture established at

// the beginning of the mouse drag.

return;

}

 

OnLButtonUp首先检查鼠标是否被当前窗口所捕获,如果不是则返回。然后画出笔划最后两点之间的极短的直线段。接着,调用CStroke::FinishStroke(),请求CStroke对象计算它的最小矩形。然后调用pDoc->UpdateAllViews(this, 0L, m_pStrokeCur)通知其他视图更新显示。

当一个视图修改了文档内容并更新显示时,一般的其它的对应于同一文档的视图也需要相应更新,这通过调用文档的成员函数UpdateAllViews完成。

void UpdateAllViews( CView* pSender, LPARAM lHint = 0L, CObject* pHint =

NULL );

UpdateAllViews带三个参数:pSender指向修改文档的视图。由于该视图已经作了更新,所以不再需要更新。比如,在上面的例子中,OnLButtonUp已经绘制了视图,因此不需要再次更新。如果为NULL,则文档对应的所有视图都被更新。

lHint和pHint包含了更新视图时所需的附加信息。在本例中,其他视图只需要重画当前绘制中的笔划,因此OnLButtonUp把当前笔划指针传给UpdateAllViews函数。该函数调用文档所对应的除pSender外的所有视图的OnUpdate函数,并将lHint和pHint传给OnUpdate函数通知更新附加信息。

OnLButtonUp最后释放对鼠标的控制,这样别的应用程序窗口就可以获得鼠标消息了。

结合上面讲到的知识,读者不难自行理解下面的OnMouseMove函数。

void CDrawView::OnMouseMove(UINT nFlags, CPoint point)

{

// TODO: Add your message handler code here and/or call default

// Mouse movement is interesting in the Scribble application

// only if the user is currently drawing a new stroke by dragging

// the captured mouse.

if (GetCapture() != this)

return; // If this window (view) didn't capture the mouse,

// then the user isn't drawing in this window.

CClientDC dc(this);

// CScrollView changes the viewport origin and mapping mode.

// It's necessary to convert the point from device coordinates

// to logical coordinates, such as are stored in the document.

OnPrepareDC(&dc);

dc.DPtoLP(&point);

m_pStrokeCur->m_pointArray.Add(point);

// Draw a line from the previous detected point in the mouse

// drag to the current point.

CPen* pOldPen = dc.SelectObject(GetDocument()->GetCurrentPen());

dc.MoveTo(m_ptPrev);

dc.LineTo(point);

dc.SelectObject(pOldPen);

m_ptPrev = point;

return;

}

至此,绘图程序的文档、视图全部设计完了,现在编译运行程序。程序启动后,在空白窗口中徒手绘图,如图8-7所示。

T8_7.tif (267124 bytes)

图8-7 多文档绘图程序窗口