.예외 처리

여기까지 이동과 크기 변경 처리를 보았는데 그럭 저럭 동작하는 같지만 테스트해 보면 많은 문제점이 있다는 것을 있다. 우선 이동을 시작할 커서를 캡처하기 때문에 캔버스 바깥의 보이지 않는 영역으로 옮겨버릴 있는 문제점이 있다. 캔버스의 오른쪽이나 아래쪽으로 사라진 도형은 윈도우의 크기를 키워 다시 있지만 음수 영역으로 사라져 버린 도형은 보이지도 않을 뿐만 아니라 다시 꺼내올 방법이 없어 있으나 마나한 도형이 된다. 캔버스 바깥으로는 도형을 이동시키지 못하도록 필요가 있다.

크기 변경의 경우는 문제가 심각하다. 변의 좌표가 변경됨으로써 정규화 원칙이 위반될 있다. 예를 들어 우하향으로 그려진 직선의 8 트래커를 잡고 1 트래커보다 왼쪽 위로 올려 버리면 직선은 정규화되어 있지 않으므로 다시 선택할 없을 것이다. 도형의 크기가 변경될 때는 새로 바뀐 도형의 영역도 정규화 조건을 만족하도록 해야 한다. 이동과 크기 변경시의 문제점을 해결하는 시점은 도형의 영역이 새로 결정되는 OnLButtonUp이다.

 

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

{

   RECT crt,irt;

   ....

   if (DragMode==DM_MOVE || DragMode==DM_SIZE) {

      GetClientRect(hWnd,&crt);

      InflateRect(&crt,-10,-10);

      IntersectRect(&irt,&crt,&dObj.rt);

      if (!IsRectEmpty(&irt)) {

         arObj[NowSel]->rt=dObj.rt;

      }

      InvalidateRect(hWnd,NULL,TRUE);

   }

   DragMode=DM_NONE;

   ReleaseCapture();

   return 0;

}

 

캔버스의 작업 영역을 안쪽으로 10픽셀만큼 축소시킨 임시 객체의 영역이 안에 있을 때만 이동 크기 조정을 허가하며 그렇지 않을 경우 선택 객체의 rt 대입문을 실행하지 않도록 하여 동작을 취소해 버린다. 여기서 10픽셀이라는 상수값은 도형을 충분히 선택할 있는 여유분으로 설정한 임의값인데 값을 주거나 아니면 도형의 크기에 비례적인 값으로 바꿀 수도 있다.

