본문 바로가기
컴퓨터/MFC_API

더블 버퍼링(double buffering)

by adnoctum 2011. 8. 20.


   더블 버퍼링은 화면에 표시해야 할 내용을 미리 메모리에 모두 그려놓은 후 화면으로 한꺼번에 전송하는 방법을 말한다. 어느 프로그램이든지 더블 버퍼링이 사용되지 않는 것이 거의 없을 정도로 자주 사용되는 방법인데, 기본적인 방법과 사용 예제를 살펴 보자. Visual Studio 2010 (C++임) 으로 작성한 예제를 첨부한다.



exe 파일을 실행시킨 후 파일 --> 열기를 하면 1.jpg 파일이 불린다. 그 후 더블 클릭을 하면 그림이 두 배 확대가 된다.

   더블 버퍼링은 자바나 Delphi, Visual Basic 등 GUI 프로그램에는 전부 적용할 수 있다. 이 글에서는 MFC에 중점을 두고 설명한다. 다른 언어에 관해서는 글의 끝에서 살펴 본다. MFC 특유의 기술적인 부분에 의한 것은 별도로 언급한다. Visual Studio 6.0 에는 CImage class가 없다. 여기서 사용할 수 있는 것은 CBitmap 인데, Visual Studio 2010 에 있는 CImage 는 CBitmap 과 CDC, 그리고 image file format parser 를 모두 모아 놓은 클래스로 보인다. 이 글은 Visual Studio 2010 을 기준으로 설명을 할텐데, 이 버전을 사용할 수 없는 경우에는 CBitmap 과 CDC 를 이용해서 CImage 를 그대로 흉내낼 수 있으므로 별 문제는 아니다.


   개념적으로 보자면 더블 버퍼링은 다음과 같다.



즉, 어떠한 내용을 그릴 때, 그리는 매번 화면으로 출력을 보내는 것이 아니라, 화면으로 내보일 내용을 미리 메모리에 모두 그려 놓은 후 그것을 한꺼번에 화면으로 딱 한 번 그려내자는 것이다. 왜냐 하면, 매번 화면으로 출력을 하는 작업은 일단 오래 걸리고, 그려지는 과정을 최종 사용자(end-user)가 굳이 볼 필요는 없기 때문이다. 간단한 예로 그림 파일을 출력하는 프로그램을 생각해 보자. 이 때, 그림 파일이 프로그램의 창보다 더 작다면 스크롤을 할 수 있게끔 구현을 해야 할텐데, 매번 스크롤 될 때마다 화면에 갱신되는 출력은 개념적으로 다음과 같은 논리를 따르게 된다.



즉, 현재 스크롤 된 위치에 기반해서 전체 이미지 중 현재 화면에 출력되는 창이 어느 부분인가가 결정이 되고, 그 부분이 원본 이미지에서 복사되어 화면에 출력이 되는 것이다. 따라서 매번 스크롤이 될 때마다 현재 출력될 부분을 메모리 이미지에 복사를 한 후, 필요한 처리를 좀 더 하고, 그것을 화면에 보이는 프로그램의 창에 대한 DC로 BitBlt 시킨다. 이 부분에 대한 코드를 보면 다음과 같다.  일단, 화면에 출력될 이미지인 [_scrImage]를 매번 만드는 부분은 다음과 같다.


  151 bool CImageViewerView::make_screen_image()

  152 {

  153     if(_transformedImage.IsNull() == true) return false;

  154     // 화면에 표시될 그림에 사용될 이미지인 [_transformedImage] 에서 현재의 client 크기

  155     // 만큼만 떼어내서 [_scrImage]를 만든다. 이 때, scroll 크기로 인해

  156     // 현재 client size가 더 작다면 scroll position 을 갖고 와서

  157     // 원본 이미지의 그 부분에서 떼어 낸다.

  158 

  159     CPoint current_scrolled_position = GetScrollPosition();

  160     CRect client_rect;

  161     GetClientRect(&client_rect);

  162     int client_width = client_rect.Width();

  163     int client_height = client_rect.Height();

  164     int image_width = _transformedImage.GetWidth();

  165     int image_height = _transformedImage.GetHeight();

  166     // 그림이 화면보다 더 작으면 그 그림만 다시 중간에 뿌려 준다.

  167     CDC *pDC = CDC::FromHandle(_scrImage.GetDC());

  168     pDC->FillSolidRect(&client_rect, 0xffffff);

  169     int xDest = 0;

  170     int nDestWidth = client_width;

  171     int xSrc = current_scrolled_position.x;

  172     if(client_width > image_width){

  173         xDest = static_cast<int>(0.5 + 0.5*(client_width - image_width));

  174         nDestWidth = image_width;

  175         xSrc = 0;

  176     }

  177     int yDest = 0;

  178     int nDestHeight = client_height;

  179     int ySrc = current_scrolled_position.y;

  180     if(client_height > image_height){

  181         yDest = static_cast<int>(0.5 + 0.5*(client_height - image_height));

  182         nDestHeight = image_height;

  183         ySrc = 0;

  184     }

  185 

  186     _transformedImage.BitBlt(pDC->GetSafeHdc(), xDest, yDest, nDestWidth, nDestHeight, xSrc, ySrc, SRCCOPY);    

  187 

  188     _scrImage.ReleaseDC();

  189 

  190 

  191     return true;

  192 }



