MFC双缓冲绘图(2015.09.24)
问题引入:
最近在尝试编写贪吃蛇游戏时遇到这么一个问题:当系统以较快频率向窗口发送WM_PAINT消息时,调用OnPaint()函数在窗口中绘制图形就会发生闪烁现象。
问题分析:
当我们把绘图过程放在OnPaint()函数中时(放在OnDraw()函数中也是如此,因为OnDraw()会被OnPaint()调用),由于频繁收到系统的WM_PAINT消息,窗口需要执行重绘。而重绘过程首先是执行了窗口内容的擦除(用当前背景色的画刷对窗口重新绘制),然后再根据绘图语句在窗口客户区中对窗口内容进行重绘。由于频率较快,当前窗口中会产生背景色和窗口内容的反复交替,二者的色差造成了闪烁的效果。
问题解决:
双缓冲绘图:双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上的过程。
先发上原来OnPaint()函数中的绘图代码:
1 void CMainWindow::OnPaint() 2 { 3 CClientDC dc(this); 4 5 CPen White_Pen(PS_SOLID, 1, RGB(255, 255, 255)); 6 CPen *pOldPen = dc.SelectObject(&White_Pen); 7 CBrush White_Brush(RGB(255, 255, 255)); 8 CBrush *pOldBrush = dc.SelectObject(&White_Brush); 9 10 //Draw background 11 dc.Rectangle(&m_rcBack); 12 13 //Draw scoreboard 14 dc.SelectStockObject(GRAY_BRUSH); 15 dc.Rectangle(&m_rcScoreBoard); 16 17 //Draw wall 18 dc.SelectStockObject(BLACK_BRUSH); 19 20 int i; 21 for (i = 0; i < MAP_SIZE_CX; ++i) 22 { 23 dc.Rectangle(m_rcGameMap[i][0]); 24 dc.Rectangle(m_rcGameMap[i][MAP_SIZE_CY - 1]); 25 } 26 for (i = 1; i < MAP_SIZE_CY - 1; ++i) 27 { 28 dc.Rectangle(m_rcGameMap[0][i]); 29 dc.Rectangle(m_rcGameMap[MAP_SIZE_CX - 1][i]); 30 } 31 32 dc.SelectObject(pOldPen); 33 dc.SelectObject(pOldBrush); 34 }
由于出现闪烁的问题将上述代码转写到OnEraseBkgnd()函数中(并且添加了WM_ERASEBKGND的消息映射,顾名思义,就是系统通知窗口擦除客户区的消息),同时在OnPaint()中对OnEraseBkgnd()显式调用(这里要用CPaintDC类,之前用CClientDC类导致WM_PAINT消息在消息队列里造成死循环,原因是因为CClientDC类的构造函数和析构函数里没有像CPaintDC类一样调用::BeginPaint()和::EndPaint()),代码如下:
1 void CMainWindow::OnPaint() 2 { 3 CPaintDC dc(this); 4 OnEraseBkgnd(&dc); 5 }
1 BOOL CMainWindow::OnEraseBkgnd(CDC *pDC) 2 { 3 CRect rect; 4 CDC dcMem; 5 GetClientRect(&rect);; 6 CBitmap bmp; 7 8 dcMem.CreateCompatibleDC(pDC); 9 bmp.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height()); 10 CBitmap *pOldBit = dcMem.SelectObject(&bmp); 11 dcMem.FillSolidRect(rect, RGB(255, 255, 255)); 12 13 // 14 //Draw 15 // 16 CPen White_Pen(PS_SOLID, 1, RGB(255, 255, 255)); 17 CPen *pOldPen = dcMem.SelectObject(&White_Pen); 18 CBrush White_Brush(RGB(255, 255, 255)); 19 CBrush *pOldBrush = dcMem.SelectObject(&White_Brush); 20 21 //Draw background 22 dcMem.Rectangle(&m_rcBack); 23 24 //Draw scoreboard 25 dcMem.SelectStockObject(GRAY_BRUSH); 26 dcMem.Rectangle(&m_rcScoreBoard); 27 28 //Draw wall 29 dcMem.SelectStockObject(BLACK_BRUSH); 30 31 int i; 32 for (i = 0; i < MAP_SIZE_CX; ++i) 33 { 34 dcMem.Rectangle(m_rcGameMap[i][0]); 35 dcMem.Rectangle(m_rcGameMap[i][MAP_SIZE_CY - 1]); 36 } 37 for (i = 1; i < MAP_SIZE_CY - 1; ++i) 38 { 39 dcMem.Rectangle(m_rcGameMap[0][i]); 40 dcMem.Rectangle(m_rcGameMap[MAP_SIZE_CX - 1][i]); 41 } 42 43 pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &dcMem, 0, 0, SRCCOPY); 44 45 dcMem.DeleteDC(); 46 bmp.DeleteObject(); 47 return TRUE; 48 }
对比上面两段代码发现,二者的差别仅仅在于前者(OnPaint()函数)是直接对客户区对象(dc)直接进行绘制,而后者(OnEraseBkgnd()函数)是先在内存中开辟了一块基于当前客户区大小的缓冲区(dcMem),用dcMem这一对象取代dc进行相同的绘制操作(二者有相似的成员函数),在内存中绘制完成后再通过如下语句将内存中绘制好的图形直接拷贝到窗口客户区,因为是直接一步完成所以避免了原来的擦除和重绘过程,也就解决了闪烁的问题。
1 pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &dcMem, 0, 0, SRCCOPY);
总结:
当窗口客户区需要绘制复杂图形时,如果内存条件允许的话最好采取双缓冲的绘制方法,一方面能解决窗口绘制时闪烁的问题,另一方面,因为减少了窗口反复的擦除过程,也在一定程度上减少了响应时间,是一种用空间换取时间的做法。
参考: