您的位置:首页 > 编程语言 > C语言/C++

孙鑫VC学习笔记 (图形的保存和重绘)

2016-04-11 16:14 381 查看



第11课  如何让CDC上输出的文字、图形具有保持功能

1.创建4个菜单,为其添加消息响应,用成员变量保存绘画类型。添加LButtonDown和Up消息。

2.当窗口重绘时,如果想再显示原先画的数据,则需要保存数据。为此创建一个新类来记录绘画类型和两个点。

[cpp] view
plain copy

class CGraph    

{  

public:  

CPoint m_ptOrigin;//起点  

CPoint m_ptEnd;//终点  

UINT m_nDrawType;//绘画类型  

CGraph();  

CGraph(UINT m_nDrawType,CPoint m_ptOrigin,CPoint m_ptEnd);//此为构造函数。  

virtual ~CGraph();  

};  

   然后在void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)中加入如下代码

[cpp] view
plain copy

//CGraph graph(m_nDrawType,m_ptOrigin,point);//不能用局部变量  

//m_ptrArray.Add(&graph);//加入这种指针数组中  

/* OnPrepareDC(&dc);//这个函数中可以重新设置窗口原点,对于滚动条中,保存数据前要调用此函数 

dc.DPtoLP(&m_ptOrigin);//将设备坐标转换为逻辑坐标 

dc.DPtoLP(&point);// 

CGraph *pGraph=new CGraph(m_nDrawType,m_ptOrigin,point);//在堆中创建新的对象 

m_ptrArray.Add(pGraph);*///加入到指针数组中  

在GraphicView.h中有如下代码  

CPtrArray m_ptrArray;  

   在OnDraw中重画时调出数据  

for(int i=0;i<m_ptrArray.GetSize();i++)  

3.在CView::OnPaint()调用了OnDraw(),但在void CGraphicView::OnPaint()中MFC的Wizard没有调用OnDraw(),要注意这个区别。如果你此时想调用,必须手动添加代码。 OnDraw(&dc);

实际上也是调用了BeginPaint和EndPaint

如果在CGraphicView里面捕获了OnPaint,必须在里面调用OnDraw函数

[cpp] view
plain copy

void CGraphicView::OnPaint()  

{  

CPaintDC dc(this);  

OnPrepareDC(&dc);//坐标转换-->-->-->-->-->  

OnDraw(&dc);  

}  

4.让窗口具有滚动条的功能。

   第1.将CGraphicView的头文件中的CView全部替换成CSrollView

   第2.添加如下的代码

[cpp] view
plain copy

  

[cpp] view
plain copy

void CGraphicView::OnInitialUpdate()   

{  

CScrollView::OnInitialUpdate();  

// TOD Add your specialized code here and/or call the base class  

SetScrollSizes(MM_TEXT,CSize(800,600));//设置映射模式,设定窗口大小。OK!  

}  

5.坐标系的转换.

[cpp] view
plain copy

MicroSoft Windows 下的程序运用坐标空间和转换来对图形输出进行缩放,平移,旋转,斜切和反射。  

一个坐标空间是一个二维空间,通过使用连个相互垂直并且长度相等的轴来定义二维对象  

坐标空间  

API使用四种坐标空间:世界坐标系空间,页面空间,设备空间,和物理设备空间。基于Win32的应用程序运用世界坐标系空间对图形输出进行旋转、斜切或者反射。  

Win32 API把世界坐标系空间和页面空间称为逻辑空间;最后一种坐标空间(即物理设备空间)通常指应用程序窗口的客户区;但是他也包括整个桌面、完整的窗口(包括框架、标题栏和菜单栏)或打印机的一页或绘图仪的一页纸。物理设备的尺寸随显示器、打印机、绘图仪的所设置的尺寸而改变。  

转换  

如果要在一个物理设备上绘制输出,Windows把一个矩形区域从一个坐标空间拷贝到(或映射到)另一个坐标空间,直至最终完整的输出呈现在物理设备上(通常是屏幕或打印机)  

如果该应用程序调用了SetWorldTransForm函数,那么映射就从应用程序的世界坐标系空间开始;否则,映射在页面空间中进行。在 Windows把矩形区域的每一点从一个空间拷贝到另一个空间时,他采用一种被称作转换的算法,转换是把对象从一个坐标空间拷贝到另一个坐标空间时改变(或转变)这一对象的大小,方位,和形态,尽管转换把对象看成一个整体,但他也作用与对象的每一个点或每一条线。  

