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

news/2025/1/11 6:00:22/

----哆啦刘小洋 原创,转载需说明出处 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的运行机制,从而也拓宽、提高了我们的编程思路、能力,只是代价稍微大一点。

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


http://www.ppmy.cn/news/7276.html

相关文章

【Qt】QtCreator远程部署、调试程序

1、添加远程设备 1)QtCreator 工具–> 选项 --> 设备 --> 添加 2)设备设置向导选择–> Generic Linux Device --> 开启向导 3)填写“标识配置的名称”(随便写)、设备IP、用户名 --> 下一步 4)选择配对秘密文件,第一次配对,可以不填写,点击“下一…

Shell程序编写猜数字的小游戏

文章目录 目录 文章目录 前言 一、设计思路 二、代码编写 三、效果图 总结 前言 在学习Linux课程中学习了一点简单的shell语法&#xff0c;实现了一个猜数字功能的程序。感兴趣的可以看完后自己手动编写玩玩~这个小游戏的编写也是把基础的shell语法基本上都用到了&#…

学习HTTP协议,这一篇就够啦 ~~

HTTP协议一、什么是HTTP1.1 应用层协议1.2 HTTP1.3 HTTP协议的工作过程二、HTTP协议格式2.1 Fiddler抓包工具2.2 协议格式三、HTTP请求 (Request)3.1 认识 "方法" (method)3.1.1 GET 方法3.1.2 POST 方法3.1.3 GET和POST比较3.1.4 其他方法3.2 认识URL3.2.1 URL基本格…

【电商】电商后台---价税管理

文章对电商后台系统中的价税管理进行了系统的介绍&#xff0c;希望通过此文能够加深你对电商系统的认识。 前面介绍了商品管理部分&#xff0c;从商品的属性、分类到商品资质、商品图片都做了说明&#xff0c;在梳理的过程中越发的感觉到每部分细节才是关键。但实话实说通过前几…

C#,图像二值化(05)——全局阈值的联高自适应算法及其源代码

阈值的选择当然希望智能、简单一些。应该能应付一般的图片。 What is Binarization? Binarization is the process of transforming data features of any entity into vectors of binary numbers to make classifier algorithms more efficient. In a simple example, trans…

工作两年半,终于学会了Jenkins部署Maven项目

上期我们讲了Linux部署Jenkins Linux安装Jenkins&#xff08;Java11最新版&#xff09; 这期我们来讲的是使用Jenkins部署一个maven项目 文章目录&#x1f46e;所需要的环境&#xff08;必须要有&#xff0c;否则不能进行下一步&#xff09;&#x1f64b;第一步&#xff0c;安装…

02 运算符

目录 第一章&#xff1a;概述 第二章&#xff1a;算术运算符 2.1 概述 2.2 应用示例 2.3 号的两种用法 2.4 自增自减运算 2.4.1 概述 2.4.2 单独使用 2.4.3 复合使用 第三章&#xff1a;赋值运算符 3.1 概述 3.2 应用示例 第四章&#xff1a;关系运算符&#xff0…

Doris-(六)-1.0 新特性

1、1.0 新特性 Doris 1.0 开始官网提供了编译好的二进制包&#xff0c;可以直接下载使用。如果老版本想滚动升 级新版本&#xff0c;可以参照官方说明&#xff1a;https://doris.apache.org/zh-CN/installing/upgrade.html 版本通告&#xff1a;https://mp.weixin.qq.com/s/Ju…