8.3 绘图程序
在了解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所示。
图8-7 多文档绘图程序窗口