第一篇:雙緩存解決閃爍及對話框背景覆蓋控件問題 (WINDOWS API)
一、閃爍問題。
閃爍問題在MFC窗體中經常見到。在網上碰到這些問題的層次不窮,解決方法也是多樣的!但是最經典也最耐用的還是靠用雙緩存解決!首先讓我們來了解一下,為什么會產生屏幕閃爍問題:閃爍可以這樣定義:當后面一幅圖像以很快的速度畫在前面一幅圖像上時,在后面圖像顯示前,你可以很快看到前面那一個圖像,這樣的現(xiàn)象就是閃爍。我認為,閃爍會讓使用者對程序很不滿,原因是:如果用戶接口編碼如此糟糕,那么程序的其他部分呢,如何能相信數(shù)據(jù)的正確性呢?一個具有平滑,快速相應的程序會給用戶帶來信心,這個道理很簡單。程序出現(xiàn)閃爍可以由多種形式造成,最常見的原因是窗口大小發(fā)生改變時,其內容重畫造成閃爍。
僅僅畫一次
這是一個黃金法則,在任何計算機(Windows或者你使用的任何操作系統(tǒng))上處理畫法邏輯都需要遵循,即永遠不要將同一像素畫兩次。一個懶惰的程序員常常不愿意在畫法邏輯上投入過多精力,而是采用簡單的處理邏輯。要避免閃爍,就需要確保不會出現(xiàn)重復繪制的情況發(fā)生。現(xiàn)在,WIndows和計算機還是很笨的,除非你給他們指令,否則他們不會做任何事情。如果閃爍的現(xiàn)象發(fā)生,那是因為你的程序刻意地多繪制了屏幕的某些區(qū)域造成的.這個現(xiàn)象可能是因為一些明確的命令,或者一些被你忽視了的地方。如果程序有閃爍的現(xiàn)象出現(xiàn),你需要你知道如何找到好的方案去解決這個問題。
WM_ERASEBKGND 通常,首先需要懷疑的是WM_ERASEBKGND消息。當一個窗口的背景需要被擦除時,這個消息會被發(fā)送。這是因為窗口的繪畫通常經歷了兩個過程
WM_ERASEBKGND: 清除背景 WM_PAINT: 在上面繪制內容
這兩個過程讓窗體在繪制內容時變得很簡單,即:每次當收到WM_PAINT消息時,你知道已經有了一個新畫布等待去繪制。然而,畫窗口兩次(一次是通過WM_ERASEBKGND畫背景,另外一次是WM_PAINT)將會導致窗口出現(xiàn)比較糟糕的閃爍現(xiàn)象。只要看看標準的編輯框-打開Windows的寫字板并改變窗口大小,就可以看到那種閃爍的效果。
那么,如何避免窗口背景的重刷呢?有如下兩種方法:
設置窗口背景刷子為NULL(當注冊Windows類時,設置WNDCLASS結構中的hbrBackground成員為零)
在WM_ERASEBKGND消息處理時 返回非零值
以上任何一種方法都可以阻止WM_ERASEBKGND 消息去清除窗口。其中,第二個方案的通常可以以如下代碼實現(xiàn):
case WM_ERASEBKGND: return 1;
當你標記窗口內容無效并試圖更新時,還有如下辦法可以防止WM_ERASEBKGND消息:InvalidateRect函數(shù)的最后一個參數(shù)可以指明在下一次窗口重畫時,是否窗口的部分背景會被重刷。將該參數(shù)置為False可以防止當窗口需要重畫時系統(tǒng)發(fā)出WM_ERASEBKGND消息。
InvalidateRect(hwnd, &rect, FALSE);
不該畫的時候一定不要畫 有一個比較普遍的現(xiàn)象:即使窗口中只有一個小的部分發(fā)生了改變,往往所有的部分都會被重畫。比如,經常地,當窗口大小被改變時,一些(不是所有)的程序會重畫所有的窗口。通常,這是個是不必要的,這是因為當窗口大小被改變時,經常是之前窗口的內容是不變的,僅僅是改變大小造成的一個小的邊界區(qū)域需要重畫。此時,沒有必要重畫所有區(qū)域。如果在這里多注意,多考慮,就可以使用好的算法以使得一次只有最小的部分被畫。
系統(tǒng)中每個窗口都有更新區(qū)域。這個區(qū)域描述了窗口中變得無效需要重畫的地方。如果一個窗口僅僅其需要更新的區(qū)域,不多繪制其他地方,那么窗口的繪制效果將會非常快。
有幾種方法可以獲得窗口的更新區(qū)域。通過GetUpdateRgn 函數(shù)可以獲得準確的更新區(qū)域,這個函數(shù)返回的結果可以使矩形的區(qū)域也可以是非矩形的區(qū)域。通過GetUpdateRect 函數(shù)可以獲得需要更新的最小矩形區(qū)域。通常使用矩形的更新區(qū)域比較容易。第三個方法是在BeginPaint/EndPaint中得到PAINTSTRUCT 結構,從而得到準確的更新區(qū)域信息。
一個常規(guī)的畫法函數(shù)是這樣的:
PAINTSTRUCT ps;HDC hdc;case WM_PAINT: hdc = BeginPaint(hwnd, &ps);// do painting EndPaint(hwnd, &ps);return 0;
BeginPaint函數(shù)初始化PS(PAINTSTRUCT)結構,其中,成員rcPaint是一個RECT結構,描述了包含了需要更新的最小矩形區(qū)域(就像GetWindowRect函數(shù))。如果僅僅在這個矩形區(qū)域上繪制窗口,速度上繪有很好地提高。
現(xiàn)在,當使用BeginPaint/EndPaint時Windows會自動剪切掉畫在更新區(qū)域外面的部分。這意味著,你沒有機會畫到更新區(qū)域以外的地方。可能你會認為,如果是這樣的話,花功夫確保代碼不試圖畫到更新區(qū)域外是沒有意義的,反正沒有畫出任何東西來。然而,你仍然可以避免不必要的API調用和相關計算,所以,我認為放一些精力在如何工作地更快上是絕對值得的。
如果還是不能解決
有些時候,當你花了很多努力去考慮非常好的畫法時,發(fā)現(xiàn)窗口還是會被全部刷新。這通常是由兩個Window 類的屬性造成的:CS_VREDRAW 和 CS_HREDRAW。如果有其中一個標志被設置時,那么當窗口水平或者豎直方向有大小被改變時,其內容每次都會被重新刷新。所有,你需要關掉這兩個標志,解決的唯一的方式是在創(chuàng)建窗體和窗體類被注冊時,確保這兩個屬性不被設置。
WNDCLASSEX wc;wc.cbSize = sizeof(wc);wc.style = 0;/* CS_VREDRAW | CS_HREDRAW;*/...RegisterClassEx(&wc);
上面的例子描述了當窗體類被注冊時,這兩個屬性不被設置的實現(xiàn)方法。
有一點需要注意:如果主窗體有了這兩個屬性,即使子窗體沒有重畫標志,會導致所有子窗體在其大小被改變時會被重繪。可以通過以下方式避免這個情況發(fā)生:
剪切子窗體
有時,閃爍的原因是因為當重畫時,父窗體沒有剪切其子窗體區(qū)域。這樣的結果導致,整個父窗口內容被重畫,而子窗體又被顯示在了上面(造成閃爍)。這個可以通過在父窗體上設置WS_CLIPCHILDREN 來解決。當這個標志被設置時,被子窗體占據(jù)的任何區(qū)域將會被排除在更新區(qū)域外。因此,即使你嘗試在子窗體所在的位置上繪制(父窗口的內容),BeginPaint中的剪切區(qū)域也會阻止其繪制效果。
雙緩沖和內存設備描述表(Memory Device Context, 簡稱Memory-DC)常見的徹底避免閃爍的方法是使用雙緩沖。其基本的思路是:將窗體的內容畫在屏幕外的一個緩沖區(qū)內,然后,將該緩沖區(qū)的內容再傳遞到屏幕上(使用BilBlt函數(shù))。這是一個非常好的減少閃爍的方法,但是經常被濫用,特別是當程序員并不真正地理解如何有效地繪制窗口時。
按照我個人的理解哦,當我做五子棋棋盤的時候,背景色設置為棕黃色,在制定區(qū)域繪制20X15的小格子作為棋盤。當每次點重置時,就是清空棋子時候,或者更改窗體大小和移動窗體時,都會產生閃爍。為什么這樣子呢?就是當窗體重繪的時候,背景圖畫了一遍,棋盤又畫了一遍,這期間有時間差,導致很不一致,給人視覺上的閃爍,讓客戶很不舒服,這是程序員需要注意也必須要解決的一個問題!于是就引進了雙緩存技術。什么是雙緩存呢?它的原理是將所有圖像合成到一起 然后一下顯示出來 移動刷新的時候也是一張一張的往整個客戶區(qū)覆蓋 這樣就不存在了重疊刷新的問題。如果是控件閃爍.可以利用獲取控件DC繪制在父窗體底部減小色差的方法 應用函數(shù)為GetDC()。在我的程序中,個人認為是這樣引用雙緩存的,當窗體收到重繪消息時,首先調用OnEraseBkgnd(CDC* pDC)返回一個true,將客戶區(qū)的背景色搽除!再調用onpaint()函數(shù)進行重繪,但是在重繪的過程中得注意,利用雙緩存技術。先獲得一個繪圖設備CDC,然后再將圖畫在位圖上,當然你重繪的背景也要畫在這個位圖上~~~最后就是將畫好的位圖按順序的顯示在客戶區(qū)上~~感覺就像跟幻燈片一樣一幅幅的出現(xiàn)吧,并且不讓人的眼球發(fā)覺正在重繪,也沒有閃爍!當然,這里面我還碰到一個問題,就客戶區(qū),重繪區(qū)問題解決了,非客戶區(qū)的問題出現(xiàn)了,非客戶區(qū)的背景是父窗口的顏色,與客戶區(qū)的不一致,當把客戶區(qū)擴大到整個窗口的時候,又會把非客戶區(qū)的控件覆蓋!解決辦法和原因后面再說~~~ 通常我們編寫雙緩存的代碼的時候,有經典的寫法: HDC hdcMem;HBITMAP hbmMem;HANDLE hOld;PAINTSTRUCT ps;HDC hdc;....case WM_PAINT: // Get DC for window hdc = BeginPaint(hwnd, &ps);// Create an off-screen DC for double-buffering hdcMem = CreateCompatibleDC(hdc);hbmMem = CreateCompatibleBitmap(hdc, win_width, win_height);hOld = SelectObject(hdcMem, hbmMem);// Draw into hdcMem // Transfer the off-screen DC to the screen BitBlt(hdc, 0, 0, win_width, win_height, hdcMem, 0, 0, SRCCOPY);// Free-up the off-screen DC SelectObject(hdcMem, hOld);DeleteObject(hbmMem);DeleteDC(hdcMem);EndPaint(hwnd, &ps);return 0;我的做法是:
BOOL CGI_PaltFormDlg::OnEraseBkgnd(CDC* pDC){ // TODO: Add your message handler code here and/or call default
//return CDialog::OnEraseBkgnd(pDC);return true;} void CGI_PaltFormDlg::OnPaint(){ CPaintDC dc(this);// device context for painting SendMessage(WM_ICONERASEBKGND,(WPARAM)dc.GetSafeHdc(), 0);
// Center icon in client rectangle int cxIcon = GetSystemMetrics(SM_CXICON);int cyIcon = GetSystemMetrics(SM_CYICON);CRect rect;GetClientRect(&rect);
dc.FillSolidRect(rect,RGB(255,128,0));
int x =(rect.Width()cyIcon + 1)/ 2;
// Draw the icon dc.DrawIcon(x, y, m_hIcon);CDC *pDC=this->GetDC();int red=0,green=0,blue=0;int width=2;
CDC MenmDC;CBitmap MemBitmap;MenmDC.CreateCompatibleDC(NULL);MemBitmap.CreateCompatibleBitmap(pDC,950,750);
CBitmap *pOldBit=MenmDC.SelectObject(&MemBitmap);MenmDC.FillSolidRect(0,0,950,750,RGB(255,128,0));
int tem=0;//畫橫線
for(int i=0;i<16;i++){ MenmDC.MoveTo(100,100+tem);MenmDC.LineTo(900,100+tem);tem=40+tem;} //畫豎線
tem=0;for(i=0;i<21;i++){ MenmDC.MoveTo(100+tem,100);MenmDC.LineTo(100+tem,700);tem=40+tem;} pDC->BitBlt(0,0,1400,900,&MenmDC,0,0,SRCCOPY);MemBitmap.DeleteObject();MenmDC.DeleteDC();ReleaseDC(pDC);} 上面是我畫棋盤的語句,只供參考而已!
二、對話框背景覆蓋控件問題。
上面已經提及到我遇到的問題。為了達到兩全其美的效果,竟讓閃屏問題解決了,又讓我的控件不收影響,我想了一個很笨的辦法!首先確定客戶區(qū)的范圍,我確定的是只雙緩存重繪棋盤部分,矩形區(qū)域為(0,0,950,750),這里面我重繪了背景色和棋盤!然后再讓非重繪區(qū)背景色設置一遍就是和控件一起重繪,說不重繪其實也是重繪的,只是不一致的重繪而已!
CPaintDC dc(this);// device context for painting CRect rect;GetClientRect(&rect);dc.FillSolidRect(rect,RGB(255,128,0));當然這里面還有個小問題,就是在onpain()函數(shù)里面最好把對話框控件和非緩存重繪區(qū)的背景重繪的代碼放在緩存區(qū)重繪代碼的前面,像我上面代碼說的!通過這樣的設置,我的問題解決了!哈哈,皆大歡喜....