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

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

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 drawing
	MemDC.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绘制基于bitmap
		FillSolidRect(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 drawing
	MemDC.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 draw
		if (pRectClipLogi == NULL)
			pDC->GetClipBox(&m_rectLogi);
		else
			m_rectLogi = *pRectClipLogi;
		m_rectLogi.NormalizeRect();

		rectDev = m_rectLogi;
		pDC->LPtoDP(&rectDev);
		rectDev.NormalizeRect();

		// Create a Memory DC
		if (m_bMemDC)
		{
			CreateCompatibleDC(pDC);

			//create bitmap
			m_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 background
			FillSolidRect(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的运行机制,从而也拓宽、提高了我们的编程思路、能力,只是代价稍微大一点。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值