1. 程式人生 > >mfc 防止視窗重繪

mfc 防止視窗重繪

如果一個程式出現閃爍現象,會讓人覺得程式編寫人員很馬虎,缺乏對細節的足夠重視。Windows程式的任何部分都沒有任何理由出現閃爍現象。這篇文章的目的是告訴讀者如何使用相關的技術防止窗口出現閃爍效果。 

什麼是閃爍
閃爍可以這樣定義:當後面一幅影象以很快的速度畫在前面一幅影象上時,在後面影象顯示前,你可以很快看到前面那一個影象,這樣的現象就是閃爍。我認為,閃爍會讓使用者對程式很不滿,原因是:如果使用者介面編碼如此糟糕,那麼程式的其他部分呢,如何能相信資料的正確性呢?一個具有平滑,快速相應的程式會給使用者帶來信心,這個道理很簡單。 

程式出現閃爍可以由多種形式造成,最常見的原因是視窗大小發生改變時,其內容重畫造成閃爍。 


僅僅畫一次
這是一個黃金法則,在任何計算機(Windows或者你使用的任何作業系統)上處理畫法邏輯都需要遵循,即永遠不要將同一畫素畫兩次。一個懶惰的程式設計師常常不願意在畫法邏輯上投入過多精力,而是採用簡單的處理邏輯。要避免閃爍,就需要確保不會出現重複繪製的情況發生。現在,WIndows和計算機還是很笨的,除非你給他們指令,否則他們不會做任何事情。如果閃爍的現象發生,那是因為你的程式刻意地多繪製了螢幕的某些區域造成的. 這個現象可能是因為一些明確的命令,或者一些被你忽視了的地方。如果程式有閃爍的現象出現,你需要你知道如何找到好的方案去解決這個問題。 

WM_ERASEBKGND
通常,首先需要懷疑的是WM_ERASEBKGND訊息。當一個視窗的背景需要被擦除時,這個訊息會被髮送。這是因為視窗的繪畫通常經歷了兩個過程 


WM_ERASEBKGND: 清除背景 
WM_PAINT: 在上面繪製內容 
這兩個過程讓窗體在繪製內容時變得很簡單,即:每次當收到WM_PAINT訊息時,你知道已經有了一個新畫布等待去繪製。然而,畫視窗兩次(一次是通過WM_ERASEBKGND畫背景,另外一次是WM_PAINT)將會導致窗口出現比較糟糕的閃爍現象。只要看看標準的編輯框-開啟Windows的寫字板並改變視窗大小,就可以看到那種閃爍的效果。 

那麼,如何避免視窗背景的重刷呢?有如下兩種方法: 

設定視窗背景刷子為NULL(當註冊Windows類時,設定WNDCLASS結構中的hbrBackground成員為零) 
在WM_ERASEBKGND訊息處理時 返回非零值 

以上任何一種方法都可以阻止WM_ERASEBKGND 訊息去清除視窗。其中,第二個方案的通常可以以如下程式碼實現: 

case WM_ERASEBKGND:
return 1; 

當你標記視窗內容無效並試圖更新時,還有如下辦法可以防止WM_ERASEBKGND訊息:InvalidateRect函式的最後一個引數可以指明在下一次視窗重畫時,是否視窗的部分背景會被重刷。將該引數置為False可以防止當視窗需要重畫時系統發出WM_ERASEBKGND訊息。 

InvalidateRect(hwnd, &rect, FALSE); 

不該畫的時候一定不要畫
有一個比較普遍的現象:即使視窗中只有一個小的部分發生了改變,往往所有的部分都會被重畫。比如,經常地,當視窗大小被改變時,一些(不是所有)的程式會重畫所有的視窗。通常,這是個是不必要的,這是因為當視窗大小被改變時,經常是之前視窗的內容是不變的,僅僅是改變大小造成的一個小的邊界區域需要重畫。此時,沒有必要重畫所有區域。如果在這裡多注意,多考慮,就可以使用好的演算法以使得一次只有最小的部分被畫。 

系統中每個視窗都有更新區域。這個區域描述了視窗中變得無效需要重畫的地方。如果一個視窗僅僅其需要更新的區域,不多繪製其他地方,那麼視窗的繪製效果將會非常快。 

有幾種方法可以獲得視窗的更新區域。通過GetUpdateRgn 函式可以獲得準確的更新區域,這個函式返回的結果可以使矩形的區域也可以是非矩形的區域。通過GetUpdateRect 函式可以獲得需要更新的最小矩形區域。通常使用矩形的更新區域比較容易。第三個方法是在BeginPaint/EndPaint中得到PAINTSTRUCT 結構,從而得到準確的更新區域資訊。 

一個常規的畫法函式是這樣的: 

PAINTSTRUCT ps;
HDC hdc;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
// do painting
EndPaint(hwnd, &ps);
return 0;

BeginPaint函式初始化PS(PAINTSTRUCT)結構,其中,成員rcPaint是一個RECT結構,描述了包含了需要更新的最小矩形區域(就像GetWindowRect函式)。
如果僅僅在這個矩形區域上繪製視窗,速度上繪有很好地提高。 

現在,當使用BeginPaint/EndPaint時Windows會自動剪下掉畫在更新區域外面的部分。這意味著,你沒有機會畫到更新區域以外的地方。可能你會認為,如果是這樣的話,花功夫確保程式碼不試圖畫到更新區域外是沒有意義的,反正沒有畫出任何東西來。然而,你仍然可以避免不必要的API呼叫和相關計算,所以,我認為放一些精力在如何工作地更快上是絕對值得的。 