위처럼 화면에 출력된 이미지를 만들고, 실제로 화면에 출력하는 부분은 별도의 함수로 만들어 둔다. 그 부분은 다음과 같다.

  218 bool CImageViewerView::draw_screen_image()

  219 {

  220     if(_scrImage.IsNull() == true) return false;

  221     CClientDC dc(this);

  222     CScrollView::OnPrepareDC(&dc);

  223     _scrImage.Draw(dc.GetSafeHdc(), 0, 0);

  224 

  225     return true;

  226 }

  227 



또한, WM_PAINT message handler 인 OnDraw는 다음과 같이 되어 있다.

   66 void CImageViewerView::OnDraw(CDC* pDC)

   67 {

   68     CImageViewerDoc* pDoc = GetDocument();

   69     ASSERT_VALID(pDoc);

   70     if (!pDoc)

   71         return;

   72 

   73     CPoint cur_pos = GetScrollPosition();

   74     _scrImage.Draw(pDC->GetSafeHdc(), cur_pos.x, cur_pos.y);

   75 }

   76



프로그램 창에 표시되는 최종 이미지인 [_scrImage]는 프로그램 창 크기와 정확히 일치한다. 따라서 이 이미지는 항상 0,0 위치에 그려지게 된다. 반면 OnDraw에서 사용되는 위치는 CScrollView 의 특성에 의해 scroll 된 위치까지 포함한 위치를 사용해야 하기 때문에 OnDraw에서 그리는 방법이 약간 다르다.

그림을 그리는 draw_screen_image 함수를 별도로 만들어 놓는 이유는, 만약 [_scrImage]에 별도의 내용을 더 표시하고자 할 때 이 함수에서 그림을 그리기 위해서인데, (현재의 설계는 함수 이름이 좀 애매하긴 한데) 일반적으로 여러 루틴에서 그림에 이것저것 표시하기보다는 한 함수에 몰아서 그림을 전부 그리고, 그것을 그리게 된다. 이렇게 하면 OnDraw에서는 그냥 [_scrImage]처럼 화면에 표시할 내용만 그리면 되기 때문에 편리하다, 그 이미지에 무엇을 표시하는지는 전혀 신경 쓸 필요 없이.

    이제 view에서 scroll bar message 를 처리해야 하는데, CScrollView를 사용할 경우, 다음과 같이 해준다.

  203 void CImageViewerView::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)

  204 {

  205     make_screen_image();

  206     Invalidate(false);

  207     CScrollView::OnHScroll(nSBCode, nPos, pScrollBar);

  208 

  209 }

  210 

  211 void CImageViewerView::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)

  212 {

  213     make_screen_image();

  214     Invalidate(false);

  215     CScrollView::OnVScroll(nSBCode, nPos, pScrollBar);

  216 

  217 }