---------------------------------------------------------------------------------  

页面空间到设备空间的转换  

页面空见到设备空间的转换是原Windows程序接口的一部分。这种转换确定与一特定设备描述表相关的所有图形输出的映射方式。  

所谓映射方式是指确定用于绘图操作的单位大小的一种量度转换,映射方式是一种影响几乎任何客户区绘图的设备环境属性。另外还有四种设备环境属性:窗口原点,视口原点,窗口范围和视口范围,这四种属性与映射关系密切相关。  

页面空间到设备空间的转换所用的是两个矩形的宽与高的比率,其中页面空间中的矩形被称为窗口,设备空间中的矩形被称为视口。Windows把窗口原点映射到视口原点,把窗口范围映射到视口范围,就完成了这种转换。  

---------------------------------------------------------------------------------  

设备空间到物理空间的转换  

设备空间到物理空间的转换:只限于平移,并由Windows窗口管理部分控制,这种转换的唯一用途是确保设备空间的原点被映射到物理设备上的适当点上。没有函数能设置这种转换,也没有函数能获取有关数据。  

所以通常我们所要考虑的是从页面空间到设备空间的转换。页面空间通常称为逻辑空间。  

---------------------------------------------------------------------------------  

默认转换  

一旦应用程序建立了设备描述表,并立即开始调用GDI绘图或输出函数,则运用默认页面空间到设备空间的转换和设备空间到客户区的转换,(在应用程序调用SetWorldTransform之前不会世界坐标空间到页面空间的转换。  

默认页面空间到设备空间的转换是一对一的映射;即页面空间上给出的一点映射到设备空间上的一个点。这种转换没有以矩阵指定,而是通过把视口宽除以窗口宽,把视口高除以窗口高而得出的,在默认情况下,视口尺寸为1*1像素,窗口尺寸为1*1页单位。  

设备空间到物理设备(客户区,桌面和打印机),得转换结果总是一对一的;既设备空间上的一个单位总是与客户区,桌面,和打印机上的一个单位对应。这一转换的唯一用途是平移,无论窗口移到桌面的什么位置,它永远取保输出能够正确无误地出现在窗口上。  

默认装换的一个独特之处是设备空间与应用程序窗口的y轴方向。  

在默认的状态下,y轴正向朝下,-y方向朝上。  

---------------------------------------------------------------------------------  

逻辑坐标和设备坐标  

几乎在所有GDI函数中使用的坐标值都是逻辑单位,Windows必须将逻辑坐标值转换为“设备单位”,  

即像素。这种转换是由映射方式,窗口和视口的原点以及窗口和视口的范围决定的。  

Windows对所有消息(如WM_SIZE,WM_MOUSEMOVE,WM_LBUTTONDOWN,WM_LBUTTONUP),所有的非GDI函数和一些GDI函数(GetDeviceCaps函数(获取设备的范围)),永远使用设备坐标。  

窗口是基于逻辑坐标的,逻辑坐标可以是像素,毫米,英寸等单位,使口是基于设备坐标(像素)的。通常,视口和客户区是相同的。  

缺省的映射模式是MM_TEXT,在这种映射模式下,逻辑单位和设备单位相同。  

---------------------------------------------------------------------------------  

窗口(逻辑)坐标和视口(设备)坐标的转换  

xViewPort=(xWindow-xWinOrg)*xViewExt/xWinExt+xViewOrg;  

yViewPort=(yWindow-yWinOrg)*yViewExt/yWinExt+yViewOrg;  

视口(设备)坐标和窗口(逻辑)坐标的转换与上面相反;  

---------------------------------------------------------------------------------  

在MM_TEXT映射方式下窗口(逻辑)坐标和视口(设备)坐标的转换:  

xViewPort=xWindow-xWinOrg+xViewOrg;  

yViewPort=yWindow-yWinOrg+yViewOrg;  

视口(设备)坐标和窗口(逻辑)坐标的转换与上面相反;  

---------------------------------------------------------------------------------  

CDC中提供两个成员函数SetViewpoitOrg和SetWindowOrg,用来改变视口和窗口的原点。  

如果将视口原点设为(xViewOrg,yViewOrg),则逻辑点(0,0)就会被映射为设备点(xViewOrg,yViewOrg),如果将窗口原点改变为(xWinOrg,yWinOrg),则逻辑点(xWinOrg,yWinOrg)就会被映射为设备点(0,0),  

即左上角。  

不管对窗口和视口原点作什么改变,设备点(0,0)始终是客户区的左上角。  

---------------------------------------------------------------------------------  

  

6.解决重绘时线跑到上面的问题。为什么会错位?因为逻辑坐标和设备坐标没有对应起来。  

在CScrollView::OnInitialUpdate()函数中: ptVpOrg=-GetDeviceScrollPosition();  

pDC->SetViewportOrg(ptVpOrg);  

解决方法:  

  在OnLButtonDown画完图后,保存之前。调用  

/* OnPrepareDC(&dc);//重新设置逻辑坐标的原点!!! 

dc.DPtoLP(&m_ptOrigin);//设备坐标转化为逻辑坐标 

dc.DPtoLP(&point); 

CGraph *pGraph=new CGraph(m_nDrawType,m_ptOrigin,point); 

m_ptrArray.Add(pGraph);*/  

7.另外两种方法来保存数据。  

  一种是用CMetaFileDC,另一种是利用兼容DC,重绘时利用 pDC->BitBlt(0,0,rect.Width(),rect.Height(),&m_dcCompatible,0,0,SRCCOPY);  

将兼容DC的图拷贝到屏幕DC上去。  

此处不再详细介绍这两种方法,因为介绍多了容易搞晕。呵呵  

图形的保存和重绘

编写画图代码,设定一个标识,在OnLButtonDown中保存鼠标按下去的点,在OnLButtonUp中捕获鼠标弹起的点,利用switch语句分别画图。这是上节课的内容,上节课还讲了窗口重绘的原理,实际上分为两步,

首先擦除以前的背景,然后再进行窗口重绘。

所以当拖动窗口改变窗口大小时,窗口要发生重绘,首先会擦除以前的背景,于是先前所画图像会消失。

解决办法是将画图代码写在OnDraw()函数中,窗口每次重绘都会调用该函数重新绘制图像。

但是怎么保存每次重绘图像需要的代码呢?

事实上,要画的图形由三个因素决定:

1.图形类型(点、直线、矩形、椭圆)

2.图形绘制初始点

3.图形绘制终点

所以我们可以用数组类保存三个变量:m_nDrawType,m_ptOrigin,m_ptEnd。

首先需要新建一个Class type为Generic Class的名为CGraph类,

在CGraph中增加三个成员变量UINT m_nDrawType,CPoint m_pOrigin,CPoint m_pEnd,

并在不带参数构造函数中将三个成员变量初始化。

然后构造一个带参数的构造函数 CGraph(UINT m_nDrawType,CPoint m_pOrigin,CPoint m_pEnd);

用来接收三个变量值,代码如下:

[cpp] view
plain copy

CGraph::CGraph(UINT m_nDrawType,CPoint m_pOrigin,CPoint m_pEnd)  

{  

  this->m_nDrawType=m_nDrawType;  

  this->m_pOrigin=m_pOrigin;  

  this->m_pEnd=m_pEnd;  

}  

至此,我们可以用CGraph类保存绘制一个图形的三要素,根据这三要素可以绘制我们所作的图形。

--------------------------------------------------------------------------------

但是如果我们绘制了多个图形怎么办?

如果用CGraph类对象的一个数组,那么它只能保存有限个对象,

而我们在绘制图形的时候,并不能限定只能绘制多少个图形。

用链表可以实现动态存储,但是操作十分复杂,

于是我们想到了MFC提供一集合类,如前面用到过的CStringArray,可以动态存储CString对象

这里要用到的是另一个集合类,CPtrArray,它用来动态保存CGraph对象。

(它的用法大家可以参见 MSDN或右边。)

于是在OnLButtonUp函数中画完图形之后 ,我们想创建一个CGraph对象,

把画这个图形所必须的三要素保存到这个对象之中。

如果我们直接在尾部增加下面两行代码:

  graph(m_nDrawType,m_ptOrigin,point)

m_ptrArray.Add(&graph);

但是运行程序之后,我们所绘制的图形没能在OnDraw函数中重绘。

这是因为我们在OnLButtonUp函数中创建的CGraph对象在本函数结束时被销毁,内存被回收。

---------------------------------------------------------------------------------

解决办法: 

在OnLButtonUp函数中创建的CGraph对象指针,然后用new方法为它在堆中分配内存。

因为new方法分配的内存在堆中,除非显式地调用Delete方法去释放它,

否则其生命周期直到程序结束时才结束 。

CGraph *=new CGraph(m_nDrawType,m_ptOrigin,point);

m_ptrArray.Add(pGraph);

虽然pGraph 指针在函数结束时也会析构,但是没有关系,m_ptrArray已经保存了该指针,

而new所建立的对象是在堆上的,除非显式地调用Delete方法去释放它,

否则其生命周期直到程序结束时才结束 。

窗口重绘会调用OnDraw函数,所以要在此函数中增加画图代码,代码如下:

注:

1. m_ptrArray.GetSize()从得到数组长度,构造for循环

2. ((CGraph*)m_ptrArray.GetAt(i))->得到CGraph类的各变量,

3. 其中m_ptrArray.GetAt(i)返回CObject*,要强制转换成CGraph*。

--------------------------------------------------------------------------------

下面研究一下,OnDraw函数为什么能在窗口重绘过程中被调用

OnDraw函数不是WM_PAINT的相应函数,为什么能在窗口重绘过程中被调用?

下面是一段MFC资源文件中的代码:(是WM_PAINT消息响应函数OnPaint())

void CView::OnPaint()

{

// standard paint routine

  CPaintDC dc(this);

  OnPrepareDC(&dc);

  OnDraw(&dc);

}

从上面的代码我们知道,原来在基类View类中调用了OnDraw函数,所以我们认为OnDraw专门用来重绘的

其实我们也可以增加WM_PAINT消息响应,自己在其响应函数写重绘窗口的代码。

前面讲过,在响应WM_PAINT时候,只能利用BeginPaint()获得dc的句柄,用EndPaint()释放dc句柄。

而上面代码并没有发现BeginPaint()与EndPaint(),查看一下CPaintDC 就知道,

CPaintDC 类的构造函数中调用了BeginPaint(),析构函数中调用了EndPaint()。

注意:CPaintDC只能在OnPaint中使用,因为在MSDN中这样说了:

CClientDC objects encapsulate working with a device context that represents only the client area of a window. The CClientDC constructor calls the GetDC function, and the destructor calls the ReleaseDC function. CWindowDC objects encapsulate a device context
that represents the whole window, including its frame.

如果要在OnPaint以外地方获得dc句柄的话,调用GetDC ,释放用ReleaseDC。



CPtrArray----->MSDN!!

 

如何把元文件保存到文件当中

1.为“打开”,“保存”添加命令相应函数。

2.用 CopyMetaFile 拷贝元文件到指定文件中

CopyMetaFile

将windows格式的元文件拷贝到指定的文件当中。

首先我们在“保存”命令响应函数OnFileSave()中完成保存元文件到文件的功能,

执行之后在项目工程文件夹中增加了一个meta.wmf的文件,用ACDsee可以打开。

--------------------------------------------------------------------------------

然后在“打开”命令响应函数OnFileOpen()中完成保存元文件到文件的功能,打开文件时用GetMetaFile或GetEnhMetaFile,接着将将文件的图形拷贝到m_dcMetaFile元文件中,最后调用Invalidate()引起窗口重画,使图形在OnDraw()重绘,代码如下:

注:CopyMetaFile,GetMetaFile函数已经被废弃现在使用增强的函数CopyEnhMetaFile,GetEnhMetaFile。用法相同,为了与16-bit Windows API兼容,老函数仍能使用。

-----------------------------------------------------------------------------



介绍利用兼容DC保存图形与重绘图形的方式

1.构造兼容DC对象:CDC  m_dcCompatible

2.在CGraphicView::OnLButtonUp写下面代码

void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point) 

