VC++深入详解(9):图形的保存和重绘
2012-10-30 14:46
253 查看
首先,我们重新实现一下上次课实现的功能:通过菜单,选择画哪种图形,然后进行绘图。但是这个功能有一个严重的问题,就是窗口发生重绘(比如我们把程序最小化以后再重新显示)以后,原来的图就不见了!怎么解决这个问题呢?我们必须保存每次画的图。然后在重绘函数中把它们重新画出来。准确的说,其实我们只需要保存3样东西就行了:类型(画点、画线、画矩形、画椭圆)、起点、终点。我们可以用一个类封装它们。
但是我们发现,当窗口重绘以后,原来的图像依然没有显示出来,问题出在了哪里呢?其实在于OnLButtonUp中的构造的graph是一个局部变量,所以,当程序运行完以后,它就被析构了。即使我们保存了它的地址,但是在OnDraw函数中使用的,实际上是已经被虚构的无效的地址了,所以不能正常绘图。解决的办法也很简单,就是在OnLButtonUp中new一个graph出来,然后增加到m_ptrArray中就行了:
在VIEWCORE.CPP中:可以看到:
第一句是构造一个DC,但是如果是在对WM_PAINT响应时,必须使用BeginPaint和EndPaint函数。这里并没有相关代码,为什么呢?因为它的构造和析构函数中调用了它们。而且CPaintDC只在响应WM_PAINT消息时使用。
OnPrepareDC函数,从MSDN中可知:当屏幕显示调用OnDraw函数之前或者打印预览调用OnPrint函数函数之前,框架类都会调用这个函数。如果这个函数是为屏幕显示而调用,那么它什么也不做,但是它在派生类中被覆盖(例如CScrollView),用来调整设备上下文的属性。简而言之,要调整设备上下文的属性,必须先调用它。这里先留点印象,后面会继续讨论。
最后看OnDraw。它也是虚函数,会引起派生类中的OnDraw调用。
所以,我们看到的是派生类的OnDraw函数被调用,实际上WM_PAINT消息由基类的OnPaint消息响应,在这个消息中,调用了虚函数,也就是派生类的OnDraw。
假如我们重写了OnPaint消息,而在其中调用OnPrepareDC和OnDraw,也能达到同样的效果。
1.头文件中把继承改为CScrollView。2.把对应源文件的所有CView都改成CScrollView。
但是修改完后运行程序出错,这是因为滚动条的相关内容(比如点击一下箭头移动多少,点击空白处滚动的数量等等)都还没有设置,可以通过SetScrollSizes来设定。这个函数放在哪里呢?我们把它放在虚函数OnInitialUpdate中它是窗口创建完成后第一个被调用的函数,在OnDraw函数之前。所以,如何想在窗口完成之后做一些初始化工作,就应该放在这里:
我们先回顾一下映射模式的知识:
视口是基于设备坐标的;窗口是基于逻辑坐标的。我们写的画图程序,都是在逻辑坐标下完成的。当把逻辑坐标转化到设备坐标时,需要有一个转化公式:
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函数,下面是部分代码:
问题就出在这里了,当我们画线时,我们画在了视口上(x0,y0),由于此时窗口与视口的原点是同一个点,所以对应过来的窗口坐标也是(x0,y0)。当我们画图时,代码里使用的是窗口坐标,但是需要把它转化为视口坐标,根据映射转换公式:
xViewport = (xWindow-xWinOrg)*xViewExt/xWinExt+xViewOrg
yViewport = (yWindow-yWinOrg)*yViewExt/yWinExt+yViewOrg
(x0,y0)就被转化成了(x0,y0-179),所以画的线就跑到了原来的位置的上面去了。
那么如何解决呢?在OnLButtonDown函数中,存入pGraph之前,将设备坐标转化成逻辑坐标,然后在保存。
下面看看另一种保存图形和绘制图形的方式——使用元文件。它也是把之前的绘图操作保存了起来,然后在窗口重绘时再重新画出来。与之前的保存内容(画图类型、起点、终点)不同,元文件里保存的是画图的那些操作,比如MoveTo 、LineTo之类的。如果你想看具体画的是什么样子,你得把元文件“播放”一遍才行。
与元文件相关的是CMetaFileDC类。我们看看如何使用它:
1.利用构造函数创建一个CMetaFileDC类的对象,然后调用create成员函数创建windows元文件设备上下文并把它与元文件相关联。
2。向元文件发送一系列的GDI指令。
3.当你发送完成后,调用close函数,这个函数会关闭设备上下文,并且返回一个元文件句柄。
4.用这个句柄作为参数,调用PlayMetaFile 来“播放”元文件。
5.播放完成以后,释放该元文件。
下面我们就修改一下我们的程序:
1.首先,为我们的view类增加一个CMetaFileDC类型的私有变量m_dcMetaFile。在构造函数中:
m_dcMetaFile.PlayMetaFile(hMetaFile);这一句就更奇妙了。它播放了先前的元文件,它会将先前元文件中绘制的图形在元文件DC中绘制。也就把先前的图形绘制命令保存到了新的m_dcMetaFile中了。接下来,用户可以利用这个新的元文件DC继续调用OnLButtonUp中的内容。所以当响应下一次OnDraw函数时,获取的就是所有图形了。
元文件还可以保存或者打开。我们可以对菜单的开打和保存做出响应:
下面另外一中保存图形的方法,使用兼容的设备描述表:我们使用兼容的DC来保存图形,然后在OnDraw函数中将兼容DC保存的图形复制到目的的窗口中。我们为自己的view类添加一个CDC类型成员变量m_dcCompatible:
在OnDraw函数中,只需要把兼容DC里面的内容一次性拷贝出来就行了:
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)3.在窗口重绘时,在OnDraw函数中播放元文件
{
// 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);
}
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); }
相关文章推荐
- 《VC++深入详解》学习笔记 第十一章 图形的保存和重绘
- 第11课 图形的保存和重绘
- MFC三种不同方式实现图形的保存和重绘---方法二: 运用CMetaFileDC
- Lesson11 图形的保存和重绘
- 图形的保存与重绘
- 孙鑫VC学习(第11课--图形的保存和重绘)
- MFC三种不同方式实现图形的保存和重绘---方法一:通过兼容DC(CompatibleDC)的方式
- 图形的保存与重绘
- MFC学习笔记之图形保存与重绘
- 孙鑫VC学习笔记:第十一讲 (二) 图形的保存与重绘方法一
- 图形的保存和重绘
- MFC三种不同方式实现图形的保存和重绘---方法一:通过兼容DC(CompatibleDC)的方式
- MFC学习笔记之图形保存与重绘
- 图形的保存和重绘
- 孙鑫VC学习笔记 (图形的保存和重绘)
- 图形的保存和重绘
- 图形的保存与重绘(1)
- VC++学习(11):图形的保存和重绘
- 孙鑫VC++第11章图形的保存和重绘
- 图形的保存和重绘