----哆啦刘小洋 原创,转载需说明出处 2022-12-28

SegeX MemDC

  • 1 简介
  • 2 基础双缓存技术
    • 2.1 MFC绘图机制
      • 2.1.1 Window绘图消息
      • 2.1.2 背景刷新与屏幕闪烁
    • 2.2 双缓存技术消除屏幕闪烁
    • 2.3 封装
  • 3 更加实用的扩充

1 简介

在VC中用MFC绘制图像时,为避免屏幕闪烁,一般使用双缓存技术(MemDC),也就是绘制时,现将所有的元素绘制到内存句柄,然后一次性显示出来,同时禁用VC自带的背景填充。原理比较简单,网上也有很多现成的代码使用,但一般都只具备基本功能,且只适用于映射模式为MM_TEXT的情况,复杂的场景,比如视图需要放大、缩小,这时很可能要出现问题。
本文旨在详细介绍实际应用的MemDC需要解决的各种问题。将从基础开始,一步一步讲解技术的实现过程、解决问题以及为什么要这么做。
本文附免费的源代码下载,源代码为SegeX组件之一,本次为首次公开。

2 基础双缓存技术

为了统一,我们以视图上的绘制为例,当然在对话框或控件窗口上绘图也是一样的,MFC均针对CDC类实现绘制操作。

2.1 MFC绘图机制

为了保持文件的完整性,先介绍一下MFC绘图的机制,如果你已经很熟悉了,可跳过2.1。
假定工程中的视图类是CcsdnBlogView,与绘制相关的地方有两处:
void CcsdnBlogView::OnDraw(CDC* pDC) 和 BOOL CcsdnBlogView::OnEraseBkgnd(CDC* pDC)。

2.1.1 Window绘图消息

我们可以很轻松的在OnDraw函数中绘制,但OnDraw是怎么实现绘制响应的呢。其实不管是CView、CWnd、CStatic…等等各种窗口,其绘图的时机均来自于Windows消息WM_PAINT,该消息不传送任何参数,只是告诉你屏幕需要绘制了,绘制原因可能是覆盖在上面的窗口移走了,或是你需要更新图像发送了WM_PAINT消息等等,这个我们不用多考虑,MFC和Windows操作系统基本上帮我们处理好了。

我们可以通过类向导添加WM_PAINT消息,添加后会生成消息函数和消息映射入口项,如下:

函数: afx_msg void OnPaint(); void CDlgTest::OnPaint()

消息映射:在BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间多了一行:ON_WM_PAINT()

本文不介绍消息映射原理。CcsdnBlogView中的OnDraw函数也是这样来的,但要隐蔽一点,它响应WM_PAINT消息是在基类CView中,代码大概是这样的:

void CView::OnPaint()
{CPaintDC dc(this); OnPrepareDC(&dc); OnDraw(&dc);
}

看到没有,OnDraw是从这里来的。这里又多了个东西CPaintDC和函数OnPrepareDC:
CPaintDC:就是一个CDC,也即是MFC绘图操作的对象,本文不过多纠结。
OnPrepareDC:这是一个虚函数,是MFC架构的一个东西,从名字就可以知道,目的是让你在真正绘图前,做一些可能需要的额外准备工作,比如设置视图滚动条的范围、视图比例等等。在CcsdnBlogView中如果你需要额外准备,可通过类向导添加此虚函数。

2.1.2 背景刷新与屏幕闪烁

但是,为什么要用MemDC呢?背景刷新就和MemDC相关了。

MFC在执行OnPaint之前,会将绘制区域进行刷新处理,也就是将绘制区域清理掉,这个一般是必须的,除非你绘制的东西可以充满绘制区域,不然你第二次绘制的东西和屏幕上以前的东西混在一起了,那就没法看了。因此,VC有个消息来执行这个动作:WM_ERASEBKGND。缺省CcsdnBlogView是不会添加这个消息的响应的,或者说缺省为基类去处理了。

BOOL CcsdnBlogView::OnEraseBkgnd(CDC* pDC)
{// TODO: 在此添加消息处理程序代码和/或调用默认值return CView::OnEraseBkgnd(pDC);
}

基类是怎么处理的呢,如上述代码,在CView::OnEraseBkgnd(pDC)中,很简单,用背景色(一般都是白色)把绘制区域整个重新画一遍。形象的比喻就是,相当于我们在画布画画,有一块要重画了,我们就把那一块用白纸贴上,然后再画。这个处理机制很合理,也是必须的。但是MFC给出的处理方式简单粗暴,会出现屏幕闪烁!!!为什么呢?其实前面的机制已经给出答案了,但为初学者快速理解,这里再啰嗦一下:

1)绘制前贴了一张白纸;
2)在白纸上绘制;
下一次:
1)绘制前又贴了一张白纸;
2)在白纸上绘制;

执行代码的顺序就是:

OnEraseBkgnd();
OnDraw();
...
OnEraseBkgnd();
OnDraw();

所以!首先屏幕先变白,然后再显示图案,因此闪烁出现了。

2.2 双缓存技术消除屏幕闪烁

如果简单粗暴的去掉背景清除,如下:

void CcsdnBlogView::OnEraseBkgnd(CDC* pDC)
{return TRUE;//CView::OnPrepareDC(pDC, pInfo);
}

就会出现绘制重叠,甚至显示后面的其他窗口内容。

怎么办呢?如果是画画,我们会怎么办?我们不先贴纸,而是先画在小纸上,然后直接将画好的纸贴上去不就行了吗?正是如此!这个就是双缓存技术的思路(比喻和实际还是有差异)。具体实现如下:

1)不用MFC提供的清除背景操作。在OnEraseBkgnd消息函数中直接返回TRUE即可。
2)在OnDraw时,先创建另一个CDC对象(相当于那小张白纸),名字就叫CMemDC吧,先将CMemDC的背景清除。(由于CMemDC是我们临时创建的一个内存DC,所以CMemDC上的所有操作都暂时不会反应到真实屏幕)
3)在CMemDC上绘制(相当于在小张白纸上画画)。
4)将CMemDC上整个区域拷贝到屏幕相关的CDC上。(相当于把画好的小纸贴到画布上)。

思路清楚了,接下来要解决下问题:

1)如何准备CMemDC,比如小白纸要多大,清除背景用什么颜色(我们需要小白纸的颜色和画布的背景颜色一样),等等。
2)如何让小白纸上画画就和在整张纸上画画的感觉一样,相当于我们小白纸虽然不先贴到大纸上,但要放到大纸上正确的位置。
3)CMemDC上画好了,如何拷贝到屏幕CDC上去。

代码如下:

void CcsdnBlogView::OnDraw(CDC* pDC)
{CDC            MemDC;CBitmap       bitmap;CBitmap* pOldBitmap;CRect        rectDev;COLORREF    bkclr = pDC->GetBkColor();pDC->GetClipBox(&rectDev);     //获取需要重画的区域MemDC.CreateCompatibleDC(pDC);   //创建的MemDC是和pDC适配的  bitmap.CreateCompatibleBitmap(pDC, rectDev.Width(), rectDev.Height());//按重画的区域大小,创建一个位图pOldBitmap = MemDC.SelectObject(&bitmap);//MemDC绘制基于bitmap   MemDC.SetBkColor(bkclr);//设置背景色 MemDC.FillSolidRect(rectDev, bkclr);// 填充背景(也即是清除背景)//do drawingMemDC.MoveTo(10, 10);for(int i=0; i< 1000; ++i)MemDC.LineTo(rand() % 1000, rand() % 600);//将画好的MemDC拷贝到屏幕CDC(pDC)pDC->BitBlt(rectDev.left, rectDev.top, rectDev.Width(), rectDev.Height(),&MemDC, rectDev.left, rectDev.top, SRCCOPY);//清理MemDC.SelectObject(&pOldBitmap);
}

尝试逐句解释一下:

1)pDC->GetClipBox(&rectDev):获取需要重画的区域,相当于小纸片要多大。大多数时候窗口不需要全部重绘,比如窗口局部被遮挡部分移走了,又比如你自己想重画窗口中局部地方。这是Windows处理的机制,目的是提高绘制效率。
2)MemDC.CreateCompatibleDC(pDC):创建临时的MemDC,就这么用就行了,是固定语式。
3)bitmap.CreateCompatibleBitmap(pDC, rectDev.Width(), rectDev.Height()):CBitmap是位图,Windows内部运行用的就是这种图像格式。就这么用就行了。
4)pOldBitmap = MemDC.SelectObject(&bitmap):将创建的位图加载到MemDC。MemDC绘制时,实际上是绘制在bitmap上的。
5)MemDC.SetBkColor(bkclr):保持MemDC和pDC的背景色一样。
6)MemDC.FillSolidRect(rectDev, bkclr):设置MemDC的背景,相当于让小纸片和画布的颜色一样。

中间的代码是画画。我们随机方式画了1000条线:

7)pDC->BitBlt(rectDev.left, rectDev.top, rectDev.Width(), rectDev.Height(), &MemDC, rectDev.left, rectDev.top, SRCCOPY):将画好的MemDC拷贝到屏幕CDC(pDC)。

这样就实现了基本的双缓存技术。双缓存指的就是这里的MemDC。为什么叫双呢?呃,…我也不知道。或许把OnDraw传递的pDC作为了屏幕的第一道缓存吧?

2.3 封装

我们利用C++特性,通过一个类的构造和析构来实现1)~6)和第7)步。

class CEvwMemDC : public CDC
{private:CDC         *m_pDC;CBitmap      m_bitmap;CBitmap*   m_pOldBitmap;CRect      m_rectDev;public:CEvwMemDC(CDC* pDC) //1)~6)步{pDC->GetClipBox(&m_rectDev);     //获取需要重画的区域CreateCompatibleDC(pDC); //创建的MemDC是和pDC适配的  m_bitmap.CreateCompatibleBitmap(pDC, m_rectDev.Width(), m_rectDev.Height());//按重画的区域大小,创建一个位图m_pOldBitmap = SelectObject(&m_bitmap);//MemDC绘制基于bitmapFillSolidRect(m_rectDev, pDC->GetBkColor());// 填充背景(也即是清除背景)m_pDC = pDC;}~CEvwMemDC() //第7)步{//将画好的MemDC拷贝到屏幕CDC(pDC)m_pDC->BitBlt(m_rectDev.left, m_rectDev.top, m_rectDev.Width(), m_rectDev.Height(),this, m_rectDev.left, m_rectDev.top, SRCCOPY);SelectObject(m_pOldBitmap);}};void CcsdnBlogView::OnDraw(CDC* pDC)
{CEvwMemDC MemDC(pDC);//do drawingMemDC.MoveTo(10, 10);for(int i=0; i< 1000; ++i)MemDC.LineTo(rand() % 1000, rand() % 600);
}

封装之后,主代码OnDraw简洁了,并且类可以复用。

3 更加实用的扩充

如果你绘制的窗口没有滚动条,也不会移动视图,映射模式也是MM_TEXT(一个像素对应一个逻辑点),视图也不会放大或缩小,并且永远不用打印,那么上述代码就够了。

如果需要应对以上各种场景,我们需要进一步完善代码。我这里尝试直接给出最终代码,再用解释的方式来介绍。

//*****************************************************
//  CEvwMemDC
//*****************************************************
class CEvwMemDC : public CDC
{private:CBitmap  m_bitmap;      CBitmap* m_pOldBitmap;  CDC*     m_pDC;         CRect    m_rectLogi;    BOOL     m_bMemDC;      public:CEvwMemDC(     CDC* pDC , CRect* pRectClipLogi = NULL //需要绘制的矩形区域(逻辑坐标)){CRect rectDev;ASSERT(pDC != NULL);m_pDC = pDC;m_pOldBitmap = NULL;m_bMemDC = !pDC->IsPrinting();// Get the rectangle to drawif (pRectClipLogi == NULL)pDC->GetClipBox(&m_rectLogi);elsem_rectLogi = *pRectClipLogi;m_rectLogi.NormalizeRect();rectDev = m_rectLogi;pDC->LPtoDP(&rectDev);rectDev.NormalizeRect();// Create a Memory DCif (m_bMemDC){CreateCompatibleDC(pDC);//create bitmapm_bitmap.CreateCompatibleBitmap(pDC, rectDev.Width(), rectDev.Height());m_pOldBitmap = SelectObject(&m_bitmap);SetMapMode(pDC->GetMapMode());SetWindowExt(pDC->GetWindowExt());SetViewportExt(pDC->GetViewportExt());CPoint ptWinOrg = pDC->GetWindowOrg();CPoint ptViewOrg = pDC->GetViewportOrg();ptViewOrg.x -= rectDev.left; ptViewOrg.y -= rectDev.top;SetWindowOrg(ptWinOrg);SetViewportOrg(ptViewOrg);// paint backgroundFillSolidRect(m_rectLogi, pDC->GetBkColor());}else {m_bPrinting = pDC->m_bPrinting;m_hDC = pDC->m_hDC;m_hAttribDC = pDC->m_hAttribDC;}}~CEvwMemDC(){if(m_pDC != NULL && m_bMemDC){m_pDC->BitBlt(m_rectLogi.left, m_rectLogi.top, m_rectLogi.Width(), m_rectLogi.Height(),this, m_rectLogi.left, m_rectLogi.top, SRCCOPY);SelectObject(m_pOldBitmap);} }
};

上述代码主要扩充了逻辑坐标和设备坐标的转换处理:
创建MemDC时,CreateCompatibleDC(pDC)函数创建了一个和pDC适配的DC,但其实有很多还不适配,比如逻辑坐标和设备坐标的对应关系,原点位置等。不知道微软为什么这么做,在我看来,CreateCompatibleDC(pDC)函数时,应该基本完全复制pDC,最起码也要提供一个更适配的CreateCompatibleDC版本吧。
上面大量代码做的就是这个事情。考虑了几个方面:逻辑坐标和设备坐标的比例、坐标方向。设备坐标总是以左上角为原点,向右和向下方向为坐标增大方向,这和我们的常用数学坐标在y方向是反的,而很多视图采用了数学坐标方向,这时而逻辑坐标和设备坐标的y方向相反,因此代码中要注意这一点。
另外介绍一下设置原点的方式,举例说明,比如最开始设置的视图比例是1:1,即一个设备坐标对应一个逻辑坐标,当通过改变设备坐标和逻辑坐标的比例来放大视图时,一个逻辑坐标就对应多个设备坐标了,这时如果设置原点的方式以设备坐标为基础,则在边缘会出现没有刷新的问题,必须以逻辑坐标为基础。如果你的程序要支持视图的放大缩小,建议初始比例不要设为1:1,而是采用0.01MM或0.001MM的映射模式,反正一个设备坐标对应大数值的逻辑坐标,然后对视图放大的比例做限制,最多放大到设备坐标和逻辑坐标1:1,这样还可以避免很多别的麻烦(这里就不多讲了)。

最后说点感想,MFC没落了,没落也是有道理的,MFC虽然十分十分庞大,但就是不给你好用,其方便性和易用性总感觉差一点,我觉得在不牺牲任何性能的前提下完全可以做的更好,可惜30余年,微软就从没有这么做,后来又转投.Net去了。另外,说实话,都2022年了,我这篇文章写得太晚了,实际上没有多大实际价值了,现在谁还去考虑什么VC、双缓存技术,特别是这个双缓存技术,本来就应该是MFC该做的事情啊!我们用户的精力应该是面对问题,而你MFC还要我们花费大量精力来写这些基础代码,不没落才怪!当然,这个也不是完全没有好处,这些基础的东西会让我们更深入理解Windows的运行机制,从而也拓宽、提高了我们的编程思路、能力,只是代价稍微大一点。

下载完整的代码资源。本资源完全免费,不需要积分。如果你觉得还好,请点个赞支持。

SegeX MemDC:实用型双缓冲内存DC (内存DC 封装MemDC)(附免费源代码)相关推荐

  1. VC中的双缓冲绘图技术

    之前在做图形绘制的时候,发现在拖动图形时候,会出现闪烁的情况,后来就上网找了一下双缓冲绘图,本文非原创,只是想保存下来,以后要用的时候不用再到处去搜,也希望能帮助有这方面困惑的朋友 原文来自http: ...