{

    // TODO: Add your message handler code here and/or call default

    CClientDC dc(this);

    CBrush *pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));

    //dc.SelectObject(pBrush);

    m_dcMetaFile.SelectObject(pBrush);

    if(!m_dcCompatible.m_hDC)

    {

        m_dcCompatible.CreateCompatibleDC(&dc);

        CRect rect;

        GetClientRect(&rect);

        CBitmap bitmap;

        bitmap.CreateCompatibleBitmap(&dc,rect.Width(),rect.Height());

        m_dcCompatible.SelectObject(&bitmap);

   

    m_dcCompatible.BitBlt(0,0,rect.Width(),rect.Height(),&dc,0,0,SRCCOPY);

        m_dcCompatible.SelectObject(pBrush);

    }

3.在CGraphicView::OnDraw中写入下面代码:

CRect rect;

GetClientRect(&rect);

pDC->BitBlt(0,0,rect.Width(),rect.Height(),&m_dcCompatible,0,0,SRCCOPY);

//出现问题,见下面粗斜体,解决代码见上面粗斜体;

CBitmap::CreateCompatibleBitmap 

通过指定的宽高创建一个兼容位图。初始化一个与指定设置相兼容的位图

BOOL CreateCompatibleBitmap(CDC* pDC,int nWidth,int nHeight );

