摘自:http://zhy1987819.blog.163.com/blog/static/841427882011614103454335/

MFC绘制动态曲线,用双缓冲绘图技术防闪烁

 

2011-07-14 10:34:54|  分类: 学习笔记 |  标签:双缓冲绘图技术  mfc  动态曲线   |举报 |字号 订阅

先上效果图

随着时间的推移,曲线向右平移,同时X轴的时间坐标跟着更新。一、如何绘制动态曲线。

所谓动画,都是一帧一帧的图像连续呈现在用户面前形成的。所以如果你掌握了如何绘制静态曲线,那么学会绘制动态曲线也不远啦,只需要创建一个定时器(比如调用MFC中的SetTimmer函数),每隔一定时间(比如1ms),调用OnPaint或者OnDraw函数,绘制当前帧图像即可。
这里需要注意的是,绘制图像的代码需要写在OnPaint或者OnDraw函数中,因为当窗口失效(比如最小化)恢复后,会重新绘制当前窗口,窗口之前的自绘图像会丢失。而把绘图代码写在OnPaint或者OnDraw中就是为了让窗口每次重绘时也能重绘你自己画的图像,避免出现窗口最小化再恢复后,自己画的图像丢失的尴尬情况。
另外绘制当前帧图像之前,记得用InvalidateRect函数清除上一帧图像,不然各帧图像会背景的堆叠。
比如我想清除窗口中(0,0)和(100,100)这两点确定的矩形中的图像,代码如下:

    CRect Rect;
    Rect.top = 0;
    Rect.left = 0;
    Rect.bottom = 100;
    Rect.right = 100;
    InvalidateRect(Rect);

根据上面的思路,我们每隔一定时间绘制一幅图像,可是如果每次绘制的图像都是完全相同的,那么图像看起来也是静态的。如何让曲线动起来呢?我们需要为自己绘图的代码设计一个输入,即在当前时刻曲线上各个点的坐标信息。随着时间的推移,令曲线上各个点的坐标随之变化,这样每次绘图都是基于当前时刻的曲线坐标绘制的,控制好曲线坐标的变化,也就能让你绘制的曲线乖乖的动起来。

上面提到了曲线上各个点的坐标信息,这个信息可以用多种数据结构储存,不过笔者推荐使用STL中的deque数据结构储存。为什么呢?需求决定选择。让我们先想想在绘制图像的过程中需要对这个数据进行哪些操作。
1、需要遍历这个数据,获取各个点的坐标以便绘图,所以选择的数据结构必须有较高的遍历效率。
2、当曲线上的点横向上充满了横坐标轴提供的显示范围,需要将曲线最右边的点的坐标移除,然后在曲线最左边添加下一个新点的坐标,以实现曲线向右平移的效果。所以选择的数据结构需要支持前端和后端元素的添加删除操作,大家很自然会想到队列。
STL中的list容器也能很轻松的实现队列功能,但是list还支持任意位置元素的添加和删除操作,功能上的冗余决定了list需要花费更多的时间来实现我们的需求,事实上遍历一个deque常常比遍历一个list快几十倍,原因在这里就不赘述啦。

于是,笔者构建了这样的数据结构deque<pair<TIME, VALUE>> m_dqDisplayData;队列中的每个元素是一个pair,pair中存放坐标。维护这个数据结构的核心代码如下:

//如果队列长度超过了X轴方向上可绘的所有点的数量

    if (m_dqDisplayData.size() >= XPointNum)
    {
        //将队列前端的坐标移除
        m_dqDisplayData.pop_front();
       //在队列后端添加新的坐标
        m_dqDisplayData.push_back(make_pair(time, value));
        m_dqDisplayData.push_back(make_pair(tiem, value));
    }

前面介绍了如何让静态的曲线动起来,下面具体介绍绘制静态图像的主要技能。

1、画图首先需要找一位画家,MFC是这样获取一位画家的。
CDC *pDC = GetDC();
记得这位画家画完本帧图像之后,打发他走人,闲人咱们养不起。
即必须用ReleaseDC(pDC);释放资源,否则会造成内存泄漏,因为GetDC();函数中分配了一些资源,这些资源关联在pDC指向的内存中,如果不调用ReleaseDC,当pDC出作用域后,只是pDC这个32位的指针变量(也可以说它是一个整数变量)的内存释放了,pDC指向的内存没有机会得到释放。这里也反映出MFC的一个原则,Get之后需要Release,这两个函数往往是成对定义好的。

