1.图形的绘制
(1)开始绘制图形:
在CGraphicView中增加一个成员变量用来保存用户的选择:
private:
UINTm_nDrawType;
初始化:
m_nDrawType=0;
在CGraphicView中再增加一个成员变量用来保存图形的起点:
private:
CPointm_ptOrigin;
初始化:
m_ptOrigin=0;
void CGraphicView::OnDot()
{
//TODO: Add your command handler code here
m_nDrawType=1;
}
void CGraphicView::OnLine()
{
//TODO: Add your command handler code here
m_nDrawType=2;
}
void CGraphicView::OnRectangle()
{
//TODO: Add your command handler code here
m_nDrawType=3;
}
void CGraphicView::OnEllipse()
{
//TODO: Add your command handler code here
m_nDrawType=4;
}
voidCGraphicView::OnLButtonDown(UINT nFlags, CPoint point)
{
//TODO: Add your message handler code here and/or call default
//将这个点保存起来
m_ptOrigin=point;
CView::OnLButtonDown(nFlags,point);
}
voidCGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
//TODO: Add your message handler code here and/or call default
//开始图形的绘制
CClientDCdc(this);
//创建一个透明的画刷
//FromHandle它可以通过一个画刷的句柄,返回一个只想CBrush的指针
//画刷的句柄通过来获取
CBrush*pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
dc.SelectObject(pBrush);
switch(m_nDrawType)
{
case1:
dc.SetPixel(point,RGB(0,0,0));
break;
case2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case3:
dc.Rectangle(CRect(m_ptOrigin,point));
break;
case4:
dc.Ellipse(CRect(m_ptOrigin,point));
break;
}
CView::OnLButtonUp(nFlags,point);
}
当改变窗口的大小时,窗口中绘制的图形消失了。这是因为窗口尺寸发生变化的时候,要引起窗口的重绘,会发送一个WM_PAINT消息。首先要擦出窗口背景,然后再进行重绘,那么擦出背景的时候,就将刚才绘制的图形擦除掉了。
(2)图形的保存:
我们希望绘制的图形能够在窗口当中始终呈现出来,那么我们就需要将绘制的图形保存起来,然后当窗口尺寸发生变化,引起窗口重绘的时候,我们将这个图形再次在窗口当中输出。那么在窗口当中输出图形,我们可以在OnDraw函数当中去完成。因为当窗口发生重绘的时候,总是要去调用OnDraw函数,我们在OnDraw函数当中再次去输出这个图形。
但是,这个图形要如何去保存呢?
要保存绘画的类型m_nDrawType还有两个点m_ptOrigin、point,用结构体来保存不同类型的变量是比较合适的。
在C++中结构体就是一个类,所以呢我们也可以利用一个类的对象来保存图形的三要素,这也比较符合面向对象的思想,一个类的对象它保存的是我们相关的图形的三个要素,所以我们可以插入一个新的类:
Class Type: Generic Class;
Name: CGraph
在这个类中添加三个成员变量:
public:
//起点
CPointm_ptOrigin;
//终点
CPointm_ptEnd;
UINTm_nDrawType;
把它们设置成public,因为待会要在CGraphicView当中要访问它的成员变量。
我们就可以通过构造CGraph这个类的对象来保存一个图形的三个要素,为了方便对这三个变量进行赋值,我们去提供一个带参数的构造方法,让用户在构造的时候,直接传递参数进来我们这三个成员变量进行赋值。
在CGraph.h中添加带参的构造函数:
CGraph(UINTm_nDrawType,CPoint m_ptOrigin,CPoint m_ptEnd);
在CGraph.cpp中:
//有了CGraph这个类我们就可以通过去构造这类的一个对象来保存一个图形的三个要素
CGraph::CGraph(UINTm_nDrawType,CPoint m_ptOrigin,CPoint m_ptEnd)
{
this->m_nDrawType=m_nDrawType;
this->m_ptOrigin=m_ptOrigin;
this->m_ptEnd=m_ptEnd;
}
利用CPtrArray去存储对象的一个地址。
voidCGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
//TODO: Add your message handler code here and/or call default
//开始图形的绘制
CClientDCdc(this);
//创建一个透明的画刷
//FromHandle它可以通过一个画刷的句柄,返回一个指向CBrush的指针
//画刷的句柄通过CBrush来获取
CBrush*pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
dc.SelectObject(pBrush);
switch(m_nDrawType)
{
case1:
dc.SetPixel(point,RGB(0,0,0));
break;
case2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case3:
dc.Rectangle(CRect(m_ptOrigin,point));
break;
case4:
dc.Ellipse(CRect(m_ptOrigin,point));
break;
}
//先构造一个对象:
//CGraphgraph(m_nDrawType,m_ptOrigin,point);
//然后将这个对象的地址保存到集合类的对象当中
//增加一个对象:private: CPtrArraym_ptArray;
//然后我们就用集合类的对象调用Add方法,保存对象的地址
//m_ptArray.Add(&graph);
//接下来在OnDraw函数当中,将集合类对象当中所保存的图形的
//元素取出来
//运行之后,改变窗口尺寸,图形还是会消失,为什么?
//我们在OnDraw函数当中,我们实际上是取出了这个地址的,但是
//这个地址想要索引到这个对象,这个对象却又不存在了,于是
//我们就没有看到这个图形。
怎样解决这个问题:
//改一下:
//我们去定义一个指针类型的变量,然后利用new在堆中为我们的
//对象分配内存,然后调用Add方法,加入指针类型的变量pGraph
//这时候加入的就是我们在堆中所分配的对象的内存地址
CGraph*pGraph=new CGraph(m_nDrawType,m_ptOrigin,point);
m_ptArray.Add(pGraph);
CView::OnLButtonUp(nFlags,point);
}
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);
//将我们在集合类当中所保存的图形对象取出来
for(inti=0;i<m_ptArray.GetSize();i++)
{
//根据我们所保存的绘画的类型,去绘制这个图形
switch(((CGraph*)m_ptArray.GetAt(i))->m_nDrawType)
{
case1:
pDC->SetPixel(((CGraph*)m_ptArray.GetAt(i))->m_ptEnd,RGB(0,0,0));
break;
case2:
pDC->MoveTo(((CGraph*)m_ptArray.GetAt(i))->m_ptOrigin);
pDC->LineTo(((CGraph*)m_ptArray.GetAt(i))->m_ptEnd);
break;
case3:
pDC->Rectangle(CRect(((CGraph*)m_ptArray.GetAt(i))->m_ptOrigin,
((CGraph*)m_ptArray.GetAt(i))->m_ptEnd));
break;
case4:
pDC->Ellipse(CRect(((CGraph*)m_ptArray.GetAt(i))->m_ptOrigin,
((CGraph*)m_ptArray.GetAt(i))->m_ptEnd));
break;
//最后还要包含头文件: #include"Graph.h"
}
}
}
(2)在窗口中可以滚动:
在GraphicView.h中只有一个CView,将CView改为CGraphicView;
在GraphicView.cpp中有多个CView,我们可以Edit->replace中将CView替换为CGraphicView;选中Match whole word only ;ReplaceAll.
运行之后出现“非法操作”。
增加一个虚函数:OnInitialUpdate,这个函数调用还在OnDraw函数调用之前,也就是说当一个窗口完全创建之后,在第一次调用OnDraw函数之前。
void CGraphicView::OnPaint()
{
CPaintDCdc(this); // device context for painting
//TODO: Add your message handler code here
//用来调整、显示上下文的一个属性
OnPrepareDC(&dc);
OnDraw(&dc);
//Do not call CScrollView::OnPaint() for painting messages
}
voidCGraphicView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
//TODO: Add your specialized code here and/or call the base class
SetScrollSizes(MM_TEXT,CSize(800,600));
}
有时候我们要在窗口创建之后做一些初始化工作,那么就可以放到OnInitialUpdate这个虚函数当中,因为它是在我们这个窗口完全创建之后,第一个调用的函数。
问题:拖动滚动条到最下端,画一条线,切换窗口再回来发现之前画得那条线跑到上面去了。
我们作图的函数都是逻辑坐标,它需要转化为设备坐标。在图形显示的时候Windows会完成这个转换。
在CGraphicView::OnLButtonUp中添加一段代码:
在case 4:
Break;之后添加:
//当图形绘制好之后,在保存坐标点之前,我们先调用一个OnPrepareDC
//调整显示上下文的属性,然后用DPtoLP设备点转换为逻辑点
OnPrepareDC(&dc);
dc.DPtoLP(&m_ptOrigin);
dc.DPtoLP(&point);
运行结果:之前画得直线还是在原先的位置显示出来,我们在理解逻辑坐标点和设备坐标点转换,要记住,无论视口原点和窗口原点怎样变换,设备坐标点的(0,0)始终指的是客户区的左上角。
3.保存图形和重绘图形的方式:
(1)方法一:
在CGraphicView中添加一个成员变量:
private:
CMetaFileDCm_dcMetaFile;
初始化:
//去调用Create方法
m_dcMetaFile.Create();
在 CGraphicView::OnLButtonUp中修改代码:
// dc.SelectObject(pBrush);
//将这个dc换一下,将这个透明的画刷选择到源文件设备上下文当中
m_dcMetaFile.SelectObject(pBrush);
switch(m_nDrawType)
{
case1:
// dc.SetPixel(point,RGB(0,0,0));
m_dcMetaFile.SetPixel(point,RGB(0,0,0));
break;
case2:
// dc.MoveTo(m_ptOrigin);
// dc.LineTo(point);
m_dcMetaFile.MoveTo(m_ptOrigin);
m_dcMetaFile.LineTo(point);
break;
case3:
// dc.Rectangle(CRect(m_ptOrigin,point));
m_dcMetaFile.Rectangle(CRect(m_ptOrigin,point));
break;
case4:
// dc.Ellipse(CRect(m_ptOrigin,point));
m_dcMetaFile.Ellipse(CRect(m_ptOrigin,point));
break;
}
当我们给源文件dc发送GDI它的绘图的命令,发送完成之后,当窗口重绘的时候,那么我们在OnDraw函数当中去关闭这个源文件去获得源文件的句柄。
void CGraphicView::OnDraw(CDC* pDC)
{
CGraphicDoc*pDoc = GetDocument();
ASSERT_VALID(pDoc);
//HMETAFILE这是一个源文件的句柄,返回之后我们就可以用PlayMetaFile函数
//来播放这个源文件。
HMETAFILEhmetaFile;
//关闭源文件的设备上下文,得到一个源文件的句柄
hmetaFile=m_dcMetaFile.Close();
//目的dc,也就是窗口的dc调用PlayMetaFile,传递一个源文件的句柄hmetaFile
pDC->PlayMetaFile(hmetaFile);
//接下来再去准备一个源文件的设备上下文
m_dcMetaFile.Create();
m_dcMetaFile.PlayMetaFile(hmetaFile);
//因为当窗口发生重绘的时候你可能想继续绘制图形,当然我们需要
//让它在源文件dc当中去绘制,我们再次去调用Create()方法去创
//建一个源文件,将它和源文件dc这个对象m_dcMetaFile关联起来。
//我们播放结束了,这个句柄我们就不再需要了。不需要我们可以将
//它的源文件删除。因为源文件它也是一种资源和我们的画刷画笔都是一种资源。
DeleteMetaFile(hmetaFile);
//因为我们调用Create的时候,是新创建了一个源文件,所以先前所绘制的
//图形都已经不存在了。如果我们想要保存先前绘制的图形,那么我们在
//创建新的源文件之后呢,我们可以利用源文件dc:m_dcMetaFile它去调用
//PlayMetaFile,去播放先前的源文件。
}
响应菜单项中的“打开”和“保存”:
void CGraphicView::OnFileSave()
{
//TODO: Add your command handler code here
//首先定义一个句柄的变量:
HMETAFILEhmetaFile;
hmetaFile=m_dcMetaFile.Close();
//windowsmetafile就是windows的源文件
CopyMetaFile(hmetaFile,"meta.wmf");
//拷贝完之后,我们可以重新创建一个源文件,以备下次的绘图
m_dcMetaFile.Create();
//这个源文件不需要了,可已经将它删除
DeleteMetaFile(hmetaFile);
}
void CGraphicView::OnFileOpen()
{
//TODO: Add your command handler code here
HMETAFILEhmetaFile;
//我们先从所保存的源文件的文件中得到一个源文件的句柄
hmetaFile=GetMetaFile("meta.wmf");
//然后在我们所构造的源文件的设备上下文中去播放这个源文件
//这个源文件输出的命令就已经记录下来了
m_dcMetaFile.PlayMetaFile(hmetaFile);
//之后对于源文件句柄来说没有用了,我们将它删除
DeleteMetaFile(hmetaFile);
//删除之后调用Invalidate引起窗口的重画,窗口重画会调用OnDraw函数
Invalidate();
}
总结:利用源文件来来保存我们所绘制的图形。它保存的并不是图形的数据,而是保存我们图形输出的命令。
(2)方法二:
利用兼容dc在内存当中准备一幅图像,然后将这副图像拷贝到目的dc当中,我们也可以利用这个兼容dc来保存图形,在OnDraw函数当中,我们将兼容dc所保存的图形拷贝到目的dc当中。
在CGraphicView当中增加一个成员变量:
private:
CDCm_dcCompatible;
在 CGraphicView::OnLButtonUp中添加代码:
添加在switch语句之前:
//判断一下这个兼容dc是否已经创建
if(!m_dcCompatible.m_hDC)
{
//那么我们可以去创建一个兼容dc,和当前的dc兼容
m_dcCompatible.CreateCompatibleDC(&dc);
//GetClientRect获取当前窗口的客户区域的大小,利用客户区域的大小
//来设定兼容位图的大小。
CRectrect;
GetClientRect(&rect);
CBitmapbitmap;
//创建一个兼容dc的位图
bitmap.CreateCompatibleBitmap(&dc,rect.Width(),rect.Height());
//这样的话,有了兼容位图了,我们就以将这个兼容位图选择到兼容dc当中
//从而确定兼容dc显示表面的大小。
//首先要选择一幅位图
m_dcCompatible.SelectObject(&bitmap);
//这样的话才能确定兼容dc显示表面的大小,通过这个兼容位图来确定
//将透明的画刷选择到兼容dc当中
m_dcCompatible.SelectObject(pBrush);
}
//将下面的dc也都改为兼容dc
switch(m_nDrawType)
{
case1:
// dc.SetPixel(point,RGB(0,0,0));
// m_dcMetaFile.SetPixel(point,RGB(0,0,0));
m_dcCompatible.SetPixel(point,RGB(0,0,0));
break;
case2:
// dc.MoveTo(m_ptOrigin);
// dc.LineTo(point);
// m_dcMetaFile.MoveTo(m_ptOrigin);
// m_dcMetaFile.LineTo(point);
m_dcCompatible.MoveTo(m_ptOrigin);
m_dcCompatible.LineTo(point);
break;
case3:
// dc.Rectangle(CRect(m_ptOrigin,point));
// m_dcMetaFile.Rectangle(CRect(m_ptOrigin,point));
m_dcCompatible.Rectangle(CRect(m_ptOrigin,point));
break;
case4:
// dc.Ellipse(CRect(m_ptOrigin,point));
// m_dcMetaFile.Ellipse(CRect(m_ptOrigin,point));
m_dcCompatible.Ellipse(CRect(m_ptOrigin,point));
break;
}
/*
void CGraphicView::OnDraw(CDC*pDC)
{
CGraphicDoc*pDoc = GetDocument();
ASSERT_VALID(pDoc);
//将兼容dc中的图形拷贝到目的dc当中
CRectrect;
//得到客户区域的大小
GetClientRect(&rect);
//拷贝
pDC->BitBlt(0,0,rect.Width(),rect.Height(),&m_dcCompatible,0,0,SRCCOPY);
}
再到CGraphicView::OnLButtonUp中添加一行代码,
在m_dcCompatible.SelectObject(&bitmap);这行代码之后添加:
//当我们选择位图之后,去调用一个BitBlt,这个调用是将目的dc的就是现在的设备描述表中原始的颜色表和像素数据块拷贝到兼容dc当中。
m_dcCompatible.BitBlt(0,0,rect.Width(),rect.Height(),&dc,0,0,SRCCOPY);
总结:这是利用兼容dc来保存图形、重绘。这个重绘实际上是用了BitBlt贴图来完成的,我们想要在图形绘制过程中,在窗口中也能够看到这个图形,可以我dc调用一次,用兼容dc再调用一次,我们就可以看到这个图形了,同时这个图形在兼容dc当中也保存起来了