一:雙緩沖原理
為了解決窗口刷新頻率過快所帶來的閃爍問題,利用雙緩沖技術進行繪圖。所謂雙緩沖技術,就是將資源加載到內存,然后復制內存數據到設備DC(這個比較快),避免了直接在設備DC上繪圖(這個比較慢)。打個簡單的比方:有個畫家在街邊辦了一個即時畫展,在同一塊畫布上根據觀眾的要求畫不同的圖像,每當有一位觀眾制定要看什么畫時,畫家先把之前畫布上的東西全部擦干凈,再重新繪畫。顯然有一些經典的畫像是大家都想看的,按照以前的老辦法,畫家每次都要重新畫這幅圖像,但這種擦了畫,畫了擦的方式很費時。所以畫家想了一個辦法,把這些經典畫像預先用一塊或幾塊畫布畫下來,等有人需要看時,把這些預備好的畫布貼在現有畫布的前面,這樣就能滿足觀眾的實時性要求。那么這些事先預備好的畫布就相當於內存DC,把資源放在內存DC里,等到要刷新顯示時,將內存DC上的東西“貼”到當前窗口DC上,就可以減少延時帶來的閃爍問題,這就是雙緩沖的原理。
詳細介紹見后面的幾片博文。下面舉兩個例子:
二: 例子
例子一:加載位圖
代碼:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: Add any drawing code here...
myDraw(hdc);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
myDraw函數的實現:
const int g_picHeight = 1024;
const int g_picWidth = 675;
void myDraw(HDC &dc)
{
RECT rect;
HBITMAP hOrgBitmap;
HBITMAP hOldBitmap;
int disHeight, disWidth;
GetClientRect(g_hWnd, &rect);//獲取客戶區大小
disHeight = rect.bottom-rect.top;
disWidth = rect.right-rect.left;
//加載圖片
hOrgBitmap = (HBITMAP)::LoadImage(hInst, _T("test2.bmp"), IMAGE_BITMAP, g_picWidth, g_picHeight, LR_LOADFROMFILE);
HDC mDc = ::CreateCompatibleDC(dc);//創建當前上下文的兼容dc(內存DC)
hOldBitmap = (HBITMAP)::SelectObject(mDc, hOrgBitmap);//將位圖加載到內存DC
//拷貝內存DC數據塊到當前DC,自動拉伸
::StretchBlt(dc, 0, 0, disWidth, disHeight, mDc, 0, 0, g_picWidth, g_picHeight, SRCCOPY);
//恢復內存原始數據
::SelectObject(mDc, hOldBitmap);
//刪除資源,防止泄漏
::DeleteObject(hOrgBitmap);
::DeleteDC(mDc);
}
結果:

