.텍스트 툴의 다른 방법

모달 대화상자를 열어서 텍스트를 편집하는 방법은 구현하기 간단하다. 모달 대화상자는 있는동안 부모 윈도우가 사용 금지되므로 별다른 주의 사항도 없고 편집이 완료될 때까지 나머지 코드는 대기 상태에 있으므로 논리가 직선적이다. 그러나 사용자들은 일일이 대화상자를 열어서 편집해야 하므로 사용하기에는 대단히 불편하다. 이런 방식보다는 캔버스에서 텍스트를 바로 입력하고 편집하는 것이 통상적인 방법인데 여기서는 직접 편집을 구현해 보자.

ApiDraw09 프로젝트의 사본을 복사하여 ApiDraw09Text 프로젝트를 만든다. , 프로젝트는 이런 식으로도 텍스트를 입력받을 있다는 것을 테스트할 뿐이지 ApiDraw 채택하지는 않을 예정이므로 임시 프로젝트로 실습을 진행하도록 한다. 캔버스에서 직접 텍스트를 편집하는 작업은 여러 개의 함수들이 협조적으로 동작해야 하므로 전역 변수들이 많이 필요하다. 다음 전역 변수들을 선언한다.

 

HWND hTextEdit;

int EditSel;

HFONT hTextEditFont;

COLORREF hTextEditColor;

int TextEditHeight;

 

편집에 사용할 에디트 컨트롤과 편집 대상 객체의 번호, 에디트에 설정할 폰트와 색상, 에디트의 현재 높이 등을 저장하는 변수들이다. 전역 변수가 이렇게 많이 필요하다는 것은 문제 해결 방법이 그다지 명쾌하지 못하다는 증거이기도 하다. 다음 함수는 텍스트 편집을 시작 종료한다.

 

void StartTextEdit(HWND hParent,int x, int y, int Sel)

{

   HDC hdc;

   TCHAR FontFace[32];

   int FontIdx;

   LOGFONT tFont;

 

   EditSel=Sel;

   if (EditSel != -1) {

      x=arObj[Sel]->rt.left;

      y=arObj[Sel]->rt.top;

      TextEditHeight=arObj[Sel]->FontSize;

      lstrcpy(FontFace,arObj[Sel]->FontFace);

      hTextEditColor=arObj[Sel]->FontColor;

   } else {

      TextEditHeight=Opt.FontSize;

      lstrcpy(FontFace,Opt.FontFace);

      hTextEditColor=Opt.FontColor;

   }

 

   hdc=GetDC(NULL);

   TextEditHeight=TextEditHeight*GetDeviceCaps(hdc,LOGPIXELSY)/72;

   ReleaseDC(NULL,hdc);

 

   FontIdx=FindFontFromFace(FontFace);

   if (FontIdx != -1) {

      tFont=logfont[FontIdx];

      tFont.lfHeight=TextEditHeight;

      tFont.lfWidth=0;

      hTextEditFont=CreateFontIndirect(&tFont);

   } else {

      hTextEditFont=(HFONT)GetStockObject(SYSTEM_FONT);

   }

 

  hTextEdit=CreateWindow("edit",NULL,WS_CHILD | WS_VISIBLE | ES_MULTILINE | ES_AUTOVSCROLL,

      x,y,300,TextEditHeight,hParent,(HMENU)10000,g_hInst,NULL);

   SendMessage(hTextEdit,WM_SETFONT,(WPARAM)hTextEditFont,MAKELONG(FALSE,0));

 

   if (EditSel != -1) {

      SetWindowText(hTextEdit,arObj[Sel]->Text);

      SetWindowPos(hTextEdit,HWND_TOP,0,0,arObj[Sel]->rt.right-arObj[Sel]->rt.left,

          arObj[Sel]->rt.bottom-arObj[Sel]->rt.top,SWP_NOMOVE);

   }

   SetFocus(hTextEdit);

}

 

void EndTextEdit()

{

   if (hTextEdit) {

      SetFocus(hCanvas);

   }

}

 

편집을 시작할 좌표 x, y 편집 대상 객체의 번호 Sel 인수로 전달받되 새로운 텍스트를 추가할 때는 Sel -1 전달한다. Sel 이미 존재하는 객체인 경우는 객체의 좌상단 좌표를 사용하고 객체의 글꼴 정보를 읽어오며 새로 만들어지는 텍스트 객체이면 전역 옵션으로부터 글꼴 정보를 읽으면 된다. 읽어온 글꼴 정보는 포인트 단위로 되어 있으므로 픽셀로 바꾸고 별도의 폰트가 지정되어 있을 경우 에디트 컨트롤이 사용할 글꼴을 생성해 놓는다. 텍스트 객체가 궁서 20pt 작성되어 있다면 객체를 편집할 에디트도 설정된 글꼴 정보대로 텍스트를 출력해야 한다.

