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

VC++深入详解(9):图形的保存和重绘

2012-10-30 14:46 253 查看
首先,我们重新实现一下上次课实现的功能:通过菜单,选择画哪种图形,然后进行绘图。但是这个功能有一个严重的问题,就是窗口发生重绘(比如我们把程序最小化以后再重新显示)以后,原来的图就不见了!怎么解决这个问题呢?我们必须保存每次画的图。然后在重绘函数中把它们重新画出来。准确的说,其实我们只需要保存3样东西就行了:类型(画点、画线、画矩形、画椭圆)、起点、终点。我们可以用一个类封装它们。

class CGraph
{
public:
CGraph();
CGraph(UINT m_nDrawType, CPoint m_ptOrigin, CPoint m_ptEnd);
virtual ~CGraph();
UINT m_nDrawType;
CPoint m_ptOrigin;
CPoint 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;
}
那么把它们存在什么地方呢?肯定不能使用数组,因为我们并不知道要存多少元素。MFC为我们封装了一个称为CPtrArray类,它支持void*类型的。通过GetAt来获得某个元素,通过Add 来增加一个元素,通过GetSize 来获得总的元素个数。我们为自己的view类增加一个CPtrArray类型的成员变量m_ptrArray,然后在每次OnLButtonUp消息的中,将前面提到的3个变量保存:

CGraph graph(m_nDrawType,m_ptOrigin,point);
m_ptrArray.Add(&graph);
然后,在OnDraw函数中把它们重新画出来:

void CCH_11_GRAPHView::OnDraw(CDC* pDC)
{
CCH_11_GRAPHDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
CBrush *pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
for(int i = 0 ; i < m_ptrArray.GetSize();++i)
{
switch (((CGraph*)m_ptrArray.GetAt(i))->m_nDrawType)
{
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;
}
}
}


但是我们发现,当窗口重绘以后,原来的图像依然没有显示出来,问题出在了哪里呢?其实在于OnLButtonUp中的构造的graph是一个局部变量,所以,当程序运行完以后,它就被析构了。即使我们保存了它的地址,但是在OnDraw函数中使用的,实际上是已经被虚构的无效的地址了,所以不能正常绘图。解决的办法也很简单,就是在OnLButtonUp中new一个graph出来,然后增加到m_ptrArray中就行了:

//	CGraph graph(m_nDrawType,m_ptOrigin,point);
//	m_ptrArray.Add(&graph);
CGraph *pGraph = new CGraph(m_nDrawType,m_ptOrigin,point);
m_ptrArray.Add(pGraph);
我们是在OnDraw函数中把图重新画上去的。窗口重绘时会发送WM_PAINT消息,响应这个消息的函数并不是onDraw,而是OnPaint!为什么我们还是能够成功的实现功能呢?

在VIEWCORE.CPP中:可以看到:

void CView::OnPaint()
{
// standard paint routine
CPaintDC dc(this);
OnPrepareDC(&dc);
OnDraw(&dc);
}
仔细看一下这3行代码:

第一句是构造一个DC,但是如果是在对WM_PAINT响应时,必须使用BeginPaint和EndPaint函数。这里并没有相关代码,为什么呢?因为它的构造和析构函数中调用了它们。而且CPaintDC只在响应WM_PAINT消息时使用。

OnPrepareDC函数,从MSDN中可知:当屏幕显示调用OnDraw函数之前或者打印预览调用OnPrint函数函数之前,框架类都会调用这个函数。如果这个函数是为屏幕显示而调用,那么它什么也不做,但是它在派生类中被覆盖(例如CScrollView),用来调整设备上下文的属性。简而言之,要调整设备上下文的属性,必须先调用它。这里先留点印象,后面会继续讨论。

最后看OnDraw。它也是虚函数,会引起派生类中的OnDraw调用。

所以,我们看到的是派生类的OnDraw函数被调用,实际上WM_PAINT消息由基类的OnPaint消息响应,在这个消息中,调用了虚函数,也就是派生类的OnDraw。