--------------------------------------------------------------------------------
CreateCompatibleBitmap 返回的位图对象只包含相应设备描述表中的位图信息头,不包含颜色表和像素数据块。因此,选入该位图对象的设备描述表不能像选入普通对象的设备描述表一样使用,必须在SelectObject函数之后,调用BitBlt将原始设备描述表的颜色表以及像素数据块拷贝到兼容设备描述表。

 

如果我们想在保存图象的同时显示图像,可以在调用

m_dcCompatible.MoveTo(m_ptOrigin);

m_dcCompatible.LineTo(point);

的同时调用

dc.MoveTo(m_ptOrigin);

dc.LineTo(point);


介绍两种保存图形与重绘图形的方式

第一种,要利用要一个类 CMetaFileDC

第一步创建一个CMetaFileDC 对象。 (private)

接着调用CMetaFileDC 构造器,然后再调用Create 函数 创建一个设备上下文与CMetaFileDC 对象关联起来。

下一步给CMetaFileDC 对象发送一系列的CDC命令

在为元文件发送完命令之后,调用Close成员函数关闭元文件的设备上下文,

并返回一个元文件 的句柄。接着就可以处理CMetaFileDC 对象。

CDC::PlayMetaFile 方法可以用来使用元文件句柄播放元文件,我们可以多次调用本函数

