MFC雙緩沖解決圖象閃爍[轉]


轉載網上找到的一篇雙緩沖的文章,很好用。http://www.cnblogs.com/piggger/archive/2009/05/02/1447917.html
_______________________________________________________________________

顯示圖形如何避免閃爍,如何提高顯示效率是問得比較多的問題。而且多數人認為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()得到裁剪區,然后在繪圖時判斷你的圖形是否在這個區內,如果在就畫,不在就不畫。

如果你的繪圖過程不復雜,這樣做可能對你的繪圖效率不會有提高。 

http://www.cnblogs.com/watsonlong/archive/2011/04/19/2021486.html

MFC雙緩沖繪圖

——————————————————————————

BOOL  CDataStructureView::OnEraseBkgnd(CDC* pDC)
 
{
      CRect rc;
      CDC dcMem;
      GetClientRect(&rc);
      CBitmap bmp; //內存中承載臨時圖象的位圖
 
 
      dcMem.CreateCompatibleDC(pDC); //依附窗口DC創建兼容內存DC
      //創建兼容位圖(必須用pDC創建,否則畫出的圖形變成黑色)
      bmp.CreateCompatibleBitmap(pDC,rc.Width(),rc.Height());
      CBitmap *pOldBit=dcMem.SelectObject(&bmp);
      //按原來背景填充客戶區,不然會是黑色
      dcMen.FillSolidRect(rc,RGB(255,255,255))
 
      //畫圖,添加你要畫圖的代碼,不過用dcMem畫,而不是pDC;
 
      ......
 
      pDC->BitBlt(0,0,rc.Width(),rc.Height(),&dcMem,0,0,SRCCOPY);
 
      //將內存DC上的圖象拷貝到前台
      //繪圖完成后的清理
      dcMem.DeleteDC();     //刪除DC
      bmp.DeleteObject(); //刪除位圖
      return  true ;
      //這里一定要用return true,如果用自動生成的,會調用基類,把畫出來的覆蓋,就什     么結果也沒有了
}

——————————————————————————

Good Luck !

MFC GDI雙緩沖避免圖形閃爍[轉]

如何實現雙緩沖
首先給出實現的程序,然后再解釋,同樣是在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();

禁止系統擦掉原來的圖象
可以重載OnEraseBkgnd()函數,讓其直接返回TRUE就可以了。如
BOOL CMyWin::OnEraseBkgnd(CDC* pDC)
{
  return TRUE;
  //return CWnd::OnEraseBkgnd(pDC);//把系統原來的這條語句注釋掉。
}

多數人認為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()得到裁剪區,然后在繪圖時判斷你的圖形是否在這個區內,如果在就畫,不在就不畫。
如果你的繪圖過程不復雜,這樣做可能對你的繪圖效率不會有提高。

關鍵字 雙緩沖
原作者姓名 戚高

介紹
在論壇中經常見到關於刷新時界面閃爍的帖子,如何控制在進行高效繪圖時不出現界面閃爍的感覺呢,下文就雙緩沖方法進行講解.

正文
圖形為什么會閃爍的原因是:我們的繪圖過程大多放在OnDraw或者OnPaint函數中,OnDraw在進行屏幕顯示時是由OnPaint進行調用的。當窗口由於任何原因需要重繪時,總是先用背景色將顯示區清除,然后才調用OnPaint,而背景色往往與繪圖內容反差很大,這樣在短時間內背景色與顯示圖形的交替出現,使得顯示窗口看起來在閃。如果將背景刷設置成NULL,這樣無論怎樣重繪圖形都不會閃了。當然,這樣做會使得窗口的顯示亂成一團,因為重繪時沒有背景色對原來繪制的圖形進行清除,而又疊加上了新的圖形。有的人會說,閃爍是因為繪圖的速度太慢或者顯示的圖形太復雜造成的,其實這樣說並不對,繪圖的顯示速度對閃爍的影響不是根本性的。
如何實現雙緩沖:在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();

以論壇的一個帖子例子為例來說明一些具體如何解決問題.
帖子那容是:

我想讓一個區域動起來,
如何解決窗口刷新時區域的閃爍。
void CJhkljklView::OnDraw(CDC* pDC)
{
    CJhkljklDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
    // TODO: add draw code for native data here
            int i;
    int x[20],y[20];
    CPen hPen;
     POINT w[5];
             x[0]=a/100+10;
             x[1]=a/100+30;
                 x[2]=a/100+80;
                 x[3]=a/100+30;
                 x[4]=a/100+10;

                     y[0]=10;
                  y[1]=10;
                   y[2]=25;
                    y[3]=40;
                     y[4]=40;   

      for (i=0;i<5;i++)
      {         w[i].x=x[i];
      w[i].y=y[i];
      }
      //CClientDC dc(this);
             //hPen=CreatePen(PS_SOLID,1,RGB(255,0,0));
      CRgn argn,Brgn;
        CBrush abrush(RGB(40,30,20));
        argn.CreatePolygonRgn(w, 5, 1);// point為CPoint數組,
        pDC->FillRgn(&argn, &abrush);
         abrush.DeleteObject();
}

