介紹
用雙緩沖進行繪圖可解決在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需要占用一定的內存,那麼在頻繁的頁面調度中,位置難免改變,於是用來標志指針的句柄也就不同了)。