天天看點

MFC繪制動态曲線,用雙緩沖繪圖技術防閃爍

轉載自http://zhy1987819.blog.163.com/blog/static/841427882011614103454335/,多謝ZHY_ongu博友的分享,如果有問題,請及時回複。

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));     }     else     {         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);

}