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成员函数。
当调用文档的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函数原型为:
第一个参数是指向要重绘的矩形的指针,第二个参数告诉视图是否要删除区域内的背景。 这样,当需要重画某一笔划时,只需要重画包围笔划的最小矩形部分就可以了,其他部分就不再重绘。这也是为什么在笔划对象中提供最小矩形信息的原因。 如果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完成。
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 多文档绘图程序窗口 |