假如我们重写了OnPaint消息,而在其中调用OnPrepareDC和OnDraw,也能达到同样的效果。

void CCH_11_GRAPHView::OnPaint()
{
CPaintDC dc(this); // device context for painting

// TODO: Add your message handler code here
OnPrepareDC(&dc);
OnDraw(&dc);
// Do not call CView::OnPaint() for painting messages
}
下面我们看滚动条窗口的实现。我们知道,可以通过把view类从CScrollView中派生就行了,但是我们的程序已经写了这么多了,不想重新写,怎么办?可以修改两个地方:

1.头文件中把继承改为CScrollView。2.把对应源文件的所有CView都改成CScrollView。

但是修改完后运行程序出错,这是因为滚动条的相关内容(比如点击一下箭头移动多少,点击空白处滚动的数量等等)都还没有设置,可以通过SetScrollSizes来设定。这个函数放在哪里呢?我们把它放在虚函数OnInitialUpdate中它是窗口创建完成后第一个被调用的函数,在OnDraw函数之前。所以,如何想在窗口完成之后做一些初始化工作,就应该放在这里:

void CCH_11_GRAPHView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
// TODO: Add your specialized code here and/or call the base class
SetScrollSizes(MM_TEXT,CSize(800,600));
}
这样就好了,但是程序有一点小问题,就是如果你在整个窗口的右下角(滚动条拖到最下面)画一条线,那么当窗口重绘时,这条线却上移了一段距离!这是为什么呢?

我们先回顾一下映射模式的知识:

视口是基于设备坐标的;窗口是基于逻辑坐标的。我们写的画图程序,都是在逻辑坐标下完成的。当把逻辑坐标转化到设备坐标时,需要有一个转化公式:

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

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

或者:

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

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

这个公式决定了转换的平移因子和尺度缩放因子。windows提供的多种映射模式,其中最常用的是MM_TEXT。这个映射模式有如下特点:

窗口原点:(0, 0) 可以改变

视口原点:(0, 0) 可以改变

窗口范围:(1, 1) 不可改变

视口范围:(1, 1) 不可改变

这意味着窗口坐标与逻辑坐标之间没有比例变换,只有原点设置引起的平移变换。所以它也称为称为“全约束”的映射方式。

可以通过SetViewportOrgEx和SetWindowOrgEx,用来改变视口和窗口的原点,但一般我们只改变一个就能达到满意的效果,两个同时改变则容易产生混乱。通过GetViewportOrgEx和GetWindowOrgEx来获取目前视端口和窗口的原点。

然后看我们的程序MFC重写了CScrollView里面的OnPrepareDC函数,下面是部分代码:
ASSERT(m_totalDev.cx >= 0 && m_totalDev.cy >= 0);
switch (m_nMapMode)
{
case MM_SCALETOFIT:
pDC->SetMapMode(MM_ANISOTROPIC);
pDC->SetWindowExt(m_totalLog);  // window is in logical coordinates
pDC->SetViewportExt(m_totalDev);
if (m_totalDev.cx == 0 || m_totalDev.cy == 0)
TRACE0("Warning: CScrollView scaled to nothing.\n");
break;

default:
ASSERT(m_nMapMode > 0);
pDC->SetMapMode(m_nMapMode);
break;
}

CPoint ptVpOrg(0, 0);       // assume no shift for printing
if (!pDC->IsPrinting())
{
ASSERT(pDC->GetWindowOrg() == CPoint(0,0));

// by default shift viewport origin in negative direction of scroll
ptVpOrg = -GetDeviceScrollPosition();

if (m_bCenter)
{
CRect rect;
GetClientRect(&rect);

// if client area is larger than total device size,
// override scroll positions to place origin such that
// output is centered in the window
if (m_totalDev.cx < rect.Width())
ptVpOrg.x = (rect.Width() - m_totalDev.cx) / 2;
if (m_totalDev.cy < rect.Height())
ptVpOrg.y = (rect.Height() - m_totalDev.cy) / 2;
}
}
pDC->SetViewportOrg(ptVpOrg);
可以看到,它先是判断你选择的是什么映射模式,我们这里是MM_TEXT,所以走得是default下面的内容。然后设了一个圆点ptVpOrg为(0,0),把GetDeviceScrollPosition的值取负数赋给ptVpOrg,最终把这个点通过SetViewportOrg设为了视口圆点。通过调试后我们发现,这个点的值变为了(0,-179)。也就是说,窗口的原点,被映射成了视口的(0,-179)。

