发布时间:2025-12-09 21:51:29 浏览次数:3
数据和界面操作分离
由于CDocument 派生自CObject,所以它就有了CObject 所支持的一切性质,包括运行时类型识别(RTTI)、动态生成(Dynamic Creation)、文件读写(Serialization)。又由于它也派生自CCmdTarget,所以它可以接收来自菜单或工具栏的WM_COMMAND 消息。
view 负责展示信息,常用做法是 创建一个类继承自 CView并重写 OnDraw 函数。
CView 继承CWnd,所以它可以接收一般Windows 消息(如WM_SIZE、WM_PAINT 等等)。
它也继承CCmdTarget,所以它可以接收来自菜单或工具栏的WM_COMMAND 消息。
在window sdk编程中,一般调用BeginPaint获取设备描述表DC绘画来处理WM_PAINT。
在MFC中,WM_PAINT消息还会触发 OnDraw 函数。
认为是view的载体,view窗口之下有frame
Document Template负责管理view、frame、document。可派生出
CMultiDocTemplate,还有一个成员变量CPtrList m_docList;操作多种类型文件;
CSingleDocTemplate,还有一个成员变量CDocument* m_pOnlyDoc;操作单文档。
成员m_pDocClass、m_pViewClass、m_pFrameClass动态生成、管理文档、视图、窗口
CWinApp管理CDocTemplate,如下所示:
BOOL CScribbleApp::InitInstance(){...CMultiDocTemplate* pDocTemplate;pDocTemplate = new CMultiDocTemplate(IDR_SCRIBTYPE,RUNTIME_CLASS(CScribbleDoc),RUNTIME_CLASS(CChildFrame),RUNTIME_CLASS(CScribbleView));AddDocTemplate(pDocTemplate);...}以打开、创建文档为例,虽然打开文档事件的响应是cwinapp
BEGIN_MESSAGE_MAP(CScribbleApp, CWinApp)ON_COMMAND(ID_APP_ABOUT, OnAppAbout)ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)END_MESSAGE_MAP()但是实际操作创建文档及视图的是Document Template,其流程如下:
函数调用流程:
CWinApp::OnFileNew→CDocManager::OnFileNew→CWinApp::OpenDocumentFile→CDocManager::OpenDocumentFile→CMultiDocTemplate::OpenDocumentFile
CreateNewDocument 利用CRuntimeClass的CreateObject 动态产生Document,CreateNewFrame 同样利用CRuntimeClass的CreateObject 动态产生Document Frame。
// in DOCTEMPL.CPPCDocument* CDocTemplate::CreateNewDocument(){...CDocument* pDocument = (CDocument*)m_pDocClass->CreateObject();...AddDocument(pDocument);return pDocument;}CFrameWnd* CDocTemplate::CreateNewFrame(CDocument* pDoc, CFrameWnd* pOther){// create a frame wired to the specified documentCCreateContext context;context.m_pCurrentFrame = pOther;context.m_pCurrentDoc = pDoc;context.m_pNewViewClass = m_pViewClass;context.m_pNewDocTemplate = this;...CFrameWnd* pFrame = (CFrameWnd*)m_pFrameClass->CreateObject();...// create new from resourcepFrame->LoadFrame(m_nIDResource,WS_OVERLAPPEDWINDOW | FWS_ADDTOTITLE, // default frame stylesNULL, &context)//应该是这里创建了view...return pFrame;}窗口创建过程:
总结:
MFC容器类:
| List | Yes | No | 快 | 慢 | 可 |
| Array | Yes | Yes(利用整数索引值) | 慢 | 慢 | 可 |
| Map | No | Yes(利用键) | 快 | 快 | 键(key)不可复制,值(value)可复制 |
MFC Collection classes 容器存放的对象,有两种特别需要说明,一是Ob 一是Ptr:
MFC的容器:
MFC容器还包含序列化、打印信息等功能需要考虑。
还可分类为:
关于 CArray 的定义CArray<TYPE, ARG_TYPE>:
TYPE是CArray所存储的数据的类型
ARG_TYPE则主要用于insert,add等等,
即传入参数的类型,对简单数据可以跟type一样,对类和struct,可以用引用。
一个参数为数组元素的类型,该例中是CPoint,即m_Array是CPoint数组;第二个参数为引用类型,一般有两种选择,一种选择与第一个参数类型相同,它意味着数组对象作为参数传递时,传递的是数组对象。第二种选择是第一个参数类型的引用,它意味着数组对象作为参数传递时,传递的是数组对象的指针。因此,尤其对于较复杂的数组结构类型,推荐使用引用传递,节约内存同时加快程序运行速度
还有就是 存放容器的容器(个人认为,也没看懂):
使用如CTypedPtrList<CObList,CStroke*> m_strokeList;(CStroke是CObList的派生类吧?)
class CScribbleDoc : public CDocument的一个成员:
CTypedPtrList<CObList,CStroke*> m_strokeList;(理解为对象指针的链表)
而CStroke类有两个成员:
UINT m_nPenWidth;
CArray<CPoint,CPoint> m_pointArray;(又是一个数组)
CScribbleDoc 内嵌一个CObList 对象,CObList 中的每个元素都是一个CStroke 对象指针,而CStroke 又内嵌一个CArray 对象。
代码(未验证)
//SCRIBBLEDOC.H/// class CStroke//// A stroke is a series of connected points in the scribble drawing.// A scribble document may have multiple strokes.class CStroke : public CObject{public:CStroke(UINT nPenWidth);protected:CStroke();DECLARE_SERIAL(CStroke)// Attributesprotected:UINT m_nPenWidth; // one pen width applies to entire strokepublic:CArray<CPoint, CPoint> m_pointArray; // series of connected points// Operationspublic:BOOL DrawStroke(CDC* pDC);public:virtual void Serialize(CArchive& ar);};/class CScribbleDoc : public CDocument{protected: // create from serialization onlyCScribbleDoc();DECLARE_DYNCREATE(CScribbleDoc)// Attributesprotected:// The document keeps track of the current pen width on// behalf of all views. We'd like the user interface of// Scribble to be such that if the user chooses the Draw// Thick Line command, it will apply to all views, not just// the view that currently has the focus.UINT m_nPenWidth; // current user-selected pen widthCPen m_penCur; // pen created according to// user-selected pen style (width)public:CTypedPtrList<CObList, CStroke*> m_strokeList;CPen* GetCurrentPen() { return &m_penCur; }// Operationspublic:CStroke* NewStroke();// Overrides// ClassWizard generated virtual function overrides//{{AFX_VIRTUAL(CScribbleDoc)public:virtual BOOL OnNewDocument();virtual void Serialize(CArchive& ar);virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);virtual void DeleteContents();//}}AFX_VIRTUAL// Implementationpublic:virtual ~CScribbleDoc();#ifdef _DEBUGvirtual void AssertValid() const;virtual void Dump(CDumpContext& dc) const;#endifprotected:void InitDocument();// Generated message map functionsprotected://{{AFX_MSG(CScribbleDoc)// NOTE - the ClassWizard will add and remove member functions here.// DO NOT EDIT what you see in these blocks of generated code !//}}AFX_MSGDECLARE_MESSAGE_MAP()}; //Scribble.cpp#include "stdafx.h"#include "Scribble.h"#include "ScribbleDoc.h"#ifdef _DEBUG#define new DEBUG_NEW#undef THIS_FILEstatic char THIS_FILE[] = __FILE__;#endif/// CScribbleDocIMPLEMENT_DYNCREATE(CScribbleDoc, CDocument)BEGIN_MESSAGE_MAP(CScribbleDoc, CDocument)//{{AFX_MSG_MAP(CScribbleDoc)// NOTE - the ClassWizard will add and remove mapping macros here.// DO NOT EDIT what you see in these blocks of generated code!//}}AFX_MSG_MAPEND_MESSAGE_MAP()// CScribbleDoc construction/destructionCScribbleDoc::CScribbleDoc(){// TODO: add one-time construction code here}CScribbleDoc::~CScribbleDoc(){}BOOL CScribbleDoc::OnNewDocument(){if (!CDocument::OnNewDocument())return FALSE;InitDocument();return TRUE;}/// CScribbleDoc serializationvoid CScribbleDoc::Serialize(CArchive& ar){if (ar.IsStoring()){}else{}m_strokeList.Serialize(ar);}/// CScribbleDoc diagnostics#ifdef _DEBUGvoid CScribbleDoc::AssertValid() const{CDocument::AssertValid();}void CScribbleDoc::Dump(CDumpContext& dc) const{CDocument::Dump(dc);}#endif //_DEBUGBOOL CScribbleDoc::OnOpenDocument(LPCTSTR lpszPathName){if (!CDocument::OnOpenDocument(lpszPathName))return FALSE;InitDocument();return TRUE;}void CScribbleDoc::DeleteContents(){while (!m_strokeList.IsEmpty()){delete m_strokeList.RemoveHead();}CDocument::DeleteContents();}void CScribbleDoc::InitDocument(){m_nPenWidth = 2; // default 2 pixel pen width// solid, black penm_penCur.CreatePen(PS_SOLID, m_nPenWidth, RGB(0, 0, 0));}CStroke* CScribbleDoc::NewStroke(){CStroke* pStrokeItem = new CStroke(m_nPenWidth);m_strokeList.AddTail(pStrokeItem);SetModifiedFlag(); // Mark the document as having been modified, for// purposes of confirming File Close.return pStrokeItem;}// CStrokeIMPLEMENT_SERIAL(CStroke, CObject, 1)CStroke::CStroke(){// This empty constructor should be used by serialization only}CStroke::CStroke(UINT nPenWidth){m_nPenWidth = nPenWidth;}void CStroke::Serialize(CArchive& ar){if (ar.IsStoring()){ar << (WORD)m_nPenWidth;m_pointArray.Serialize(ar);}else{WORD w;ar >> w;m_nPenWidth = w;m_pointArray.Serialize(ar);}}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;}CScribbleDoc用于存储线条信息,每个线条是一系列的点集,其结构如下:
线条的绘制放在CStroke中,其过程如下
主要为设计 CScribbleView 类。
当Framework 收到WM_PAINT,表示画面需要重绘,它会先调用OnPaint,OnPaint 再调用OnDraw,由OnDraw 执行真正的绘图动作。
什么时候会产生重绘消息WM_PAINT 呢:当使用者改变窗口大小,或是将窗口最小化之后再恢复原状,或是来自程序(自己或别人)刻意的制造。除了在必须重绘时重绘之外,做为一个绘图软件,Scribble 还必须「实时」反应鼠标左键在窗口上移动的轨迹,不能等到WM_PAINT 产生了才有所反应。所以,我们必须在OnMouseMove 中也做绘图动作,那是针对一个点一个点的绘图,而OnDraw 是
大规模的全部重绘。
创建基于单个文档的mfc工程,其中 CscribbledocView 代码:
//头文件// scribbledocView.h: CscribbledocView 类的接口#pragma onceclass CscribbledocView : public CView{protected: // 仅从序列化创建CscribbledocView() noexcept;DECLARE_DYNCREATE(CscribbledocView)// 特性public:CscribbledocDoc* GetDocument() const;protected:CStroke* m_pStrokeCur; // the stroke in progressCPoint m_ptPrev; // the last mouse pt in the stroke in progress// 操作public:// 重写public:virtual void OnDraw(CDC* pDC); // 重写以绘制该视图virtual BOOL PreCreateWindow(CREATESTRUCT& cs);protected:virtual BOOL OnPreparePrinting(CPrintInfo* pInfo);virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);// 实现public:virtual ~CscribbledocView();#ifdef _DEBUGvirtual void AssertValid() const;virtual void Dump(CDumpContext& dc) const;#endifprotected:// 生成的消息映射函数protected:afx_msg void OnFilePrintPreview();//afx_msg void OnRButtonUp(UINT nFlags, CPoint point);//afx_msg void OnContextMenu(CWnd* pWnd, CPoint point);afx_msg void OnLButtonDown(UINT nFlags, CPoint point);afx_msg void OnLButtonUp(UINT nFlags, CPoint point);afx_msg void OnMouseMove(UINT nFlags, CPoint point);DECLARE_MESSAGE_MAP()};#ifndef _DEBUG // scribbledocView.cpp 中的调试版本inline CscribbledocDoc* CscribbledocView::GetDocument() const{ return reinterpret_cast<CscribbledocDoc*>(m_pDocument); }#endif //实现文件// scribbledocView.cpp: CscribbledocView 类的实现#include "pch.h"#include "framework.h"// SHARED_HANDLERS 可以在实现预览、缩略图和搜索筛选器句柄的// ATL 项目中进行定义,并允许与该项目共享文档代码。#ifndef SHARED_HANDLERS#include "scribbledoc.h"#endif#include "scribbledocDoc.h"#include "scribbledocView.h"#ifdef _DEBUG#define new DEBUG_NEW#endif// CscribbledocViewIMPLEMENT_DYNCREATE(CscribbledocView, CView)BEGIN_MESSAGE_MAP(CscribbledocView, CView)// 标准打印命令ON_COMMAND(ID_FILE_PRINT, &CView::OnFilePrint)ON_COMMAND(ID_FILE_PRINT_DIRECT, &CView::OnFilePrint)ON_COMMAND(ID_FILE_PRINT_PREVIEW, &CscribbledocView::OnFilePrintPreview)//ON_WM_CONTEXTMENU()//ON_WM_RBUTTONUP()ON_WM_LBUTTONDOWN()ON_WM_LBUTTONUP()ON_WM_MOUSEMOVE()END_MESSAGE_MAP()// CscribbledocView 构造/析构CscribbledocView::CscribbledocView() noexcept{// TODO: 在此处添加构造代码}CscribbledocView::~CscribbledocView(){}BOOL CscribbledocView::PreCreateWindow(CREATESTRUCT& cs){// TODO: 在此处通过修改// CREATESTRUCT cs 来修改窗口类或样式return CView::PreCreateWindow(cs);}// CscribbledocView 绘图void CscribbledocView::OnDraw(CDC* pDC){CscribbledocDoc* pDoc = GetDocument();ASSERT_VALID(pDoc);//if (!pDoc)//return;CTypedPtrList<CObList, CStroke*>& strokeList = pDoc->m_strokeList;POSITION pos = strokeList.GetHeadPosition();while (pos != NULL){CStroke* pStroke = strokeList.GetNext(pos);pStroke->DrawStroke(pDC);}// TODO: 在此处为本机数据添加绘制代码}// CscribbledocView 打印void CscribbledocView::OnFilePrintPreview(){#ifndef SHARED_HANDLERSAFXPrintPreview(this);#endif}BOOL CscribbledocView::OnPreparePrinting(CPrintInfo* pInfo){// 默认准备return DoPreparePrinting(pInfo);}void CscribbledocView::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/){// TODO: 添加额外的打印前进行的初始化过程}void CscribbledocView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/){// TODO: 添加打印后进行的清理过程}//void CscribbledocView::OnRButtonUp(UINT /* nFlags */, CPoint point)//{//ClientToScreen(&point);//OnContextMenu(this, point);//}////void CscribbledocView::OnContextMenu(CWnd* /* pWnd */, CPoint point)//{//#ifndef SHARED_HANDLERS//theApp.GetContextMenuManager()->ShowPopupMenu(IDR_POPUP_EDIT, point.x, point.y, this, TRUE);//#endif//}// CscribbledocView 诊断#ifdef _DEBUGvoid CscribbledocView::AssertValid() const{CView::AssertValid();}void CscribbledocView::Dump(CDumpContext& dc) const{CView::Dump(dc);}CscribbledocDoc* CscribbledocView::GetDocument() const // 非调试版本是内联的{ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CscribbledocDoc)));return (CscribbledocDoc*)m_pDocument;}#endif //_DEBUG// CscribbledocView 消息处理程序// CScribbleView message handlersvoid CscribbledocView::OnLButtonDown(UINT, CPoint point){// Pressing the mouse button in the view window starts a new strokem_pStrokeCur = GetDocument()->NewStroke();// Add first point to the new strokem_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;}void CscribbledocView::OnLButtonUp(UINT, CPoint point){// Mouse button up 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.CscribbledocDoc* pDoc = GetDocument();CClientDC dc(this);CPen* pOldPen = dc.SelectObject(pDoc->GetCurrentPen());dc.MoveTo(m_ptPrev);dc.LineTo(point);dc.SelectObject(pOldPen);m_pStrokeCur->m_pointArray.Add(point);ReleaseCapture(); // Release the mouse capture established at// the beginning of the mouse drag.return;}void CscribbledocView::OnMouseMove(UINT, CPoint point){// 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);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;}成员变量:
m_pStrokeCur:一个指针,指向目前正在工作的线条。
m_ptPrev:线条中的前一个工作点。我们将在这个点与目前鼠标按下的点之间画一条直线。虽说理想情况下鼠标轨迹的每一个点都应该被记录下来,但如果鼠标移动太快来不及记录,只好在两点之间拉直线。
成员函数:
OnDraw:这是一个虚拟函数,负责将Document 的数据显示出来。
GetDocument:AppWizard 为我们创建出来,以inline 方式定义于头文件,其中m_pDocument 是CView 的成员变量。可以推测,当程序设定好Document Template 之后,每次Framework 动态产生View 对象,其内的m_pDocument 已经被Framework 设定指向对应Document 。
OnPreparePrinting,OnBeginPrinting,OnEndPrinting:这三个CView 函数将用于打印。AppWizard 只是先帮我们做出空函数。
在OnDraw函数中,获取 Document,遍历其中的每条曲线(m_strokeList),调用曲线 CStroke::DrawStroke的方法绘制线段。
涉及两个CObList 成员函数:
GetNext:返回当前位置的数据,之后位置后移一位。
GetHeadPosition:传回链表的第一个元素的「位置」。
OnLButtonDown:
// 当鼠标左键按下,
// 利用CScribbleDoc::NewStroke 产生一个新的线条空间;
// 利用CArray::Add 把这个点加到线条上去;
// 调用SetCapture 取得鼠标捕捉权(mouse capture);
// 把这个点记录为「上一点」(m_ptPrev);
OnMouseMove:
// 当鼠标左键按住并开始移动,
// 利用CArray::Add 把新坐标点加到线条上;
// 在上一点(m_ptPrev)和这一点之间画直线;
// 把这个点记录为「上一点」(m_ptPrev);
OnLButtonUp:
// 当鼠标左键放开,
// 在上一点(m_ptPrev)和这一点之间画直线;
// 利用CArray::Add 把新的点加到线条上;
// 调用ReleaseCapture() 释放鼠标捕捉权(mouse capture)。
类视图中右键要修改的类,选择 类向导,即可弹出设置弹窗。
也就是存储和读取文件,对象必须能够永续生存,也就是它们必须能够在程序结束时储存到文件中,并且在程序重新激活时再恢复回来。
如 将文件 mydoc.doc 的所有文字转换为小写:
char* pBuffer = new char[0x1000];try {CFile file("mydoc.doc", CFile::modeReadWrite);DWORD dwBytesRemaining = file.GetLength();UINT nBytesRead;DWORD dwPosition;while (dwBytesRemaining) {dwPosition = file.GetPosition();nBytesRead = file.Read(pBuffer, 0x1000);::CharLowerBuff(pBuffer, nBytesRead);file.Seek((LONG)dwPosition, CFile::begin);file.Write(pBuffer, nBytesRead);dwBytesRemaining -= nBytesRead;}}catch (CFileException* e) {if (e->cause == CFileException::fileNoteFound)MessageBox("File not found");else if (e->cause == CFileException::tooManyOpeFiles)MessageBox("File handles not enough");else if (e->cause == CFileException::hardIO)MessageBox("Hardware error");else if (e->cause == CFileException::diskFull)MessageBox("Disk full");else if (e->cause == CFileException::badPath)MessageBox("All or part of the path is invalid");elseMessageBox("Unknown file error");e->Delete();}delete[] pByffer;调用 Serialize 时会传一个CArchive 对象,可以认为它代表一个文件,通过其IsStoring 成员函数,即可知道究竟要读还是写。
CObList 的Serialize 函数源代码:
void CObList::Serialize(CArchive& ar){ASSERT_VALID(this);CObject::Serialize(ar);if (ar.IsStoring()){ar.WriteCount(m_nCount);for (CNode* pNode = m_pNodeHead; pNode != NULL;pNode = pNode->pNext){ //ASSERT(AfxIsValidAddress(pNode, sizeof(CNode)));ar << pNode->data;}}else{DWORD nNewCount = ar.ReadCount();CObject* newData;while (nNewCount--){ //ar >> newData;AddTail(newData);}}}CDWordArray 的Serialize 函数源代码:
void CDWordArray::Serialize(CArchive& ar){ASSERT_VALID(this);CObject::Serialize(ar);if (ar.IsStoring()){ar.WriteCount(m_nSize); //ar.Write(m_pData, m_nSize * sizeof(DWORD)); //}else{DWORD nOldSize = ar.ReadCount();SetSize(nOldSize); //ar.Read(m_pData, m_nSize * sizeof(DWORD)); //}}CArray 的Serialize 函数源代码:
template<class TYPE>void AFXAPI SerializeElements(CArchive& ar, TYPE* pElements, int nCount){ASSERT(nCount == 0 ||AfxIsValidAddress(pElements, nCount * sizeof(TYPE)));// default is bit-wise read/writeif (ar.IsStoring())ar.Write((void*)pElements, nCount * sizeof(TYPE));elsear.Read((void*)pElements, nCount * sizeof(TYPE));}template<class TYPE, class ARG_TYPE>void CArray<TYPE, ARG_TYPE>::Serialize(CArchive& ar){ASSERT_VALID(this);CObject::Serialize(ar);if (ar.IsStoring()){ar.WriteCount(m_nSize);}else{DWORD nOldSize = ar.ReadCount();SetSize(nOldSize, -1);}SerializeElements(ar, m_pData, m_nSize);}其中 CObList 对象的 serialize 方法会内部调用各个链表中对象的 serialize 方法(通过多态、虚函数等);而CArray 对象可直接使用,内部将数组各个元素序列号,不必参与其内部过程。
内部通过封装使用CFile的CArchive类进行文件的读写,类的动态生成通过各个类的DECLARE_DYNCREATE / IMPLEMENT_DYNCREATE宏完成。
此处省略大篇代码。
DECLARE_DYNAMIC / IMPLEMENT_DYNAMIC:
afx.h
如
class CFoo : public CObject{DECLARE_DYNAMIC(CFoo)...}//变成class CFoo : public CObject{public:static AFX_DATA CRuntimeClass classCFoo;virtual CRuntimeClass* GetRuntimeClass() const;...}afx.h
#define IMPLEMENT_DYNAMIC(class_name, base_class_name) \_IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, 0xFFFF, NULL)#define _IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, pfnNew) \AFX_DATADEF CRuntimeClass class_name::class##class_name = { \#class_name, sizeof(class class_name), wSchema, pfnNew, \RUNTIME_CLASS(base_class_name), NULL }; \static const AFX_CLASSINIT _init_##class_name(&class_name::class##class_name);CRuntimeClass* class_name::GetRuntimeClass() const \{ return &class_name::class##class_name; } \#define RUNTIME_CLASS(class_name) \(&class_name::class##class_name)如:
IMPLEMENT_DYNAMIC(CFoo, CObject)//变成AFX_DATADEF CRuntimeClass CFoo::classCFoo = {"CFoo", sizeof(class CFoo), 0xFFFF, NULL, &CObject::classCObject, NULL }; static const AFX_CLASSINIT _init_CFoo(&CFoo::classCFoo); CRuntimeClass* CFoo::GetRuntimeClass() const {return &CFoo::classCFoo; }afx.h
#define DECLARE_SERIAL(class_name) \DECLARE_DYNCREATE(class_name) \friend CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb);如
class CDynobj : public CObject{DECLARE_SERIAL(CDynobj)...}//变成class CDynobj : public CObject{static AFX_DATA CRuntimeClass classCDynobj;virtual CRuntimeClass* GetRuntimeClass() const;static CObject* PASCAL CreateObject();friend CArchive& AFXAPI operator>>(CArchive& ar, CDynobj* &pOb);...}afx.h
#define IMPLEMENT_SERIAL(class_name, base_class_name, wSchema) \CObject* PASCAL class_name::CreateObject() \{ return new class_name; } \_IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, \class_name::CreateObject) \CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb) \{ pOb = (class_name*) ar.ReadObject(RUNTIME_CLASS(class_name)); \return ar; } \如
IMPLEMENT_SERIAL(CDynobj, CObject, 1)//变成CObject* PASCAL CDynobj::CreateObject() \{ return new CDynobj; } \AFX_DATADEF CRuntimeClass CDynobj::classCDynobj = {"CDynobj", sizeof(CDynobj), 1, CDynobj::CreateObject,&CObject::classCObject, NULL };static const AFX_CLASSINIT _init_CDynobj(&CDynobj::classCDynobj);CRuntimeClass* CDynobj::GetRuntimeClass() const{ return &CDynobj::classCDynobj; }CArchive& AFXAPI operator>>(CArchive& ar, CDynobj* &pOb){ pOb = (CDynobj*)ar.ReadObject(&CDynobj::classCDynobj);return ar;}实现步骤:
document尺寸:
将CScribbleView 的父类由CView 改变为CScrollView。
// in SCRIBBLEVIEW.Hclass CScribbleView : public CScrollView{public:virtual void OnInitialUpdate();...};// in SCRIBBLEVIEW.CPPIMPLEMENT_DYNCREATE(CScribbleView, CScrollView)BEGIN_MESSAGE_MAP(CScribbleView, CScrollView)...END_MESSAGE_MAP()改写OnInitialUpdate,在其中设定滚动条范围。这个函数的被调用时机是在View 第一次附着到Document 但尚未显现时,由Framework 调用。它会调用OnUpdate。
// in SCRIBVW.CPPvoid CScribbleView::OnInitialUpdate(){SetScrollSizes(MM_TEXT, GetDocument()->GetDocSize());//这是CScrollView 的成员函数。}SetScrollSizes 总共有四个参数:int nMapMode:代表映射模式(Mapping Mode)SIZE sizeTotal:代表文件大小const SIZE& sizePage:代表一页大小(默认是文件大小的1/10)const SIZE& sizeLine:代表一行大小(默认是文件大小的1/100)当滚动条移动了DC 原点,CScrollView 自动会做调整,让资料的某一部份显示而某一部份隐藏。
如果我们想在绘图之前(也就是进入OnDraw 之前)调整DC , 我们可以改写虚函数OnPrepareDC , 因为Framework 是先调用OnPrepareDC,然后才调用OnDraw 并把DC 传进去。CScrollView 已经改写了CView的OnPrepareDC 虚函数。Framework 先调用CScrollView::OnPrepareDC 再调用CScribbleView::OnDraw,所有因为滚动条而必须做的特别处理都已经在进入OnDraw 之前完成了。
DC 就是Device Context,在Windows 中凡绘图动作之前一定要先获得一个DC,它可能代表屏幕,也可能代表一个窗口,或一块内存,或打印机…。DC 中有许多绘图所需的元素,包括坐标系统(映射模式)、原点、绘图工具(笔、刷、颜色…)等等。
由于鼠标消息的坐标为设备坐标,需要转换为逻辑坐标供绘图使用。
// in SCRIBVW.CPPvoid CScribbleView::OnLButtonDown(UINT, CPoint point){//由于CScrollView 改变了DC 原点和映射模式,所以我们必须先把装置坐标转换为逻辑坐标,再储存到Document 中//CClientDC dc(this);OnPrepareDC(&dc);dc.DPtoLP(&point);m_pStrokeCur = GetDocument()->NewStroke();m_pStrokeCur->m_pointArray.Add(point);SetCapture(); //m_ptPrev = point; //做为直线绘图的第一个点return;}void CScribbleView::OnLButtonUp(UINT, CPoint point){...if (GetCapture() != this)return;CScribbleDoc* pDoc = GetDocument();CClientDC dc(this);OnPrepareDC(&dc); //设定映射模式和DC 原点dc.DPtoLP(&point);...}void CScribbleView::OnMouseMove(UINT, CPoint point){...if (GetCapture() != this)return;CClientDC dc(this);OnPrepareDC(&dc);dc.DPtoLP(&point);m_pStrokeCur->m_pointArray.Add(point);...} void CScribbleView::OnUpdate(CView* /* pSender */, LPARAM /* lHint */,CObject* pHint){if (pHint != NULL){if (pHint->IsKindOf(RUNTIME_CLASS(CStroke))){// hintCStroke* pStroke = (CStroke*)pHint;CClientDC dc(this);OnPrepareDC(&dc);CRect rectInvalid = pStroke->GetBoundingRect();dc.LPtoDP(&rectInvalid);InvalidateRect(&rectInvalid);return;}}//Invalidate(TRUE);return;}需要三个窗口协作:
修改前:
修改:
新增类CScribbleFrame,父类为CMDIChildWnd。
成员对象:CSplitterWnd 对象,名为m_wndSplitter
改写OnCreateClient 虚函数,调用m_wndSplitter.Create 产生拆分窗口、设定窗口个数、设定窗口的最初尺寸等初始状态。
修改后:
貌似不是这么简单,测试成功一个,参考https://blog.csdn.net/u012750259/article/details/43016919
用MFC AppWizard创建一个默认的单文档应用程序。
打开框架窗口类MainFrm.h头文件,为CMainFrame类添加一个保护型的切分窗口的数据成员,如下面的定义:
CSplitterWnd m_wndSplitter;
用MFC类向导创建一个新的视图类CDemoView(基类为CView)用于与静态切分的窗格相关联。
重写CMainFrame类的OnCreateClient函数(当主框架窗口客户区创建的时候自动调用该函数),并添加下列代码:
Windows 的所有绘图指令,都集中在GDI 模块之中,称为GDI 绘图函数,例如:
TextOut(hPr, 50, 50, szText, strlen(szText)); // 输出一字符串Rectangle(hPr, 10, 10, 50, 40); // 画一个四方形Ellipse(hPr, 200, 50, 250, 80); // 画一个椭圆形Pie(hPr, 350, 50, 400, 100, 400, 50, 400, 100); // 画一个圆饼图MoveTo(hPr, 50, 100); // 将画笔移动到新位置LineTo(hPr, 400, 50); // 从前一位置画直线到新位置根据DC(也就是第一个参数hPr)的不同,图形被绘制到不同地方(屏幕、打印机)。
显示器DC可以通过GetDC或BeginPaint 函数所获得的,如:
打印机DC可以CreateDC 获得:
HDC hPr;hPr = CreateDC(lpPrintDriver, lpPrintType, lpPrintPort, (LPSTR) NULL);//参数是与打印机有关的信息字符串(驱动,类型,端口吧)计算打印的行数等:
TEXTMETRIC TextMetric;int LineSpace;int nPageSize;int LinesPerPage;GetTextMetrics(hPr, &TextMetric); //取得字题数据LineSpace = TextMetric.tmHeight + TextMetric.tmExternalLeading; //计算字高nPageSize = GetDeviceCaps(hPr, VERTRES); //取得纸张大小LinesPerPage = nPageSize / LineSpace - 1; //一页容纳多少行然后再循环将每一行文字送往打印机:
Escape(hPr, STARTDOC, 4, "PrntFile text", (LPSTR) NULL);CurrentLine = 1;for (...) {... //取得一行文字,放在char pLine[128] 中,长度为LineLength。TextOut(hPr, 0, CurrentLine*LineSpace, (LPSTR)pLine, LineLength);if (++CurrentLine > LinesPerPage ) {CurrentLine = 1; //重设行号IOStatus = Escape(hPr, NEWFRAME, 0, 0L, 0L); //换页if (IOStatus < 0 || bAbort)break;}}if (IOStatus >= 0 && !bAbort) {Escape(hPr, NEWFRAME, 0, 0L, 0L);Escape(hPr, ENDDOC, 0, 0L, 0L);}其中的Escape 用来传送命令给打印机(打印机命令一般称为escape code),它是一个·Windows API 函数。
打印过程中提供一个中断机制给使用者。通过 Modeless(非模态) 对话框 实现。类似如下:
需要在开始打印前创建对话框:
如果点击了按钮,则进行中断:
int FAR PASCAL PrintingDlg(HWND hDlg, unsigned msg, WORD wParam, LONG lParam){switch(msg) {case WM_COMMAND:return (bAbort = TRUE);case WM_INITDIALOG:SetFocus(GetDlgItem(hDlg, IDCANCEL));SetDlgItemText(hDlg, IDC_FILENAME, FileName);return (TRUE);}return (FALSE);}打印过程会产生记录用的临时文件,为了及时清除或者其他操作,可以准备一个回调函数,供操作用,通常名为AbortProc:
FARPROC lpAbortProc;lpAbortProc = MakeProcInstance(AbortProc, hInst);Escape(hPr, SETABORTPROC, NULL, (LPSTR)(long)lpAbortProc, (LPSTR)NULL);GDI 模块在执行Escape(hPr, NEWFRAME…) 的过程中会持续调用这个callback 函数
int FAR PASCAL AbortProc(hDC hPr, int Code){MSG msg;while (!bAbort && PeekMessage(&msg, NULL, NULL, NULL, TRUE))if (!IsDialogMessage(hAbortDlgWnd, &msg)) {TranslateMessage(&msg);DispatchMessage(&msg);}return (!bAbort);}
SDK代码如下:
在MFC 应用程序中,View 和application framework 分工合力完成打印工作。
Applicationframework 的责任是:
View 对象上的责任是:
使用框架创建工程时,如果选择打印功能,会自动加入对应消息事件:
// in SCRIBBLEVIEW.Hclass CScribbleView : public CScrollView{...protected:virtual BOOL OnPreparePrinting(CPrintInfo* pInfo);virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);...};// in SCRIBBLEVIEW.CPPBOOL CScribbleView::OnPreparePrinting(CPrintInfo* pInfo){// default preparationreturn DoPreparePrinting(pInfo);}void CScribbleView::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/){// TODO: add extra initialization before printing}void CScribbleView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/){// TODO: add cleanup after printing}当【File/Print…】被按下,命令消息将由CView::OnFilePrint 去处理
// in VIEWPRNT.CPPvoid CView::OnFilePrint(){// get default print infoCPrintInfo printInfo; //****//ASSERT(printInfo.m_pPD != NULL); // must be setif (GetCurrentMessage()->wParam == ID_FILE_PRINT_DIRECT){CCommandLineInfo *pCmdInfo = AfxGetApp()->m_pCmdInfo;if (pCmdInfo != NULL){if (pCmdInfo->m_nShellCommand == CCommandLineInfo::FilePrintTo) //**利用::CreateDC产生一个「打印机DC**//{printInfo.m_pPD->m_pd.hDC = ::CreateDC(pCmdInfo->m_strDriverName,pCmdInfo->m_strPrinterName, pCmdInfo->m_strPortName, NULL);if (printInfo.m_pPD->m_pd.hDC == NULL){AfxMessageBox(AFX_IDP_FAILED_TO_START_PRINT);return;}}}printInfo.m_bDirect = TRUE;//跳过打印对话框,直接打印}if (OnPreparePrinting(&printInfo)) //弹出对话框配置打印的设置{// hDC must be set (did you remember to call DoPreparePrinting?)ASSERT(printInfo.m_pPD->m_pd.hDC != NULL);// gather file to print to if print-to-file selectedCString strOutput;if (printInfo.m_pPD->m_pd.Flags & PD_PRINTTOFILE)//弹窗选择要打印到的文件{// construct CFileDialog for browsingCString strDef(MAKEINTRESOURCE(AFX_IDS_PRINTDEFAULTEXT));CString strPrintDef(MAKEINTRESOURCE(AFX_IDS_PRINTDEFAULT));CString strFilter(MAKEINTRESOURCE(AFX_IDS_PRINTFILTER));CString strCaption(MAKEINTRESOURCE(AFX_IDS_PRINTCAPTION));CFileDialog dlg(FALSE, strDef, strPrintDef,OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, strFilter);dlg.m_ofn.lpstrTitle = strCaption;if (dlg.DoModal() != IDOK)return;// set output device to resulting path namestrOutput = dlg.GetPathName();}// set up document info and start the document printing process//****//CString strTitle;CDocument *pDoc = GetDocument();if (pDoc != NULL)strTitle = pDoc->GetTitle();elseGetParentFrame()->GetWindowText(strTitle);if (strTitle.GetLength() > 31)strTitle.ReleaseBuffer(31);DOCINFO docInfo;memset(&docInfo, 0, sizeof(DOCINFO));docInfo.cbSize = sizeof(DOCINFO);docInfo.lpszDocName = strTitle;CString strPortName;int nFormatID;if (strOutput.IsEmpty()){docInfo.lpszOutput = NULL;strPortName = printInfo.m_pPD->GetPortName();nFormatID = AFX_IDS_PRINTONPORT;}else{docInfo.lpszOutput = strOutput;AfxGetFileTitle(strOutput,strPortName.GetBuffer(_MAX_PATH), _MAX_PATH);nFormatID = AFX_IDS_PRINTTOFILE;}// setup the printing DC//****//CDC dcPrint;dcPrint.Attach(printInfo.m_pPD->m_pd.hDC); // attach printer dc「打印机DC」附着到CDC 对象上dcPrint.m_bPrinting = TRUE;OnBeginPrinting(&dcPrint, &printInfo); //事件函数,原本什么也没做。可以改写它,设定打印前的任何初始状态。dcPrint.SetAbortProc(_AfxAbortProc); //****//// disable main window while printing & init printing status dialogAfxGetMainWnd()->EnableWindow(FALSE); //父窗口非使能CPrintingDialog dlgPrintStatus(this);//创建打印状态对话框CString strTemp;dlgPrintStatus.SetDlgItemText(AFX_IDC_PRINT_DOCNAME, strTitle);dlgPrintStatus.SetDlgItemText(AFX_IDC_PRINT_PRINTERNAME,printInfo.m_pPD->GetDeviceName());AfxFormatString1(strTemp, nFormatID, strPortName);dlgPrintStatus.SetDlgItemText(AFX_IDC_PRINT_PORTNAME, strTemp);dlgPrintStatus.ShowWindow(SW_SHOW);dlgPrintStatus.UpdateWindow();// start document printing processif (dcPrint.StartDoc(&docInfo) == SP_ERROR) //通知打印机开始打印工作。这个函数其实就是激活Windows 打印引擎{// enable main window before proceedingAfxGetMainWnd()->EnableWindow(TRUE);// cleanup and show error messageOnEndPrinting(&dcPrint, &printInfo);dlgPrintStatus.DestroyWindow();dcPrint.Detach(); // will be cleaned up by CPrintInfo destructorAfxMessageBox(AFX_IDP_FAILED_TO_START_PRINT);return;}// Guarantee values are in the valid rangeUINT nEndPage = printInfo.GetToPage();UINT nStartPage = printInfo.GetFromPage();if (nEndPage < printInfo.GetMinPage())nEndPage = printInfo.GetMinPage();if (nEndPage > printInfo.GetMaxPage())nEndPage = printInfo.GetMaxPage();if (nStartPage < printInfo.GetMinPage())nStartPage = printInfo.GetMinPage();if (nStartPage > printInfo.GetMaxPage())nStartPage = printInfo.GetMaxPage();int nStep = (nEndPage >= nStartPage) ? 1 : -1;nEndPage = (nEndPage == 0xffff) ? 0xffff : nEndPage + nStep;VERIFY(strTemp.LoadString(AFX_IDS_PRINTPAGENUM));// begin page printing loopBOOL bError = FALSE;for (printInfo.m_nCurPage = nStartPage; printInfo.m_nCurPage != nEndPage; printInfo.m_nCurPage += nStep) {//针对文件中的每一页开始做打印动作OnPrepareDC(&dcPrint, &printInfo); //事件函数。如果要在每页前面加表头,就改写这个虚函数。// check for end of printif (!printInfo.m_bContinuePrinting)break;// write current pageTCHAR szBuf[80];wsprintf(szBuf, strTemp, printInfo.m_nCurPage);dlgPrintStatus.SetDlgItemText(AFX_IDC_PRINT_PAGENUM, szBuf); //修改【打印状态】对话框中的页数// set up drawing rect to entire page (in logical coordinates)printInfo.m_rectDraw.SetRect(0, 0,dcPrint.GetDeviceCaps(HORZRES),dcPrint.GetDeviceCaps(VERTRES));dcPrint.DPtoLP(&printInfo.m_rectDraw);// attempt to start the current pageif (dcPrint.StartPage() < 0) //StartPage 开始新的一页;调用windows API ::StartPage{bError = TRUE;break;}// must call OnPrepareDC on newer versions of Windows because// StartPage now resets the device attributes.if (afxData.bMarked4)OnPrepareDC(&dcPrint, &printInfo);ASSERT(printInfo.m_bContinuePrinting);// page successfully started, so now render the pageOnPrint(&dcPrint, &printInfo); //OnPrint调用OnDraw; 我们应该在CScribbleView 中改写OnDraw 以绘出自己的图形if (dcPrint.EndPage() < 0 || !_AfxAbortProc(dcPrint.m_hDC, 0)) //一页结束,调用dcPrint.EndPage ::EndPage{bError = TRUE;break;}}// cleanup document printing processif (!bError)dcPrint.EndDoc(); //文件结束,调用EndDoc ::EndDocelsedcPrint.AbortDoc();AfxGetMainWnd()->EnableWindow(); // enable main windowOnEndPrinting(&dcPrint, &printInfo); // clean up after printing//可以在OnEndPrinting进行绘图资源需要释放dlgPrintStatus.DestroyWindow(); //关闭 打印状态 对话框dcPrint.Detach(); // will be cleaned up by CPrintInfo destructor//将「打印机DC」解除绑定,CPrintInfo 的析构会把DC 还给Windows。}}关键步骤说明:
对话框类:
class CPrintDialog : public CCommonDialog{public:PRINTDLG& m_pd;BOOL GetDefaults();LPDEVMODE GetDevMode() const; // return DEVMODECString GetDriverName() const; // return driver nameCString GetDeviceName() const; // return device nameCString GetPortName() const; // return output port nameHDC GetPrinterDC() const; // return HDC (caller must delete)HDC CreatePrinterDC();...};OnPreparePrinting 是一个虚函数,也就是上面说到的事件函数,该函数默认会调用CView::DoPreparePrinting 将贮存在CPrintInfo 结构中的对话框CPrintDialog* m_pPD显示出来,在这个对话框中使用者对打印机进行各种设定,然后产生一个「打印机DC」,储存在printinfo.m_pPD->m_pd.hDC, 默认调用过程如下:
如果使用者在【打印】对话框中选按【打印到文件】,则再显示一个【Print to File】弹窗选择要打印到的文件。
设置中断用的回调函数,使用默认的回调函数_AfxAbortProc
产生打印状态对话框
通知打印机开始新的打印工作
整体流程 及对应 响应事件函数如下:
补充说明:
OnPrint与OnDraw 区别:
OnPrint:负责「只在打印时才做(屏幕显示时不做)」的动作。例如印出表头和页尾。
OnDraw :共通性绘图动作(包括输出到屏幕或打印机上)都在此完成。
还有长得很像的OnPaint:
// in VIEWCORE.CPPvoid CView::OnPaint(){// standard paint routineCPaintDC dc(this);OnPrepareDC(&dc);OnDraw(&dc);}窗口有滚动条而打印机没有,这伴随而来的就是必须计算Document 的大小和纸张的大小,以解决分页的问题,还包括:
关于打印的大部份信息都记录在CPrintInfo 中(页码从1(而不是0)开始。)
| GetMinPage/SetMinPage | Document 中的第一页 |
| GetMaxPage/SetMaxPage | Document 中的最后一页 |
| GetFromPage | 将被印出的第一页(出现在【打印】对话框,图12-1b) |
| GetToPage | 将被印出的最后一页(出现在【打印】对话框) |
| m_nCurPage | 目前正被印出的一页(出现在【打印状态】对话框) |
| m_nNumPreviewPages | 预览窗口中的页数 |
CPrintInfo 结构体中记录的「页」数,指的是打印机的页数;Framework 针对每一「页」调用OnPrepareDC 以及OnPrint 时,所指的「页」也是打印机的页。当你改写OnPreparePrinting 时指定Document 的长度,所用的单位也是打印机的「页」。如果Document 的一页不等于打印机的一页(一张纸),必须在两者之间做转换。
此次目标打印样子为:
第一页只是单纯印出文件名称(文件名称),第二页才是文件内容。
第二页(文件内容)最顶端留一点空间,做为表头。
配置GDI 对象的最理想时机是OnBeginPrinting:
释放GDI 对象的最理想时机是在OnEndPrinting,这是每当一份打印工作结束后,Application Framework 会调用的函数。
CScribbleDoc 的成员变量m_sizeDoc,用来记录Document 的大小。它是一个CSize 对象
修改映射模式:
void CScribbleView::OnInitialUpdate(){SetScrollSizes(MM_LOENGLISH, GetDocument()->GetDocSize());//原来是MM_TEXTCScrollView::OnInitialUpdate();}MM_TEXT 是Y 轴向下,MM_LOENGLISH(以及其它任何映射模式)是Y 轴向上。点坐标是先经过DPtoLP 才储存到CStroke 对象并且然后才由LineTo 画出的,所以这里不受影响;但是用到用到CRect 的地方需要调整。
//在FinishStroke for (int i=1; i < m_pointArray.GetSize(); i++){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);}m_rectBounding.InflateRect(CSize(m_nPenWidth, -(int)m_nPenWidth));矩形的最顶点位置应该是找Y 坐标最小者;而在Y 轴向上的系统中,矩形的最顶点位置应该是找Y 坐标最大者
在OnDraw 中以IntersectRect 计算两个四方形是否有交集,这里需要使用设备坐标系:修改为
void CScribbleView::OnDraw(CDC* pDC){CScribbleDoc* 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 inpidual 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 nothingif (!rectStroke.IntersectRect(&rectStroke, &rectClip))continue;pStroke->DrawStroke(pDC);}}本例一份Document 打印时被视为一张标题和一张图片的组合,因此打印一份Document 固定要耗掉两张打印纸。
BOOL CScribbleView::OnPreparePrinting(CPrintInfo* pInfo){pInfo->SetMaxPage(2); //文件总共有两页经线://第一页是标题页(title page)//第二页是文件页(图形)BOOL bRet = DoPreparePrinting(pInfo); // default preparationpInfo->m_nNumPreviewPages = 2; // Preview 2 pages at a time// Set this value after calling DoPreparePrinting to override// value read from .INI filereturn bRet;}设计一个函数用以输出标题页,一个函数用以输出文件页(OnDraw 负责)。
class CScribbleView : public CScrollView{public:virtual void OnPrint(CDC* pDC, CPrintInfo* pInfo);void PrintTitlePage(CDC* pDC, CPrintInfo* pInfo);void PrintPageHeader(CDC* pDC, CPrintInfo* pInfo, CString& strHeader);...} void CScribbleView::OnPrint(CDC *pDC, CPrintInfo *pInfo){if (pInfo->m_nCurPage == 1) // page no. 1 is the title page{PrintTitlePage(pDC, pInfo);return; // nothing else to print on page 1 but the page title}CString strHeader = GetDocument()->GetTitle();PrintPageHeader(pDC, pInfo, strHeader);// PrintPageHeader() subtracts out from the pInfo->m_rectDraw the// amount of the page used for the header.pDC->SetWindowOrg(pInfo->m_rectDraw.left, -pInfo->m_rectDraw.top);// Now print the rest of the pageOnDraw(pDC);}void CScribbleView::PrintTitlePage(CDC *pDC, CPrintInfo *pInfo){// Prepare a font size for displaying the file nameLOGFONT logFont;memset(&logFont, 0, sizeof(LOGFONT));logFont.lfHeight = 75; // 3/4th inch high in MM_LOENGLISH// (1/100th inch)CFont font;CFont *pOldFont = NULL;if (font.CreateFontIndirect(&logFont))pOldFont = pDC->SelectObject(&font);// Get the file name, to be displayed on title pageCString strPageTitle = GetDocument()->GetTitle();// Display the file name 1 inch below top of the page,// centered horizontallypDC->SetTextAlign(TA_CENTER);pDC->TextOut(pInfo->m_rectDraw.right / 2, -100, strPageTitle);if (pOldFont != NULL)pDC->SelectObject(pOldFont);}在OnPrint 调用OnDraw 之前调整窗口的原点和范围,以避免该页的主内容把页眉页脚给盖掉了。
要补偿被页眉页脚占据的空间,可以利用CPrintInfo 结构中的m_rectDraw,这个字段记录着本页的可绘图区域。
我们可以在输出主内容之前先输出页眉页脚,然后扣除m_rectDraw 四方形的一部份,代表页眉页脚所占空间。OnPrint 也可以根据m_rectDraw的数值决定有多少内容要放在打印页的主体上。
一般 在改写OnPreparePrinting 时,利用SetMaxPage 为CPrintInfo 结构设定一个最大页码,方便计算打印结束的时机。如果不知道文档的长度,可利用赖CPrintInfo 的m_bContinuePrinting 字段。此字段如果是FALSE,Framework 就中止打印。默认情况下OnPrepareDC 把此字段设为FALSE。(如果Document 长度没有指明,Framework 就假设这份Document只有一页长。)
为了完成预览功能,MFC 在CDC 之下设计了一个子类,名为CPreviewDC。
需要改动的只有如下: