一、保存重绘图形(1)
1、建立单文档界面程序
2、增加菜单绘图及菜单项点、直线、矩形和椭圆,并在VIEW类中为之生成相应的消息响应函数。
3、在VIEW类中增加私有成员变量UINT m_nDrawType=0,用于保存绘图的类型。
4、在点、直线、矩形和椭圆的消息函数中,成员变量m_nDrawType的值分别为1、2、3、4。
5、在VIEW类中增加CPoint类型的私有成员变量m_ptOrigin,用于保存起点坐标,并在头文件中置初始值为0。
6、在VIEW类中增加消息处理WM_LBUTTONDOWN和WM_LBUTTONUP。在CGraphicView::OnLButtonDown函数中增加代码:
void CGraphicView::OnLButtonDown(UINT nFlags, CPoint point)
{
m_ptOrigin=point;
}
void CGraphicView::OnLButtonDown(UINT nFlags, CPoint point)
{
m_ptOrigin=point;
}
7、在CGraphicView::OnButtonUp函数中增加代码:
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
CClientDC dc(this);
CBrush *pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
dc.SelectObject(pBrush);
switch(m_nDrawType)
{
case 1:
dc.SetPixel(point,RGB(0,0,0));
break;
case 2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case 3:
dc.Rectangle(CRect(m_ptOrigin,point));
break;
case 4:
dc.Ellipse(CRect(m_ptOrigin,point));
break;
default:
break;
}
}
{
CClientDC dc(this);
CBrush *pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
dc.SelectObject(pBrush);
switch(m_nDrawType)
{
case 1:
dc.SetPixel(point,RGB(0,0,0));
break;
case 2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case 3:
dc.Rectangle(CRect(m_ptOrigin,point));
break;
case 4:
dc.Ellipse(CRect(m_ptOrigin,point));
break;
default:
break;
}
}
当拉动窗口,改变窗口尺寸时,原先绘制的图形消失。这是因为窗口尺寸改变会引起窗口的重绘,被发送一个WM_PAINT消息.首先擦除窗口背景,然后再进行重绘.要使我们绘制的图形始终呈现出来,就需要将绘制的图形保存起来,当窗口重绘时再次输出图形。在窗口中输出图形可以在OnDraw()函数中完成,因为当窗口重绘时,总是要去调用OnDraw()函数。
要保存图形,只要保存绘画的类型(点、直线、矩形和椭圆)、坐标原点和终点这三个要素、然后在OnDraw()函数中根据我们保存的绘画类型和起点终点,再次在窗口中输出我们的图形。
可以定义一个结构体来保存图形的三要素。
8、Insert->New Class,Class Type选择:Generic Class,类名为:CGraph
9、在CGraph类中增加三个成员变量:
public:
UINT m_nDrawType;
CPoint m_ptOrigin;
CPoint m_ptEnd;
public:
UINT m_nDrawType;
CPoint m_ptOrigin;
CPoint m_ptEnd;
10、构造CGraph类的一个对象来保存图形的三要素,首先构造一个带参数的构造函数:
CGraph(UINT m_nDrawType,CPoint m_ptOrigin,CPoint m_ptEnd);
在CGraph类的源文件当中插入代码:
CGraph::CGraph(UINT m_nDrawType,CPoint m_ptOrigin,CPoint m_ptEnd)
{
//在这里对类的成员变量进行赋值:
this->m_nDrawType=m_nDrawType;
this->m_ptOrigin=m_ptOrigin;
this->m_ptEnd=m_ptEnd;
}
CGraph::CGraph(UINT m_nDrawType,CPoint m_ptOrigin,CPoint m_ptEnd)
{
//在这里对类的成员变量进行赋值:
this->m_nDrawType=m_nDrawType;
this->m_ptOrigin=m_ptOrigin;
this->m_ptEnd=m_ptEnd;
}
我们可能绘制多副图形!那么,需要定义多少个CGraph类的对象呢?对象又需要用什么样的数据结构去存储呢?可以用数组、链表来存储。但数组容量有限,链表实现比较复杂。前面的章节中我们用了CString类来实现,这里我们采用MFC提供的一个集合类CPtrArray来实现,利用CPtrArray来存储一个对象的地址。
11、构造一个CGraph类的一个对象
在CGraphicView::OnButtonUp函数中增加代码:
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
switch(...)
{
...
}
在CGraphicView::OnButtonUp函数中增加代码:
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
switch(...)
{
...
}
CGraph graph(m_nDrawType,m_ptOrigin,point);
//在VIEW类中增加一个成员变量:private:CPtrArray m_ptrArray
m_ptrArray.Add(&graph);
...
}
...
}
12、接下来,将在VIEW类OnDraw()函数中将保存的图形取出来。
void CGraphicView::OnDraw(CDC* pDC)
{
CGraphicDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
//创建透明画刷
CBrush *pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
//将画刷选入设备描述表中
pDC->SelectObject(pBrush);
//接下来,作一个循环,将我们在CPtrArray集合类中保存的对象取出来
for(int i=0;i<m_ptrArray.GetSize();i++)
{
switch(((CGraph*)m_ptrArray.GetAt(i))->m_nDrawType) //m_ptrArray.GetAt()返回的是一个VOID指针
{
case 1:
pDC->SetPixel(((CGraph*)m_ptrArray.GetAt(i))->m_ptEnd,RGB(0,0,0));
break;
case 2:
pDC->MoveTo(((CGraph*)m_ptrArray.GetAt(i))->m_ptOrigin);
pDC->LineTo(((CGraph*)m_ptrArray.GetAt(i))->m_ptEnd);
break;
case 3:
pDC->Rectangle(CRect(((CGraph*)m_ptrArray.GetAt(i))->m_ptOrigin,
((CGraph*)m_ptrArray.GetAt(i))->m_ptEnd));
break;
case 4:
pDC->Ellipse(CRect(((CGraph*)m_ptrArray.GetAt(i))->m_ptOrigin,
((CGraph*)m_ptrArray.GetAt(i))->m_ptEnd));
break;
}
}
void CGraphicView::OnDraw(CDC* pDC)
{
CGraphicDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
//创建透明画刷
CBrush *pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
//将画刷选入设备描述表中
pDC->SelectObject(pBrush);
//接下来,作一个循环,将我们在CPtrArray集合类中保存的对象取出来
for(int i=0;i<m_ptrArray.GetSize();i++)
{
switch(((CGraph*)m_ptrArray.GetAt(i))->m_nDrawType) //m_ptrArray.GetAt()返回的是一个VOID指针
{
case 1:
pDC->SetPixel(((CGraph*)m_ptrArray.GetAt(i))->m_ptEnd,RGB(0,0,0));
break;
case 2:
pDC->MoveTo(((CGraph*)m_ptrArray.GetAt(i))->m_ptOrigin);
pDC->LineTo(((CGraph*)m_ptrArray.GetAt(i))->m_ptEnd);
break;
case 3:
pDC->Rectangle(CRect(((CGraph*)m_ptrArray.GetAt(i))->m_ptOrigin,
((CGraph*)m_ptrArray.GetAt(i))->m_ptEnd));
break;
case 4:
pDC->Ellipse(CRect(((CGraph*)m_ptrArray.GetAt(i))->m_ptOrigin,
((CGraph*)m_ptrArray.GetAt(i))->m_ptEnd));
break;
}
}
}
13、在VIEW类中,要想访问CGraph类,必须包含它的头文件:#include "Graph.h"。
14、编译->执行。当改变窗口大小时,图形仍然消失。可能的原因有两个:一是在OnDraw()函数中图形元素的数据没有取出来,二是在保存图形元素的时候出问题了。
原因分析:在OnLButtonUp()函数中定义的graph对象是一个局部对象m_ptrArray.Add(&graph)保存了graph对象的内存地址。但是,当OnLButtonUp()函数结束时,对象graph发生析构,内存被回收。虽然我们保存了对象的地址,但对象本身已经不存在了!这就好象一个牧童,手下里捏着缰绳,在大树底下睡着了。马咬断了缰绳跑了。虽然牧童手里仍拿着缰绳,但已经牧不到马了。
实际上,我们在集合类中是取出了地址的确,但当它去索引对象时,对象却不存在了!!!
如何解决这个问题呢?
14、定义一个指针类型的变量,然后在堆中为对象分配内存:
CGraph *pGraph=new CGraph(m_nDrawType,m_ptOrigin,point);
m_ptrArray.Add(pGraph);
CGraph *pGraph=new CGraph(m_nDrawType,m_ptOrigin,point);
m_ptrArray.Add(pGraph);
定义一个局部的指针变量,用new为对象在堆上分配内存空间,对象在堆上分配的内存空间,如果不显示地调用Delete去释放它,这个对象的生命周期和应用程序的生命周期是一样的!!当OnLButtonUp()函数执行完毕,局部指针变量pGraph被析构,但因为此时集合类对象保存了CGraph类对象在堆上的内存地址,因此它还能索引到对象在堆上分配的内存。
二、使窗口具有滚动功能
1、在VIEW类中增加消息处理WM_PAINT,在OnPaint()函数中增加代码:
void CGraphicView::OnPaint()
{
...
OnPrepareDC(&dc);
OnDraw(&dc);
...
}
void CGraphicView::OnPaint()
{
...
OnPrepareDC(&dc);
OnDraw(&dc);
...
}
2、在CGraphicView类头文件中,将基类CView改为CScrollView
3、在CGraphicView类源文件中将所有CView替换为CScrollView。方法为:Edit->Replace,选中Match whole word only,->Replace All
4、编译运行,出现非法操作!这是因为没有设置滚动条。
那么,应该在什么时候调用SetScrollSizes()函数设置滚动条呢?显然,应该在窗口创建完成之后。
5、在CGraphicView类中增加虚函数OnInitialUpdate()
OnInitialUpdate()函数调用发生在OnDraw()函数调用之前.OnInitialUpdate()是窗口创建完成之后第一个被调用的函数
OnInitialUpdate()函数调用发生在OnDraw()函数调用之前.OnInitialUpdate()是窗口创建完成之后第一个被调用的函数
6、在OnInitialUpdate()函数中增加代码:
void CGraphicView::OnInitialUpdate()
{
...
SetScrollSizes(MM_TEXT,CSize(800,600));
}
void CGraphicView::OnInitialUpdate()
{
...
SetScrollSizes(MM_TEXT,CSize(800,600));
}
编译运行,窗口就具有了水平和垂直滚动条。当我们把垂直滚动条拖动到最下边,在窗口右下方画图形。然后切换窗口到VC++IDE开发环境,之后再切换回窗口,这时在右下方绘制的图形跑到右上方去了。即产生图形的错位问题!这是因为窗口发生切换时,窗口要发生重绘,调用OnPrepareDC()函数显示上下文属性。
附1:关于图形错位的说明
当我们在窗口中点鼠标左键的时候,得到的是设备坐标(680,390),在MM_TEXT的映射模式下,逻辑坐标和设备坐标是相等的,所以我们利用集合类保存的这个点的坐标是以象素为单位,坐标值为(680,390).在调用OnDraw()函数前,在OnPaint()函数中调用了
当我们在窗口中点鼠标左键的时候,得到的是设备坐标(680,390),在MM_TEXT的映射模式下,逻辑坐标和设备坐标是相等的,所以我们利用集合类保存的这个点的坐标是以象素为单位,坐标值为(680,390).在调用OnDraw()函数前,在OnPaint()函数中调用了
OnPrepareDC()函数,调整了显示上下文的属性,将视口的原点设置为(0,-150),这样的话,窗口的原点,也就是逻辑坐标(0,0)被映射为设备坐标(0,-150),在画线的时候,因为GDI的函数使用的逻辑坐标,而图形在显示的时候,Windows需要将逻辑坐标转换为设备坐
标.因此,原先保存的坐标点(680,390)(在GDI函数中,作为逻辑坐标使用),根据转换公式xViewport=xWindow-xWinOrg+xViewOrg和yViewport=yWindow-yWinOrg+ViewOrg,得到设备点的x坐标为680-0+0=680,设备点的y坐标为390-0+(-150)=240,于是我们看到图形
在原先显示地方的上方出现了.
附2:关于解决方法的说明
首先我们在绘制图形之后,在保存坐标点之前,调用OnPrepareDC()函数,调整显示上下文的属性,将视口的原点设置为(0,-150),这样的话,窗口的原点,也就是逻辑坐标(0,0)将被映射为设备坐标(0,-150).然后我们调用DPtoLP()函数将设备坐标(680,390)转换为
首先我们在绘制图形之后,在保存坐标点之前,调用OnPrepareDC()函数,调整显示上下文的属性,将视口的原点设置为(0,-150),这样的话,窗口的原点,也就是逻辑坐标(0,0)将被映射为设备坐标(0,-150).然后我们调用DPtoLP()函数将设备坐标(680,390)转换为
逻辑坐标,根据设备坐标转换为逻辑坐标的公式:
xWindow=xViewport-xViewOrg+xWinOrg,yWindow=yViewport-yViewOrg+yWinOrg,得到逻辑点x坐标为680-0+0=680,y坐标为390-(-150)+0=540,将逻辑坐标(680,540)保存起来,在窗口重绘时,会先调用OnPrepareDC()函数,调整显示上下文的属性,将视口的原点设
xWindow=xViewport-xViewOrg+xWinOrg,yWindow=yViewport-yViewOrg+yWinOrg,得到逻辑点x坐标为680-0+0=680,y坐标为390-(-150)+0=540,将逻辑坐标(680,540)保存起来,在窗口重绘时,会先调用OnPrepareDC()函数,调整显示上下文的属性,将视口的原点设
置为(0,-150),然后GDI函数用逻辑坐标点(680,540)绘制图形,被Windows转换为设备坐标点(680,390),和原先显示图形的设备点是一样的,当然图形就还在原先的地方显示出来
7、在CGraphicView::OnLButtonUp函数中增加代码:
{
...
//CGraph graph(m_nDrawType,m_ptOrigin,point);
//m_ptrArray.Add(&graph);
{
...
//CGraph graph(m_nDrawType,m_ptOrigin,point);
//m_ptrArray.Add(&graph);
OnPrepareDC(&dc);
dc.DPtoLP(&m_ptOrigin);
dc.DPtoLP(&point);
dc.DPtoLP(&m_ptOrigin);
dc.DPtoLP(&point);
CGraph *pGraph=new CGraph(m_nDrawType,m_ptOrigin,point);
m_ptrArray.Add(pGraph);
...
}
m_ptrArray.Add(pGraph);
...
}
注意:OnPrepareDC()函数会随时根据滚动窗口的位置来调整视口的原点!
三、保存重绘图形(2)
CMetaFileDC
1、在CGraphicView类中增加私有成员变量CMetaFileDC m_dcMetaFile,并在构造函数中调用Create()函数:
m_dcMetaFile.Create();
m_dcMetaFile.Create();
2、在CGraphicView::OnLButtonUp将dc.SelectObject(pBrush);注释替换为m_dcMetaFile.SelectObject(pBrush);switch(){}中
的dc也注释替换为m_dcMetaFile.并将以下代码先注释:
/* OnPrepareDC(&dc);
dc.DPtoLP(&m_ptOrigin);
dc.DPtoLP(&point);
/* OnPrepareDC(&dc);
dc.DPtoLP(&m_ptOrigin);
dc.DPtoLP(&point);
CGraph *pGraph=new CGraph(m_nDrawType,m_ptOrigin,point);
m_ptrArray.Add(pGraph);*/
m_ptrArray.Add(pGraph);*/
3、在OnDraw()函数中关闭元文件,获得元文件的句柄。
在CGraphicView::OnDraw(...)函数中注释以下代码:
{
...
/* CBrush *pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
pDC->SelectObject(pBrush);
在CGraphicView::OnDraw(...)函数中注释以下代码:
{
...
/* CBrush *pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
pDC->SelectObject(pBrush);
for(int i=0;i<m_ptrArray.GetSize();i++)
{
...
}*/
//增加的代码:
HMETAFILE hmetaFile; //定义一个元文件的HMETAFILE句柄
hmetaFile=m_dcMetaFile.Close(); //关闭元文件的设备上下文,得到一个元文件的句柄
pDC->PlayMetaFile(hmetaFile); //播放元文件
m_dcMetaFile.Create(); //再次准备一个元文件的设备上下文,用于窗口重绘后再次绘图
m_dcMetaFile.PlayMetaFile(hmetaFile); //保存原先绘制的图形
DeleteMetaFile(hmetaFile); //删除元文件资源
}
{
...
}*/
//增加的代码:
HMETAFILE hmetaFile; //定义一个元文件的HMETAFILE句柄
hmetaFile=m_dcMetaFile.Close(); //关闭元文件的设备上下文,得到一个元文件的句柄
pDC->PlayMetaFile(hmetaFile); //播放元文件
m_dcMetaFile.Create(); //再次准备一个元文件的设备上下文,用于窗口重绘后再次绘图
m_dcMetaFile.PlayMetaFile(hmetaFile); //保存原先绘制的图形
DeleteMetaFile(hmetaFile); //删除元文件资源
}
编译运行,在窗口中画图形。当切换窗口引起窗口重绘时,我们原先绘制的图形就出现了。当再次绘图时,原先绘制的图形不见了。如果要保存原先绘制的图形,要在m_dcMetaFile.Create();后增加代码:m_dcMetaFile.PlayMetaFile(hmetaFile);
接下来,将我们绘制的图形保存在我们的文件当中。所用到的函数是CopyMetaFile
4、在菜单项"打开"和"保存"增加成员函数OnFileSave和OnFileOpen,ClassName选择CGraphView,编辑代码
在CGraphicView::OnFileSave()函数中增加代码:
void CGraphicView::OnFileSave()
{
// TODO: Add your command handler code here
HMETAFILE hmetaFile;
hmetaFile=m_dcMetaFile.Close();
CopyMetaFile(hmetaFile,"meta.wmf");
m_dcMetaFile.Create();
DeleteMetaFile(hmetaFile);
}
在CGraphicView::OnFileSave()函数中增加代码:
void CGraphicView::OnFileSave()
{
// TODO: Add your command handler code here
HMETAFILE hmetaFile;
hmetaFile=m_dcMetaFile.Close();
CopyMetaFile(hmetaFile,"meta.wmf");
m_dcMetaFile.Create();
DeleteMetaFile(hmetaFile);
}
编译运行,在窗口中绘画,然后点击保存按钮,在源文件目录下就生成了"meta.wmf"文件。注意:这里的保存并不是保存图形的数据,而只是保存图形输出的命令!
接着,我们打开图形。所用到的函数是:GetMetaFile
MSDN:
The GetMetaFile function is not implemented in the Win32 API. This function is provided for compatibility with
The GetMetaFile function is not implemented in the Win32 API. This function is provided for compatibility with
16-bit versions of Windows. Win32-based applications should use the GetEnhMetaFile function
意思是:GetMetaFile这个函数已经废弃了,它主要是为了兼容16位Window版本的。基于32位的应用程序应当使用增强型GetEnhMetaFile函数。
意思是:GetMetaFile这个函数已经废弃了,它主要是为了兼容16位Window版本的。基于32位的应用程序应当使用增强型GetEnhMetaFile函数。
5、CGraphicView::OnFileOpen()中增加代码:
void CGraphicView::OnFileOpen()
{
HMETAFILE hmetaFile;
hmetaFile=GetMetaFile("meta.wmf");
m_dcMetaFile.PlayMetaFile(hmetaFile);
DeleteMetaFile(hmetaFile);
Invalidate();
}
void CGraphicView::OnFileOpen()
{
HMETAFILE hmetaFile;
hmetaFile=GetMetaFile("meta.wmf");
m_dcMetaFile.PlayMetaFile(hmetaFile);
DeleteMetaFile(hmetaFile);
Invalidate();
}
编译运行,点击打开按钮,直接打开原先保存的文件。然后再绘画->保存->打开...
四、保存重绘图形(3)--利用兼容DC
原理:利用兼容DC在内存当中准备一幅图像,然后将图像拷贝到时目的DC。也可以利用兼容DC保存图形,然后在OnDraw()函数将兼容DC保存的图形拷贝到目的DC当中。
1、在CGraphView类中增加私有成员变量:CDC m_dcCompatible
2、在OnLButtonUp()函数中判断兼容DC是否已经创建,因为OnLButtonUp()函数会被多次调用
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
...
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);
//解决步骤3中出现的问题所要增加的代码:
m_dcCompatible.BitBlt(0,0,rect.Width(),rect.Height(),&dc,0,0,SRCCOPY);
m_dcCompatible.SelectObject(pBrush);
}
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
...
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);
//解决步骤3中出现的问题所要增加的代码:
m_dcCompatible.BitBlt(0,0,rect.Width(),rect.Height(),&dc,0,0,SRCCOPY);
m_dcCompatible.SelectObject(pBrush);
}
switch(m_nDrawType)
{
//在这里将m_dcMetaFile替换为m_dcCompatible,并将原先的注释掉
}
...
}
{
//在这里将m_dcMetaFile替换为m_dcCompatible,并将原先的注释掉
}
...
}
3、在OnDraw()函数中利用兼容DC将图像拷贝到目的DC当中.首先将OnDraw()函数中增加的代码注释掉,并在其中增加代码:
void CGraphicView::OnDraw(CDC* pDC)
{
...
CRect rect;
GetClientRect(&rect);
pDC->BitBlt(0,0,rect.Width(),rect.Height(),&m_dcCompatible,0,0,SRCCOPY);
}
void CGraphicView::OnDraw(CDC* pDC)
{
...
CRect rect;
GetClientRect(&rect);
pDC->BitBlt(0,0,rect.Width(),rect.Height(),&m_dcCompatible,0,0,SRCCOPY);
}
编译运行,在窗口中绘画,当改变窗口尺寸时,窗口变黑!为什么会出现这种问题呢?这是因为:CreateCompatibleBitmap返回的位图对象只包含相应设备描述表中的位图信息头,不包含颜色表和像素数据块。因此,选入该位图对象的设备描述表不能像选入普通位图对象的设备描述表一样应用,必须在SelectObject()函数之后,调用BitBlt将原始设备描述表的颜色表及像素数据块拷贝到兼容设备描述表。
4、OnLButtonUp()函数中m_dcCompatible.SelectObject(&bitmap);代码之后调用BitBlt:
m_dcCompatible.BitBlt(0,0,rect.Width(),rect.Height(),&dc,0,0,SRCCOPY);
m_dcCompatible.BitBlt(0,0,rect.Width(),rect.Height(),&dc,0,0,SRCCOPY);
编译运行->绘图->切换窗口->图形正常显示。
注:以上是根据孙鑫老师VC++视频教程所作的笔记,所有权归孙老师所有,转载请注明所有权!
转载于:https://blog.51cto.com/debugger/167324