问题就出在这里了,当我们画线时,我们画在了视口上(x0,y0),由于此时窗口与视口的原点是同一个点,所以对应过来的窗口坐标也是(x0,y0)。当我们画图时,代码里使用的是窗口坐标,但是需要把它转化为视口坐标,根据映射转换公式:

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

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

(x0,y0)就被转化成了(x0,y0-179),所以画的线就跑到了原来的位置的上面去了。

那么如何解决呢?在OnLButtonDown函数中,存入pGraph之前,将设备坐标转化成逻辑坐标,然后在保存。
OnPrepareDC(&dc);
dc.DPtoLP(&m_ptOrigin);
dc.DPtoLP(&point);
CGraph *pGraph = new CGraph(m_nDrawType,m_ptOrigin,point);
总而言之,程序编写的内容,始终是针对逻辑坐标的。所以如果你获取了设备的坐标,那你得进行转化后再使用。

下面看看另一种保存图形和绘制图形的方式——使用元文件。它也是把之前的绘图操作保存了起来,然后在窗口重绘时再重新画出来。与之前的保存内容(画图类型、起点、终点)不同,元文件里保存的是画图的那些操作,比如MoveTo 、LineTo之类的。如果你想看具体画的是什么样子,你得把元文件“播放”一遍才行。

与元文件相关的是CMetaFileDC类。我们看看如何使用它:

1.利用构造函数创建一个CMetaFileDC类的对象,然后调用create成员函数创建windows元文件设备上下文并把它与元文件相关联。

2。向元文件发送一系列的GDI指令。

3.当你发送完成后,调用close函数,这个函数会关闭设备上下文,并且返回一个元文件句柄。

4.用这个句柄作为参数,调用PlayMetaFile 来“播放”元文件。

5.播放完成以后,释放该元文件。

下面我们就修改一下我们的程序:

1.首先,为我们的view类增加一个CMetaFileDC类型的私有变量m_dcMetaFile。在构造函数中:

CCH_11_GRAPHView::CCH_11_GRAPHView()
{
// TODO: add construction code here
m_nDrawType = -1;
m_ptOrigin = 0;
m_dcMetaFile.Create();
}
2.在OnLButtonUp中,将原来使用dc的地方换为使用m_dcMetaFile。

void CCH_11_GRAPHView::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);
switch(m_nDrawType)
{
case 1:
// dc.SetPixel(point,RGB(0,0,0));
m_dcMetaFile.SetPixel(point,RGB(0,0,0));
break;
case 2:
// dc.MoveTo(m_ptOrigin);
// dc.LineTo(point);
m_dcMetaFile.MoveTo(m_ptOrigin);
m_dcMetaFile.LineTo(point);
break;
case 3:
// dc.Rectangle(CRect(m_ptOrigin,point));
m_dcMetaFile.Rectangle(CRect(m_ptOrigin,point));
break;
case 4:
// dc.Ellipse(CRect(m_ptOrigin,point));
m_dcMetaFile.Ellipse(CRect(m_ptOrigin,point));
break;
}
// 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);
*/
CScrollView::OnLButtonUp(nFlags, point);
}
3.在窗口重绘时,在OnDraw函数中播放元文件

