.더블 버퍼링

ApiDraw06까지 만들어진 결과를 테스트해 보면 깜박임이 무척 심하다는 것을 있다. 도형을 많이 그려 놓고 선택을 변경하면 화면 전체가 깜박거리는데 이전 트래커를 지우기 위해 배경색으로 완전히 지운 모든 도형을 다시 그리기 때문이다. 키보드로 도형을 이동시키면 깜박임이 특히 심해지는데 한번 입력할 때마다 작업 영역 전체가 무효화되므로 어쩔 수가 없다. 깜박임의 원인은 화면에 조금이라도 변경이 가해질 때마다 배경 브러시로 작업 영역을 지우고 다시 그리기 때문인데 문제를 해결하려면 우선 캔버스의 배경 브러시를 없애야 한다. ApiDraw07 프로젝트를 만든 다음과 같이 수정해 보자.

 

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

     ,LPSTR lpszCmdParam,int nCmdShow)

{

   ....

   WndClass.hbrBackground=NULL;

   WndClass.lpfnWndProc=CanvasProc;

   WndClass.lpszClassName="Canvas";

   WndClass.lpszMenuName=NULL;

   WndClass.style=CS_DBLCLKS;

   RegisterClass(&WndClass);

 

또는 WM_ERASEBKGND 메시지를 막아 버려도 동일한 효과가 있다. 상태에서 프로그램을 실행해 보면 작업 영역을 지우지 않기 때문에 깜박임은 없어지지만 이전 도형과 트래커가 지워지지 않기 때문에 화면은 그야말로 엉망이 된다. 결국 화면에 변화가 생길 이전 그림을 지운 다시 그려야 한다는 얘기가 되는데 다시 그리면서도 깜박임을 없애는 유일하고도 완전한 방법은 더블 버퍼링을 하는 것이다. 화면이 아닌 메모리에서 완성된 그림을 비트맵에 그린 결과만 화면으로 복사해야 한다. 더블 버퍼링에 사용할 비트맵 핸들을 전역으로 선언한다.

 

HBITMAP hBackBit;

 

비트맵의 표면에 그림을 그릴 것이다. OnCreate에서 비트맵을 NULL 초기화하고 더블 버퍼링을 하기 직전에 필요한 크기만큼 생성할 것이다.

 

LRESULT OnCreate(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

   ....

   hBackBit=NULL;

 

더블 버퍼링의 주체는 OnPaint이므로 함수를 대대적으로 수정해야 한다. 출력을 보내기 전에 작업 영역 크기만큼 비트맵을 생성하고 비트맵에 모든 그림을 그린 최종 결과만 화면 DC 전송한다.

 

LRESULT OnPaint(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

   HDC hdc;

   PAINTSTRUCT ps;

   int idx;

   HPEN hPen,hOldPen;

   HBRUSH hBrush,hOldBrush;

   RECT crt;

   HDC hMemDC;

   HBITMAP hOldBitmap;

 

   hdc=BeginPaint(hWnd, &ps);

   hMemDC=CreateCompatibleDC(hdc);

   GetClientRect(hWnd,&crt);

   if (hBackBit == NULL) {

      hBackBit=CreateCompatibleBitmap(hdc,crt.right,crt.bottom);

   }

   hOldBitmap=(HBITMAP)SelectObject(hMemDC,hBackBit);

   FillRect(hMemDC,&crt,GetSysColorBrush(COLOR_WINDOW));

 

   for (idx=0;idx<arNum;idx++) {

      ....

   }

 

   BitBlt(hdc,0,0,crt.right,crt.bottom,hMemDC,0,0,SRCCOPY);

   SelectObject(hMemDC,hOldBitmap);

   DeleteDC(hMemDC);

   EndPaint(hWnd, &ps);

   return 0;

}

 

화면 DC 호환되는 메모리 DC 만들고 hBackBit NULL 경우 작업 영역 크기만한 비트맵을 생성한다. 한번 생성된 비트맵은 다시 만들 필요없이 계속 사용된다. 메모리 DC hBackBit 선택해 놓고 메모리 DC 출력을 보내면 출력은 모두 비트맵 표면에 그려질 것이다. 먼저 FillRect 함수로 비트맵 전체를 흰색으로 가득 채워 이전에 그려져 있던 그림을 모두 지워 깨끗한 상태로 만든다.

그리고 for 루프를 돌며 배열에 기록된 모든 도형을 출력하되 대상 DC 화면 hdc 아니라 메모리의 hMemDC여야 한다. for 루프 내부의 모든 GDI 함수의 번째 인수를 hMemDC 바꾼다. 이렇게 그려진 그림은 최종적으로 BitBlt 함수에 의해 화면으로 고속 전송된다. 전송 완료 메모리 DC 삭제하되 hBackBit 다음 그리기를 다시 사용해야 하므로 삭제하지 않고 그대로 남겨 둔다. hBackBit 바로 더블 버퍼링의 결과물이다.

그리기 작업이 모두 메모리 내부에서 이루어지기 때문에 사용자는 도형이 지워졌다가 다시 그려지는 과정을 전혀 없으며 완성된 모습만 있다. OnPaint에서는 작업 영역 크기만한 비트맵을 한번 만들어 놓고 계속 재사용하고 있는데 크기가 바뀌면 비트맵의 크기도 같이 바뀌어야 한다. 작업 영역 크기가 변경되는 OnSize에서 비트맵의 크기를 관리하는데 단순히 비트맵을 삭제하기만 하면 된다.

 

LRESULT OnSize(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

   if (wParam != SIZE_MINIMIZED) {

      if (hBackBit) {

          DeleteObject(hBackBit);

          hBackBit=NULL;

      }

   }

   return 0;

}

 

비트맵을 삭제하고 hBackBit NULL 만들어 놓기만 하면 다음번 OnPaint에서 변경된 작업 영역의 크기에 맞게 비트맵을 다시 생성할 것이다. 비트맵을 생성하는 코드는 OnPaint 있고 OnSize 비트맵을 무효화시켜 다시 만들어야 한다는 것을 OnPaint에게 알리기만 한다. , 최소화될 때는 어차피 화면에 보이지 않으므로 비트맵을 파괴할 필요가 없다. 어차피 다시 복구되면 이전 크기와 같아지므로 번씩이나 비트맵을 새로 만드는 불필요한 동작을 필요가 없는 것이다.

결국 hBackBit 비트맵은 프로그램이 실행되는동안 캔버스의 작업 영역 크기와 같은 크기로 항상 존재한다. 비트맵은 캔버스가 파괴되는 OnDestroy에서 해제된다.

 

LRESULT OnDestroy(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

   int idx;

 

   for (idx=0;idx<arNum;idx++) {

      free(arObj[idx]);

   }

   free(arObj);

   if (hBackBit) {

      DeleteObject(hBackBit);

      hBackBit=NULL;

   }

   return 0;

}

 

더블 버퍼링이 완료되었다. 이제 테스트해 보면 깜박임을 전혀 느낄 없을 것이다. 모든 InvalidateRect 호출문의 마지막 인수 bErase TRUE에서 FALSE 바꾸는 것이 논리상 맞다. 그러나 어차피 배경 브러시가 없는 상황에서 인수는 TRUE 주나 FALSE 주나 전혀 차이가 없으므로 그냥 둬도 상관없으며 아무런 차이점도 없다.