介绍一些与本操作相关的成员函数

--------------------------------------------------------------------------------

CMetaFileDC::Create 

BOOL Create(LPCTSTR lpszFilename = NULL );

如果指定一个字符串,那么就指定了元文件的文件名,用来保存元文件

如果变量pszFilename 为空,那么就会在内存中创建一个元文件

--------------------------------------------------------------------------------

CMetaFileDC::Close 

关闭一个设备上下文,返回一个DC句柄。

--------------------------------------------------------------------------------

用元文件来重绘图形的具体步骤:

1.先在CGraphicView类中,增加一个CMetaFileDC变量m_dcMetaFille (private)

2.在CGraphicView类的构造函数中调用m_dcMetaFille的Create()方法,

  Create方法的参数的NULL时创建一个内存的元文件

3.在CGraphicView类的OnLButtonUp()方法中,写入绘图命令,

   OnLButtonUp()方法代码如下:

m_dcMetaFile.SelectObject(pBrush);

 

HMETAFILE hmetaFile=m_dcMetaFile.Close();

pDC->PlayMetaFile(hmtaFile);

m_dcMetaFile.Create();//前面已经关闭了,可能再绘制图形,故重新创建一个

m_dcMetaFile.PlayMetaFile(hmtaFile);

DeleteMetaFile(hmtaFile);

  有了元文件的句柄,就可以利用CDC::PlayMetaFile来播放它。

   注意:关闭元文件并不意味着删除元文件

View::OnFileSave()

HMETAFILE hmetaFile=m_dcMetaFile.Close();

CopyMetaFile(hmetaFile,"meta.wmf");

m_dcMetaFile.Create();//前面已经关闭了,可能再绘制图形,故重新创建一个

DeleteMetaFile(hmtaFile);

View::OnFileOpen

GetEnhMetaFile();

void CGraphicView::OnFileOpen() 

{

    // TODO: Add your command handler code here

    HMETAFILE hmetaFile;

    hmetaFile=GetMetaFile("meta.wmf");

    m_dcMetaFile.PlayMetaFile(hmetaFile);

    DeleteMetaFile(hmetaFile);

    Invalidate();

}
4.窗口重绘时,即在OnDraw()函数中,去关闭元文件,从而获得元文件句柄。




0
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息