MFC繪制圖片閃爍詳解


用MFC如何高效地繪圖   
          顯示圖形如何避免閃爍,如何提高顯示效率是問得比較多的問題。   
  而且多數人認為MFC的繪圖函數效率很低,總是想尋求其它的解決方案。   
  MFC的繪圖效率的確不高但也不差,而且它的繪圖函數使用非常簡單,   
  只要使用方法得當,再加上一些技巧,用MFC可以得到效率很高的繪圖程序。   
  我想就我長期(呵呵當然也只有2年多)使用MFC繪圖的經驗談談   
  我的一些觀點。   
  1、顯示的圖形為什么會閃爍?   
          我們的繪圖過程大多放在OnDraw或者OnPaint函數中,OnDraw在進行屏   
  幕顯示時是由OnPaint進行調用的。當窗口由於任何原因需要重繪時,   
  總是先用背景色將顯示區清除,然后才調用OnPaint,而背景色往往與繪圖內容   
  反差很大,這樣在短時間內背景色與顯示圖形的交替出現,使得顯示窗口看起來   
  在閃。如果將背景刷設置成NULL,這樣無論怎樣重繪圖形都不會閃了。   
  當然,這樣做會使得窗口的顯示亂成一團,因為重繪時沒有背景色對原來   
  繪制的圖形進行清除,而又疊加上了新的圖形。   
          有的人會說,閃爍是因為繪圖的速度太慢或者顯示的圖形太復雜造成的,   
  其實這樣說並不對,繪圖的顯示速度對閃爍的影響不是根本性的。   
  例如在OnDraw(CDC   *pDC)中這樣寫:   
    pDC->MoveTo(0,0);   
    pDC->LineTo(100,100);   
  這個繪圖過程應該是非常簡單、非常快了吧,但是拉動窗口變化時還是會看見   
  閃爍。其實從道理上講,畫圖的過程越復雜越慢閃爍應該越少,因為繪圖用的   
  時間與用背景清除屏幕所花的時間的比例越大人對閃爍的感覺會越不明顯。   
  比如:清楚屏幕時間為1s繪圖時間也是為1s,這樣在10s內的連續重畫中就要閃   
  爍5次;如果清楚屏幕時間為1s不變,而繪圖時間為9s,這樣10s內的連續重畫   
  只會閃爍一次。這個也可以試驗,在OnDraw(CDC   *pDC)中這樣寫:   
    for(int   i=0;i<100000;i++)   
    {   
      pDC->MoveTo(0,i);   
      pDC->LineTo(1000,i);   
    }   
  呵呵,程序有點變態,但是能說明問題。   
          說到這里可能又有人要說了,為什么一個簡單圖形看起來沒有復雜圖形那么   
  閃呢?這是因為復雜圖形占的面積大,重畫時造成的反差比較大,所以感覺上要   
  閃得厲害一些,但是閃爍頻率要低。   
          那為什么動畫的重畫頻率高,而看起來卻不閃?這里,我就要再次強調了,   
  閃爍是什么?閃爍就是反差,反差越大,閃爍越厲害。因為動畫的連續兩個幀之間   
  的差異很小所以看起來不閃。如果不信,可以在動畫的每一幀中間加一張純白的幀,   
  不閃才怪呢。   
    
  2、如何避免閃爍   
          在知道圖形顯示閃爍的原因之后,對症下葯就好辦了。首先當然是去掉MFC   
  提供的背景繪制過程了。實現的方法很多,   
      *   可以在窗口形成時給窗口的注冊類的背景刷付NULL   
      *   也可以在形成以后修改背景   
    static   CBrush   brush(RGB(255,0,0));   
    SetClassLong(this->m_hWnd,GCL_HBRBACKGROUND,(LONG)(HBRUSH)brush);   
      *   要簡單也可以重載OnEraseBkgnd(CDC*   pDC)直接返回TRUE   
          這樣背景沒有了,結果圖形顯示的確不閃了,但是顯示也象前面所說的一樣,   
  變得一團亂。怎么辦?這就要用到雙緩存的方法了。雙緩沖就是除了在屏幕上有   
  圖形進行顯示以外,在內存中也有圖形在繪制。我們可以把要顯示的圖形先在內存中繪制好,然后再一次性的將內存中的圖形按照一個點一個點地覆蓋到屏幕上去(這個過程非常快,因為是非常規整的內存拷貝)。這樣在內存中繪圖時,隨便用什么反差大的背景色進行清除都不會閃,因為看不見。當貼到屏幕上時,因為內存中最終的圖形與屏幕顯示圖形差別很小(如果沒有運動,當然就沒有差別),這樣看起來就不會閃。   
    
  3、如何實現雙緩沖   
          首先給出實現的程序,然后再解釋,同樣是在OnDraw(CDC   *pDC)中:   
    CDC   MemDC;   //首先定義一個顯示設備對象   
    CBitmap   MemBitmap;//定義一個位圖對象   
    //隨后建立與屏幕顯示兼容的內存顯示設備   
    MemDC.CreateCompatibleDC(NULL);   
    //這時還不能繪圖,因為沒有地方畫   ^_^   
    //下面建立一個與屏幕顯示兼容的位圖,至於位圖的大小嘛,可以用窗口的大小   
    MemBitmap.CreateCompatibleBitmap(pDC,nWidth,nHeight);   
        
    //將位圖選入到內存顯示設備中   
    //只有選入了位圖的內存顯示設備才有地方繪圖,畫到指定的位圖上   
    CBitmap   *pOldBit=MemDC.SelectObject(&MemBitmap);   
    //先用背景色將位圖清除干凈,這里我用的是白色作為背景   
    //你也可以用自己應該用的顏色   
    MemDC.FillSolidRect(0,0,nWidth,nHeight,RGB(255,255,255));   
    //繪圖   
    MemDC.MoveTo(……);   
    MemDC.LineTo(……);   
      
    //將內存中的圖拷貝到屏幕上進行顯示   
    pDC->BitBlt(0,0,nWidth,nHeight,&MemDC,0,0,SRCCOPY);   
    //繪圖完成后的清理   
    MemBitmap.DeleteObject();   
    MemDC.DeleteDC();   
  上面的注釋應該很詳盡了,廢話就不多說了。   
    
  4、如何提高繪圖的效率   
          我主要做的是電力系統的網絡圖形的CAD軟件,在一個窗口中往往要顯示成千上萬個電力元件,而每個元件又是由點、線、圓等基本圖形構成。如果真要在一次重繪過程重畫這么多元件,可想而知這個過程是非常漫長的。如果加上了圖形的瀏覽功能,鼠標拖動圖形滾動時需要進行大量的重繪,速度會慢得讓用戶將無法忍受。怎么辦?只有再研究研究MFC的繪圖過程了。   
          實際上,在OnDraw(CDC   *pDC)中繪制的圖並不是所有都顯示了的,例如:你   
  在OnDraw中畫了兩個矩形,在一次重繪中雖然兩個矩形的繪制函數都有執行,但是很有可能只有一個顯示了,這是因為MFC本身為了提高重繪的效率設置了裁剪區。裁剪區的作用就是:只有在這個區內的繪圖過程才會真正有效,在區外的是無效的,即使在區外執行了繪圖函數也是不會顯示的。因為多數情況下窗口重繪的產生大多是因為窗口部分被遮擋或者窗口有滾動發生,改變的區域並不是整個圖形而只有一小部分,這一部分需要改變的就是pDC中的裁剪區了。因為顯示(往內存或者顯存都叫顯示)比繪圖過程的計算要費時得多,有了裁剪區后顯示的就只是應該顯示的部分,大大提高了顯示效率。但是這個裁剪區是MFC設置的,它已經為我們提高了顯示效率,在進行復雜圖形的繪制時如何進一步提高效率呢?那就只有去掉在裁剪區外的繪圖過程了。可以先用pDC->GetClipBox()得到裁剪區,然后在繪圖時判斷你的圖形是否在這個區內,如果在就畫,不在就不畫。   
  如果你的繪圖過程不復雜,這樣做可能對你的繪圖效率不會有提高

 