코드에 의해 정규화 원칙도 항상 지켜지는데 IntersectRect, IsRectEmpty 함수는 정규화되지 않은 사각형은 영역으로 간주하기 때문이다. 이런 예외 처리를 설계할 당시부터 미리 생각하기는 무척 어렵고 대개의 경우 테스트를 하는 중에 문제점을 발견하게 된다. 물론 경험이 많은 사람은 설계할 때부터 문제를 예측할 수도 있을 것이다. 예외 처리에 사각형 관련 함수를 여러 사용했는데 간단한 함수들이라 C 코드로 풀어쓸 수도 있다.

 

   if (DragMode==DM_MOVE || DragMode==DM_SIZE) {

      SwapResult=NormalizeRect(&dObj.rt);

      GetClientRect(hWnd,&crt);

//    InflateRect(&crt,-10,-10);

      crt.left-=-10;

      crt.top-=-10;

      crt.right+=-10;

      crt.bottom+=-10;

 

//    IntersectRect(&irt,&crt,&dObj.rt);

      irt.left=max(crt.left,dObj.rt.left);

      irt.top=max(crt.top,dObj.rt.top);

      irt.right=min(crt.right,dObj.rt.right);

      irt.bottom=min(crt.bottom,dObj.rt.bottom);

 

//    if (!IsRectEmpty(&irt)) {

      if (!(irt.left >= irt.right || irt.top >= irt.bottom)) {

 

그러나 이렇게 코드를 작성하면 소스가 길어질 뿐만 아니라 줄이 어떤 작업을 하는지 얼른 알아 보기 힘들어 가독성이 떨어지는 단점이 있다. SetRect, PtInRect, IsRectEmpty 같은 간단한 동작이라도 가급적이면 운영체제가 제공하는 API 함수를 사용하는 것이 코드 작성도 쉽고 이후 코드를 관리하기도 쉽다.

예외 처리에 의해 캔버스 바깥으로 나가거나 정규화되지 않는 상태가 동작을 취소하는 식으로 1차적인 에러 처리를 했다. 처리에 의해 프로그램이 잘못 동작한다거나 도형이 사라지는 문제점은 해결했지만 프로그램의 과다 방어로 인해 사용자는 다소 불편함을 느낄 수도 있다. 사용자는 우하단을 끌어 좌상단으로 이동시킬 금지시키는 것보다 프로그램이 알아서 정규화하는 것을 좋아할 것이다. 프로그램의 요구 조건에 사용자가 맞추는 것보다는 사용자의 편집 동작을 최대한 인정하여 프로그램이 자료의 형태를 바꾸는 것이 훨씬 좋은 정책이다. 정책을 구현하기 위해 다음 함수를 추가하자. 운영체제가 이런 함수를 제공하지 않으므로 직접 만들어 써야 한다.

 

void NormalizeRect(RECT *prt)

{

   int t;

 

   if (prt->left > prt->right) {

      t=prt->left;

      prt->left=prt->right;

      prt->right=t;

   }

   if (prt->top > prt->bottom) {

      t=prt->top;

      prt->top=prt->bottom;

      prt->bottom=t;

   }

}

 

함수는 전달된 RECT 구조체를 강제로 정규화하는데 방법은 간단하다. left right 비교해 보고 만약 left right보다 오른쪽에 있다면 값을 교환하여 정규화하는 것이다. 수직 방향도 마찬가지로 top bottom 값을 교환하여 정규화한다. OnLButtonUp에서 편집된 결과를 객체에 대입하기 전에 함수를 호출하여 임시 객체의 영역을 정규화하면 결과를 대입받는 도형도 항상 정규화 조건을 만족한다.

 

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

{

   ....

   if (DragMode==DM_MOVE || DragMode==DM_SIZE) {

      NormalizeRect(&dObj.rt);

 

상태로 테스트해 보면 타원, 사각형은 방향을 바꾸어 드래그해도 정규화가 되지만 직선의 경우 정규화되면서 방향이 바뀌는 부작용이 있다. 강제 정규화를 했을 경우 정규화 결과에 따라 직선의 Flag 같이 조정해야 하며 그러기 위해서는 정규화시에 수평, 수직 쪽으로 교환된 값을 알아야 한다. NormalizeRect 함수를 다음과 같이 수정한다.

 

int NormalizeRect(RECT *prt)

{

   int t;

   int SwapResult=0;

 

   if (prt->left > prt->right) {

      t=prt->left;

      prt->left=prt->right;

      prt->right=t;

      SwapResult|=1;

   }

   if (prt->top > prt->bottom) {

      t=prt->top;

      prt->top=prt->bottom;

      prt->bottom=t;

      SwapResult|=2;

   }

   return SwapResult;

}

 

SwapResult 비트 0 수평 교환 여부, 비트 1 수직 교환 여부를 리턴한다. 개의 리턴값을 넘길 없기 때문에 정수값의 비트에 교환 여부를 기록해서 넘기는 것이다. 도형의 원래 플래그와 수평, 수직 교환 여부에 따라 바뀌어야 값을 표로 그려 보면 다음과 같다.

 

원래 플래그

수평 교환시

수직 교환시

LT

RT

LB

RT

LT

RB

LB

RB

LT

RB

LB

RT

 

표를 그대로 코드로 구현하면 다음과 같아진다. 수평, 수직 각각에 대해 플래그를 조정하면 정규화에 맞게 방향도 바뀌게 된다.

 

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

{

   int SwapResult;

   ....

   if (DragMode==DM_MOVE || DragMode==DM_SIZE) {

      SwapResult=NormalizeRect(&dObj.rt);

      GetClientRect(hWnd,&crt);

      InflateRect(&crt,-10,-10);

      IntersectRect(&irt,&crt,&dObj.rt);

      if (!IsRectEmpty(&irt)) {

          arObj[NowSel]->rt=dObj.rt;

          if (SwapResult & 1) {

             switch (arObj[NowSel]->Flag) {

             case DS_LT:arObj[NowSel]->Flag=DS_RT;break;

             case DS_RT:arObj[NowSel]->Flag=DS_LT;break;

             case DS_LB:arObj[NowSel]->Flag=DS_RB;break;

             case DS_RB:arObj[NowSel]->Flag=DS_LB;break;

             }

          }

          if (SwapResult & 2) {

             switch (arObj[NowSel]->Flag) {

             case DS_LT:arObj[NowSel]->Flag=DS_LB;break;

             case DS_RT:arObj[NowSel]->Flag=DS_RB;break;

             case DS_LB:arObj[NowSel]->Flag=DS_LT;break;

             case DS_RB:arObj[NowSel]->Flag=DS_RT;break;

             }

          }

      }

      InvalidateRect(hWnd,NULL,TRUE);

   }

 

이제 테스트해 보면 직선이 강제 정규화될 방향도 바뀔 것이다. 그런데 코드가 너무 길어져서 조금 불만인데 교환된 방향과 플래그의 변경된 부분과의 연관 관계를 관찰해 보면 코드를 압축할 있다. DS_LT, DS_RT, DS_LB, DS_RB 플래그들이 비트 0 수평 방향, 비트 1 수직 방향을 기록하도록 되어 있으므로 교환된 방향에 따라 비트들만 반전시켜 주면 된다. 간단한 연산식은 다음과 같다.

 

if (SwapResult & 1) arObj[NowSel]->Flag ^= 1;

if (SwapResult & 2) arObj[NowSel]->Flag ^= 2;

 

테스트해 보면 정확하게 동작함을 확인할 있다. 코드의 길이도 짧아졌고 실행 속도도 훨씬 빨라졌으므로 모든 면에서 압축된 코드가 좋다고 있다. 압축한다면 다음 줄로도 압축할 있다.

 

arObj[NowSel]->Flag ^= SwapResult;

 

SwapResult 1 비트 부분만 선택적으로 반전시키는 것이며 하나의 비트 연산문으로 연산을 있다. 코드가 앞의 줄짜리코드보다 빠르고 작아서 점수를 매긴다면 가장 높다. 그러나 이런 이진 연산 코드는 어셈블리 코딩의 경험이 없다면 쉽게 생각하기 어렵다. 저수준 프로그래밍 경험의 유용성이 이런 식으로 가끔 드러나는 경우가 있는데 이런 것을 내공의 차이라고 한다.