  2. C++MFC(13)-双缓冲技术实现绘图

    双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度. MARK一下实现步骤,略去了项目的绘画代码,亲测 ...

  3. android双缓冲绘图技术分析

    转载请说明出处:http://www.jianshu.com/p/efc0bebfd22e 双缓冲.多缓冲技术在计算机科学中其实是一个广义的概念,不过其本质上的意思都是差不多的.今天我们就来讲一讲双缓 ...

  4. Cimage下实现双缓冲绘图

    双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度. 双缓冲绘图的步骤: 1.在内存中创建与画布一致的 ...

  5. 无缓冲channel的内存泄漏问题

    无缓冲channel的内存泄漏问题:无缓冲channel在go程里done <- hardWork(job)时,如果外层执行完了后,done <- hardWork(job)写操作< ...

  6. MFC VC 双缓冲绘图基本原理与实现,详细解释

    MFC做了一些时间了,不得不面对 的是在界面上画图的. 当然你可以直接搜索到能用的代码,并且基本能满足要求.不过这样总不是学习的态度.本着学习分享的态度,现做一些基本的分析吧. 在MSDN上知道,我们 ...

  7. VC双缓冲画图技术介绍

    双缓冲画图,它是一种主要的图形图像画图技术.首先,它在内存中创建一个与屏幕画图区域一致的对象,然后将图形绘制到内存中的这个对象上,最后把这个对象上的图形数据一次性地拷贝并显示到屏幕上. 这样的技术能够 ...

  8. VC绘图中的双缓冲技术

    VC绘图中的双缓冲技术 转自:VC 绘图,使用双缓冲技术实现 ********************所有的GDI绘图函数使用的都是逻辑坐标(逻辑范围)******************* **** ...

  9. MFC双缓冲解决图象闪烁[转]

    转载网上找到的一篇双缓冲的文章,很好用.http://www.cnblogs.com/piggger/archive/2009/05/02/1447917.html _________________ ...

最新文章

  1. Oracle三级联动单表地址数据
  2. Android菜鸟的成长笔记(25)——可爱的小闹钟
  3. IO虚拟化——virtio 原理
  4. python打包成安装包_把 python 程序打包成 egg 或者 whl 安装包
  5. 3D深度估计,让视频特效更梦幻!
  6. SCCM SP 1中文版安装前需要更新的内容-Part1
  7. 不到4个小时,我找到了一枚苹果 0day
  8. 黑马程序员java学生管理系统
  9. OSPF特殊区域(末梢区域、NSSA) 路由优化
  10. 计算机中怎样算2的21次方,脑筋急转弯:2的31次方与3的21次方哪个大?天才知道!...
  11. 高速公路联网收费二义性路径识别系统原理及开发
  12. 浙大玉泉校区-武林门民航售票处-萧山机场
  13. 基于Java的地理位置定位系统
  14. matplot画图-画曲线(一)
  15. ImportError: No module named datetime全局python解决time显示问题
  16. 这款开源的STM32外设驱动库,可以直接拿来用!
  17. 拥有一台云服务器如何安装宝塔面板(图文教程)
  18. 计算机表格应用试卷,2020年7月网络教育统考《计算机应用基础》电子表格模拟题试卷操作题...
  19. 改善羽毛球比赛心理有“妙招”
  20. matlab ezplot fplot,[求助]fplot和ezplot功能一样啊?

热门文章

  1. 高速公路为什么会塞车?
  2. 关于精英蚁群算法matlab,蚁群算法MATLAB解VRP问题
  3. Internet Explorer 编程简述(四)“添加到收藏夹”对话框
  4. 2021年4月份自考总结
  5. 2015工作生活回想
  6. solr入门之自定义排序之构建自己的权重计算方法及相应的排序字段
  7. 1087: 获取出生日期(多实例测试) C语言
  8. WCF医院管理系统技术解析(五)体检登记(四)
  9. 穷举法解八皇后问题和凑硬币问题
  10. 关于wamp图标是黄色的问题解决