MFC绘制动态曲线,用双缓冲绘图技术防闪烁
轉載自:ZHY_ongu的博客:MFC繪制動態曲線,用雙緩沖繪圖技術防閃爍
先上效果圖?
?
隨著時間的推移,曲線向右平移,同時X軸的時間坐標跟著更新。
一、如何繪制動態曲線。
所謂動畫,都是一幀一幀的圖像連續呈現在用戶面前形成的。所以如果你掌握了如何繪制靜態曲線,那么學會繪制動態曲線也不遠啦,只需要創建一個定時器(比如調用MFC中的SetTimer函數),每隔一定時間(比如1ms),調用OnPaint或者OnDraw函數,繪制當前幀圖像即可。
這里需要注意的是,繪制圖像的代碼需要寫在OnPaint或者OnDraw函數中,因為當窗口失效(比如最小化)恢復后,會重新繪制當前窗口,窗口之前的自繪圖像會丟失。而把繪圖代碼寫在OnPaint或者OnDraw中就是為了讓窗口每次重繪時也能重繪你自己畫的圖像,避免出現窗口最小化再恢復后,自己畫的圖像丟失的尷尬情況。?
另外繪制當前幀圖像之前,記得用InvalidateRect函數清除上一幀圖像,不然各幀圖像會背景的堆疊。?
比如我想清除窗口中(0,0)和(100,100)這兩點確定的矩形中的圖像,代碼如下:
根據上面的思路,我們每隔一定時間繪制一幅圖像,可是如果每次繪制的圖像都是完全相同的,那么圖像看起來也是靜態的。如何讓曲線動起來呢?我們需要為自己繪圖的代碼設計一個輸入,即在當前時刻曲線上各個點的坐標信息。隨著時間的推移,令曲線上各個點的坐標隨之變化,這樣每次繪圖都是基于當前時刻的曲線坐標繪制的,控制好曲線坐標的變化,也就能讓你繪制的曲線乖乖的動起來。
上面提到了曲線上各個點的坐標信息,這個信息可以用多種數據結構儲存,不過筆者推薦使用STL中的deque數據結構儲存。為什么呢?需求決定選擇。讓我們先想想在繪制圖像的過程中需要對這個數據進行哪些操作。?
1. 需要遍歷這個數據,獲取各個點的坐標以便繪圖,所以選擇的數據結構必須有較高的遍歷效率。?
2. 當曲線上的點橫向上充滿了橫坐標軸提供的顯示范圍,需要將曲線最右邊的點的坐標移除,然后在曲線最左邊添加下一個新點的坐標,以實現曲線向右平移的效果。所以選擇的數據結構需要支持前端和后端元素的添加刪除操作,大家很自然會想到隊列。?
STL中的list容器也能很輕松的實現隊列功能,但是list還支持任意位置元素的添加和刪除操作,功能上的冗余決定了list需要花費更多的時間來實現我們的需求,事實上遍歷一個deque常常比遍歷一個list快幾十倍,原因在這里就不贅述啦。?
于是,筆者構建了這樣的數據結構deque
前面介紹了如何讓靜態的曲線動起來,下面具體介紹繪制靜態圖像的主要技能。
畫圖首先需要找一位畫家,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函數)為其選擇畫紙了。
畫家畫圖之前,首先要準備好畫圖工具。?
MFC提供了很多畫圖工具,比如畫刷(CBrush),畫筆(CPen)等。(呵呵,其實筆者也沒用過幾種)
另外說明一點:關于畫筆不再使用后,是否需要調用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++摧毀時,釋放資源。
扯遠啦,扯遠啦。。。。。。下面繼續。
怎么只能畫直線??
曲線是什么?不過是無數小段的直線。?
另外,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、誰會在內存上畫畫啊?
2、內存里面說好給的那種新的紙在哪啊?
//創建一個內存中的圖紙CBitmap MemBitmap;MemBitmap.CreateCompatibleBitmap(pDC, 800, 600);為什么上面要傳入一個當前窗口類中通過GetDC得到的pDC??
因為CreateCompatibleBitmap初始化了一個與pDC指定的設備上下文兼容的位圖,位圖與指定的設備上下文具有相同的顏色位面數或相同的每個像素的位數。你可以試一試,如果此處傳入&MemDC,完啦完啦,畫家怎么畫,圖上都是灰色的線條,郁悶死啦。?
至于CreateCompatibleBitmap的后面兩個參數,指定的是圖紙的大小,具體指你可以根據自己窗口大小等實際情況確定,大了無所謂,用不完后面復制的時候可以截取指定尺寸。
3、怎么讓畫家在這張紙上畫畫??
//呵呵,醬紫就搞定啦~?
MemDC.SelectObject(&MemBitmap);
4、內存中的畫家如何畫畫??
完全一樣,只不過是MemDC在揮筆。
對啦,溫馨提示,大家多半想用一種顏色填充指定矩形區域,因為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启动和关闭线程
- 下一篇: MFC SetWindowPos