==============================================================================

在OnEraseBkGnd中,如果你不調用原來缺省
的OnEraseBkGnd只是重畫背景則不會有閃爍.而在OnPaint里面,
由於它隱含的調用了OnEraseBkGnd,而你又沒有處理OnEraseBkGnd
函數,這時就和窗口缺省的背景刷相關了.缺省的
OnEraseBkGnd操作使用窗口的缺省背景刷刷新背景(一般情況
下是白刷),而隨后你又自己重畫背景造成屏幕閃動.
另外一個問題是OnEraseBkGnd不是每次都會被調用的.如果你
調用Invalidate的時候參數為TRUE,那么在OnPaint里面隱含
調用BeginPaint的時候就產生WM_ERASEBKGND消息,如果參數
是FALSE,則不會重刷背景.

所以解決方法有三個半:
1.用OnEraseBkGnd實現,不要調用原來的OnEraseBkGnd函數.
2.用OnPaint實現,同時重載OnEraseBkGnd,其中直接返回.
3.用OnPaint實現,創建窗口時設置背景刷為空
4.用OnPaint實現,但是要求刷新時用Invalidate(FALSE)這樣
的函數.(不過這種情況下,窗口覆蓋等造成的刷新還是要閃一
下,所以不是徹底的解決方法)
都挺簡單的.
------------------------------------------------------
在MFC中 任何一個window組件的繪圖 都是放在這兩個member function中
在設定上 OnEraseBkgnd()是用來畫底圖的 而OnPaint()是用來畫主要對象的
舉例說明 一個按鈕是灰色的 上面還有文字
則OnEraseBkgnd()所做的事就是把按鈕畫成灰色
而OnPaint()所做的事 就是畫上文字

