一:雙緩沖原理
為了解決窗口刷新頻率過快所帶來的閃爍問題,利用雙緩沖技術進行繪圖。所謂雙緩沖技術,就是將資源加載到內存,然后復制內存數據到設備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