void CJhkljklView::OnTimer(UINT nIDEvent)
{
    // TODO: Add your message handler code here and/or call default
        InvalidateRect(NULL,true);
        UpdateWindow();
        a+=100;
    CView::OnTimer(nIDEvent);
}

int CJhkljklView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CView::OnCreate(lpCreateStruct) == -1)
        return -1;
    // TODO: Add your specialized creation code here
    SetTimer(1,10,NULL);
    return 0;
}

利用定時器直接進行10毫秒的屏幕刷新,這樣效果會出現不停的閃爍的情況.

解決方法利用雙緩沖,首先觸發WM_ERASEBKGND,然后修改返回TRUE;
定義變量:
CBitmap *m_pBitmapOldBackground ;
CBitmap m_bitmapBackground ;
CDC m_dcBackground;

//繪制背景
if(m_dcBackground.GetSafeHdc()== NULL|| (m_bitmapBackground.m_hObject == NULL))
    {
        m_dcBackground.CreateCompatibleDC(&dc);
        m_bitmapBackground.CreateCompatibleBitmap(&dc,rect.Width(),rect.Height()) ;
        m_pBitmapOldBackground = m_dcBackground.SelectObject(&m_bitmapBackground) ;
        //DrawMeterBackground(&m_dcBackground, rect);
        CBrush brushFill, *pBrushOld;        
        // 背景色黑色
        brushFill.DeleteObject();
        brushFill.CreateSolidBrush(RGB(255, 255, 255));
        pBrushOld = m_dcBackground.SelectObject(&brushFill);
        m_dcBackground.Rectangle(rect);
        m_dcBackground.SelectObject(pBrushOld);
    }
    memDC.BitBlt(0, 0, rect.Width(), rect.Height(),
                       &m_dcBackground, 0, 0, SRCCOPY) ;

    //繪制圖形
    int i;
    int x[20],y[20];
    CPen hPen;
    POINT w[5];
    x[0]=a/100+10;
    x[1]=a/100+30;
    x[2]=a/100+80;
    x[3]=a/100+30;
    x[4]=a/100+10;
    y[0]=10;
    y[1]=10;
    y[2]=25;
    y[3]=40;
    y[4]=40;
    for (i=0;i<5;i++)
    {         w[i].x=x[i];
    w[i].y=y[i];
    }
    //CClientDC dc(this);
    //hPen=CreatePen(PS_SOLID,1,RGB(255,0,0));
    CRgn argn,Brgn;
    CBrush abrush(RGB(40,30,20));
    argn.CreatePolygonRgn(w, 5, 1);// point為CPoint數組,
    memDC.FillRgn(&argn, &abrush);
    abrush.DeleteObject();
}

這樣編譯運行程序就會出現屏幕不閃爍的情況了.

《MFC游戲開發》筆記六 圖像雙緩沖技術:實現一個流暢的動畫

本系列文章由七十一霧央編寫,轉載請注明出處。

 http://blog.csdn.net/u011371356/article/details/9334121

作者:七十一霧央 新浪微博:http://weibo.com/1689160943/profile?rightmod=1&wvr=5&mod=personinfo

 

 

       在前幾節的筆記里,大家肯定會為一個問題感到心煩:畫面怎么老是一閃一閃的啊,太難受了。確實是的,如果玩這樣的游戲簡直就是一種折磨。但是大家玩游戲的時候,從來沒有遇到過這種情況吧?那么游戲開發者是怎么解決這個問題的呢?霧央在這一節筆記里給大家講解一種簡單通用的方法——圖像雙緩沖。

 

