双缓冲解决VC++绘图时屏幕闪烁

所属分类: 软件编程 / C 语言 阅读数: 89
收藏 0 赞 0 分享

通常来说程序根据需要调用Invalidate(FALSE)使窗口客户区无效引起重绘,然后在窗口OnPaint函数(基于文档视图的程序则是OnDraw)中进行稳定绘图就行了。但是,我们在OnPaint中进行多重绘制(画背景、棋盘、棋子等),前后绘制的反差造成了闪烁现象。以前知道Java中解决屏幕闪烁问题是用双缓冲的方法,现在发现在vc++中也是可以这么做的。简单来说,双缓冲就是先把需要绘制的东西全部一口气画在内存中,最后把内存中的数据搬到屏幕上显示。

最近做中国象棋,绘制界面时遇到些问题,绘图过程中屏幕闪烁,估计都会想到利用双缓冲来解决问题,但查了下网上双缓冲的资料,发现基本是MFC的,转化为VC++后,大概代码如下:

void DrawBmp(HDC hDC, HBITMAP hBitmap)
{
  HDC hdcMEM; //用于缓冲作图的内存DC
  HBITMAP bmp; //内存中承载临时图象的位图
  HANDLE hOld;
  hdcMEM = CreateCompatibleDC(hDC);//依附窗口DC创建兼容内存DC
  bmp = CreateCompatibleBitmap(hDC, 100, 100); //创建与hDC环境相关的设备兼容的位图

  SelectObject(hdcMEM, bmp);
  hOld = SelectObject(hdcImage, hBitmap);

  StretchBlt(hDC, 0, 0, 100, 100, hdcMEM, 0, 0, 100, 100, SRCCOPY);

  SelectObject(hdcImage, hOld);
  DeleteObject(hOld);
}

但以上代码似乎没有用到hBitmap,当然屏幕上也不会有任何输出,但网上的资料基本一样。查了一番资料,才明白如果hDC中已经有位图数据,BitBlt的时候,就会直接把hDC中的数据画到内存缓冲区里。所以,还需要建一DC,名为hdcImage,把要画的位图选入内存hdcImage中,然后再在内存缓冲区上绘图。

整理代码如下:

void DrawBmp(HDC hDC, HBITMAP hBitmap)
{
  HDC hdcImage;
  HDC hdcMEM; //注意此处,创建了两个HDC
  hdcMEM = CreateCompatibleDC(hDC);
  hdcImage = CreateCompatibleDC(hDC);
  HBITMAP bmp = ::CreateCompatibleBitmap(hDC, nWidth, nHeight);//创建与hDC环境相关的设备兼容的位图

  SelectObject(hdcMEM, bmp);

  SelectObject(hdcImage, hBitmap);//注意此处,将要画的位图选入hdcImage

  StretchBlt(hdcMEM, 0, 0, 100, 100, hdcImage, 0, 0, 100, 100, SRCCOPY); //这里才能正常画图,将hdcImage中的位图直接复制到内存缓冲区
  StretchBlt(hDC, 0, 0, 100, 100, hdcMEM, 0, 0, 100, 100, SRCCOPY); //再将内存缓冲区中的数据绘制到屏幕上.

  DeleteObject(hdcImage);

}

当然,要注意的一点就是,如果要绘制多张图片,比如两张,如果大家这样调用:

DrawBmp(hDC, hBitmap1);
DrawBmp(hDC, hBitmap2);

依然会发生闪烁,下面解释原因:

举个例子,屏幕绘图就像现场作画,如果两次调用绘图函数,就相当于在观众面前作画,第一次画第一张(例如中国象棋的背景)。第二次画第二张(如棋盘)。这样,在画背景和棋盘时,由于颜色有反差,必然在贴第二张图时会发现闪烁,这样利用双缓冲相当于没用,还浪费了内存空间。

双缓冲的原理是:在内存中先把第一张图画好,此时不要转画到屏幕上,然后继续在原来的内存中画第二张,等把所有的图全画好后,再一次性贴到屏幕上。那样内存中存在的就是完整的图形,观众看不到绘图的过程,只能看到绘图的结果,而最后是一次性复制到屏幕上的,当然不会发生闪烁现象。