scroll이 되면 WM_PAINT가 호출이 되고, 그러면 자동으로 OnDraw에 의해 그림이 그려지므로 여기에 그림을 그리는 함수를 넣을 필요는 없다.

   이제, MFC 의존적인 부분들을 살펴 보자. CScrollView의 경우, scroll bar message는 자동으로 해주기는 하는데, mouse wheel 메세지가 발생했을 경우에는 scroll bar의 thumb의 위치를 변경하기는 하지만 scroll bar message를 발생시키지는 않고, WM_PAINT와 WM_ERASEBKGND만 발생시킨다. 하지만 지금처럼 화면이 update 된 경우에는 screen에 표시할 이미지를 scroll bar의 변경된 위치에 따라 다시 만들어야 한다. 따라서 WM_MOUSEWHEEL message handler에서 이 이미지를 다시 만들어 준다.

   또한, 프로그램의 창의 크기가 변경될 경우, 화면에 표시될 이미지인 [_scrImage] 역시 크기가 변해야 하므로 WM_WINDOWPOSCHANGED message handler에서 다음과 같이 해준다.

 

 284 void CImageViewerView::OnWindowPosChanged(WINDOWPOS* lpwndpos)

  285 {

  286     CScrollView::OnWindowPosChanged(lpwndpos);

  287     if(_scrImage.IsNull() == false){

  288         _scrImage.Destroy();

  289         _scrImage.Create(lpwndpos->cx, lpwndpos->cy, 24);

  290         make_screen_image();

  291         if(_transformedImage.IsNull() == false){

  292             SetScrollSizes(MM_TEXT, CSize::CSize(_transformedImage.GetWidth(), _transformedImage.GetHeight()));

  293         }

  294     }

  295 }


여기서 약간 애매한 부분이 있는데, 만약 WM_WINDOWPOSCHANGED 대신 WM_SIZE 메세지로 위와 같은 일을 처리할 경우, 창의 크기를 변경함에 따라 스크롤 바가 나타났다 사라지거나 그 반대의 경우가 있는데, 그 때 창의 크기가 제대로 계산이 안 되는 경우가 발생한다. 즉, 스크롤바가 사라졌지만 뷰의 client size는 여전히 스크롤바가 있을 때의 크기로 계산이 되기 때문에 스크롤바가 있던 부분이 그려지지 않고 남게 된다.



  또한, View가 스크롤이 되면 WM_PAINT 와 WM_ERASEBKGND가 같이 호출이 된다. 만약 WM_ERASEBKGND handler 를  default 로 되어 있는 것을 사용하게 되면 배경이 일단 한 번 칠해진 후 다시 [_scrImage] 가 그려지는데 이렇게 되면 깜빡거리게 된다. 따라서 WM_ERASEBKGND message handler에서 아무 일도 하지 않게 다음과 같이 바꿔 준다.

  269 BOOL CImageViewerView::OnEraseBkgnd(CDC* pDC)

  270 {

  271     return true; //CScrollView::OnEraseBkgnd(pDC); // Do nothing.

  272 }

  273 





   델파이나 Visual Basic 은 모두 HDC 를 얻어서 사용할 수 있다. Visual Basic 은 각 API에 대한 함수 형태를 미리 포함해야 하며, 델파이는 그냥 된다. 또한 TImage 등 각 언어에서 제공하는 Image 용 클래스를 이용해서 위와 같은 작업을 한다. 자바의 경우, 아예 BufferedImage 로 제공이 되는 것에서 알 수 있듯이 더블 버퍼링을 사용할 수 있게끔 설계되어 있는듯 하다.   




CBitmap 을 이용한 예는 추후 추가한다. 사실 double buffering 에 CImage를 사용해 본 것은 위 예제가 처음이다. VS2010을 사용해도 여전히 난 CBitmap 을 이용해 double buffering 을 한다, 습관적으로.

또한 자바로 된 예제도 추후에 추가한다. 이 예제는 랩 동생이 이 논문[각주:1]을 쓸 때 double buffering 부분만 내가 살짝 조언해 준 부분이 있는데, 그 부분 소스 코드를 랩 동생에게 이야기 해서 받아서 올릴 계획이다.





ㅎ, 이 글을 이제서야 쓴다. 실제론 오래 전부터 아주 자주 사용하는 방법이긴 한데.









  1. Mitochondrial Network Determines Intracellular ROS Dynamics and Sensitivity to Oxidative Stress through Switching Inter-Mitochondrial Messengers. [본문으로]