📄 chap8_3.htm
字号:
<p>void FinishStroke();</p>
<p>// Operations</p>
<p>public:</p>
<p>//绘制笔划</p>
<p>BOOL DrawStroke(CDC* pDC);</p>
<p>public:</p>
<p>virtual void Serialize(CArchive& ar);</p>
<p>};</p>
<p> 文档的初始化</p>
<p> 文档的初始化在OnNewDocument()和OnOpenDocument()中完成。对于Draw程序来说,两者的初始化相同,因此设计一个InitDocument()函数用于文档初始化:</p>
<b>
<p>void CDrawDoc::InitDocument()</p>
<p>{</p>
<p>m_nPenWidth=2;</p>
<p>m_nPenCur.CreatePen(PS_SOLID,m_nPenWidth,RGB(0,0,0));</p>
<p>//缺省文档大小设置为800X900个逻辑单位</p>
<p>m_sizeDoc = CSize(800,900);</p>
<p>}</p>
</b>
<p> InitDocument()函数将笔的宽度初值设为2,然后创建一个画笔对象。该对象在以后绘图是要用到。最后将文档尺寸大小设置为800X900个逻辑单位。</p>
<p> 然后在OnNewDocument()和OnOpenDocument()中调用它:</p>
<p>void CDrawDoc::OnNewDocument()</p>
<p>{</p>
<p>if (!CDocument::OnNewDocument())</p>
<p>return FALSE;</p>
<p>// TODO: add reinitialization code here</p>
<p>// (SDI documents will reuse this document)</p>
<p><b> </b></p>
<b>
<p>InitDocument();</p>
</b>
<p>return TRUE;</p>
<p>}</p>
<p>AppWizard并没有生成OnOpenDocument()的代码,因此要用ClassWizard来生成OnOpenDocument()的框架。生成框架后,在其中加入代码:</p>
<p>BOOL CDrawDoc::OnOpenDocument(LPCTSTR lpszPathName) </p>
<p>{</p>
<p>if (!CDocument::OnOpenDocument(lpszPathName))</p>
<p>return FALSE;</p>
<p> </p>
<p>// TODO: Add your specialized creation code here</p>
<p><b> </b></p>
<b>
<p>InitDocument();</p>
</b>
<p>return TRUE;</p>
<p>}</p>
<p> 文档的清理</p>
<p> 在关闭文档的最后一个子窗口时,框架要求文档清理数据。文档清理在文档类的DeleteContents()中完成。同样需要用ClassWizard生成DeleteContents的框架。</p>
<p>void CDrawDoc::DeleteContents() </p>
<p>{</p>
<p>// TODO: Add your specialized code here and/or call the base class</p>
<p><b> </b></p>
<b>
<p>while (!m_strokeList.IsEmpty())</p>
<p>{</p>
<p>delete m_strokeList.RemoveHead();</p>
<p>}</p>
</b>
<p>CDocument::DeleteContents();</p>
<p>}</p>
<p>DeleteContents()从头到尾遍里链表中的所有对象指针,并通过指针删除对象,然后用RemoveHead()删除该指针。</p>
<p> 文档的串行化</p>
<p> 现在设计文档的Serialize函数,实现文档数据的保存和载入:</p>
<p>void CDrawDoc::Serialize(CArchive& ar)</p>
<p>{</p>
<p>if (ar.IsStoring())</p>
<p>{</p>
<p><b> </b></p>
<b>
<p>ar << m_sizeDoc;</p>
</b>
<p>}</p>
<p>else</p>
<p>{</p>
<p><b> </b></p>
<b>
<p>ar >> m_sizeDoc;</p>
</b>
<p>}</p>
<p><b> </b></p>
<b>
<p>m_strokeList.Serialize(ar);</p>
</b>
<p>}</p>
<p>文档的Serialize()函数首先分别保存和载入文档大小,然后调用m_strokeList的Serialize()方法。m_strokeList.Serialize()又会自动调用存放在m_strokeList中的每一个元素CStroke的串行化方法CStroke.Serialize()最终实现文档的串行化即文档所包含的对象的存储和载入。</p>
<p> 在DrawDoc.cpp的末尾加上CStroke::Serialize()函数的定义:</p>
<b>
<p>void CStroke::Serialize(CArchive& ar)</p>
<p>{</p>
<p>if (ar.IsStoring())</p>
<p>{</p>
<p>ar << m_rectBounding;</p>
<p>ar << (WORD)m_nPenWidth;</p>
<p>m_pointArray.Serialize(ar);</p>
<p>}</p>
<p>else</p>
<p>{</p>
<p>ar >> m_rectBounding;</p>
<p>WORD w;</p>
<p>ar >> w;</p>
<p>m_nPenWidth = w;</p>
<p>m_pointArray.Serialize(ar);</p>
<p>}</p>
<p>}</p>
</b>
<p>CStroke的Serialize()依次保存(载入)笔划的矩形边界、线宽度以及点数组。注意m_nPenWidth是UINT类型的,>>和<<操作符并不支持UINT类型但却支持WORD,因此要作UINT和DWORD之间的类型转换。点数组的串行化通过调用数组的每个CPoint类元素的Serialize()完成,CPoint类是MFC类,它本身支持串行化。</p>
<p> 8.3.3 设计绘图程序的视图类</p>
<p>视图类数据成员</p>
<p> 现在着手设计绘图程序的视图类。首先,需要在视图中增加两个数据成员:</p>
<p>class CDrawView : public CScrollView</p>
<p>{</p>
<p>protected: // create from serialization only</p>
<p>CDrawView();</p>
<p>DECLARE_DYNCREATE(CDrawView)</p>
<p>// Attributes</p>
<p>public:</p>
<p>CDrawDoc* GetDocument();</p>
<p><b> </b></p>
<b>
<p>protected:</p>
<p>CStroke* m_pStrokeCur; // the stroke in progress</p>
<p>CPoint m_ptPrev; // the last mouse pt in the stroke in progress</p>
</b>
<p> // 其它数据成员和成员函数......</p>
<p> };</p>
<p> m_pStrokeCur代表正在画的那一个笔划。m_ptPrev保存鼠标上次移动位置。画图时,LineTo从这个点到当前鼠标位置画一条直线。</p>
<p> 视图初始化</p>
<p> 接下去,要初始化视图。由于是卷滚视图,因此要在OnInitialUpdate()中设置卷滚范围。在用户选择File->New菜单或File->Open菜单时,框架调用OnInitialUpdate函数。</p>
<p>void CDrawView::OnInitialUpdate()</p>
<p>{</p>
<p><b> </b></p>
<b>
<p>SetScrollSizes(MM_LOENGLISH, GetDocument()->GetDocSize());</p>
</b>
<p>CScrollView::OnInitialUpdate();</p>
<p>}</p>
<p>注意我们这里将映射模式设置为MM_LOENGLISH,MM_LOENGLISH以0.01英寸为逻辑单位,y轴方向向上递增,同MM_TEXT的y轴递增方向相反。</p>
<p> 视图绘制</p>
<p> 在CDrawView::OnDraw()内完成视图绘制工作。在以前的文档视结构程序中,在需要绘图的时侯都是绘制整个窗口。如果窗口只有很小的一部分被覆盖,是否可以只绘制那些需要重画的部分?</p>
<p> 回答是肯定的,而且大部分程序都这么做了。</p>
<p> 比如,象下图这种情况:</p>
<p> </p>
<p> 图8-5
窗口的重绘</p>
<p> 当窗口2从窗口1上移开后,只需要重画阴影线所包围的区域就够了。</p>
<p align="JUSTIFY"> 当Windows通知窗口要重绘用户区时,并非整个用户区都需要重绘,需要重绘的区域称为“无效矩形区”,如上图中的阴影区域。用户区中出现一个无效矩形提示Windows在应用程序队列中放置WM_PAINT消息。由于WM_PAINT消息优先级最低,可调用UpdateWindows直接立即向窗口发送WM_PAINT消息,从而立即重绘。无效矩形区限制程序只能在该区域中绘图,越界的绘图将被裁剪掉。下面三个函数与无效矩形有关:</p>
<p> InvalidateRect 产生一个无效矩形,并生成WM_PAINT消息</p>
<p> ValidateRect 使无效矩形区有效</p>
<p> GetUpdateRect 获得无效矩形坐标(逻辑)</p>
<p> Windows为每个窗口保留一个PAINTSTRUCT结构,其中包含无效矩形区域的坐标值。</p>
<p> 要想在自己的程序高效绘图、只绘制无效矩形,首先需要重载视图的OnUpdate成员函数。</p>
<blockquote>
<blockquote>
<p><b>virtual</b> <b>void</b> <b>CView::OnUpdate(</b> <b>CView*</b>
<i>pSender</i><b>,</b> <b>LPARAM</b> <i>lHint</i><b>,</b> <b>CObject*</b>
<i>pHint</i> <b>);</b></p>
</blockquote>
</blockquote>
<p align="JUSTIFY"> 当调用文档的UpdateAllViews时,框架会自动调用OnUpdate函数,也可在视图类中直接调用该函数。OnUpdate函数一般是这样处理的:访问文档,读取文档的数据,然后对视图的数据成员或控制进行更新,以反映文档的改动。可以用OnUpdate函数使视图的某部分无效。以便触发视的OnDraw,利用文档数据重绘窗口。缺省的OnUpdate使窗口整个客户区都无效,在重新设计时,要利用提示信息lHint和pHint定义一个较小的无效矩形。修改后的OnUpdate成员函数如清单8.5。</p>
<p> <b>清单</b><b>8.5
修改后的OnUpdate成员函数</b></p>
<p>void CDrawView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
</p>
<p>{</p>
<p>// TODO: Add your specialized code here and/or call the base class</p>
<p>// The document has informed this view that some data has changed.</p>
<p><b> </b></p>
<b>
<p>if (pHint != NULL)</p>
<p>{</p>
<p>if (pHint->IsKindOf(RUNTIME_CLASS(CStroke)))</p>
<p>{</p>
<p>// The hint is that a stroke as been added (or changed).</p>
<p>// So, invalidate its rectangle.</p>
<p>CStroke* pStroke = (CStroke*)pHint;</p>
<p>CClientDC dc(this);</p>
<p>OnPrepareDC(&dc);</p>
<p>CRect rectInvalid = pStroke->GetBoundingRect();</p>
<p>dc.LPtoDP(&rectInvalid);</p>
<p>InvalidateRect(&rectInvalid);</p>
<p>return;</p>
<p>}</p>
</b>
<p>}</p>
<p>// We can't interpret the hint, so assume that anything might</p>
<p>// have been updated.</p>
<p>Invalidate(TRUE);</p>
<p>return;</p>
<p>}</p>
<p align="JUSTIFY">这里,传给pHint指针的内容是指向需要绘制的笔画对象的指针。采用强制类型转换将它转换为笔划指针,然后取得包围该笔划的最小矩形。OnPrepareDC用于调整视图坐标原点。由于InvalidateRect需要设备坐标,因此调用LPToDP(&rectInvalid)将逻辑坐标转换为设备坐标。最后,调用InvalidateRect是窗口部分区域“无效”,也就是视图在收到WM_PAINT消息后需要重绘这一区域。</p>
<p> InvalidateRect函数原型为:</p>
<blockquote>
<blockquote>
<p>void InvalidateRect( LPCRECT lpRect, BOOL bErase = TRUE );</p>
</blockquote>
</blockquote>
<p>第一个参数是指向要重绘的矩形的指针,第二个参数告诉视图是否要删除区域内的背景。</p>
<p> 这样,当需要重画某一笔划时,只需要重画包围笔划的最小矩形部分就可以了,其他部分就不再重绘。这也是为什么在笔划对象中提供最小矩形信息的原因。</p>
<p> 如果pHint为空,则表明是一般的重绘,此时需要重绘整个客户区。</p>
<p> 现在,在OnDraw中,根据无效矩形绘制图形,而不是重绘全部笔划,见清单8.6。</p>
<p> <b>清单</b><b>8.6
根据无效矩形绘制图形的OnDraw成员函数</b></p>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -