怎樣在內存緩衝中畫圖

關鍵字 內存緩衝,畫圖
原作者姓名 相生昌

介紹
用雙緩衝進行繪圖可解決在VC中繪圖時的閃爍現象

正文
用vc做程序,如何畫圖是一個大家都很關心,但是卻感到很難以理解的問題,因爲在mfc的封裝之下,沒有現成的畫圖函數供你直接調用,像vb等等裏面直接來個point之類的,常常讓人感到無從下手。這兩天幫人解決了一個用內存緩衝畫圖的問題,順便也就談談這些東西,也算是總結。

我先來解釋一下在mfc裏面很關鍵的設備環境描述符,也就是所謂的DC(device context)。

還是從歷史來看吧,dos時代,我們如果要繪圖,必須通過一系列系統函數來啓動圖形環境(用過turbo pascal或者turbo c的人該還有印象吧),這之間對各種硬件的初始化參數都不相同,非常的煩人,常常還要查閱硬件手冊,那時的程序智能針對最流行的硬件來編寫,對不流行的就沒有辦法了。windows操作系統爲了屏蔽不同的硬件環境,讓編程時候不考慮具體的硬件差別,採取了一系列辦法,設備環境描述符就是這樣產生的。簡單的說,設備描述符抽象了不同的硬件環境爲標準環境,用戶編寫時使用的是這個虛擬的標準環境,而不是真實的硬件,與真實硬件打交道的工作一般交給了系統和驅動程序完成(這同樣解釋了爲什麼我們需要經常更新驅動程序的問題)。使用在windows圖形系統(gdi,而不包括direct x)上面,就體現在一系列的圖形DC上面,我們如果要在gdi上面繪圖,就必須先得到圖形DC的句柄(handle),然後指定句柄的基礎上進行圖形操作。

再來回憶一下,我們怎麼在sdk的環境下面繪圖呢,我想這個大家都不太清楚吧,但是確實很基礎。在windows的sdk環境下面,我們用傳統的c編寫程序,在需要的繪圖地方(比如響應WM_PAINT消息的分支)這樣做:

hdc = GetDC( hwnd );
oldGdiObject = SelectObject( hdc,newGdiObject );
...繪圖操作...
SelectObject( hdc,oldGdiObject );
DeleteObject( newGdiObject );
ReleaseDC( hdc);

或者是這樣

BeginPaint( hwnd,&ps );//PAINTSTRUCT ps -- ps is a paint struct
...繪圖操作...
EndPaint( hwnd )

這就是大概的過程,我們看到了hdc(圖形DC句柄)的應用,在繪圖的部分,每一個繪圖函數基本上也要用到這個句柄,最後我們還必須釋放它,否則將嚴重影響性能。每次我們都必須調用GetDC這個api函數得到(不能用全局變量保存結果重複使用,我在後面解釋)。這些是最最基本的windows圖形操作的方式,相比dos時代簡單了些,但是有些概念也難理解了些。vb裏面的簡單的point函數其實最後也是被轉化爲這樣的方式來執行,系統幫助做了很多事情。

到了mfc裏面,由於有了封裝,所有的hdc被隱藏在對象中做爲隱藏參數傳遞(就是DC類的this啦~~),所以我們的關鍵話題就轉變爲了怎樣得到想要的DC類而已,這個過程其實大同小異的。在消息響應的過程中,WM_PAINT被轉變爲OnDraw(),OnPaint()一系列函數來響應,這些函數一般都有個參數CDC *pDC傳入進來,因此在這些函數裏面,我們就只需要直接畫圖就可以了,和以前sdk的方式一樣。

但是WM_PAINT消息響應的頻度太高了,比如最小化最大化,移動窗體,覆蓋等等都引起重繪,經常的這樣畫圖,很是消耗性能;在有些場合,比如隨機作圖的場合,每一次就改變,還導致了程序的無法實現。怎麼解決後一種問題呢。

ms在msdn的例子裏面交給我們document/view的經典解決辦法,將圖形的數據存儲在document類裏面,view類只是根據這些數據繪圖。比如你要畫個圓,只是將圓心和半徑存在document裏面,view類根據這個裏面的數據在屏幕上面重新繪製。那麼,我們只需要隨機產生一次數據就可以了。

這樣還是存在性能的問題,於是我們開始考慮另外的解決方法。我們知道,將內存中的圖片原樣輸出到屏幕是很快的,這也是我們在dos時代經常做的事情,能不能在windows也重新利用呢?答案就是內存緩衝繪圖,我們今天的主題。

我們還是回到DC上來,既然DC是繪圖對象,我們也就可以自己來在內存裏面造一個,讓它等於我們想要的圖,圖(CBitmap)可以存儲在document類裏面,每一次刷新屏幕都只是將這個圖輸出到屏幕上面,每一次作圖都是在內存裏面繪製,保存在document的圖裏面,必要時還可以將圖輸出到外存保存。這樣既保證了速度,也解決了隨機的問題,在複雜作圖的情況下對內存的開銷也不大(總是一副圖片的大小)。這是一個很好的解決辦法,現在讓我們來實現它們。

我們在document類裏面保存一個圖片

CBitmap m_bmpBuf;//這裏面保存了我們做的圖,存在於內存中

在view類裏面我們需要將這個圖拷貝到屏幕上去
位於OnDraw(CDC *pDC)函數中:

CDC dcMem;//以下是輸出位圖的標準操作
CBitmap *pOldBitmap = NULL;
dcMem.CreateCompatibleDC(NULL);
pOldBitmap = dcMem.SelectObject(&pDoc->m_bmpBuf);
BITMAP bmpinfo;
pDoc->m_bmpBuf.GetBitmap(&bmpinfo);
pDC->BitBlt(0,0,bmpinfo.bmWidth,bmpinfo.bmHeight,&dcMem,0,0,SRCCOPY);
dcMem.SelectObject(pOldBitmap);
dcMem.DeleteDC();

在我們需要畫圖的函數裏面,我們完成繪圖工作

CBmpDrawDoc *pDoc = GetDocument(); //得到document中的bitmap對象
CDC *pDC = GetDC();
CDC dcMem;
dcMem.CreateCompatibleDC(NULL);//這裏我們就在內存中虛擬建造了DC
pDoc->m_bmpBuf.DeleteObject();
pDoc->m_bmpBuf.CreateCompatibleBitmap(pDC,100,100);//依附DC創建bitmap
CBitmap *pOldBitmap = dcMem.SelectObject(&pDoc->m_bmpBuf);//我們調入了我們bitmap目標

dcMem.FillSolidRect(0,0,100,100,RGB(255,255,255));//這些時繪圖操作,隨便你^_^
dcMem.TextOut(0,0,"Hello,world!");
dcMem.Rectangle(20,20,40,40);
dcMem.FillSolidRect(40,40,50,50,RGB(255,0,0));

pDC->BitBlt(0,0,100,100,&dcMem,0,0,SRCCOPY);//第一次拷貝到屏幕
dcMem.SelectObject(pOldBitmap);
dcMem.DeleteDC();


全部的過程就是這樣,很簡單吧。以此爲例子還可以實現2個緩衝或者多個緩衝等等,視具體情況而定。當然在緩衝區還可以實現很多高級的圖形操作,比如透明,合成等等,取決於具體的算法,需要對內存直接操作(其實就是當年dos怎麼做,現在還怎麼做)。

再來解釋一下前面說的爲什麼不能用全局變量保存DC問題。其實DC也是用句柄來標識的,所以也具有句柄的不確定性,就是隻能隨用隨取,不同時間兩次取得的是不同的(使用過文件句柄地話,應該很容易理解的)。那麼我們用全局變量保存的DC就沒什麼意義了,下次使用只是什麼也畫不出來。(這一點的理解可以這樣:DC需要佔用一定的內存,那麼在頻繁的頁面調度中,位置難免改變,於是用來標誌指針的句柄也就不同了)。

 
發佈了144 篇原創文章 · 獲贊 6 · 訪問量 38萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章