既然這兩個member function都是用來畫出組件的
那為何還要分OnPaint() 與 OnEraseBkgnd() 呢
其實OnPaint() 與 OnEraseBkgnd() 特性是有差的
1. OnEraseBkgnd()的要求是快速 在里面的繪圖程序最好是不要太耗時間
因為 每當window組件有任何小變動 都會馬上呼叫OnEraseBkgnd()
2. OnPaint() 是只有在程序有空閑的時候才會被呼叫
3. OnEraseBkgnd() 是在 OnPaint() 之前呼叫的
所以 OnPaint()被呼叫一次之前 可能會呼叫OnEraseBkgnd()好幾次


如果我們是一個在做圖形化使用者接口的人
常會需要把一張美美的圖片設為我們dialog的底圖
把繪圖的程序代碼放在OnPaint() 之中 可能會常碰到一些問題
比方說拖曳一個窗口在我們做的dialog上面一直移動
則dialog會變成灰色 直到動作停止才恢復
這是因為每次需要重繪的時候 程序都會馬上呼叫OnEraseBkgnd()
OnEraseBkgnd()就把dialog畫成灰色
而只有動作停止之后 程序才會呼叫OnPaint() 這時才會把我們要畫的底圖貼上去


這個問題的解法 比較差點的方法是把OnEraseBkgnd() 改寫成不做事的function
如下所示
BOOL CMyDlg::OnEraseBkgnd(CDC* pDC)
{
return TRUE;
}
以上本來是會呼叫CDialog::OnEraseBkgnd() 但是如果我們不呼叫的話
程序便不會畫上灰色的底色了


比較好的做法是直接將繪圖的程序從OnPaint()移到OnEraseBkgnd()來做
如下所示

// m_bmpBKGND 為一CBitmap對象 且事先早已加載我們的底圖
// 底圖的大小與我們的窗口client大小一致


BOOL CMyDlg::OnEraseBkgnd(CDC* pDC)
{
CRect rc;
GetUpdateRect(&rc);
CDC srcDC;
srcDC.CreateCompatibleDC(pDC);
srcDC.SelectObject(m_bmpBKGND);

pDC->BitBlt(rc.left,rc.top,rc.GetWidth(),
rc.GetHeight(),&srcDC,rc.left,rc.top,SRCCOPY);
return TRUE;
}

特別要注意的是 取得重畫大小是使用GetUpdateRect() 而不是GetClientRect()
如果使用GetClientRect() 會把不該重畫的地方重畫


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM