.비트맵 추가

ApiDraw09 프로젝트에서는 타원이나 사각형보다 훨씬 복잡한 모양의 객체들을 추가해 보기로 한다. 실습에서 추가할 객체는 텍스트, 비트맵, 메타 파일인데 객체들이 기본 도형과 근본적으로 다른 차이점은 길이를 예측할 없다는 점이다. 텍스트는 얼마든지 길어질 있으므로 정적인 문자형 배열로는 관리할 없으며 실행중에 사용자가 입력한 길이만큼 메모리를 할당해야 한다. 비트맵이나 메타 파일도 마찬가지로 삽입되는 그림 파일의 크기를 예측하거나 제한할 없다.

프로그래밍의 세계에서 예측할 없다는 상황은 코드가 복잡해져야 한다는 얘기(=골때린다) 거의 같다. 조사된 길이만큼 동적할당을 해야 하며 결국 초보자들이 두려워하는 포인터가 등장해야 한다는 뜻이다. 종류의 객체를 다루기 위해 DObject 구조체에 추가되어야 멤버는 포인터 변수와 할당된 길이이다. 그리고 텍스트의 경우는 글꼴의 모양과 크기를 지정할 있는 별도의 멤버를 가져야 한다. DObject 구조체를 다음과 같이 확장한다.

 

struct DObject

{

   DTool Type;

   RECT rt;

   unsigned short Flag;

   short LineWidth;

   COLORREF LineColor;

   COLORREF PlaneColor;

   union {

      TCHAR *Text;

      BYTE *Bitmap;

      BYTE *Meta;

   };

   int Len;

   COLORREF FontColor;

   TCHAR FontFace[32];

   int FontSize;

};

 

텍스트, 비트맵, 메타 파일을 위한 멤버는 모두 동적할당된 번지를 저장할 포인터이되 객체가 가지 정보를 동시에 가져야 하는 것은 아니므로 이름없는 공용체로 선언했다. DObject 객체는 텍스트나 비트맵, 메타 파일 하나만 있으므로 포인터 멤버가 같은 기억 장소를 공유해도 아무 문제가 없다. Len 동적 할당된 메모리의 크기이며 나머지 멤버들은 텍스트의 글꼴 속성들이다. DObject 확장되면 전역 옵션도 같이 늘어나므로 OnCreate에서 적절히 초기화해야 한다.

 

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

{

   ....

   Opt.LineWidth=3;

   Opt.LineColor=RGB(0,0,0);

   Opt.PlaneColor=RGB(0,255,0);

   Opt.FontColor=RGB(0,0,0);

   Opt.FontSize=10;

   lstrcpy(Opt.FontFace,"굴림");

 

텍스트의 글꼴 속성들을 굴림 10포인트의 검정색으로 초기화했다. 메인 메뉴에서 새로 추가된 객체들의 툴을 선택할 있도록 한다.

 

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

{

   switch(LOWORD(wParam)) {

   ....

   case IDM_SHAPE_TEXT:

      NowTool=DT_TEXT;

      break;

   case IDM_SHAPE_BITMAP:

      NowTool=DT_BITMAP;

      break;

   case IDM_SHAPE_META:

      NowTool=DT_META;

      break;

 

NowTool 대응되는 툴로 바꾸기만 하면 된다. 메인 메뉴에서 NowTool 바꾸는 명령만 작성하면 툴바의 버튼들도 같은 ID 가지고 있으므로 별도의 코드를 작성하지 않아도 바로도 도구를 바꿀 있다. 뿐만 아니라 도구가 바뀌면 바의 상태도 OnIdle 의해 자동으로 관리된다. 가지 객체 상대적으로 간단한 비트맵부터 삽입해 보자. 다음 함수는 지정한 위치에 비트맵을 삽입한다.

 

void InsertBitmap(int x,int y)

{

   OPENFILENAME OFN;

   char lpstrFile[MAX_PATH]="";

   HANDLE hFile;

   DWORD FileSize, dwRead;

   BYTE *pBmp;

   BITMAPINFOHEADER *ih;

 

   memset(&OFN, 0, sizeof(OPENFILENAME));

   OFN.lStructSize = sizeof(OPENFILENAME);

   OFN.hwndOwner=hWndMain;

   OFN.lpstrFilter="비트맵 파일\0*.bmp\0모든 파일(*.*)\0*.*\0";

   OFN.lpstrFile=lpstrFile;

   OFN.nMaxFile=256;

   if (GetOpenFileName(&OFN)!=0) {

      hFile=CreateFile(lpstrFile,GENERIC_READ,0,NULL,

          OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

      if (hFile!=INVALID_HANDLE_VALUE) {

          FileSize=GetFileSize(hFile,NULL);

          pBmp=(BYTE *)malloc(FileSize);

          ReadFile(hFile,pBmp,FileSize,&dwRead,NULL);

          CloseHandle(hFile);

          if (*pBmp != 0x42 || *(pBmp+1) != 0x4d) {

             free(pBmp);

             return;

          }

          ih=(BITMAPINFOHEADER *)(pBmp+sizeof(BITMAPFILEHEADER));

          AppendObject(DT_BITMAP,x,y,x+ih->biWidth,y+ih->biHeight);

          arObj[arNum-1]->Bitmap=pBmp;

          arObj[arNum-1]->Len=FileSize;

 

          InvalidateRect(hWndMain,NULL,TRUE);

          NowTool=DT_SELECT;

          NowSel=arNum-1;

      }

   }

}

 

파일 열기 공통 대화상자를 열어 BMP 파일을 선택하도록 파일의 크기만큼 pBmp 할당하여 모두 읽어들였다. 파일 선두의 매직 넘버를 읽어 보고 값이 "BM"인지, 비트맵 파일이 맞는지 간단한 점검을 비트맵의 크기만한 객체를 배열에 추가한다. 매직 넘버만으로 비트맵 파일인지 확신하는 것은 완벽하지 않지만 99.99% 이상의 경우를 걸러낸다. AppendObject 함수는 객체의 영역과 속성을 초기화하기는 하지만 Bitamp, Len 멤버는 변경하지 않으므로 일단 추가한 따로 대입해야 한다.

비트맵을 추가한 작업 영역을 무효화하여 다시 그리도록 하고 선택 툴로 자동 변경한다. 타원이나 직선처럼 여러 개의 비트맵을 동시에 삽입하는 경우가 많지 않기 때문에 비트맵은 한번에 하나만 삽입하도록 했다. 방금 삽입된 비트맵은 선택 상태가 되며 선택툴로 자동 전환되므로 삽입 곧바로 이동이나 크기 변경을 있다. 함수를 호출하는 시점은 비트맵 툴을 선택한 상태에서 마우스 왼쪽 버튼을 누를 때이다.

 

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

{

   ....

   if (NowTool==DT_BITMAP) {

      InsertBitmap(LOWORD(lParam),HIWORD(lParam));

      return 0;

   }

 

InsertBitmap 함수만 부르면 사용자에게 삽입할 파일을 묻고 배열에 추가하는 모든 동작이 함수 내부에서 처리되며 사용자의 취소나 에러 상황도 함수가 처리하므로 OnLButtonDown 함수만 호출하고 리턴하면 된다. 삽입된 비트맵은 OnPaint에서 그린다.

 

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

{

   ....

   BITMAPFILEHEADER *fh;

   BITMAPINFOHEADER *ih;

   int bx,by;

   BYTE *pRaster;

   ....

      case DT_BITMAP:

          fh=(BITMAPFILEHEADER *)arObj[idx]->Bitmap;

          pRaster=(PBYTE)fh+fh->bfOffBits;

          ih=(BITMAPINFOHEADER *)((PBYTE)fh+sizeof(BITMAPFILEHEADER));

          bx=ih->biWidth;

          by=ih->biHeight;

          StretchDIBits(hMemDC,arObj[idx]->rt.left,arObj[idx]->rt.top,

             arObj[idx]->rt.right-arObj[idx]->rt.left,

             arObj[idx]->rt.bottom-arObj[idx]->rt.top,0,0,bx,by,

             pRaster,(BITMAPINFO *)ih,DIB_RGB_COLORS,SRCCOPY);

          break;

 

객체의 Bitmap 포인터에 비트맵 파일의 이진 데이터를 모두 넣어 두었으므로 정보를 읽어 DIB 출력 함수에게 넘기기만 하면 된다. 사용자가 삽입된 도형의 크기를 변경할 있으므로 스트레칭하여 객체의 영역에 맞게 출력해야 한다. 여기까지 코드를 작성한 테스트해 보면 삽입된 비트맵이 캔버스에 출력될 것이다.

StretchDiBits 함수는 DIB 매번 DDB 변환한 출력하므로 다소 비효율적이기는 하다. 그러나 비트맵을 넣지는 않을 것이고 기껏해야 두개 정도의 비트맵만 사용하므로 문제는 없다. 속도를 높이고 싶다면 비트맵을 삽입할 DDB 미리 바꿔 놓고 DObject DDB 핸들을 저장해 놓을 수는 있다. 그렇다라도 파일로 저장할 때는 반드시 DIB 저장해야 하므로 DIB 버릴 수는 없다.

다음은 삽입된 비트맵을 관리하는 코드를 작성해 보자. 객체의 선택, 이동, 크기 변경 코드가 이미 마우스 핸들러에 작성되어 있으므로 코드를 그대로 사용할 있다. 비트맵도 다른 도형과 마찬가지로 선택되며 이동, 크기 변경되므로 특별히 코드를 작성할 것은 없다. 다만 이동, 크기 변경중일 임시 객체를 그리는 코드만 조금 추가하면 된다.

 

void DrawTemp(const DObject *pObj)

{

   ....

   case DT_RECTANGLE:

   case DT_TEXT:

   case DT_BITMAP:

   case DT_META:

      Rectangle(hdc,pObj->rt.left,pObj->rt.top,pObj->rt.right,pObj->rt.bottom);

      break;

 

비트맵은 직사각형 모양을 가지므로 사각 도형과 마찬가지로 Rectangle 함수로 사각 영역을 점선으로 그렸다. 이동중일 비트맵 모양까지 그릴 필요는 없다. 이후 추가될 메타 파일이나 텍스트도 마찬가지이므로 미리 코드에 추가해 놓기로 하자.

비트맵은 다른 객체와는 달리 동적으로 할당된 메모리를 사용한다. 직선, , 비트맵, 사각형 도형 개가 비트맵에 추가되어 있는 상황을 그림으로 그려 보면 다음과 같다.

arObj 배열은 DObject 포인터 배열이며 arObj 배열 요소가 가리키는 곳에는 DObject 구조체가 있다. 직선이나 타원은 DObject 구조체 하나만 있지만 비트맵은 구조체에 속한 Bitmap 포인터가 가리키는 곳에 비트맵의 이진 정보가 할당되어 있다. 이후 추가될 텍스트나 메타 파일도 마찬가지이다. 그래서 객체를 삭제할 DObject 구조체만 해제해서는 안되면 구조체의 Bitmap 멤버가 가리키는 곳의 메모리를 먼저 해제해야 한다. DelObject 함수에 다음 해제 코드를 추가한다.

 

void DelObject(int idx)

{

   if (arObj[idx]->Type >= DT_TEXT && arObj[idx]->Type <= DT_META) {

      free(arObj[idx]->Text);

   }

   free(arObj[idx]);

   memmove(arObj+idx,arObj+idx+1,(arNum-idx-1)*sizeof(DObject *));

   arNum--;

}

 

삭제할 객체가 텍스트나 비트맵, 메타 파일일 경우 객체의 Text 멤버가 가리키는 곳의 할당된 메모리를 먼저 해제한 DObject 구조체를 해제해야 한다. Text, Bitmap, Meta 이름만 다를 같은 번지를 공유하는 멤버이므로 어떤 멤버로 해제하든지 결과는 마찬가지이다. 캔버스가 파괴될 때도 마찬가지로 메모리를 해제해야 한다.

 

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

{

   int idx;

 

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

      if (arObj[idx]->Type >= DT_TEXT && arObj[idx]->Type <= DT_META) {

          free(arObj[idx]->Text);

      }

      free(arObj[idx]);

   }

   free(arObj);

   ....

 

만약 메모리를 해제하지 않고 DObject 구조체만 해제하면 비트맵이 차지하는만큼 메모리 누수가 발생할 것이다.