편집을 위해 캔버스의 차일드로 에디트 컨트롤을 생성하되 경계선이 없는 에디트로 만들어 마치 캔버스가 직접 편집을 하는 것처럼 보이게 한다. 이런 기법은 탐색기로 파일명을 편집할 때도 사용되는 것인데 텍스트 편집을 위해 잠시 에디트 컨트롤을 생성하는 것이다. 임시로 사용할 컨트롤이므로 ID 10000으로 대충 지정했다. 텍스트를 생성한 폰트를 변경하고 이미 존재하는 객체인 경우 편집 대상 텍스트를 읽어와 에디트 컨트롤에 표시하고 에디트의 크기를 객체의 정보대로 조정한다. 마지막으로 이렇게 생성한 에디트 컨트롤에 포커스를 주면 텍스트 편집이 시작된다.

EndTextEdit 함수는 에디트의 포커스를 회수함으로써 편집을 종료하는 함수이다. 부모 윈도우가 EN_KILLFOCUS 통지 메시지를 받았을 편집을 종료하는 처리를 하므로 포커스만 없애면 나머지 뒷처리는 통지 메시지가 처리한다. 텍스트 입력을 시작할 시점은 왼쪽 마우스 버튼을 누를 때이다.

 

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

{

   ....

   EndTextEdit();

 

   if (NowTool==DT_TEXT) {

      StartTextEdit(hWnd,LOWORD(lParam),HIWORD(lParam),-1);

      return 0;

   }

   ....

 

DT_TEXT 툴이 선택된 경우 StartTextEdit 함수를 호출하면 편집용 에디트 컨트롤을 생성한다. 새로 텍스트 객체를 만드는 것이므로 마지막 인수는 -1이다. 텍스트 상태에서 다시 클릭할 경우는 일단 편집중인 텍스트를 먼저 종료하기 위해 EndTextEdit 호출했다. 텍스트 편집은 에디트 컨트롤이 스스로 처리하되 부모 윈도우는 에디트의 크기와 파괴 시점을 관리해야 한다. OnCommand 다음 코드를 작성하여 에디트의 통지 메시지를 처리해야 한다.

 

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

{

   int LineCount;

   RECT crt;

   int Len;

   TCHAR *Text;

   HWND hEdit;

 

   switch(LOWORD(wParam)) {

   case 10000:

      switch (HIWORD(wParam)) {

      case EN_CHANGE:

          LineCount=SendMessage(hTextEdit,EM_GETLINECOUNT,0,0);

          GetWindowRect(hTextEdit,&crt);

          if (LineCount*TextEditHeight > crt.bottom-crt.top) {

             crt.bottom=crt.top+LineCount*TextEditHeight;

             SetWindowPos(hTextEdit,HWND_TOP,0,0,crt.right-crt.left,

                crt.bottom-crt.top,SWP_NOMOVE);

          }

          break;

      case EN_KILLFOCUS:

          hEdit=(HWND)lParam;

          Len=GetWindowTextLength(hEdit);

          GetWindowRect(hEdit,&crt);

          ScreenToClient(GetParent(hEdit),(LPPOINT)&crt);

          ScreenToClient(GetParent(hEdit),(LPPOINT)&crt.right);

          if (EditSel == -1) {

             if (Len) {

                Text=(TCHAR *)malloc(Len+1);

                GetWindowText(hEdit,Text,Len+1);

                AppendObject(DT_TEXT,&crt);

                arObj[arNum-1]->Text=Text;

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

                arObj[arNum-1]->PlaneColor=-1;

             }

             NowTool=DT_SELECT;

             NowSel=arNum-1;

          } else {

             Text=arObj[EditSel]->Text;

             Text=(TCHAR *)realloc(Text,Len+1);

             arObj[EditSel]->Len=Len+1;

             arObj[EditSel]->Text=Text;

             GetWindowText(hEdit,Text,Len+1);

             arObj[EditSel]->rt=crt;

          }

          DestroyWindow(hEdit);

          hTextEdit=NULL;

          DeleteObject(hTextEditFont);

          break;

      }

      break;

   ....

 

캔버스는 에디트로부터 전달되는 개의 통지 메시지를 처리하는데 우선 EN_CHANGE 받았을 에디트가 줄의 텍스트를 가지고 있는지 조사하여 지금 높이보다 많은 줄을 가지게 되었으면 에디트의 높이를 확장한다. 캔버스가 직접 편집하는 것처럼 보이도록 하고 싶은데 여기에 스크롤 바가 나타난다거나 일부 텍스트가 숨어 버리면 보기 좋지 않으므로 에디트를 편집 텍스트의 크기에 맞게 늘리도록 했다.

에디트의 높이를 관리하는 코드이므로 엔터키가 입력될 때마다 줄씩 늘리면 같지만 그것보다는 조금 복잡하다. 왜냐하면 명시적인 개행 입력외에 줄이 에디트의 오른쪽 끝에 닿아서 자동 개행되는 경우도 있기 때문이다. 그래서 텍스트가 바뀔 때마다 수를 보고 폰트 높이를 곱해 현재 에디트 높이가 적절한지를 점검했다. 텍스트를 삭제할 때는 에디트의 높이를 줄일 수도 있지만 사용자가 객체의 크기를 미리 크게 만들어 놓고 편집을 시작할 수도 있으므로 처리는 하지 않았다.

다음은 EN_KILLFOCUS 통지 메시지를 처리하는데 에디트가 포커스를 잃을 때를 편집 완료 시점으로 인식한다. 통지 메시지를 받았을 텍스트 객체를 새로 만들거나 아니면 기존 객체의 텍스트를 새로 편집된 내용으로 변경한다. 임시로 만든 에디트는 포커스를 잃으면 이상 존재할 필요가 없으므로 파괴시키고 에디트가 사용하던 폰트도 해제하였다. 다음 코드는 에디트의 색상을 관리한다.

 

LRESULT CALLBACK CanvasProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

   ....

   case WM_CTLCOLOREDIT:

      if ((HWND)lParam == hTextEdit) {

         DefWindowProc(hWnd,WM_CTLCOLOREDIT,wParam,lParam);

         SetTextColor((HDC)wParam,hTextEditColor);

         return TRUE;

      }

      return 0;

   }

   return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

에디트는 자신의 색상 결정을 위해 보무 윈도우에게 WM_CTLCOLOREDIT 메시지를 보내는데 메시지에서 에디트의 텍스트 색상을 변경할 있다. 면의 색상까지는 관리할 필요없이 글자의 색상만 관리하도록 했다. 텍스트 편집중에 다른 툴을 선택할 때도 편집을 즉시 종료해야 하며 기타 텍스트 편집 이외의 동작을 하면 즉시 종료하는 것이 좋다.

 

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

{

   EndTextEdit();

   ....

 

메인 메뉴의 특정 메뉴를 선택하면 텍스트 편집을 즉시 중지하도록 했다. 텍스트 객체를 더블클릭하면 기존 객체를 편집한다. 이때는 StartTextEdit함수를 호출하되 선택된 객체의 번호를 넘겨 객체에 대한 에디트를 생성하도록 했다.

 

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

{

   int TempSel;

 

   TempSel=FindObject(LOWORD(lParam),HIWORD(lParam));

   if (TempSel == -1) {

      return 0;

   }

   if (arObj[TempSel]->Type == DT_TEXT) {

      StartTextEdit(hWnd,LOWORD(lParam),HIWORD(lParam),TempSel);

   }

   return 0;

}

 

이상으로 캔버스에서 직접 편집하는 코드를 작성해 보았는데 동작하는 같지만 사실 그다지 정확한 코드는 아니다. 편집을 종료하는 시점을 잡기가 굉장히 어려운데 이런 코드는 지속적인 관리가 필요해서 유지 비용이 높은 편이다. 기능을 확장할 때마다 부분이 계속 말썽을 부릴 위험이 있다. 또한 현재 구현된 기능도 완벽하지 않은데 원래 이런 편집중에 Esc키를 누르면 즉시 편집을 종료하든가 취소해야 한다. 에디트가 포커스를 가진 상태에서 입력을 부모가 수는 없기 때문에 이를 처리하려면 서브클래싱이 필요하다. 지금보다 복잡해져야 한다는 얘기다.

조금 코드를 섬세하게 다듬으면 직접 편집하는 기능을 완벽하게 구현하는 것이 가능하기는 하겠지만 예측되는 것보다 훨씬 다량의 코드가 작성되어야 한다. 게다가 버그도 조금 보이는데 더블클릭해서 재편집할 트래커의 일부가 지워지지 않는 문제가 있다. 물론 문제를 발견했으면 해결해야 하고 역시나 개발자의 노력을 요구한다. 이는 실습의 목표와 부합되지 않아 학습자를 지치게 만드는 요인이 되기에 충분하다. 그래서 실습에서는 방법을 채택하지 않고 모달 대화상자로 텍스트를 편집하는 방법을 계속 사용하기로 한다.