.그리기 정보의 저장

ApiDraw02 예제는 화면 복구를 하지 않는다. 마우스 동작으로부터 도형을 그리기는 하지만 정보를 따로 저장하지 않기 때문에 다른 윈도우에 의해 도형이 지워져도 다시 그릴 방법이 없다. 화면을 복구하기 위해서는 그려진 도형에 대한 정보를 기억해 두고 WM_PAINT에서 정보를 바탕으로 다시 그리기를 해야 한다.

프로그램이 생성하는 정보는 도형들의 집합이므로 동일한 타입의 변수 집합을 다룰 있는 자료 구조를 선택해야 한다. 생성 가능한 도형의 최대 개수가 정해져 있지 않으므로 임의의 개수에 대한 정보를 다룰 있어야 하는데 이런 용도로 사용할 있는 자료 구조에는 동적 배열과 연결 리스트가 있다. 어떤 것을 사용하든지 상관없지만 프로그램의 경우는 삽입, 삭제보다는 읽기 동작이 훨씬 빈번하고 새로운 도형은 제일 끝에 추가되므로 동적 배열이 유리하다.

동적 배열에 대한 소스는 많이 공개되어 있고 사용하기 편리한 C++객체로도 작성되어 있지만 여기서는 직접 코드를 작성해 보기로 한다. 객체는 사용하기 편리하지만 내부를 숨기기 때문에 학습할 때는 바람직하지 않다. 공개된 동적 배열 함수 집합도 많이 있는데 가져다 수도 있지만 응용 프로그램에 맞게 최적화된 코드를 작성하는 것이 성능상 유리하므로 실습에서는 처음부터 동적 배열을 직접 만들기로 한다. ApiDraw02 예제의 디렉토리를 통채로 복사해서 디렉토리 이름만 ApiDraw03으로 바꾼 그리기 정보를 저장해 보자. 다음 타입과 변수를 추가한다.

 

struct DObject

{

   DTool Type;

   RECT rt;

};

DObject **arObj;

int arSize;

int arNum;

int arGrowBy;

 

DObject 구조체가 도형 하나에 정보를 가진다. Type 도형의 종류를 표시하는 열거형 멤버이며 그리기 도구의 열거값을 그대로 저장한다. rt 멤버는 도형이 차지하고 있는 영역인데 현재 프로그램이 그릴 있는 직선, 타원, 사각형은 모두 개의 점으로 구성되므로 사각형 하나로 표현할 있다. 객체의 속성이나 종류가 늘어나면 구조체의 멤버가 늘어나게 것이다.

arObj DObject 구조체를 저장하는 동적 배열의 포인터이다. DObject 구조체의 포인터를 저장하는 배열이며 배열이 실행중에 동적으로 할당되어야 하므로 이중 포인터로 선언되어 있다. arSize 배열이 할당된 크기이며 arNum 배열에 저장된 도형의 개수이고 arGrowBy 배열이 재할당될 때의 여유분이다. 개의 도형이 그려져 있는 상태라면 arObj 다음과 같은 모양이 것이다.

변수들은 OnCreate에서 초기화한다.

 

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

{

   NowTool=DT_LINE;

   DragMode=DM_NONE;

   arSize=100;

   arNum=0;

   arGrowBy=50;

   arObj=(DObject **)malloc(sizeof(DObject *)*arSize);

   return 0;

}

 

배열의 초기 크기는 일단 100으로 설정하여 100개의 도형 정보를 저장할 있는 공간을 마련했다. 물론 크기는 어디까지나 초기 크기일 뿐이며 도형이 100개를 넘으면 arGrowBy값인 50 단위로 배열이 계속 확장된다. 최초 도형이 생성되어 있지 않으므로 arNum 0으로 초기화했다. OnDestroy에서 배열을 해제한다.

 

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

{

   int idx;

 

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

      free(arObj[idx]);

   }

   free(arObj);

   return 0;

}

 

arObj 구조체의 배열이 아니라 구조체의 포인터 배열이므로 요소가 가리키는 번지의 구조체를 먼저 해제하고 배열 자체를 해제해야 한다. 만약 구조체를 따로 해제하지 않고 arObj 해제해 버리면 메모리 누수가 발생할 것이며 할당된 구조체에는 이상 접근할 없게 된다. 객체가 추가될 객체에 대한 정보를 배열에 작성한다. 다음 함수는 배열 끝에 새로 생성된 객체를 추가한다. 새로운 함수를 작성했으면 항상 소스 선두에 원형도 같이 선언하도록 하자.

 

