绘制窗体时防止闪烁
如果一個程序出現(xiàn)閃爍現(xiàn)象,會讓人覺得程序編寫人員很馬虎,缺乏對細(xì)節(jié)的足夠重視。Windows程序的任何部分都沒有任何理由出現(xiàn)閃爍現(xiàn)象。這篇文章的目的是告訴讀者如何使用相關(guān)的技術(shù)防止窗口出現(xiàn)閃爍效果。
什么是閃爍
閃爍可以這樣定義:當(dāng)后面一幅圖像以很快的速度畫在前面一幅圖像上時,在后面圖像顯示前,你可以很快看到前面那一個圖像,這樣的現(xiàn)象就是閃爍。我認(rèn)為,閃爍會讓使用者對程序很不滿,原因是:如果用戶接口編碼如此糟糕,那么程序的其他部分呢,如何能相信數(shù)據(jù)的正確性呢?一個具有平滑,快速相應(yīng)的程序會給用戶帶來信心,這個道理很簡單。
程序出現(xiàn)閃爍可以由多種形式造成,最常見的原因是窗口大小發(fā)生改變時,其內(nèi)容重畫造成閃爍。
僅僅畫一次
這是一個黃金法則,在任何計算機(jī)(Windows或者你使用的任何操作系統(tǒng))上處理畫法邏輯都需要遵循,即永遠(yuǎn)不要將同一像素畫兩次。一個懶惰的程序員常常不愿意在畫法邏輯上投入過多精力,而是采用簡單的處理邏輯。要避免閃爍,就需要確保不會出現(xiàn)重復(fù)繪制的情況發(fā)生。現(xiàn)在,WIndows和計算機(jī)還是很笨的,除非你給他們指令,否則他們不會做任何事情。如果閃爍的現(xiàn)象發(fā)生,那是因為你的程序刻意地多繪制了屏幕的某些區(qū)域造成的. 這個現(xiàn)象可能是因為一些明確的命令,或者一些被你忽視了的地方。如果程序有閃爍的現(xiàn)象出現(xiàn),你需要你知道如何找到好的方案去解決這個問題。
WM_ERASEBKGND
通常,首先需要懷疑的是WM_ERASEBKGND消息。當(dāng)一個窗口的背景需要被擦除時,這個消息會被發(fā)送。這是因為窗口的繪畫通常經(jīng)歷了兩個過程
- WM_ERASEBKGND: 清除背景
- WM_PAINT: 在上面繪制內(nèi)容
這兩個過程讓窗體在繪制內(nèi)容時變得很簡單,即:每次當(dāng)收到WM_PAINT消息時,你知道已經(jīng)有了一個新畫布等待去繪制。然而,畫窗口兩次(一次是通過WM_ERASEBKGND畫背景,另外一次是WM_PAINT)將會導(dǎo)致窗口出現(xiàn)比較糟糕的閃爍現(xiàn)象。只要看看標(biāo)準(zhǔn)的編輯框-打開Windows的寫字板并改變窗口大小,就可以看到那種閃爍的效果。
那么,如何避免窗口背景的重刷呢?有如下兩種方法:
- 設(shè)置窗口背景刷子為NULL(當(dāng)注冊Windows類時,設(shè)置WNDCLASS結(jié)構(gòu)中的hbrBackground成員為零)
- 在WM_ERASEBKGND消息處理時 返回非零值
以上任何一種方法都可以阻止WM_ERASEBKGND 消息去清除窗口。其中,第二個方案的通常可以以如下代碼實現(xiàn):
case WM_ERASEBKGND:
??? return 1;
當(dāng)你標(biāo)記窗口內(nèi)容無效并試圖更新時,還有如下辦法可以防止WM_ERASEBKGND消息:InvalidateRect函數(shù)的最后一個參數(shù)可以指明在下一次窗口重畫時,是否窗口的部分背景會被重刷。將該參數(shù)置為False可以防止當(dāng)窗口需要重畫時系統(tǒng)發(fā)出WM_ERASEBKGND消息。
InvalidateRect(hwnd, &rect, FALSE);
不該畫的時候一定不要畫
有一個比較普遍的現(xiàn)象:即使窗口中只有一個小的部分發(fā)生了改變,往往所有的部分都會被重畫。比如,經(jīng)常地,當(dāng)窗口大小被改變時,一些(不是所有)的程序會重畫所有的窗口。通常,這是個是不必要的,這是因為當(dāng)窗口大小被改變時,經(jīng)常是之前窗口的內(nèi)容是不變的,僅僅是改變大小造成的一個小的邊界區(qū)域需要重畫。此時,沒有必要重畫所有區(qū)域。如果在這里多注意,多考慮,就可以使用好的算法以使得一次只有最小的部分被畫。
系統(tǒng)中每個窗口都有更新區(qū)域。這個區(qū)域描述了窗口中變得無效需要重畫的地方。如果一個窗口僅僅其需要更新的區(qū)域,不多繪制其他地方,那么窗口的繪制效果將會非常快。
有幾種方法可以獲得窗口的更新區(qū)域。通過GetUpdateRgn 函數(shù)可以獲得準(zhǔn)確的更新區(qū)域,這個函數(shù)返回的結(jié)果可以使矩形的區(qū)域也可以是非矩形的區(qū)域。通過GetUpdateRect 函數(shù)可以獲得需要更新的最小矩形區(qū)域。通常使用矩形的更新區(qū)域比較容易。第三個方法是在BeginPaint/EndPaint中得到PAINTSTRUCT 結(jié)構(gòu),從而得到準(zhǔn)確的更新區(qū)域信息。
一個常規(guī)的畫法函數(shù)是這樣的:
PAINTSTRUCT? ps;
HDC????????? hdc;
case WM_PAINT:
??? hdc = BeginPaint(hwnd, &ps);
??? // do painting
??? EndPaint(hwnd, &ps);
??? return 0;
如果僅僅在這個矩形區(qū)域上繪制窗口,速度上繪有很好地提高。
現(xiàn)在,當(dāng)使用BeginPaint/EndPaint時Windows會自動剪切掉畫在更新區(qū)域外面的部分。這意味著,你沒有機(jī)會畫到更新區(qū)域以外的地方。可能你會認(rèn)為,如果是這樣的話,花功夫確保代碼不試圖畫到更新區(qū)域外是沒有意義的,反正沒有畫出任何東西來。然而,你仍然可以避免不必要的API調(diào)用和相關(guān)計算,所以,我認(rèn)為放一些精力在如何工作地更快上是絕對值得的。
如果還是不能解決
?有些時候,當(dāng)你花了很多努力去考慮非常好的畫法時,發(fā)現(xiàn)窗口還是會被全部刷新。這通常是由兩個Window 類的屬性造成的:CS_VREDRAW?和CS_HREDRAW。如果有其中一個標(biāo)志被設(shè)置時,那么當(dāng)窗口水平或者豎直方向有大小被改變時,其內(nèi)容每次都會被重新刷新。所有,你需要關(guān)掉這兩個標(biāo)志,解決的唯一的方式是在創(chuàng)建窗體和窗體類被注冊時,確保這兩個屬性不被設(shè)置。
WNDCLASSEX wc;
wc.cbSize? = sizeof(wc);
wc.style?? = 0; /*?CS_VREDRAW | CS_HREDRAW; */?
...
RegisterClassEx(&wc);
有一點需要注意:如果主窗體有了這兩個屬性,即使子窗體沒有重畫標(biāo)志,會導(dǎo)致所有子窗體在其大小被改變時會被重繪。可以通過以下方式避免這個情況發(fā)生:
剪切子窗體
有時,閃爍的原因是因為當(dāng)重畫時,父窗體沒有剪切其子窗體區(qū)域。這樣的結(jié)果導(dǎo)致,整個父窗口內(nèi)容被重畫,而子窗體又被顯示在了上面(造成閃爍)。這個可以通過在父窗體上設(shè)置WS_CLIPCHILDREN 來解決。當(dāng)這個標(biāo)志被設(shè)置時,被子窗體占據(jù)的任何區(qū)域?qū)慌懦诟聟^(qū)域外。因此,即使你嘗試在子窗體所在的位置上繪制(父窗口的內(nèi)容),BeginPaint中的剪切區(qū)域也會阻止其繪制效果。
雙緩沖和內(nèi)存設(shè)備描述表(Memory Device Context, 簡稱Memory-DC)
常見的徹底避免閃爍的方法是使用雙緩沖。其基本的思路是:將窗體的內(nèi)容畫在屏幕外的一個緩沖區(qū)內(nèi),然后,將該緩沖區(qū)的內(nèi)容再傳遞到屏幕上(使用BilBlt函數(shù))。這是一個非常好的減少閃爍的方法,但是經(jīng)常被濫用,特別是當(dāng)程序員并不真正地理解如何有效地繪制窗口時。
典型的雙緩沖代碼如下:
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;這個方法比較慢,因為在每次窗體需要重畫的時候內(nèi)存設(shè)備描述表(Memory-DC)都需要被重新創(chuàng)建。更有效的方法是,僅僅創(chuàng)建內(nèi)存設(shè)備描述表(Memory-DC)一次,并使其足夠大到能滿足任何時候的整個窗體刷新。當(dāng)程序結(jié)束時,再銷毀這個內(nèi)存設(shè)備描述表(Memory-DC)。這兩種方法都存在對內(nèi)存開銷的問題,特別是如果內(nèi)存設(shè)備描述表(Memory-DC)是針對真?zhèn)€屏幕的大小。雙緩沖也需要兩倍的時間去畫。這是因為其第一次是在內(nèi)存設(shè)備描述表(Memory-DC)上畫,然后再使用BitBlt畫回到屏幕上。當(dāng)然,好的顯卡會使BitBlt更快,但是仍然會耗CPU 時間。
如果程序需要顯示相當(dāng)復(fù)雜的信息,比如像網(wǎng)頁,那么你應(yīng)該使用內(nèi)存設(shè)備描述表(Memory-DC)。比如IE,如果不使用雙緩沖,是沒有辦法在繪制網(wǎng)頁時不閃爍的。
沒有必要將雙緩沖技術(shù)用于整個窗體的繪制中。可以這樣設(shè)想,窗口中僅僅有一個小部分包含了復(fù)雜的圖形對象(比如半透明的位圖或者其他)。你應(yīng)該將內(nèi)存設(shè)備描述表(Memory-DC)僅僅用于著一個小區(qū)域,其他區(qū)域使用常規(guī)的方法。 有時,通過仔細(xì)的思考,經(jīng)常可以避免使用雙緩沖而直接將結(jié)果畫到屏幕上。只要你不破壞黃金法則,即“永遠(yuǎn)不要將一個像素畫兩次”,就可以防止閃爍的出現(xiàn)。
避免過度繪制
我想說的關(guān)于這個話題是這樣的:有一個需要自己定義畫法的窗體的標(biāo)題欄。首先,你畫了標(biāo)題,接著在上面畫一些其他的圖形。現(xiàn)在,只要標(biāo)題需要被重畫,就會出現(xiàn)閃爍現(xiàn)象。這是因為你沒有合乎黃金法則。這里,標(biāo)題被很快地顯示在其他圖形在上面繪制時,導(dǎo)致了閃爍。
有兩種技術(shù)可以組織這種類型的閃爍。第一個是使用剪切,第二個是使用你的大腦。
使用剪切時,你可以使用ExcludeClipRect?函數(shù)在設(shè)備描述表中去標(biāo)記一個特定的區(qū)域。當(dāng)一個區(qū)域被標(biāo)記上時,即使在該區(qū)域上面重畫也不會產(chǎn)生效果。一旦背景已經(jīng)被繪制了,可以通過SelectClipRgn移掉該標(biāo)記的區(qū)域,其他圖形能被畫到前面標(biāo)記的區(qū)域上。通過準(zhǔn)確的標(biāo)記(剪切),可以在很多時候被避免過度繪制。
另外一個方案就是找更聰明的解決辦法。比如,當(dāng)你需要畫一個表格,通常應(yīng)該先畫空的背景,再畫網(wǎng)格線從而產(chǎn)生表格。但是,這個方法會使網(wǎng)格線產(chǎn)生閃爍,這是因為在網(wǎng)格線被畫之前,下面背景被很快地顯示了一下。然而可以使用不同的做法達(dá)到想要的結(jié)果。即,不是一次畫一個大的空背景,而是畫一系列的空方塊,每一個方塊邊是被一個像素的寬度分開。這樣,當(dāng)畫網(wǎng)格線時,他們剛好能被畫到一個之前沒有畫過的地方。其結(jié)果是不會有閃爍現(xiàn)象,因為沒有像素被畫了超過兩次。
使用你的頭腦去想一個好的算法可能需要長一點的時間,但是卻是值得的,因為這能讓結(jié)果更好。
結(jié)論
希望你再也不會問:“為什么我的窗體會閃爍”這樣的問題。我已經(jīng)講解了閃爍的主要原因和解決辦法。如果你遇到了閃爍的問題,你應(yīng)該能找到原因并且使用這里提到的技術(shù)來解決了。
=============
附原文:
Flicker is the sign of sloppy programming and a lack of attention to detail. There is no reason why any part of a Windows program should flicker. The aim of this article is to present the reader (that's you) with the techniques used to prevent their windows applications from flickering.
What is flickering?
Flicker is simply this: the display of one image over the top of another in rapid succession. The result of this is screen flicker, where you can see one image briefly before another one is shown on top. Personally I find applications that "flicker" annoying to use, for this one reason: If the user-interface has been badly coded, then what does this say about the rest of the application, the part that you trust your data with? An application that has a smooth, fast user interface inspires confidence in it's users - it's as simple as that.
An application can flicker in many ways. The most common cause is when a window is resized, causing the contents to flicker badly as it is redrawn.
Only draw things once
This is the golden rule when doing any kind of painting on a computer, be it Windows or whatever OS you are using. You must never draw over the same pixel twice. A lazy programmer will often avoid putting any thought into the painting process, instead opting to take the easy route.
With the case of flickering, it is your responsiblity to ensure that no "overdraw" occurs. Now, Windows and your computer are fundamentally stupid; they won't do anything unless you instruct them explicitly. If any flickering is occuring, it is because some part of your program has deliberately overdrawn some area of the screen.
This may be because of some explicit command, or something which you have neglected to do. In either case, if your Windows program has a flickering problem, you need to understand how best to remove the problem.
WM_ERASEBKGND
The prime suspect is usually the WM_ERASEBKGND message. This message is sent to a window when it's background needs to be erased. This happens because windows are usually painted using a 2-stage process:
- WM_ERASEBKGND: Clear the background
- WM_PAINT: Draw the contents on top
This makes it easy to draw a window's contents: Every time you receive a WM_PAINT message, you know that you have a nice fresh canvas to draw on. However, drawing a window twice (once with WM_ERASEBKGND, once again with WM_PAINT) will cause the window to badly flicker. Just take a look at the standard Edit control in Windows - open up Notepad.exe and resize the window, and see how the contents flicker as it is redrawn.
Right then, how do we avoid erasing the background of a window? There are two methods.
- Set the window's background brush to NULL. (Set the?hbrBackground?member of the?WNDCLASS?structure to zero when you register the window class).
- Return non-zero in the WM_ERASEBKGND message handler.
Any one of these will steps will prevent the WM_ERASEBKGND message from clearing the window. The last option is usually easiest to implement:
case WM_ERASEBKGND: return 1;It is also possible to prevent WM_ERASEBKGND when you invalidate and update a window. The InvalidateRect API call's last parameter specifies whether or not a portion of a window is to have it's background erased when it is next redrawn. Specifying FALSE for this paramter prevents WM_ERASEBKGND from being sent when the window is redrawn.
InvalidateRect(hwnd, &rect, FALSE);Don't draw things when you don't have to
It is quite common for a Windows application to redraw it's entire window contents, even if only a small part of it changed. This is most usually the case when a window is resized - some (but not all) programs redraw the whole window. This is normally not necessary, because when a window is resized, more often than not the previous window contents is left unchanged, and the resize has just uncovered a small border which needs painting. It is not necessary to redraw the entire contents in this case. If a little thought and care is used, the painting algorithms can be written so that only the bare minimum is painted at any one time.
Every window in the system keeps an update region. This region describes the area of a window that has become invalidated and needs repainting. If a windows only updates the required area, and no more, then the window will draw much quicker as a result.
There are several ways to retrieve the update region for a window. The?GetUpdateRgn?API call retrieves the exact region, be it rectangular, or a more irregular shape. The?GetUpdateRect?API call retrieves the smallest bounding rectangle that encloses the update region. It is usually easier to just work with a rectangular area like this. The third method is to use the?PAINTSTRUCT?structure in conjunction with the?BeginPaint/EndPaint?API calls.
A normal painting procedure looks like this:
PAINTSTRUCT ps; HDC hdc; case WM_PAINT: hdc = BeginPaint(hwnd, &ps); // do painting EndPaint(hwnd, &ps); return 0;BeginPaint initializes the ps (PAINTSTRUCT) structure. One member,?rcPaint, is a RECT structure which describes the smallest bounding rectangle that encloses the update region (Just like the GetWindowRect API call). By only limiting drawing to just this rectangular region, painting can be dramatically sped up.
Now, Windows automatically clips any drawing you perform outside the update region when you use BeginPaint/EndPaint. This means that there is no way you can draw outside the update region even if you tried. You might think that it is pointless to make sure your code doesn't try to draw outside the update region, even when nothing will be drawn anyway. However, you are still avoiding unnecessary API calls and calculations, so I think it is always worth putting in a little more effort to get things working as fast as possible.
When you just can't help it
There are occasions when you spend alot of time and effort getting your super-duper drawing code working, only to find that your window is still getting redrawn in it's entirety. This is usually the cause of two window class styles -?CS_VREDRAW?and?CS_HREDRAW. When a window class has either of these two styles set, the window contents will be completely redrawn every time it is resized either vertically or horizontally (or both). So, you need to turn off these two class styles. The only way to do this is to make sure your window isn't created with them in the first place, and to prevent this from happening, you have to make sure that CS_HREDRAW and CS_VREDRAW aren't included when the window class is registered.
WNDCLASSEX wc; wc.cbSize = sizeof(wc); wc.style = 0; /* CS_VREDRAW | CS_HREDRAW; */ ... RegisterClassEx(&wc);The above example is just to help illustrate the point that these two styles must not be included when the window class is registered.
Just a word of warning here: If the main window in an application has these two class styles set, then this will cause all child windows to be redrawn during a resize, even if those children don't have the redraw flags set. This can be avoided by following the next step:
Clipping child windows
Sometimes flickering occurs because a parent window doesn't clip it's children when it paints itself. This results in the entire parent window contents being shown, and the the child windows being displayed on top (causing flicker). This can be easily solved by setting the WS_CLIPCHILDREN style on the parent window.
When a window has this style set, any areas that its child windows occupy are excluded from the update region. So, even if you try to draw over a child control, the clipping region that BeginPaint assigns will prevent you from doing so.
Double-buffing and memory-DC's
A common method to completely eliminate flickering windows is to use a technique called double-buffering. This basic idea is to draw a window's contents into an off-screen buffer, and then transfer this buffer to the screen in one fell-swoop (using BitBlt). This is a pretty good way to reduce flicker, but is often overused, especially by programmers who don't really understand how get efficient drawing working.
The basic way double-buffering works is like this:
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;This method is a little slow, because the offscreen memory-DC is created from scratch every time the window needs to be drawn. A more efficient method would be to create the memory DC only once, big enough so that the entire window can be painted at any time. When the application terminates, the memory DC would then be destroyed. Both these methods are potentially quite memory-intensive, especially if the memory DC needs to be the size of a screen (1024 * 768 * 32 bytes=2.5 Mb).
Double-buffering will also be twice as slow as it needs to be. Because you are drawing once to the memory-DC, then again during the "blit", you are using up clock cycles when you don't need to. Granted, a fast graphics card will perform a BitBlt very quickly, but it's still wasted CPU.
If your application needs to display quite complicated information (say, like a web-page), then you would need to use the memory-DC method. Take Internet Explorer, for instance. There is no way it would be able to render a web-page with no flickering without using double-buffering.
Double-buffering doesn't have to be used to paint a whole window. Imagine that you had just a small portion of a window that contained a complex graphic object (maybe a semi-transparent bitmap or something). You could use an off-screen DC to draw just this one region, and BitBlt that to the screen, whilst drawing the rest of the window normally.
Sometimes though, with a little careful thinking, it is often possible to avoid double-buffering and draw straight to the screen. As long as you don't break the golden rule, "Never draw over the same pixel twice", you will achieve flicker-free drawing.
Avoiding deliberate overdraw
What I mean by this is the following type of situation. Say, you are custom-drawing the titlebar of a window. You draw the caption first, then draw some additional graphics over the top. Now, whenever the caption needs to be painted, it will flicker. This is because you haven't followed the "golden rule". In this case, the caption is being shown briefly before additional graphics are painted on top, which appear to flicker.
There are two techniques you can use to prevent this type of flickering. The first is to use clipping, the second is to use your brain.
In the case of clipping, you can use the?ExcludeClipRect?API call to mask out certain areas of a device context. When an area is masked, it is not affected when painted over. Once a background has been drawn, the clipping area can be removed with?SelectClipRgn, and another graphic can be painted in the previously masked-out area. By using appropriate masking (or clipping), overdraw can be eliminated in alot of cases.
The other option is to take a more intelligent approach. Imagine you had to draw a grid. A grid would normally be painted by first drawing a blank background, and then drawing a series of lines (horizontal and vertical) to create the grid effect. The problem with this type of approach is that the grid lines will appear to flicker, because the background is briefly appearing underneath each line before the lines are drawn. However, the same effect can be achieved with a different approach. Instead of drawing a single blank background, draw a series of blank squares, separated by a pixel-wide space on each side. When you come to draw the grid lines, they can be placed in the pixel-wide gaps which haven't been painted over yet. The result is the same, but this time there is no flickering because no pixel has been painted over twice.
Using your brain to think around a problem may take slightly longer than the direct "no-brainer" approach, but I think it is worth the extra effort, because the results can be so much better.
Conclusion
Hopefully you should never have to ask the question "Why does my window flicker?" ever again. I have presented the major causes of flickering in a windows program, and also the techniques you can use to remove this flickering. If you encounter flickering in a program you are developing, you should be able to identify the possible causes, and use the techniques described in this tutorial to completely eliminate flicker from your applications.
總結(jié)
- 上一篇: USB信号线标准定义
- 下一篇: 美赛参赛回忆录