GDI+ 雙緩衝 的起因以及解決辦法

重繪導致原因:UpdateData、Invalidate、InvalidateRect和UpdateWindow函數。

1. UpdateData重繪控件函數

 UpdateData(TRUE)——刷新控件的值到對應的變量。(外部輸入值交給內部變量)

 即:控件的值—>變量。
 

UpdateData(FALSE) —— 拷貝變量值到控件顯示。(變量的最終運算結果值交給外部輸出顯示)

 即:變量值—>控件顯示。 

2. Invalidate()

      該函數的作用是使整個窗口客戶區無效。窗口的客戶區無效意味着需要重繪,例如,如果一個被其它窗口遮住的窗口變成了前臺窗口,那麼原來被遮住的部分就是無效的,需要重繪。這時Windows會在應用程序的消息隊列中放置WM_PAINT消息。MFC爲窗口類提供了WM_PAINT的消息處理函數OnPaint,OnPaint負責重繪窗口。視圖類有一些例外,在視圖類的OnPaint函數中調用了OnDraw函數,實際的重繪工作由OnDraw來完成。參數bErase爲TRUE時,重繪區域內的背景將被擦除,否則,背景將保持不變。 

3. InvalidateRect

    用InvalidateRect函數只重繪部分區域,而且不重繪背景(第二個參數用FALSE)就可以解決大部分的屏閃問題。

    比如:CRect rect(10,47,10+120,47+70);
    InvalidateRect(rect,FALSE);

 4. UpdateWindow函數
UpdateWindow( )的作用是使窗口立即重繪。調用Invalidate等函數後窗口不會立即重繪,這是由於WM_PAINT消息的優先級很低,它需要等消息隊列中的其它消息發送完後才能被處理。調用UpdateWindow函數可使WM_PAINT被直接發送到目標窗口,從而導致窗口立即重繪。注意:函數繞過應用程序的消息隊列,直接發送WM_PAINT消息給指定窗口的窗口過程,如果更新區域爲空,則不發送消息。

 

解決方法:

       雙緩衝是一種基本的技術。我們知道,如果窗體在響應WM_PAINT消息的時候要進行復雜的圖形處理,那麼窗體在重繪時由於過頻的刷新而引起閃爍現象。解決這一問題的有效方法就是雙緩衝技術。

      因爲窗體在刷新時,總要有一個擦除原來圖象的過程,它利用背景色填充窗體繪圖區,然後在調用新的繪圖代碼進行重繪,這樣一擦一寫造成了圖象顏色的反差。當WM_PAINT的響應很頻繁的時候,這種反差也就越發明顯。於是我們就看到了閃爍現象。(

                      當窗口由於任何原因需要重繪時,
總是先用背景色將顯示區清除,然後才調用OnPaint,而背景色往往與繪圖內容
反差很大,這樣在短時間內背景色與顯示圖形的交替出現,使得顯示窗口看起來
在閃。如果將背景刷設置成NULL,這樣無論怎樣重繪圖形都不會閃了。
當然,這樣做會使得窗口的顯示亂成一團,因爲重繪時沒有背景色對原來
繪製的圖形進行清除,而又疊加上了新的圖形。) 我們會很自然的想到,避免背景色的填充是最直接的辦法。但是那樣的話,窗體上會變的一團糟。因爲每次繪製圖象的時候都沒有將原來的圖象清除,造成了圖象的殘留,於是窗體重繪時,畫面往往會變的亂七八糟。所以單純的禁止背景重繪是不夠的。我們還要進行重新繪圖,但要求速度很快,於是我們想到了使用BitBlt函數。它可以支持圖形塊的複製,速度很快。我們可以先在內存中作圖,然後用此函數將做好的圖複製到前臺,同時禁止背景刷新,這樣就消除了閃爍。以上也就是雙緩衝繪圖的基本的思路。






 如何提高繪圖的效率
    電力系統的網絡圖形的CAD軟件,在一個窗口中往往要顯示成千上萬個電力元件,而每個元件又是由點、線、圓等基本圖形構成。如果真要在一次重繪過程重畫這麼多元件,可想而知這個過程是非常漫長的。如果加上了圖形的瀏覽功能,鼠標拖動圖形滾動時需要進行大量的重繪,速度會慢得讓用戶將無法忍受。怎麼辦?只有再研究研究MFC的繪圖過程了。
    實際上,在OnDraw(CDC *pDC)中繪製的圖並不是所有都顯示了的,例如:你
在OnDraw中畫了兩個矩形,在一次重繪中雖然兩個矩形的繪製函數都有執行,但是很有可能只有一個顯示了,這是因爲MFC本身爲了提高重繪的效率設置了裁剪區。裁剪區的作用就是:只有在這個區內的繪圖過程纔會真正有效,在區外的是無效的,即使在區外執行了繪圖函數也是不會顯示的。因爲多數情況下窗口重繪的產生大多是因爲窗口部分被遮擋或者窗口有滾動發生,改變的區域並不是整個圖形而只有一小部分,這一部分需要改變的就是pDC中的裁剪區了。因爲顯示(往內存或者顯存都叫顯示)比繪圖過程的計算要費時得多,有了裁剪區後顯示的就只是應該顯示的部分,大大提高了顯示效率。但是這個裁剪區是MFC設置的,它已經爲我們提高了顯示效率,在進行復雜圖形的繪製時如何進一步提高效率呢?那就只有去掉在裁剪區外的繪圖過程了。可以先用pDC->GetClipBox()得到裁剪區,然後在繪圖時判斷你的圖形是否在這個區內,如果在就畫,不在就不畫。
如果你的繪圖過程不復雜,這樣做可能對你的繪圖效率不會有提高。




囉嗦一下:



首先,ShowWindow本身是不會產生重畫消息的,它的作用僅僅是把窗口顯示出來。不過,當窗口顯示的時候,Windows會自動探測窗口的內容是否需要重畫、以及需要重畫的區域組成,比如你的窗口位置直接在屏幕外,或者你的窗口被別的窗口完全擋住,當然就不需要重畫,如果你的窗口只露出一部分,那麼就只有這一部分需要重畫。這個過程與你移動窗口、切換窗口的時候Windows所做的事情是一樣的——自動判定你的窗口有哪一部分原來不顯示而現在需要顯示,然後對這部分區域調用InvalidateRect()。這個函數的作用並不是立刻重畫這些區域,而是對這些區域做上標記。多次調用這個函數,新標記的區域會與以前標記的區域合併。

之後,當你的消息隊列完全空了的時候,假若windows又發現你窗口所標記的重畫區域不爲空,那麼Windows就在你的消息隊列裏放一個WM_PAINT消息,讓你重畫。根據這一流程可知,假若我們的消息隊列一直很忙的話,那麼窗口是沒機會獲得WM_PAINT消息的。其次,假定消息隊列裏有若干個消息,每個都導致一部分窗口區域需要重畫,那麼最後只會重畫一次,只不過重畫的範圍是幾個區域的合併。再有,某些特殊情況下,有可能會不希望窗口被重畫、或者至少其中某一部分不要重畫,那麼你可以在消息隊列被取空之前(尚未發出WM_PAINT),用ValidateRect把窗口的某一部分乃至全部都取消標記。如果所有以前被標記的部分全被你取消掉了,那麼等消息隊列空了以後,也不會再有WM_PAINT發出了。

當你在處理WM_PAINT消息進行重畫的時候,BeginPaint的一個重要作用,就是在它返回的DC裏,用原來標記的區域製作一個剪裁區域(ClipRegion),從而使你的所有重畫操作都被限定在這一個區域中。這是一個很重要的特性。舉例來說,假若你的窗口用一張位圖作爲背景,處理WM_PAINT的時候用BitBlt之類的方法往屏幕上貼圖。如果某一次有一個別的窗口僅僅蓋住了你窗口的一個小角,當它拿開的時候,如果沒有剪裁區域的話,那麼就會對整個窗口貼圖,這不僅很慢,而且會引起你窗口中的各個子窗口的閃爍。但有了剪裁區域的話,你的代碼雖然還是在對整個窗口貼圖,但實際上只有位於剪裁區域內的那部分操作有效,其它的都被Windows放棄了,所以速度會快得多。有時我們自己也需要更新窗口的顯示內容,這時候也是通過調用InvalidateRect來做。不過,很多人在這種情況下習慣於將整個窗口統統Inalidate,這樣做倒是很方便,不過這是一個很不好的習慣。除非你需要更新的內容波及到整個窗口,否則應該僅僅把需要改變的那部分Invalidate。

上面說的剪裁區域僅僅是BeginPaint的一個作用,BeginPaint還有其它作用,都是跟重畫這個任務緊密連接的,因此,在響應WM_PAINT消息的時候,必須使用BeginPaint所獲取的DC句柄來畫圖,絕不能用GetDC等其它方式。相對應的,這個句柄也必須使用EndPaint來釋放。如果在響應WM_PAINT的時候沒有調用BeginPaint和EndPaint(例如用GetDC和ReleaseDC來畫圖),其中一個副作用就是:重畫區域的標記不會被取消。於是當你響應完這一個WM_PAINT之後,Windows會發現你的窗口還有區域被標記爲重畫,於是再次發出WM_PAINT,於是你就永無休止地重畫下去了。

從上述可知,單純一個ShowWindow,照樣會正確重畫窗口內容,只不過重畫是在消息隊列取空之後。有時我們希望窗口被立即重畫,而不是去等待那個不確定的消息隊列,此時就需要用到UpdateWindow。這個函數的作用只有一個:假若當前被標記爲重畫的區域存在(不存在的話它什麼也不做),那麼立刻讓Windows使用SendMessage的方式來對你的窗口發送WM_PAINT。

說道這裏,就要說一下SendMessage與PostMessage的區別了。PostMessage是把消息放到消息隊列尾部,然後通過程序的消息環逐個從消息隊列裏取出來進行處理。SendMessage卻不是這樣,它實際上根本不經過消息隊列。對SendMessage的處理分兩種情況:

1、由本線程發出的SendMessage,例如在自己的消息處理過程中調用UpdateWindow,從而發出的WM_PAINT。對於這種情況,SendMessage實際上直接調用你窗口的消息處理函數。也就是說,在進行消息處理的時候對本窗口SendMessage,實際上是遞歸調用。整個過程是:消息環取出消息A(假定A是PostMessage放進消息隊列的)-> 處理消息A -> SendMessage對本線程的窗口發送消息B -> 處理消息B -> 繼續處理消息A -> 消息環再取下一個消息。

2、由另一個線程發出的SendMessage。此時當然不能直接調用了。這種情況下,windows會把發送消息的線程掛起,然後,當接收消息的線程調用PeekMessage或者GetMessage的時候,這兩個函數會立刻調用你窗口的消息處理函數,直到處理完畢(此時發送線程的SendMessage才返回),PeekMessage或者GetMessage纔會去檢查消息隊列,並從中取出一個返回。

總之,不論哪種情況,SendMessage發送的消息,跟消息隊列均沒有任何關係,而且也不通過消息環執行(前者是直接調用,後者是在PeekMessage或GetMessage函數內部調用)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章