多文档视图结构
2009-03-31
第16单元 多文档界面程序
本单元教学目标
介绍多文档界面(MDI)程序的构造和编程方法。
学习要求
理解多文档界面(MDI)程序的构造,掌握其编程方法。
授课内容
和框架窗口界面程序、单文档界面(SDI)程序和基于对话框的应用程序一样,多文档界面(MDI)程序也是基本的MFC应用程序结构。MDI程序的结构最复杂,功能也最强。其特点是用户一次可以打开多个文档,每个文档均对应不同的窗口;主窗口的菜单会自动随着当前活动的子窗口的变化而变化;可以对子窗口进行层叠、平铺等各种操作;子窗口可以在MDI主窗口区域内定位、改变大小、最大化和最小化,当最大化子窗口时,它将占满MDI主窗口的全部客户区。MDI不仅可以在同一时间内同时打开多个文档,还可以为同一文档打开多个视图。
16.1 MDI应用程序
从程序员角度看,每个MDI应用程序必须有一个CMDIFrameWnd或其派生类的对象,该窗口称作MDI框架窗口。CMDIFrameWnd是CFrameWnd的派生类,除了拥有CFrameWnd类的全部特性外,还具有以下与MDI相关的特性:
1.与SDI不同,MDI的框架窗口并不直接与一个文档和视图相关联。MDI的框架窗口拥有客户窗口,在显示或隐藏控制条(包括工具条、状态栏、对话条)时,重新定位该子窗口。
2.MDI客户窗口是MDI子窗口的直接父窗口,它负责管理主框架窗口的客户区以及创建子窗口。每个MDI主框架窗口都有且只有一个MDI客户窗口。
3.MDI子窗口是CMDIChildWnd或其派生类对象,CMDIChildWnd也是CFrameWnd的派生类,用于容纳视图和文档,相当于SDI下的主框架窗口。每打开一个文档,框架就自动为文档创建一个MDI子窗口。一个MDI应用程序负责动态的创建和删除MDI子窗口。在任何时刻,最多只有一个子窗口是活动的(窗口标题栏颜色呈高亮显示)。MDI框架窗口始终与当前活动子窗口相关联,命令消息在传给MDI框架窗口之前首先分派给当前活动子窗口。
4.在没有任何活动的MDI子窗口时,MDI框架窗口可以拥有自己的缺省菜单。当有活动子窗口时,MDI框架窗口的菜单条会自动被子窗口的菜单所替代。框架会自动监视当前活动的子窗口类型,并相应的改变主窗口的菜单。例如,在Visual Studio中,当选择对话框模板编辑窗口或源程序窗口时,菜单会有所不同。但是,对于程序员来说,只需在InitInstance()中注册文档时指定每一类子窗口(严格的讲是文档)所使用的菜单,而不必显式的通过调用函数去改变主框架窗口的菜单,因为框架会自动完成这一任务。
5.MDI框架窗口为层叠、平铺、排列子窗口和新建子窗口等一些标准窗口操作提供了缺省的菜单响应。在响应新建子窗口命令时,框架调用CDocTemplate::CreateNewFrame()为当前活动文档创建一个子窗口。CreateNewFrame()不仅创建子窗口,还创建与文档相对应的视图。
与开发基于对话框的应用程序和SDI应用程序一样,使用AppWizard可生成一个MDI应用程序框架,在此基础上,程序员可使用ClassWizard和各种资源编辑器来充实自己的应用程序。
AppWizard为MDI程序框架创建了以下类:
类 说 明
CAboutDlg “关于”对话框
CChildFrame 子框架窗口,用于容纳视图
CMyApp 应用程序类
CmyDoc 绘图程序视图类
CMyView 绘图视图类
CMainFrame 框架窗口(用来容纳子窗口),是MDI应用程序的主窗口
可以看出,MDI比SDI多了一个CchildFrame(子框架窗口)类,而且CMainFrame的职责也不同了。
另外,MDI和SDI的初始化应用程序实例方法上也有所不同。MDI应用程序的InitInstance()函数代码为:
BOOL CDrawApp::InitInstance()
{
… … // 初始化工作
CMultiDocTemplate* pDocTemplate; // MDI文档模板
pDocTemplate = new CMultiDocTemplate(
IDR_DRAWTYPE,
RUNTIME_CLASS(CDrawDoc),
RUNTIME_CLASS(CChildFrame),
RUNTIME_CLASS(CDrawView));
AddDocTemplate(pDocTemplate);
CMainFrame* pMainFrame = new CMainFrame; // 建立MDI主框架窗口
if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
return FALSE;
m_pMainWnd = pMainFrame;
m_pMainWnd->DragAcceptFiles(); // 设置框架窗口特性
EnableShellOpen();
RegisterShellFileTypes(TRUE);
CCommandLineInfo cmdInfo; // 处理命令行
ParseCommandLine(cmdInfo);
if (!ProcessShellCommand(cmdInfo))
return FALSE;
pMainFrame->ShowWindow(m_nCmdShow); // 显示主框架窗口
pMainFrame->UpdateWindow();
return TRUE;
}
注册文档模板时,首先创建一个CMultiDocTemplate类(对SDI是CSingleDocTemplate)的模板对象,然后用AddDocTemplate()把它加入到文档模板链表中去。
CmultiDocTemplate()构造函数有四个参数,第1个参数是文档使用的资源ID定义,第2个是文档类型,第3个是子窗口类型,第4个是视图类型。
与SDI不同,由于MDI的主框架窗口并不直接与文档相对应,因此无法通过创建文档来创建主框架窗口,而需要自己去创建:
CMainFrame* pMainFrame = new CMainFrame;
if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
return FALSE;
m_pMainWnd = pMainFrame;
[例16-1] 绘图程序。用户可以鼠标“拖曳”方式在视图中画直线段,线的粗细和颜色可调。采用MDI结构,可同时打开多个子窗口作图,所作图形可以文件形式保存在磁盘上。
说 明:用AppWizard生成一个MDI程序框架,在第4步打开Advanced Options(高级选项)对话框,在Document Template Strings(文档模板字符串)选项卡中将File extension(文件扩展名)设置为“pic”,即该程序的图形文件名后缀为“.pic”。其他选项均使用缺省设置。
编辑该程序的主菜单,添加一个“颜色”选项和一个“宽度菜单”,其中包括4个选项,其ID和Caption分别设置为:
ID Caption 命令消息响应函数
ID_COLOR “颜色” OnColor()
ID_WIDTH1 “宽度=1” OnWidth1()
ID_WIDTH3 “宽度=3” OnWidth3()
ID_WIDTH5 “宽度=5” OnWidth5()
ID_WIDTH7 “宽度=7” OnWidth7()
用Class Wizard在视图类中为上述菜单选项建立相应的消息响应函数,以及几个宽度菜单选项相应的更新用户界面消息函数。
程 序:用Class Wizard添加一个用于描述线段的类,并为其添加代码:
class CLine : public CObject
{
public:
CPoint m_pointFrom; // 线段起点
CPoint m_pointTo; // 线段终点
COLORREF m_colorLine; // 线段颜色
int m_nWidth; // 线段宽度
CLine(){}
CLine(POINT from, POINT to, COLORREF color, int width);
CLine& operator=(CLine& line);
void Serialize(CArchive& ar);
void DrawLine(CDC *pDC);
virtual ~CLine(){}
DECLARE_SERIAL(CLine);
};
IMPLEMENT_SERIAL(CLine, CObject, 1)
CLine::CLine(POINT from, POINT to, COLORREF color, int width)
{
m_pointFrom = from;
m_pointTo = to;
m_colorLine = color;
m_nWidth = width;
}
CLine& CLine::operator =(CLine& line)
{
m_pointFrom = line.m_pointFrom;
m_pointTo = line.m_pointTo;
m_colorLine = line.m_colorLine;
m_nWidth = line.m_nWidth;
return *this;
}
void CLine::Serialize(CArchive &ar)
{
if(ar.IsStoring())
ar << m_pointFrom << m_pointTo << m_colorLine << m_nWidth;
else
ar >> m_pointFrom >> m_pointTo >> m_colorLine >> m_nWidth;
}
void CLine::DrawLine(CDC *pDC)
{
CPen penNew, *ppenOld;
penNew.CreatePen(PS_SOLID, m_nWidth, m_colorLine);
ppenOld = pDC->SelectObject(&penNew);
pDC->MoveTo(m_pointFrom);
pDC->LineTo(m_pointTo);
pDC->SelectObject(ppenOld);
}
修改文档类头文件。在文件首部添加如下代码:
#include "line.h"
#define MAX_LINES 300
并在文档类定义中添加如下数据成员:
public:
CLine m_lineList[MAX_LINES];
int m_nCount;
然后利用Class Wizard为文档类重载成员函数DeleteContents()。修改该函数及文档类的Serialize()函数:
void CMyDoc::DeleteContents()
{
m_nCount = 0;
CDocument::DeleteContents();
}
void CMyDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
ar << m_nCount;
else
ar >> m_nCount;
for(int i=0; i<m_nCount; i++)
m_lineList[i].Serialize(ar);
}
修改视图类头文件,在视图类定义中添加如下数据成员:
public:
COLORREF m_colorCurr; // 当前颜色
int m_nCurrWidth; // 当前线宽
BOOL m_bCaptured; // 是否按下鼠标左键
CPoint m_pointFrom; // 当前线始端
CPoint m_pointTo; // 当前线终端
并在视图类构造函数中加入相应的初始化代码:
CMyView::CMyView(): m_pointFrom(0,0), m_pointTo(0,0)
{
m_bCaptured = FALSE;
m_colorCurr = RGB(255, 0, 0);
m_nCurrWidth = 3;
}
然后修改视图类的OnDraw()函数和消息响应函数:
void CMyView::OnDraw(CDC* pDC)
{
CMyDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
for(int i=0; i<pDoc->m_nCount; i++)
pDoc->m_lineList[i].DrawLine(pDC);
}
void CMyView::OnLButtonDown(UINT nFlags, CPoint point)
{
CView::OnLButtonDown(nFlags, point);
m_pointFrom = m_pointTo = point;
SetCapture();
m_bCaptured = TRUE;
}
void CMyView::OnLButtonUp(UINT nFlags, CPoint point)
{
CView::OnLButtonUp(nFlags, point);
if(m_bCaptured)
{
CMyDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
m_pointTo = point;
m_bCaptured = FALSE;
ReleaseCapture();
pDoc->m_lineList[pDoc->m_nCount] = CLine(m_pointFrom,m_pointTo,
m_colorCurr, m_nCurrWidth);
pDoc->m_nCount++;
pDoc->SetModifiedFlag();
pDoc->UpdateAllViews(this);
Invalidate();
}
}
void CMyView::OnMouseMove(UINT nFlags, CPoint point)
{
CView::OnMouseMove(nFlags, point);
if(m_bCaptured)
{
CClientDC dc(this);
dc.SetROP2(R2_NOT);
dc.MoveTo(m_pointFrom);
dc.LineTo(m_pointTo);
m_pointTo = point;
dc.MoveTo(m_pointFrom);
dc.LineTo(m_pointTo);
}
}
void CMyView::OnColor()
{
CColorDialog dlg(m_colorCurr);
if(dlg.DoModal() == IDOK)
m_colorCurr = dlg.GetColor();
}
void CMyView::OnWidth1()
{
m_nCurrWidth = 1;
}
void CMyView::OnWidth3()
{
m_nCurrWidth = 3;
}
void CMyView::OnWidth5()
{
m_nCurrWidth = 5;
}
void CMyView::OnWidth7()
{
m_nCurrWidth = 7;
}
void CMyView::OnUpdateWidth1(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_nCurrWidth == 1);
}
void CMyView::OnUpdateWidth3(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_nCurrWidth == 3);
}
void CMyView::OnUpdateWidth5(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_nCurrWidth == 5);
}
void CMyView::OnUpdateWidth7(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_nCurrWidth == 7);
}
输入输出:在窗口客户区按下鼠标左键后移动鼠标(“拖曳”)可显示一条始端不变、终端移动的黑色细线段,放松鼠标按键后,该线变为预先确定的颜色和粗细。使用菜单选项可改变线段的颜色和粗细(图16-1)。
图16-1 绘图程序
分 析:为了存储所绘图形,自定义的线段类CLine应可序列化。为此,CLine类包含了一个没有参数的构造函数、一个重载的赋值运算符、Serialize()成员函数,以及DECLARE_SERIAL()宏和IMPLEMENT_SERIAL宏。
在文档类的Serialize()成员函数中调用了CLine类的Serialize()函数。
所有的鼠标消息和菜单消息响应函数均在视图类中。其中用到了CDC类的成员函数SetROP2()在移动鼠标期间将绘图模式设置为异或,该模式下有两个特点,一是第2次画同一条线可恢复背景色(擦除),一是无论背景色和画线色的设置如何,均可保证所画线段可见,因此特别适合绘制变化图形。
但是,由于用户的鼠标可以在屏幕上任意移动。当鼠标移出窗口外时,窗口无法收到鼠标消息。此时,如果松开了鼠标左键,应用程序由于无法接受到该条消息而不会终止当前笔划,这样就造成了错误。如何避免这种情况发生呢?解决的办法是要让窗口在鼠标移出窗口外时仍然能接受到鼠标消息。幸好,Windows提供了一个API函数SetCapture()解决了这一问题。
OnLButtonDown()中调用SetCapture()用于捕获鼠标,无论鼠标光标位置在何处,都会将鼠标消息送给调用它的那一个窗口。在用完后,需要用ReleaseCapture()释放窗口对鼠标的控制,否则其他窗口将无法接收到鼠标消息(在OnLButtonUp()函数中)。这样,即使用户在“拖曳”鼠标时越出当前窗口的客户区,也不会发生错误。
OnLButtonUp()函数中的语句
UpdateAllViews(this);
用于通知所有的视图数据已更新,这是MDI的特别用法。
OnUpdateWidth1()等函数用来在相应菜单选项前加上被选择标记。
自学内容
16.2 滚动视图
到目前为止,我们接触到的程序的窗口客户区均受限于计算机屏幕的大小,而这对某些应用来说是不方便的。例如,若要开发“所见即所得”的文本编辑或图形编辑程序,就希望屏幕上出现的文字或图形与打印机的输出大小接近。在Windows程序中,这可以通过“滚动视图”技术实现,即在窗口客户区的右方和下方分别添加一个垂直滚动条和一个水平滚动条,使客户区变为一个更大的虚拟客户区的观察窗口。
MFC提供了CScrollView类来实现滚动视图。CScrollView类自动处理滚动条消息并“滚动”客户区画面。因此利用CScrollView类显示文档时,可以不必理会客户区的实际大小,只要将其当作一张很大的输出平面即可。CScrollView类的OnPrepareDC()成员函数会根据水平和垂直滚动条的位置自动设定DC原点。因此也可以说,客户区的左上角坐标其实是水平、垂直滚动条的位置,而两个滚动条的活动范围(scroll size)就是虚拟客户区的大小。
CScrollView类有以下重要成员函数实现上述功能:
1.设置虚拟客户区的大小:
void CScrollView::SetScrollSizes ( int nMapMode, SIZE sizeTotal,
const SIZE& sizePage = sizeDefault, const SIZE& sizeLine = sizeDefault );
其中参数nMapMode为映射模式,可参看10.3:“GDI坐标系”。参数sizeTotal是整个虚拟客户区的大小;sizePage是每次“翻页”时的滚动量,也就是用户按下滚动条把柄时的滚动量。sizeLine是每次跳一小格的滚动量,以上参数的单位均为逻辑坐标的单位。
2.取滚动条坐标(客户区左上角坐标)和虚拟客户区大小:
CPoint CScrollView::GetScrollPosition() const;
CSize CScrollView::GetTotalSize() const;
3.将客户区左上角滚动到指定坐标
void CScrollView::ScrollToPosition(POINT pt);
其中参数pt为指定的客户区坐标。
4.取滚动位置和虚拟客户区大小的物理坐标
CPoint CScrollView::GetDeviceScrollPosition()const;
void CScrollView::GetDeviceScrollSizes(int& nMapMode, SIZE& sizeTotal,
SIZE& sizePage, SIZE& sizeLine)const;
要使应用程序支持卷滚,可在用AppWizard生成框架程序时就指定视图的基类为CSrollView。做法是在AppWizard的MFC AppWizard-Step 6 of 6对话框中,在应用程序所包含的类中选择视图类,然后在Base Class下拉列表框中选择应用程序视图类的基类为CScrollView。
如果要手工修改视图类的基类为CScrollView,可按以下步骤操作:
1.修改视图类所对应的头文件,将所有用到CView的地方改为CScrollView。可以使用文本替换对话框(在编辑菜单中)中的替换功能,进行全局替换。
2.确定虚拟客户区的大小。这项工作通常在视图派生类的OnInitialUpdate()成员函数或OnCreate()成员函数中通过调用SetScrollSizes()成员函数来完成。
3.如果在视图类的消息响应函数(如鼠标消息函数)中使用了CClientDC设备,则要注意该设备仍以实际客户区的左上角为原点,在存储有关数据时,可能要将其转换为虚拟客户区坐标。转换方法很简单,只要将坐标值加上客户区左上角在虚拟客户区的坐标值即可。然而,OnDraw()函数无需任何修改,因其使用的设备由CScrollView自动维护。
16.3 对话视图
对于人事档案管理、名片管理、图书管理这类应用程序,视图的主要作用是显示各项文档资料,同时又要提供方便的修改手段。如例15-1,采用对话框编辑档案材料比较方便。然而,基于对话框的应用程序不提供文档类,数据存取不方便。
MFC提供了CFormView类,该类成员兼有对话框和视图的特点,最适合作文档管理类应用程序的用户界面。
要使应用程序支持CFormView类,可在用AppWizard生成框架程序时就指定视图的基类为CFormView。做法是在AppWizard的MFC AppWizard-Step 6 of 6对话框中,在应用程序所包含的类中选择视图类,然后在Base Class下拉列表框中选择应用程序视图类的基类为CFormView。
设计CFormView的对话框模板方法与设计一般对话框模板的方法完全相同。
16.4 文本编辑视图
CEditView类是CView类的派生类,内含一个CEdit对象,具有很强的文字编辑功能,包括剪贴、搜索替换和打印等功能。
CEditView类本身有存放文本的内存,有序列化功能,几乎不用任何编程工作便可成为一个功能相当强大的文本编辑器程序。
如果要对CEditView中的CEdit对象进行操作,可使用成员函数:
CEdit& CEditView::GetEditCtrl()const;
取得该CEdit对象。
要说明的是,CEditView类只能编辑长度小于64K的文本文件。
[例16-2] 文本编辑器程序。该程序的功能与Windows的记事本程序类似,但是一个MDI程序。
说 明:用AppWizard建立一个MDI程序,在第4步设置文件名后缀(File extension)为“txt”,在第6步将视图类的基类设置为CEditView。
注意,经上述步骤生成的程序直接编译、联接后就已经是一个可以使用的文本编辑器了。下面步骤仅是为该程序添加一个字体选择对话框。
编辑子窗口菜单,在“文件”菜单中添加一个选项“字体”,标识符为ID_FONT,并使用ClassWizard在视图类中为其添加相应的消息响应函数OnFont()。
程 序:首先在视图类中添加一个字体类的数据成员:
public:
CFont m_fontEdit;
然后编辑OnFont()函数:
void CMy1504View::OnFont()
{
CFontDialog dlg;
LOGFONT logfont;
if(dlg.DoModal() == IDOK)
{
dlg.GetCurrentFont(&logfont);
textcolor = dlg.GetColor();
m_fontEdit.DeleteObject();
m_fontEdit.CreateFontIndirect(&logfont);
GetEditCtrl().SetFont(&m_fontEdit);
}
}
输入输出:可编辑文本文件,具有存储、打印和各种剪贴板操作功能(图16-2)。
分 析:通过字体选择公用对话框选择字体并创建该字体,然后用CEdit类的成员函数SetFont()设置字体。
图16-2 文本编辑器程序
调试技术
16.5 使用AppWizard建立MDI程序框架
使用AppWizard建立MDI程序的方法与建立SDI程序的方法基本相同,只是在第1步时选择Multiple Documents即可。
另外,如果是创建MDI项目,则在AppWizard的第4步中的高级选项(Advanced Options)对话框中的Window styles选项卡中可以控制文档窗口的外貌。在该选项卡的下方有一个MDI child frame styles组框,其中包括5个复选框:
Thick Frame 粗边框
Minimize box 子框架窗口右上角的最小化按钮
Maximize box 子框架窗口右上角的最大化按钮
Minimized 子框架窗口最小化
Maximized 子框架窗口最大化
程序设计举例
[例16-3] 改写例16-1的绘图程序,使之使用滚动视图,同时选用合适的映射模式,使程序具有“所见即所得”的打印风格。
按例16-1前面的说明用AppWizard建立程序框架并编辑其菜单资源,只是在AppWizard的第6步将视图类的基类设置为CScrollView类。
程 序:CLine类和文档类的代码与例16-1相同。
视图类的定义及其构造函数、OnColor()函数和OnDraw()函数与例16-1相同。用ClassWizard为视图类重载OnInitUpdate()函数和OnPrepareDC()函数。其他需修改的成员函数的代码如下:
void CMy1603View::OnWidth1()
{
m_nCurrWidth = 15;
}
void CMy1603View::OnUpdateWidth1(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_nCurrWidth == 15);
}
void CMy1603View::OnWidth3()
{
m_nCurrWidth = 30;
}
void CMy1603View::OnUpdateWidth3(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_nCurrWidth == 30);
}
void CMy1603View::OnWidth5()
{
m_nCurrWidth = 60;
}
void CMy1603View::OnUpdateWidth5(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_nCurrWidth == 60);
}
void CMy1603View::OnWidth7()
{
m_nCurrWidth = 120;
}
void CMy1603View::OnUpdateWidth7(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_nCurrWidth == 120);
}
void CMy1603View::OnColor()
{
CColorDialog dlg(m_colorCurr);
if(dlg.DoModal() == IDOK)
m_colorCurr = dlg.GetColor();
}
void CMy1603View::OnLButtonDown(UINT nFlags, CPoint point)
{
CScrollView::OnLButtonDown(nFlags, point);
CClientDC dc(this);
OnPrepareDC(&dc);
dc.DPtoLP(&point);
m_pointFrom = m_pointTo = point;
SetCapture();
m_bCaptured = TRUE;
}
void CMy1603View::OnLButtonUp(UINT nFlags, CPoint point)
{
CScrollView::OnLButtonUp(nFlags, point);
if(m_bCaptured)
{
CMy1603Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CClientDC dc(this);
OnPrepareDC(&dc);
dc.DPtoLP(&point);
m_pointTo = point;
m_bCaptured = FALSE;
ReleaseCapture();
CPoint org = GetScrollPosition();
pDoc->m_lineList[pDoc->m_nCount] = CLine(m_pointFrom+org,
m_pointTo+org, m_colorCurr, m_nCurrWidth);
pDoc->m_nCount++;
pDoc->SetModifiedFlag();
pDoc->UpdateAllViews(this);
Invalidate();
}
}
void CMy1603View::OnMouseMove(UINT nFlags, CPoint point)
{
CScrollView::OnMouseMove(nFlags, point);
if(m_bCaptured)
{
CClientDC dc(this);
OnPrepareDC(&dc);
dc.DPtoLP(&point);
dc.SetROP2(R2_NOT);
dc.MoveTo(m_pointFrom);
dc.LineTo(m_pointTo);
m_pointTo = point;
dc.MoveTo(m_pointFrom);
dc.LineTo(m_pointTo);
}
}
void CMy1603View::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
SetScrollSizes(MM_TWIPS, CSize(11520, 15120));
}
void CMy1603View::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo)
{
pDC->SetMapMode(MM_TWIPS);
CScrollView::OnPrepareDC(pDC, pInfo);
}
输入输出:与例16-1基本相同,但使用了滚动视图,可在虚拟的客户区中作图。打印比例适当,在A4纸上可做到“所见即所得”。
分 析:CScrollView需要预先设置虚拟客户区的大小等参数,这一工作在其成员函数OnInitUpdate()中进行。
为了实现“所见即所得”的打印效果,本程序使用了MM_TWIPS逻辑映射模式。映射模式在视图类的OnprepareDC()成员函数中设置。
MM_TWIPS映射模式的单位为1/1440英寸。设置线宽时应以此为标准。由于鼠标消息中的位置参数是基于物理坐标的,所以在鼠标消息响应函数中调用CDC类的成员函数LPtoDP()将其转换为逻辑坐标。由于这些坐标是相对于实际客户区左上角的坐标,而文档中存储的应是相对于虚拟客户区左上角的坐标,所以在OnLButtonUp()函数中还要在这些坐标上加上实际客户区左上角相对于虚拟客户区左上角的坐标方可存入数组。