一、閃爍原因

       

       為了解決問題,我們得首先搞清楚閃爍的原因是什么,然后才能對症下葯。能夠導致游戲畫面閃爍的原因非常多,但是對於我們做游戲開發的同學來說,最主要的就是一種:貼圖貼的太頻繁。

       如果大家是一個細心人的話,那么應該可以發現,在筆記三講解貼圖的時候,當我們貼出背景圖的時候,是根本不會閃爍的,但是當我們貼出人物后,閃爍就出來了,而當我們移動人物的時候,閃爍的畫面簡直慘不忍睹啊。

       想弄清楚真正的原因就得要理解GDI繪圖的原理:GDI繪圖的時候是先繪制到顯存里面,然后顯存每隔一段時間就需要把里面的內容輸出到屏幕上,這個時間就是刷新周期。在繪圖的時候,系統會先用一種背景色擦除掉原來的圖像,然后再繪制新的圖像。如果這幾次繪制不在同一個刷新周期中,那么我們看到的就是先看到背景色,再看到內容出來,就會有閃爍的感覺,而繪制的次數越多,看到這種現象的可能性就越大,就閃爍的越厲害。

 

二、圖像雙緩沖技術

 

       大家清楚了閃爍的原因后,再結合我們只貼出背景的時候並沒有閃爍的事實,那么或許大家就可以想到一種解決方法了:我們事先將要畫的所有東西畫在一張圖片上,然后將這張圖直接貼出來,不就解決了嗎?

       如果你想到這里,那么恭喜你,你已經想到了圖像雙緩沖技術。其實看起來很高端的這個名詞其實非常簡單。我們之前畫圖的時候都是直接畫在窗口DC上,在之前我們可以自己先創建一個內存DC,然后把畫圖都畫在內存DC中,最后再一次性的將內存DC輸出到窗口DC中,就可以解決畫面閃爍的問題了。

       下面我們講述寫代碼的方法

       1.定義變量

       首先在CChildView.h中定義兩個變量

 

CDC m_cacheDC;   //緩沖DC
CBitmap m_cacheCBitmap;//緩沖位圖

 

 

       2.創建緩沖DC

       然后呢,在CChildView.cpp中OnPaint中創建緩沖DC

 

//創建緩沖DC
m_cacheDC.CreateCompatibleDC(NULL);
m_cacheCBitmap.CreateCompatibleBitmap(cDC,m_client.Width(),m_client.Height());
m_cacheDC.SelectObject(&m_cacheCBitmap);

 

 

       3.在緩沖DC上繪圖

       后面貼圖都貼在緩沖DC上就可以了,如

 

m_bg.Draw(m_cacheDC,m_client);

 

 

      4.緩沖DC輸出到窗口DC

       最后一次性的將緩沖DC中的內容輸出到窗口DC中去,函數都是之前筆記二介紹過的,不熟悉的同學請閱讀筆記二。 

 

cDC->BitBlt(0,0,m_client.Width(),m_client.Height(),&m_cacheDC,0,0,SRCCOPY);

 

 

       此時OnPaint函數中的內容就如同下面這樣

 

void CChildView::OnPaint() 
{
	//獲取窗口DC指針
	CDC *cDC=this->GetDC();
	//獲取窗口大小
	GetClientRect(&m_client);
	//創建緩沖DC
	m_cacheDC.CreateCompatibleDC(NULL);
	m_cacheCBitmap.CreateCompatibleBitmap(cDC,m_client.Width(),m_client.Height());
	m_cacheDC.SelectObject(&m_cacheCBitmap);
	
	//————————————————————開始繪制——————————————————————
	//貼背景,現在貼圖就是貼在緩沖DC:m_cache中了
	m_bg.Draw(m_cacheDC,m_client);
	//貼英雄
	MyHero.hero.Draw(m_cacheDC,MyHero.x,MyHero.y,80,80,MyHero.frame*80,MyHero.direct*80,80,80);
	//最后將緩沖DC內容輸出到窗口DC中
	cDC->BitBlt(0,0,m_client.Width(),m_client.Height(),&m_cacheDC,0,0,SRCCOPY);

	//————————————————————繪制結束—————————————————————
	
	//在繪制完圖后,使窗口區有效
	ValidateRect(&m_client);
	//釋放緩沖DC
	m_cacheDC.DeleteDC();
	//釋放對象
	m_cacheCBitmap.DeleteObject();
	//釋放窗口DC
	ReleaseDC(cDC);
}

 

 

 

三、實現一個真正意義上的動畫demo

 

        霧央在這里實現的是一個騎着白馬的少年在場景中閑逛的demo,按下WASD人物會向四個方向移動,移動的過程中動態更換圖片。圖片使用的是一張大圖,然后每次去取其中一小塊顯示出來,當然大家也可以使用一張張分開好的圖。具體的請看代碼。

       先來幾張截圖看看效果,呵呵。





頭文件

 

// ChildView.h : CChildView 類的接口
//


#pragma once


// CChildView 窗口

class CChildView : public CWnd
{
// 構造
public:
	CChildView();

// 特性
public:
	struct shero
	{
		CImage hero;     //保存英雄的圖像
		int x;             //保存英雄的位置
		int y;
		int direct;        //英雄的方向
		int frame;         //運動到第幾張圖片
	}MyHero;