另外,GetDC和ReleaseDC都是CWnd的成员函数,我们需要在哪个窗口上画图,就在那个窗口类的OnPaint或者OnDraw函数中创建一位会在该窗口上画画的画家,其实GetDC中隐含的操作是,创建一位画家,将自己所在的窗口的绘图区作为画纸交给这位画家,然后再把画家返回给用户。当我们直接建立CDC对象时(比如:CDC  MemDC;),就需要用其他方法(比如:SelectObject函数)为其选择画纸了。

2、画家画图之前,首先要准备好画图工具。
MFC提供了很多画图工具,比如画刷(CBrush),画笔(CPen)等。(呵呵,其实笔者也没用过几种)
    //下面就实例化了一个画实线,宽度为1,颜色为RGB(0, 128, 64)的画笔

    CPen PenForDrawAxis(PS_SOLID, 1, RGB(0, 128, 64));
    //画家使用SelectObject技能,将画笔握入手中
    pDC->SelectObject(PenForDrawAxis);

另外说明一点:关于画笔不再使用后,是否需要调用PenForDrawAxis.DeleteObject();释放资源的问题,网上说法不一。各大书籍上,作者们都常常下意识的显式地调用了DeleteObject函数,以体现释放资源的动作。

如果需要及时释放内存资源,为后面的程序运行扫清障碍,那显式的调用DeleteObject函数我觉得没有问题。但是如果说不调用DeleteObject函数,CPen对象分配的资源就无法释放,就会造成内存泄漏,这点我深表怀疑。

因为CPen对象的资源在构造函数中分配,自然在其析构函数中应该有对应的释放函数,因为作为MFC用户来说, 在使用CPen时,根本不知道是否分配了需要显式释放的资源。对象应该对自己负责,不应该将冗余责任移交给用户,这是设计C++类的基本原则。通俗的说就是,自己干了哪些好事自己心理清楚,走人的时候自己要收拾干净。微软在代码上不会耍流氓吧(虽然其他地方经常流氓)。

MSDN上的原话是:When an application no longer requires a given pen, it should call the CGdiObject::DeleteObject member function or destroy the CPen object so the resource is no longer in use. An application should not delete a pen when the pen is selected in a device context.
要释放CPen资源,微软给我们指了两条明路,第一是:call the CGdiObject::DeleteObject member function,第二是:destroy the CPen object。何为destroy the CPen object,一种方法就是让对象出作用域,自动调用析构函数把自己给了结了。
可见,CPen对象即使不调用DeleteObject,也能在自己出作用域被C++摧毁时,释放资源。

扯远啦,扯远啦。。。。。。下面继续。

3、画家开始挥笔啦~
    //将笔移动到(60,220)这个坐标指示的位置(只是选地方,还没落笔)

    pDC->MoveTo(60, 220); 
    //将笔在纸上从(60,220)拉到(520,550),一条直线诞生了
    pDC->LineTo(520, 220);
    //将笔在纸上从(520,220)移动到(510,223),另外一条直线跃然纸上
    pDC->LineTo(510, 223);

怎么只能画直线?
曲线是什么?不过是无数小段的直线。

另外,MoveTo和LineTo不必要成对出现,一般一条连续的曲线只需要调用一次MoveTo。

二、如何使用双缓冲技术防止画面闪烁

上面介绍了如何绘制动态曲线,但是这样绘制动态曲线往往会出现画面闪烁的问题。

不管是用什么语言什么构架画图,出现闪烁的根本原因都在于画面变化不连贯。

也许你要问,我每次画的一帧图像都只是在上帧图像的基础上变化了一点点,怎么就不连贯了。确实如此,不过别忘了我们在画每帧图像之前,还调用了InvalidateRect来清除前一帧图像,所谓清除,就是用窗口默认背景色填充指定矩形区域,相当于在每两帧图像之间,实际还插入了一副大煞风景的纯色背景图。

终于,大家想到了一种办法,不使用InvalidateRect来清除前一帧图像,直接重新请一位会在内存上画画的画家,将该帧图像画在内存中的一张新的纸上,然后在窗口上画画的画家使用自己的终极技能BOOL BitBlt( int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, DWORD dwRop );将在内存里面画画的老实画家手上的画直接复制过来(剽窃可耻,但很管用~)。于是,问题解决啦,爱装B的程序员们给这种方法取了个很拉风的名字 ------  双缓冲技术。

这个方法涉及到了以下几个主要技能:
1、谁会在内存上画画啊?

    //创建一个会在内存中画画的画家
    CDC MemDC;
    MemDC.CreateCompatibleDC(NULL);

2、内存里面说好给的那种新的纸在哪啊?

    //创建一个内存中的图纸
    CBitmap MemBitmap;
    MemBitmap.CreateCompatibleBitmap(pDC, 800, 600);
为什么上面要传入一个当前窗口类中通过GetDC得到的pDC?
因为CreateCompatibleBitmap初始化了一个与pDC指定的设备上下文兼容的位图,位图与指定的设备上下文具有相同的颜色位面数或相同的每个像素的位数。你可以试一试,如果此处传入&MemDC,完啦完啦,画家怎么画,图上都是灰色的线条,郁闷死啦。
至于CreateCompatibleBitmap的后面两个参数,指定的是图纸的大小,具体指你可以根据自己窗口大小等实际情况确定,大了无所谓,用不完后面复制的时候可以截取指定尺寸。
3、怎么让画家在这张纸上画画?

    //呵呵,酱紫就搞定啦~
    MemDC.SelectObject(&MemBitmap);

4、内存中的画家如何画画?
完全一样,只不过是MemDC在挥笔。

    MemDC.MoveTo(60, 220); 
    MemDC.LineTo(520, 220);
    MemDC.LineTo(510, 223);
对啦,温馨提示,大家多半想用一种颜色填充指定矩形区域,因为InvalidateRect就是干的这事嘛,你把人家挤下来了,自然这事就得自己做啦。
    MemDC.FillSolidRect(0, 0, 580, 250, RGB(1,4,1));
上面这个函数表示的是,以图纸的(0,0)位置(也就是图纸的最左上角)作为矩形的左上角坐标,画一个颜色为RGB(1,4,1),长为580,宽为250的矩形。尺寸什么的大家不必过于纠结,根据自己的窗口大小,多试几种尺寸和坐标,就能找出最合适的参数了。
需要注意的是,MemDC是在MemBitmap上画画,所以MemDC调用函数传入的坐标是MemBitmap这个图纸上的坐标,不是窗口上的坐标。

4、如何让在窗口蹲点的那位画家直接从内存画家手上复制图纸?

    //下面函数的意思是:在MemDC手中的画纸上,以(0,0)处作为矩形框的左上角坐标,拉一个长为580,宽为250的复制矩形框,这个框框里面框中的图像复制到窗口中,复制矩形框的左上角与窗口的(20,50)处重合。580,250决定了复制框框的大小,(0,0)决定了复制框框在MemBitmap上的位置,(20,50)决定了复制框框在窗口上的位置。
    pDC->BitBlt(20, 50, 580, 250, &MemDC, 0, 0, SRCCOPY);

下面是部分画图代码,删除了很多周边功能,如果不小心多删到了什么还请大家海涵,主要留着看个思路和框架。

1、画坐标轴的函数,你们看,我就是让 ”内存画家“ --- MemDC 画画的,表示用了双缓冲的哦,呵呵。

    void CXXXDlg::DrawAxis(CDC &MemDC, LPTSTR TitleForX, LPTSTR TitleForY)
    //选择画坐标轴的画笔
    CPen PenForDrawAxis(PS_SOLID, 1, RGB(0, 128, 64));
    MemDC.SelectObject(PenForDrawAxis);
    //绘制X轴
    MemDC.MoveTo(60, 220);
    MemDC.LineTo(520, 220);
    //绘制箭头
    MemDC.LineTo(510, 223);
    MemDC.LineTo(510, 217);
    MemDC.LineTo(520, 220);
    //绘制Y轴
    MemDC.MoveTo(60, 220);
    MemDC.LineTo(60, 30);
    //绘制箭头
    MemDC.LineTo(57, 40);
    MemDC.LineTo(63, 40);
    MemDC.LineTo(60, 30);
    //设置文本的颜色
    COLORREF OldColor = MemDC.SetTextColor(RGB(255, 255, 0));
    //绘制标注
    MemDC.TextOut(480, 230, TitleForX);
    MemDC.TextOut(40, 10, TitleForY);
    //还原文本颜色
    MemDC.SetTextColor(OldColor);
2、画图例的函数

void CXXXDlg::DrawLegend(CDC &MemDC, CPen &PenForDraw, CPen &PenForDrawAB, CPen &PenForDrawBE)
    //设置文本的颜色
    COLORREF OldColor = MemDC.SetTextColor(RGB(0, 128, 64));
    //绘制图例
    MemDC.SelectObject(PenForDraw);
    MemDC.TextOut(530, 30, _T("Global"));
    MemDC.MoveTo(530, 50);
    MemDC.LineTo(570, 50);
    MemDC.SelectObject(PenForDrawAB);
    MemDC.TextOut(530, 70, _T("AB"));
    MemDC.MoveTo(530, 90);
    MemDC.LineTo(570, 90);
    MemDC.SelectObject(PenForDrawBE);
    MemDC.TextOut(530, 110, _T("BE"));
    MemDC.MoveTo(530, 130);
    MemDC.LineTo(570, 130);
    //还原文本颜色
    MemDC.SetTextColor(OldColor);
}

3、画曲线的函数

void CXXXDlg::DrawDynamicCurve(CDC &MemDC, CPen &Pen, deque<pair<TIME, VALUE>> &DisplayData, double Proportion)
    //选择画笔
    MemDC.SelectObject(Pen);
    //进入临界区
    EnterCriticalSection(&(m_cControllingParameters.m_cCriticalSection));
    //绘制曲线
    if (DisplayData.size() >= 2)
        COORDINATE XPos = 60;
        for (UINT PointIndex = 1; PointIndex != DisplayData.size(); 
            PointIndex++)
            MemDC.MoveTo(XPos++, 220 - (COORDINATE)((double)(DisplayData[PointIndex - 1].second) / Proportion));
            MemDC.LineTo(XPos, 220 - (COORDINATE)((double)(DisplayData[PointIndex].second) / Proportion));
    //离开临界区
    LeaveCriticalSection(&(m_cControllingParameters.m_cCriticalSection));
    //还原文本颜色
    MemDC.SetTextColor(OldColor);
}

4、重点来啦,Onpaint函数,有双缓冲技术的主流程

void CXXXDlg::OnPaint()
    CDC *pDC = GetDC();
    //创建一个内存中的显示设备
    CDC MemDC;
    MemDC.CreateCompatibleDC(NULL); 
    //创建一个内存中的图像
    CBitmap MemBitmap;
    MemBitmap.CreateCompatibleBitmap(pDC, 580, 250);
    //定义各种类型的画笔
    CPen PenForDraw(PS_SOLID, 1, RGB(0, 232, 255));
    CPen PenForDrawAB(PS_SOLID, 1, RGB(0, 98, 0));
    CPen PenForDrawBE(PS_SOLID, 1, RGB(221, 0, 221));
    //指定内存显示设备在内存中的图像上画图
    MemDC.SelectObject(&MemBitmap);

    //先用一种颜色作为内存显示设备的背景色
    MemDC.FillSolidRect(0, 0, 580, 250, RGB(1,4,1));
    //绘制坐标轴
    DrawAxis(MemDC, _T("time(s)"), _T("length(kbit)"));
    //绘制图例
    DrawLegend(MemDC, PenForDraw, PenForDrawAB, PenForDrawBE);
    //绘制曲线
    DrawDynamicCurve(MemDC, PenForDraw, m_dqDisplayData,  1000);
    //将内存中画好的图像直接拷贝到屏幕指定区域上
    pDC->BitBlt(20, 50, 580, 250, &MemDC, 0, 0, SRCCOPY);

    //释放相关资源
    ReleaseDC(pDC);
}

【MFC】MFC绘制动态曲线,用双缓冲绘图技术防闪烁的更多相关文章

  1. [转载] MFC绘制动态曲线,用双缓冲绘图技术防闪烁

    转载的原文地址 先上效果图 随着时间的推移,曲线向右平移,同时X轴的时间坐标跟着更新. 一.如何绘制动态曲线 所谓动画,都是一帧一帧的图像连续呈现在用户面前形成的.所以如果你掌握了如何绘制静态曲线,那 ...

  2. win32下的双缓冲绘图技术

    一:双缓冲原理 为了解决窗口刷新频率过快所带来的闪烁问题,利用双缓冲技术进行绘图.所谓双缓冲技术,就是将资源加载到内存,然后复制内存数据到设备DC(这个比较快),避免了直接在设备DC上绘图(这个比较慢 ...

  3. Win32下双缓冲绘图技术

    一:双缓冲原理 为了解决窗口刷新频率过快所带来的闪烁问题,利用双缓冲技术进行绘图.所谓双缓冲技术,就是将资源加载到内存,然后复制内存数据到设备DC(这个比较快),避免了直接在设备DC上绘图(这个比较慢 ...

  4. [Android学习笔记]双缓冲绘图技术

    双缓冲技术绘图: 什么情况下产生的双缓冲技术?当数据量很大时,绘图可能需要花费很长的时间,这样屏幕就会出现卡顿,闪烁等现象. 什么是双缓冲技术?双缓冲是在内存中创建一个与屏幕绘制区域一致的对象,先将图 ...

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

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

  6. MFC双缓冲绘图(2015.09.24)

    问题引入: 最近在尝试编写贪吃蛇游戏时遇到这么一个问题:当系统以较快频率向窗口发送WM_PAINT消息时,调用OnPaint()函数在窗口中绘制图形就会发生闪烁现象. 问题分析: 当我们把绘图过程放在 ...

  7. MFC双缓冲绘图实例

    本人之前一直了解双缓冲绘图的基本原理,但是在研究很久之后才大概知道具体的使用过程,本文将详细介绍本人在实际项目中使用双缓冲绘图的案例. 实现功能:主界面显示某张包含人脸的图片,通过dlib detec ...

  8. mfc双缓冲绘图

    1.要求 在界面加载本地图片并显示,每过100ms改变一张图片显示 2.现象 通过定时器控制CImage,Load,Draw,Destroy,会非常的卡顿.因为Load图片时,会是非常大的数据[所有C ...

  9. 双缓冲绘图和窗口控件的绘制——ATL ActiveX 窗口控件生成向导绘制代码OnDraw的一个错误 .

    双缓冲绘图和窗口控件的绘制 ---ATL ActiveX 窗口控件生成向导绘制代码OnDraw的一个错误 cheungmine 我们通常使用ATL COM组件,生成一个带窗口的ActiveX控件,然后 ...

随机推荐

  1. 执行用例,并生成报告——discover,HTMLRunner

    HTMLRunner需要下载Python3的格式,懒人链接:http://pan.baidu.com/s/1tp3Ts 参考:http://bbs.chinaunix.net/thread-41547 ...

  2. unity,  在编辑界面中隐藏公开变量

    unity默认声明为public的变量都是在编辑界面可见的,如果要隐藏的话就可以这样做 一种是使用属性 Public float Age { get; set; } 另一种是使用标签 [HideInI ...

  3. 二叉树遍历,递归,栈,Morris

    一篇质量非常高的关于二叉树遍历的帖子,转帖自http://noalgo.info/832.html 二叉树遍历(递归.非递归.Morris遍历) 2015年01月06日 |  分类:数据结构 |  标 ...

  4. MySQL SQL Injection(注入)

    如果通过网页接收用户输入,而后再把这些数据插入到数据库中,那么你可能就会碰到 SQL 注入式攻击.本节简要介绍如何防范这种攻击,确保脚本和 MySQL 语句的安全性. 注入式攻击往往发生在要求用户输入 ...

  5. linux 文本分析工具---awk命令(7/1)

    awk是一个强大的文本分析工具,相对于grep的查找,sed的编辑,awk在其对数据分析并生成报告时,显得尤为强大.简单来说awk就是把文件逐行的读入,以空格为默认分隔符将每行切片,切开的部分再进行各 ...

  6. jQuery带缩略图的宽屏焦点图插件

    在线演示 本地下载

  7. 将CString写入到本地文件中

    void SocketU::WritePacket2File(CString packet_str)//packet_str为待写入的字符串{ CTime time = CTime::GetCurre ...

  8. 伸展树基础(Splay)

    3224: Tyvj 1728 普通平衡树 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 3948  Solved: 1627 [Submit][St ...

  9. Effective C++ 条款11:在operator=中处理"自我赋值"

    "自我赋值"发生在对象被赋值给自己时: class Widget { ... }; Widget w; ... w = w; // 赋值给自己 a[i] = a[j]; // 潜在 ...

  10. eclipse中的错误解决——Servlet cannot be resolved to a type

    问题如图 解决问题方法