void CCH_11_GRAPHView::OnDraw(CDC* pDC)
{
CCH_11_GRAPHDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
HMETAFILE hMetaFile;
hMetaFile = m_dcMetaFile.Close();
pDC->PlayMetaFile(hMetaFile);
m_dcMetaFile.Create();
m_dcMetaFile.PlayMetaFile(hMetaFile);
DeleteMetaFile(hMetaFile);
}
我们看到,首先,我们定义了一个元文件句柄,然后通过close函数的返回值对它赋值,然后调用PlayMetaFile函数播放元文件,最后是删除元文件。那么中间的两句是干什么的呢?当窗口重绘后,用户很有可能要继续绘图,所以我们还得重新调用Create函数创建设备上下文对象。

m_dcMetaFile.PlayMetaFile(hMetaFile);这一句就更奇妙了。它播放了先前的元文件,它会将先前元文件中绘制的图形在元文件DC中绘制。也就把先前的图形绘制命令保存到了新的m_dcMetaFile中了。接下来,用户可以利用这个新的元文件DC继续调用OnLButtonUp中的内容。所以当响应下一次OnDraw函数时,获取的就是所有图形了。

元文件还可以保存或者打开。我们可以对菜单的开打和保存做出响应:

void CCH_11_GRAPHView::OnFileSave()
{
// TODO: Add your command handler code here
HMETAFILE hMetaFile;
hMetaFile = m_dcMetaFile.Close();
CopyMetaFile(hMetaFile,"meta.wmf");
m_dcMetaFile.Create();
DeleteMetaFile(hMetaFile);
}

void CCH_11_GRAPHView::OnFileOpen()
{
// TODO: Add your command handler code here
HMETAFILE hMetaFile;
hMetaFile = GetMetaFile("meta.wmf");
m_dcMetaFile.PlayMetaFile(hMetaFile);
DeleteMetaFile(hMetaFile);
Invalidate();
}
注意,保存使用的是CopyMetaFile函数。保存以后用户可能还要继续画图,所以要创建。打开使用的是GetMetaFile,函数打开后需要显示之前画的内容,所以调用了PlayMetaFile,并把窗口设为了无效区,引起重绘。

下面另外一中保存图形的方法,使用兼容的设备描述表:我们使用兼容的DC来保存图形,然后在OnDraw函数中将兼容DC保存的图形复制到目的的窗口中。我们为自己的view类添加一个CDC类型成员变量m_dcCompatible:

void CCH_11_GRAPHView::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));
//如果没有创建过
if(!m_dcCompatible.m_hDC)
{
//与当前DC兼容
m_dcCompatible.CreateCompatibleDC(&dc);
//获取客户区的大小
CRect rect;
GetClientRect(&rect);
//创建兼容位图
CBitmap bitmap;
bitmap.CreateCompatibleBitmap(&dc,rect.Width(),rect.Height());
//把兼容位图选入DC中
m_dcCompatible.SelectObject(&bitmap);
//把画刷选入DC中
m_dcCompatible.SelectObject(pBrush);
//把原始设备描述表中的颜色和数据复制到兼容的设备描述表中
m_dcCompatible.BitBlt(0,0,rect.Width(),rect.Height(),
&dc,0,0,SRCCOPY);
}
switch(m_nDrawType)
{
case 1:
m_dcCompatible.SetPixel(point,RGB(0,0,0));
break;
case 2:
m_dcCompatible.MoveTo(m_ptOrigin);
m_dcCompatible.LineTo(point);
break;
case 3:
m_dcCompatible.Rectangle(CRect(m_ptOrigin,point));
break;
case 4:
m_dcCompatible.Ellipse(CRect(m_ptOrigin,point));
break;
}
Invalidate();
CScrollView::OnLButtonUp(nFlags, point);
}
有一点需要格外注意:在调用SelectObject把兼容的位图选入兼容DC之后,还需要调用BitBlt将原始设备描述表的颜色和数据复制到兼容的设备被描述表中

在OnDraw函数中,只需要把兼容DC里面的内容一次性拷贝出来就行了:

void CCH_11_GRAPHView::OnDraw(CDC* pDC)
{
CCH_11_GRAPHDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
CRect rect;
GetClientRect(rect);
pDC->BitBlt(0,0,rect.Width(),rect.Height(),
&m_dcCompatible,0,0,SRCCOPY);

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