// 일반 함수

BOOL AppendObject(DTool Type,int x1,int y1,int x2,int y2)

{

   if (Type == DT_LINE) {

      if (x1 == x2 && y1 == y2) {

          return FALSE;

      }

   } else {

      if (x1 == x2 || y1 == y2) {

          return FALSE;

      }

   }

 

   if (arNum >= arSize) {

      arSize+=arGrowBy;

      arObj=(DObject **)realloc(arObj,sizeof(DObject *)*arSize);

   }

 

   arObj[arNum]=(DObject *)malloc(sizeof(DObject));

   arObj[arNum]->Type=Type;

   SetRect(&arObj[arNum]->rt,x1,y1,x2,y2);

   arNum++;

   return TRUE;

}

 

BOOL AppendObject(DTool Type,RECT *prt)

{

   return AppendObject(Type,prt->left,prt->top,prt->right,prt->bottom);

}

 

AppendObject 객체의 타입과 좌표를 인수로 전달받으며 정보로부터 DObject 구조체 하나를 할당하여 포인터를 배열에 추가한다. 배열 공간이 부족할 경우 약간의 여유분까지 더해 재할당하여 늘리는 처리도 하고 있으므로 얼마든지 많은 객체를 저장할 있다. 객체의 좌표는 LTRB 직접 전달받을 수도 있고 아니면 RECT 구조체의 포인터로도 받을 있도록 가지 형태의 함수를 정의했다.

함수 선두에 있는 조건문은 도형을 삽입하지 않도록 하는 일종의 예외처리인데 보이지도 않는 도형을 배열에 추가할 필요는 없기 때문이다. 직선은 수평, 수직 하나가 다르면 되지만 타원과 사각형은 달라야 한다. 높이나 폭이 0이면 존재할 없는 객체이므로 이런 객체를 배열에 삽입해서는 안된다. 삽입해도 어차피 보이지 않으므로 말썽이 되지는 않겠지만 괜히 메모리만 축내고 실행 속도만 느려진다. 도형이 배열에 실제로 추가되는 시점은 그리기를 마치고 마우스 버튼을 놓을 때이다.

 

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

{

   if (DragMode==DM_DRAW) {

      AppendObject(NowTool,sx,sy,oldx,oldy);

      InvalidateRect(hWnd,NULL,TRUE);

   }

   DragMode=DM_NONE;

   ReleaseCapture();

   return 0;

}

 

그리기가 끝나면 확정된 좌표대로 도형을 배열에 추가하고 작업 영역을 무효화하여 추가된 도형을 화면에 그리도록 했다. 이제 그려지는 도형에 대한 모든 정보가 arObj 배열에 완전하게 작성되므로 OnPaint에서 저장된 도형을 그리기만 하면 된다.

 

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

{

   HDC hdc;

   PAINTSTRUCT ps;

   int idx;

 

   hdc=BeginPaint(hWnd, &ps);

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

      switch (arObj[idx]->Type) {

      case DT_LINE:

          MoveToEx(hdc,arObj[idx]->rt.left,arObj[idx]->rt.top,NULL);

          LineTo(hdc,arObj[idx]->rt.right,arObj[idx]->rt.bottom);

          break;

      case DT_ELLIPSE:

          Ellipse(hdc,arObj[idx]->rt.left,arObj[idx]->rt.top,

             arObj[idx]->rt.right,arObj[idx]->rt.bottom);

          break;

      case DT_RECTANGLE:

          Rectangle(hdc,arObj[idx]->rt.left,arObj[idx]->rt.top,

             arObj[idx]->rt.right,arObj[idx]->rt.bottom);

          break;

      }

   }

   EndPaint(hWnd, &ps);

   return 0;

}

 

arObj 배열의 처음부터 도형의 개수인 arNum까지 순회하면서 도형의 타입에 따라 적당한 작도 함수로 도형을 그리면 된다. OnPaint에서 화면을 복구하고 있으므로 화면이 가려지거나 최소화되었다가 복구 되더라도 한번 그려 놓은 도형은 항상 자리에 있을 것이다. 윈도우즈 프로그램은 이런 식으로 항상 화면을 다시 그릴 있는 완벽한 정보를 저장해야 한다.