如果還是不能解決
有些時候,當你花了很多努力去考慮非常好的畫法時,發現視窗還是會被全部重新整理。這通常是由兩個Window 類的屬性造成的:CS_VREDRAW 和 CS_HREDRAW。如果有其中一個標誌被設定時,那麼當視窗水平或者豎直方向有大小被改變時,其內容每次都會被重新重新整理。所有,你需要關掉這兩個標誌,解決的唯一的方式是在建立窗體和窗體類被註冊時,確保這兩個屬性不被設定。 

WNDCLASSEX wc;
wc.cbSize = sizeof(wc);
wc.style = 0; /* CS_VREDRAW | CS_HREDRAW; */ 
...
RegisterClassEx(&wc);

上面的例子描述了當窗體類被註冊時,這兩個屬性不被設定的實現方法。
有一點需要注意:如果主窗體有了這兩個屬性,即使子窗體沒有重畫標誌,會導致所有子窗體在其大小被改變時會被重繪。可以通過以下方式避免這個情況發生: 

剪下子窗體
有時,閃爍的原因是因為當重畫時,父窗體沒有剪下其子窗體區域。這樣的結果導致,整個父視窗內容被重畫,而子窗體又被顯示在了上面(造成閃爍)。這個可以通過在父窗體上設定WS_CLIPCHILDREN 來解決。當這個標誌被設定時,被子窗體佔據的任何區域將會被排除在更新區域外。因此,即使你嘗試在子窗體所在的位置上繪製(父視窗的內容),BeginPaint中的剪下區域也會阻止其繪製效果。 
雙緩衝和記憶體裝置描述表(Memory Device Context, 簡稱Memory-DC)
常見的徹底避免閃爍的方法是使用雙緩衝。其基本的思路是:將窗體的內容畫在螢幕外的一個緩衝區內,然後,將該緩衝區的內容再傳遞到螢幕上(使用BilBlt函式)。這是一個非常好的減少閃爍的方法,但是經常被濫用,特別是當程式設計師並不真正地理解如何有效地繪製視窗時。 

典型的雙緩衝程式碼如下:

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; 
這個方法比較慢,因為在每次窗體需要重畫的時候記憶體裝置描述表(Memory-DC)都需要被重新建立。更有效的方法是,僅僅建立記憶體裝置描述表(Memory-DC)一次,並使其足夠大到能滿足任何時候的整個窗體重新整理。當程式結束時,再銷燬這個記憶體裝置描述表(Memory-DC)。這兩種方法都存在對記憶體開銷的問題,特別是如果記憶體裝置描述表(Memory-DC)是針對真個螢幕的大小。雙緩衝也需要兩倍的時間去畫。這是因為其第一次是在記憶體裝置描述表(Memory-DC)上畫,然後再使用BitBlt畫回到螢幕上。當然,好的顯示卡會使BitBlt更快,但是仍然會耗CPU 時間。 

如果程式需要顯示相當複雜的資訊,比如像網頁,那麼你應該使用記憶體裝置描述表(Memory-DC)。比如IE,如果不使用雙緩衝,是沒有辦法在繪製網頁時不閃爍的。 

沒有必要將雙緩衝技術用於整個窗體的繪製中。可以這樣設想,視窗中僅僅有一個小部分包含了複雜的圖形物件(比如半透明的點陣圖或者其他)。你應該將記憶體裝置描述表(Memory-DC)僅僅用於著一個小區域,其他區域使用常規的方法。 有時,通過仔細的思考,經常可以避免使用雙緩衝而直接將結果畫到螢幕上。只要你不破壞黃金法則,即“永遠不要將一個畫素畫兩次”,就可以防止閃爍的出現。 

避免過度繪製
我想說的關於這個話題是這樣的:有一個需要自己定義畫法的窗體的標題欄。首先,你畫了標題,接著在上面畫一些其他的圖形。現在,只要標題需要被重畫,就會出現閃爍現象。這是因為你沒有合乎黃金法則。這裡,標題被很快地顯示在其他圖形在上面繪製時,導致了閃爍。 

有兩種技術可以組織這種型別的閃爍。第一個是使用剪下,第二個是使用你的大腦。 

使用剪下時,你可以使用ExcludeClipRect 函式在裝置描述表中去標記一個特定的區域。當一個區域被標記上時,即使在該區域上面重畫也不會產生效果。一旦背景已經被繪製了,可以通過SelectClipRgn移掉該標記的區域,其他圖形能被畫到前面標記的區域上。通過準確的標記(剪下),可以在很多時候被避免過度繪製。 

另外一個方案就是找更聰明的解決辦法。比如,當你需要畫一個表格,通常應該先畫空的背景,再畫網格線從而產生表格。但是,這個方法會使網格線產生閃爍,這是因為在網格線被畫之前,下面背景被很快地顯示了一下。然而可以使用不同的做法達到想要的結果。即,不是一次畫一個大的空背景,而是畫一系列的空方塊,每一個方塊邊是被一個畫素的寬度分開。這樣,當畫網格線時,他們剛好能被畫到一個之前沒有畫過的地方。其結果是不會有閃爍現象,因為沒有畫素被畫了超過兩次。 

使用你的頭腦去想一個好的演算法可能需要長一點的時間,但是卻是值得的,因為這能讓結果更好。 

結論
希望你再也不會問:“為什麼我的窗體會閃爍”這樣的問題。我已經講解了閃爍的主要原因和解決辦法。如果你遇到了閃爍的問題,你應該能找到原因並且使用這裡提到的技術來解決了。