为了更好解释双缓冲的原理,附图片如下:

PS:以上照片来自网络,只为能更好理解,本人无意侵权。

在以上代码的基础上作如下更改:

void DrawBmp(HDC hDC, HBITMAP hBitmap) //此处返回类型改为HDC
{
  HDC hdcMEM;
  hdcMEM = CreateCompatibleDC(hDC);

  SelectObject(hdcMEM, hBitmap); //将位图选择进内存DC

  StretchBlt(hDC, 0, 0, 100, 100, hdcMEM, 0, 0, 100, 100, SRCCOPY);//这里才能正常画图,将hdcImage中的位图直接复制到内存缓冲区

  DeleteObject(hdcMEM);
}

调用以上函数在内存中画第一张图:

DrawBmp(hdcTmp , hBitmap1); 

画第二张图

复制代码 代码如下:
DrawBmp(hdcTmp, hBitmap2); //此时传的为hdcTmp,其中hdcTmp中已经有第一张图片的数据,此次调用后就会把第二张图片绘到原来的基础上。

如果要画多张图,就依次调用本函数绘制,记得一定要把所有的图全画到一个设备DC上,最后再一次性画到屏幕上,才不会出现闪烁现象。
等把所有图全画到hdcTmp中后,hdcTmp中已经有了完整的图形,再把完整的图形绘制到屏幕上:

复制代码 代码如下:
BitBlt(hDC, 0, 0, 100, 100, hdcTmp, 0, 0, SRCCOPY); //此处第一个参数才为hDC,即窗口句柄

至此,双缓冲画多幅图绘制完毕。

再给大家一个实例:

void C****Dlg::OnPaint() 
{
  if (IsIconic())
  {
    //......
  }
  else
  {
    //CDialog::OnPaint(); //不要调用这个
    CPaintDC dc(this);//对话框的dc//通常CPaintDC用来响应WM_PAINT消息。
    //CPaintDC是从CDC派生出来的:在构造时自动调用CWnd::BeginPaint,析构时调用CWnd::EndPaint。
    
    RECT rect;// 客户区矩形
    GetClientRect(&rect);
    
    // 使用双缓冲避免屏幕刷新时闪烁
    CDC dcMem;// 内存dc
    CBitmap bmpMem; // 位图
    dcMem.CreateCompatibleDC(NULL);// 创建兼容dc
    bmpMem.CreateCompatibleBitmap(&dc, rect.right-rect.left, rect.bottom-rect.top);//创建跟客户区域大小一样的(空)位图
    // 把位图选到设备上下文环境中
    CBitmap *pOld = dcMem.SelectObject(&bmpMem);
    //  dcMem.FillSolidRect(&rect, RGB(255,255,255));  
    // 在此处将绘制内容全画到dcMem内存中,(即把之前使用CPaintDC绘制的dc换成dcMem即可)
  
    DrawTable(dcMem);//画棋盘
    DrawChesses(dcMem); // 画棋子
    //......
    // 至此,内存中绘图完毕
    
    // 从内存拷贝到设备dc
    dc.BitBlt(0, 0, rect.right - rect.left, rect.bottom - rect.top, &dcMem, 0, 0, SRCCOPY);
    
    dc.SelectObject(pOld);
    // 释放资源
    bmpMem.DeleteObject();
    dcMem.DeleteDC(); 
    
  }  
  
}

解决方法:

1)添加BOOL类型的成员变量bgroundChanged,初始化为FALSE;
2)在切换背景图片前调用ModifyStyle(WS_CLIPCHILDREN, 0)去掉WS_CLIPCHILDREN属性,并把bgroundChanged设置为TRUE;
3)在OnPaint中最后增加

    if (TRUE == bgroundChanged)
    {
      bgroundChg = FALSE;
      ModifyStyle(0, WS_CLIPCHILDREN);
    }

希望本文能够对大家熟练掌握双缓冲问题有所帮助。

更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多