調整窗口大小,發現無閃爍現象。
例子二:畫各種直線和曲線。這是一個稍微復雜點的例子,是我平時做的一個demo。可以順便熟悉一下Windows繪圖的一些操作。
代碼:
void CRTVIEW_win32DlgDlg::OnPaint()
{
if (IsIconic())
{
/********此段代碼忽略*********/
}
else
{
CDialog::OnPaint(); //調用基類的默認控件繪制
CRect ctrlRect;
CStatic *pDegreePicCtrl = (CStatic *)GetDlgItem(IDC_STC_DEGREEPIC);
pDegreePicCtrl->GetClientRect(&ctrlRect);//獲取靜態控件尺寸
CDC *pdc = pDegreePicCtrl->GetWindowDC();//獲取控件DC
CDC dcMemory;
dcMemory.CreateCompatibleDC(pdc);//創建內存DC
CBitmap *pOldMapMemory;
CBitmap mapMemory;
mapMemory.CreateCompatibleBitmap(pdc, ctrlRect.Width(), ctrlRect.Height());//創建控件DC的兼容位圖。其實就是與控件DC大小相同的一塊區域
pOldMapMemory = dcMemory.SelectObject(&mapMemory);//加載兼容位圖,只有制定了“桌布”尺寸之后,你才能在內存DC上面繪圖
DrawDegreePicBkGrd(&dcMemory);//在內存DC上繪圖
pdc->BitBlt(0, 0, ctrlRect.Width(), ctrlRect.Height(), &dcMemory, 0, 0, SRCCOPY);//將內存DC上的內容復制到控件DC上
dcMemory.SelectObject(pOldMapMemory);//還原原來的內存DC
::DeleteObject(mapMemory);//刪除兼容位圖資源
::DeleteDC(dcMemory);//刪除內存DC
ReleaseDC(pdc);//釋放控件DC
}
}
void CRTVIEW_win32DlgDlg::DrawDegreePicBkGrd(CDC *pdc)
{
CRect stcRect, picRect;
CStatic *pDegreePicCtrl = (CStatic *)GetDlgItem(IDC_STC_DEGREEPIC);
pDegreePicCtrl->GetClientRect(&stcRect);
if(stcRect.Width() > stcRect.Height()) {
int diff = (stcRect.Width() - stcRect.Height()) / 2;
picRect.left = stcRect.left + diff;
picRect.right = stcRect.right - diff;
picRect.top = stcRect.top;
picRect.bottom = stcRect.bottom;
} else {
int diff = (stcRect.Height() - stcRect.Width()) / 2;
picRect.left = stcRect.left;
picRect.right = stcRect.right;
picRect.top = stcRect.top + diff;
picRect.bottom = stcRect.bottom - diff;
}
CBrush *pOldBrush;
/**************畫圓形***************/
CBrush newBrush1;
newBrush1.CreateSolidBrush(RGB(0, 255, 0));
pOldBrush = pdc->SelectObject(&newBrush1);
pdc->Ellipse(&picRect);
/**************畫原點***************/
CRect orgRect(stcRect.Width()/2-2, stcRect.Height()/2-2, stcRect.Width()/2+2, stcRect.Height()/2+2);
CBrush newBrush2;
newBrush2.CreateSolidBrush(RGB(255,0,0));
pOldBrush = pdc->SelectObject(&newBrush2);
pdc->Ellipse(&orgRect);
pdc->SelectObject(pOldBrush);
/*************畫刻度***************/
CPoint center(stcRect.Width()/2, stcRect.Height()/2);
double radias = (double)picRect.Width()/2;
CPen newPen(PS_SOLID, 1, RGB(255,0,0));
CPen *poldPen = pdc->SelectObject(&newPen);
CPoint startPoint, endPoint;
for(int i=0; i<360; i=i+5) {
double cosval = cos(DEGREETORADIAN(i));
double sinval = sin(DEGREETORADIAN(i));
startPoint.x = center.x + int(radias * cosval); //當前角度對應的圓上的點的x坐標
startPoint.y = center.y - int(radias * sinval); //當前角度對應的圓上的點的y坐標
if(i%10 == 0) {
endPoint.x = startPoint.x - int(10 * cosval);
endPoint.y = startPoint.y + int(10 * sinval);
} else {
endPoint.x = startPoint.x - int(5 * cosval);
endPoint.y = startPoint.y + int(5 * sinval);
}
pdc->MoveTo(startPoint);
pdc->LineTo(endPoint);
}
pdc->SelectObject(poldPen);
}
效果:

三:小結
這兩個例子里面,其實每次重繪都是重新申請內存DC,然后復制到窗口DC。雖然這樣子比較繁瑣,但是也不影響效果,如果在響應onpaint消息時,不擦除背景(如調用Invalidate(FALSE)),也不會產生閃爍。不過最好的辦法,就是文章開頭說的,只畫一次,把那個內存DC的句柄保存下來,每次在onpaint里面重繪時,直接調用BitBlt復制即可。不過要注意這些句柄對象的銷毀,以免內存泄漏。
下面這些文章也可以看看:
1 http://baike.baidu.com/view/1149326.htm
2 http://blog.csdn.net/xsc2001/article/details/5378601
3 http://www.cppblog.com/wrhwww/archive/2011/03/01/140913.html
4 http://www.cnblogs.com/afarmer/archive/2012/03/31/2427315.html
5 http://www.programlife.net/mfc-draw-pictures-with-memory-dc-buffer.html
6 http://blog.csdn.net/zxzerster/article/details/5659775