	CRect m_client;    //保存客戶區大小
	CImage m_bg;      //背景圖片

	CDC m_cacheDC;   //緩沖DC
	CBitmap m_cacheCBitmap;//緩沖位圖
// 操作
public:

// 重寫
	protected:
	virtual BOOL PreCreateWindow(CREATESTRUCT& cs);

// 實現
public:
	virtual ~CChildView();

	// 生成的消息映射函數
protected:
	afx_msg void OnPaint();
	DECLARE_MESSAGE_MAP()
public:
	afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
	afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
	afx_msg void OnTimer(UINT_PTR nIDEvent);
	afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
};


CPP文件

 

 

// ChildView.cpp : CChildView 類的實現
//

#include "stdafx.h"
#include "GameMFC.h"
#include "ChildView.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

//定時器的名稱用宏比較清楚
#define TIMER_PAINT 1
#define TIMER_HEROMOVE 2
//四個方向
#define DOWN 0
#define LEFT 1
#define RIGHT 2
#define UP 3

// CChildView

CChildView::CChildView()
{
}

CChildView::~CChildView()
{
}


BEGIN_MESSAGE_MAP(CChildView, CWnd)
	ON_WM_PAINT()
	ON_WM_KEYDOWN()
	ON_WM_LBUTTONDOWN()
	ON_WM_TIMER()
	ON_WM_CREATE()
END_MESSAGE_MAP()


//將png貼圖透明
void TransparentPNG(CImage *png)
{
	for(int i = 0; i <png->GetWidth(); i++)
	{
		for(int j = 0; j <png->GetHeight(); j++)
		{
			unsigned char* pucColor = reinterpret_cast<unsigned char *>(png->GetPixelAddress(i , j));
			pucColor[0] = pucColor[0] * pucColor[3] / 255;
			pucColor[1] = pucColor[1] * pucColor[3] / 255;
			pucColor[2] = pucColor[2] * pucColor[3] / 255;
		}
	}
}

// CChildView 消息處理程序

BOOL CChildView::PreCreateWindow(CREATESTRUCT& cs) 
{
	if (!CWnd::PreCreateWindow(cs))
		return FALSE;

	cs.dwExStyle |= WS_EX_CLIENTEDGE;
	cs.style &= ~WS_BORDER;
	cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS, 
		::LoadCursor(NULL, IDC_ARROW), reinterpret_cast<HBRUSH>(COLOR_WINDOW+1), NULL);
	
	//-----------------------------------游戲數據初始化部分-------------------------
	
	//加載背景
	m_bg.Load("bg.png");
	//加載英雄圖片
	MyHero.hero.Load("heroMove.png");
	TransparentPNG(&MyHero.hero);
	//初始化英雄狀態
	MyHero.direct=UP;
	MyHero.frame=0;
	//設置英雄初始位置
	MyHero.x=100;    
	MyHero.y=400;
	
	return TRUE;
}

void CChildView::OnPaint() 
{
	//獲取窗口DC指針
	CDC *cDC=this->GetDC();
	//獲取窗口大小
	GetClientRect(&m_client);
	//創建緩沖DC
	m_cacheDC.CreateCompatibleDC(NULL);
	m_cacheCBitmap.CreateCompatibleBitmap(cDC,m_client.Width(),m_client.Height());
	m_cacheDC.SelectObject(&m_cacheCBitmap);
	
	//————————————————————開始繪制——————————————————————
	//貼背景,現在貼圖就是貼在緩沖DC:m_cache中了
	m_bg.Draw(m_cacheDC,m_client);
	//貼英雄
	MyHero.hero.Draw(m_cacheDC,MyHero.x,MyHero.y,80,80,MyHero.frame*80,MyHero.direct*80,80,80);
	//最后將緩沖DC內容輸出到窗口DC中
	cDC->BitBlt(0,0,m_client.Width(),m_client.Height(),&m_cacheDC,0,0,SRCCOPY);

	//————————————————————繪制結束—————————————————————
	
	//在繪制完圖后,使窗口區有效
	ValidateRect(&m_client);
	//釋放緩沖DC
	m_cacheDC.DeleteDC();
	//釋放對象
	m_cacheCBitmap.DeleteObject();
	//釋放窗口DC
	ReleaseDC(cDC);
}

//按鍵響應函數
void CChildView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
	//nChar表示按下的鍵值
	switch(nChar)
	{
	case 'd':         //游戲中按下的鍵當然應該不區分大小寫了
	case 'D':
		MyHero.direct=RIGHT;
		MyHero.x+=5;
		break;
	case 'a':
	case 'A':
		MyHero.direct=LEFT;
		MyHero.x-=5;
		break;
	case 'w':
	case 'W':
		MyHero.direct=UP;
		MyHero.y-=5;
		break;
	case 's':
	case 'S':
		MyHero.direct=DOWN;
		MyHero.y+=5;
		break;
	}
}

//鼠標左鍵單擊響應函數
void CChildView::OnLButtonDown(UINT nFlags, CPoint point)
{
	char bufPos[50];
	sprintf(bufPos,"你單擊了點X:%d,Y:%d",point.x,point.y);
	AfxMessageBox(bufPos);
}

//定時器響應函數
void CChildView::OnTimer(UINT_PTR nIDEvent)
{
	
	switch(nIDEvent)
	{
	case TIMER_PAINT:OnPaint();break;  //若是重繪定時器,就執行OnPaint函數
	case TIMER_HEROMOVE:               //控制人物移動的定時器
		{
			MyHero.frame++;              //每次到了間隔時間就將圖片換為下一幀
			if(MyHero.frame==4)          //到最后了再重頭開始
				MyHero.frame=0;
		}
		break;
	}
}


int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	if (CWnd::OnCreate(lpCreateStruct) == -1)
		return -1;

	// TODO:  在此添加您專用的創建代碼

	//創建一個10毫秒產生一次消息的定時器
	SetTimer(TIMER_PAINT,10,NULL);
	//創建人物行走動畫定時器
	SetTimer(TIMER_HEROMOVE,100,NULL);
	return 0;
}

 

       對於代碼中一些地方,可能有一些同學有疑惑,霧央在這里作一下解釋。

       首先是人物的移動動畫,霧央使用了下面這張圖作為素材

          

       這張圖片是320x320,也就是說每個人物大小是80x80,第一行是人物向下移動,第二行是向左,第三行是向右,第四行是向下。

       我們在人物結構體中使用了一個變量direct記錄人物的方向,並且宏定義了四個方向分別為0,1,2,3

       比如當前人物移動方向是右,即RIGHT,也就是2,那么我們就應該截取這張圖片的第三行畫出來,第三行y的起始坐標就是80*2,也就是80*direct

       我們還使用了一個變量frame記錄當前方向上的幀數,即在x方向上的起始坐標,比如當前應該顯示第二張圖片,frame為1,那么就是80*1,即80*frame

 

       在Draw函數中最后四個參數的含義分別是源圖片的x起始坐標,y起始坐標,寬度,高度,即畫出源圖片的從x,y開始的寬為width,高為height的部分。

 

 

現在大家應該清楚了這個過程吧?

         

       下面霧央再舉一個例子來幫大家充分的理解這個過程。

       當前玩家按下了D鍵,人物要向右邊行走,那么此時direct=RIGHT,RIGHT被我們宏定義為2

       開始frame=0;那么我們就畫出原來圖片中80*frame,80*direct開始高為80,寬為80的圖片,即下圖

                   

         在行走過程中frame++;接下來畫的就是下面這個

          

         依次類推,當frame=4的時候,即畫到最后一個的時候再從頭開始,frame=0

 

        然后有同學提到在一直按着鍵的時候剛開始會感覺會停一下,霧央在這里說一下自己的想法。

        關於停一下,這個是由於第一次按下鍵后,人物移動很小,后來一直按着鍵,可以認為是以非常高的頻率不斷按鍵,人物移動的非常快,這之間一個對比就感覺停了一下,如果你把人物每次按鍵移動的距離增大很多,比如到50,就會覺得這種感覺小了很多。事實上,這樣按鍵后人物突然增加一段位移是非常不科學的,用在游戲中也是非常不合適的,在后面的講解中我會講解一種新的人物移動方式,會很流暢,敬請關注。
       

         如果大家還有疑問,歡迎留言

 

      由於這一節筆記我覺得對於新手來說可能內容較多,所以我把源代碼上傳了,大家可以下載回去自己試試,本着分享的精神,當然是0積分下載了。

《MFC游戲開發》筆記六 源代碼下載

 

       《MFC游戲開發》筆記六到這里就結束了,更多精彩請關注下一篇。如果您覺得文章對您有幫助的話,請留下您的評論,點個贊,能看到你們的留言是我最高興的事情,因為這讓我知道我正在幫助曾和我一樣迷茫的少年,你們的支持就是我繼續寫下去的動力,願我們一起學習,共同努力,復興國產游戲。

        對於文章的疏漏或錯誤,歡迎大家的指出。

 

 

 

 


免責聲明!

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



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