. 이동 및 삽입

다음은 캐럿 위치를 커서이동키로 움직여 보고 중간에 문자열을 삽입하거나 삭제할 수 있도록 만들어보자. 아직 여러 줄은 지원하지 않으므로 좌우이동만 가능하지만 적어도 한 줄 범위에서는 문자열 편집이 가능하다.

 

 Ime2 프로젝트를 복사하여 Ime3 프로젝트를 생성하고 lpszClass Ime3으로 수정하도록 하자. 앞으로도 사본은 계속 이런 식으로 작성하면 된다.

 편집을 하기 위해서는 먼저 앞뒤로 이동해야 하는데 전후 이동이란 곧 현재 편집 위치인 off변수를 이전, 이후 문자 위치로 바꾼다는 뜻이다. 다음 세 함수는 앞뒤의 문자 위치를 찾아준다.

 

inline BOOL IsDBCS(int nPos)

{

     return (IsDBCSLeadByte(buf[nPos]));

}

 

int GetPrevOff(int nPos)

{

     int n, size;

 

     if (nPos==0) {

          return 0;

     }

 

     for (n=0;;) {

          if (IsDBCS(n)) {

               size=2;

          } else {

               size=1;

          }

          n+=size;

          if (n >= nPos)

               break;

     }

     return n-size;

}

 

int GetNextOff(int nPos)

{

     if (IsDBCS(nPos)) {

          return nPos+2;

     } else {

          return nPos+1;

     }

}

 

먼저 다음 문자를 찾아주는 GetNextOff 함수를 보자. 이 함수는 nPos 위치의 문자가 한글이면 2바이트 다음 위치를 찾고 그렇지 않으면 1바이트 다음의 위치를 찾는다. 현재 위치의 문자가 한글인지 아닌지는 IsDBCS 함수를 호출해보면 쉽게 알 수 있는데 이 함수는 IsDBCSLeadByte API 함수를 호출하여 2바이트 문자인지 조사한다. 이 예제에서는 IsDBCS 함수가 이렇게 간단하지만 다음 예제에서는 조건이 좀 더 추가될 것이다. 이 함수는 자주 호출되므로 inline으로 선언하여 호출속도를 높이도록 하였다. GetNextOff 함수가 찾아 주는 다음 번지로 이동하면 오른쪽으로 한 칸 이동하게 된다.

다음 문자로 이동하는 것은 아주 간단하지만 이전 문자로의 이동은 그리 쉽지 않다. 정확한 위치를 찾기 위해 버퍼의 선두에서부터 문자 수를 세어 와야 한다. 한글이면 2문자를 건너뛰고 영문이면 1문자를 건너뛰면서 현재 위치에 도달했을 때 바로 이전 문자 위치를 취하면 된다. DBCS 문자셋은 문자 길이가 고정적이지 않기 때문에 이런 식으로 이전 문자를 찾을 수밖에 없는 단점이 있다.

이 방법대로라면 문서의 길이가 길어질수록 이전 문자의 위치를 찾는데 시간이 많이 걸리게 된다. 이 예제는 아주 간단하므로 버퍼의 처음부터 검색을 하는 비효율적인 코드를 쓰고 있는데 물론 조금만 더 신경을 쓰면 훨씬 더 효율적인 코드를 만들 수 있다. 여러 줄 입력도 안되는 상황이므로 현재는 GetPrevOff 함수의 코드로 만족하도록 하자.

 편집 위치를 변경하는 시점은 사용자가 커서이동키를 누를 때이므로 키보드 메시지에서 위치를 변경하면 된다. WM_KEYDOWN 메시지 처리 루틴을 다음과 같이 작성하였다.

 

     case WM_KEYDOWN:

          switch (wParam)

          {

          case VK_LEFT:

               if (off > 0) {

                   off=GetPrevOff(off);

                   SetCaret();

               }

               return 0;

          case VK_RIGHT:

               if (off < (int)strlen(buf)) {

                   off=GetNextOff(off);

                   SetCaret();

               }

               return 0;

          case VK_HOME:

               off=0;

               SetCaret();

               return 0;

          case VK_END:

               off=lstrlen(buf);

               SetCaret();

               return 0;

          }

          break;

 

오른쪽으로 이동할 때는 GetNextOff 함수를 부르되 단 문서 끝에서는 더 이상 오른쪽이 없으므로 이동할 수 없다. off에 오른쪽 다음 위치를 대입하고 SetCaret 함수를 호출하면 커서가 변경된 off 위치로 이동할 것이다. 왼쪽으로의 이동은 GetPrevOff 함수로 이전 문자의 위치를 찾아 변경하되 줄의 처음에 와 있으면 이동할 수 없다. 줄의 처음과 끝으로 이동하는 Home, End는 따로 설명이 필요없을 정도로 쉽다. 줄의 처음은 오프셋이 0이고 줄의 끝은 문자열의 길이 위치이다.

 여기까지 작성한 후 실행해보면 커서가 잘 이동하기는 하는데 문자를 입력할 때 현재 위치에 상관없이 무조건 뒤로 가서 붙는다. 왜냐하면 WM_CHAR, WM_IME_COMPOSITION 메시지에서 현재 위치에 삽입하지 않고 무조건 뒤에 붙이도록 코드가 작성되어 있기 때문이다. 그래서 이 함수들이 현재 위치인 off를 참조하여 이 위치에 문자를 삽입하도록 수정해야 한다.

이 예제부터는 캐럿의 위치를 옮길 수 있기 때문에 lstrcat 함수로 버퍼 끝에 문자를 단순히 추가해서는 안되며 현재 위치 이후의 문자를 뒤로 복사해야 한다. 삭제도 마찬가지로 버퍼 끝의 두 바이트를 잘라내는 정도로는 안되며 현재 위치 이후의 문자를 앞쪽으로 복사해야 한다.

그다지 복잡한 동작은 아니지만 이런 작업을 WndProc에서 직접 하는 것은 번거롭다. 또한 앞으로 기능을 계속 확장하려면 문자열, 삽입 삭제는 여러 군데서 필요하므로 아예 함수를 만들어 놓도록 하자.

 

void Insert(int nPos, TCHAR *str)

{

     int len;

     int movelen;

 

     len=lstrlen(str);

     if (len==0) return;

     movelen=lstrlen(buf+nPos)+1;

     memmove(buf+nPos+len,buf+nPos,movelen);

     memcpy(buf+nPos,str,len);

}

 

void Delete(int nPos, int nCount)

{

     int movelen;

 

     if (nCount == 0) return;

     if (lstrlen(buf) < nPos+nCount) return;

 

     movelen=lstrlen(buf+nPos+nCount)+1;

     memmove(buf+nPos, buf+nPos+nCount, movelen);

}

 

이 두 함수는 IME와는 전혀 상관이 없는 일반적인 문자열처리 함수이다. Insert 함수는 nPos 위치에 str 문자열을 삽입한다. 삽입하는 방법은 아주 간단한데 ABCDEFGH 문자열의 오프셋 4 XYZ 문자열을 삽입한다고 해보자.

삽입될 위치의 뒤쪽에 있는 문자를 삽입 문자열 길이만큼 뒤로 이동시킨다. 이때 이동시킬 길이 movelen nPos 위치 이후의 문자열 길이에 널 종료문자 길이를 더한 만큼이며 이동될 새 위치는 삽입 위치에서 삽입 문자열의 길이를 더한 곳이다. EFGH\0 문자열의 길이 5만큼을 오프셋 7(4+3)로 이동시키면 오프셋 4이후부터 3바이트만큼 공간이 생긴다. 이 공간에 새로 삽입할 문자열 XYZ를 끼워넣으면 문자열 삽입이 무사히 완료된다.

문자열을 이동시킬 때 NULL 종료문자도 같이 포함시켜야 한다는 점만 주의하면 그리 어렵지 않다. 만약 삽입할 문자열의 길이가 0이면 삽입할 필요가 없으므로 아무것도 하지 않고 그냥 리턴하면 된다. 문자열을 삭제하는 Delete 함수도 비슷한 방식이다. 삭제할 문자열의 길이가 0이거나 삭제할 위치가 버퍼의 길이보다 더 크면 에러이므로 그냥 리턴한다. 그렇지 않으면 삭제할 문자열 뒤쪽 문자열을 nPos 위치로 옮기기만 하면 된다. 이때도 물론 널 종료문자를 같이 포함시켜 이동해야 한다.

 WM_CHAR, WM_IME_COMPOSITION을 수정하여 off 위치에 문자열을 삽입, 삭제하도록 수정하였다.

 

     case WM_CHAR:

          szChar[0]=(BYTE)wParam;

          szChar[1]=0;

          for (i=0;i<LOWORD(lParam);i++) {

            Insert(off,szChar);

               off+=lstrlen(szChar);

          }

          bComp=FALSE;

          InvalidateRect(hWnd,NULL,TRUE);

          SetCaret();

          return 0;

     case WM_IME_COMPOSITION:

          if (lParam & GCS_COMPSTR) {

               hImc=ImmGetContext(hWnd);

               len=ImmGetCompositionString(hImc,GCS_COMPSTR,NULL,0);

               szComp=(TCHAR *)malloc(len+1);

               ImmGetCompositionString(hImc,GCS_COMPSTR,szComp,len);

               szComp[len]=0;

               if (bComp) {

                   off-=2;

               Delete(off,2);

               }

               if (len == 0) {

                   bComp=FALSE;

               } else {

                   bComp=TRUE;

               }

            Insert(off,szComp);

               off+=len;

               ImmReleaseContext(hWnd,hImc);

               free(szComp);

               InvalidateRect(hWnd,NULL,TRUE);

               SetCaret();

               }

          break;

     case WM_IME_CHAR:

          if (IsDBCSLeadByte((BYTE)(wParam >> 8))) {

               szChar[0]=HIBYTE(LOWORD(wParam));

               szChar[1]=LOBYTE(LOWORD(wParam));

               szChar[2]=0;

          } else {

               szChar[0]=(BYTE)wParam;

               szChar[1]=0;

          }

          if (bComp) {

               off-=2;

            Delete(off,2);

          }

        Insert(off,szChar);

          off+=lstrlen(szChar);

          bComp=FALSE;

          InvalidateRect(hWnd,NULL,TRUE);

          SetCaret();

          return 0;

 

lstrcat 대신 Insert 함수로 off 위치에 문자를 삽입하도록 했으며 buf를 직접 조작하는 대신 Delete 함수로 지정한 바이트만큼 삭제하도록 하였다. 이동 후 문자열 삽입이 가능해졌다. 캐럿을 문자열의 중간쯤으로 이동한 후 문자를 입력하면 현재 위치에 문자가 잘 삽입된다.

 다음은 삭제도 가능하도록 해보자. 삭제는 <BS> <Del>키를 사용하는데 먼저 상대적으로 쉬운 <Del>키의 코드부터 작성해보자.

 

     case WM_KEYDOWN:

          switch (wParam)

          {

          ....

          case VK_DELETE:

               if (IsDBCS(off)) {

                   Delete(off, 2);

               } else {

                   Delete(off, 1);

               }

               InvalidateRect(hWnd,NULL,TRUE);

               return 0;

 

<Del>키는 현재 위치의 문자를 삭제하는 것이므로 off 위치의 문자 길이를 조사한 후 Delete 함수만 호출하면 된다. Delete 함수에서 범위 점검을 하므로 문자열 끝인지는 굳이 점검하지 않아도 된다. 현재 위치의 문자를 삭제했으므로 off는 변함이 없으며 따라서 SetCaret 함수도 호출할 필요가 없다.

 <BS>키는 현재 위치에서 뒤로 삭제하는 것이기 때문에 한 칸 왼쪽으로 간 후 <Del>키를 누르는 것과 동일하다. 단 제일 왼쪽에 있을 때는 아무 것도 할 필요가 없다.

 

     case WM_KEYDOWN:

          switch (wParam)

          {

          ....

          case VK_BACK:

               if (off == 0)

                   return 0;

               off=GetPrevOff(off);

               SendMessage(hWnd,WM_KEYDOWN,VK_DELETE,0);

               SetCaret();

               return 0;

 

GetPrevOff 함수로 왼쪽 문자 위치를 찾고 이 위치에서 <Del>키를 누르면 원하는 대로 뒤로 삭제된다. 현재 위치가 앞으로 이동했으므로 SetCaret 함수는 호출해야 한다.

 여기까지 작성한 코드를 실행한 후 <BS>키로 문자를 삭제해보면 앞 문자가 지워지는 것이 아니라 이상한 문자로 바뀌기만 할 것이다. 123456 입력 후 <BS>를 눌러보면 6 BS 문자인 로 변경된다. 왜냐하면 <BS>키는 커서이동키나 <Del>키와는 달리 문자키이기 때문에 WM_CHAR 메시지를 발생시키며 WM_CHAR에서는 무조건 wParam으로 전달된 문자를 현재 위치에 삽입하기 때문이다. 그래서 <BS>키는 문자로 취급하지 않도록 특별한 예외 처리를 해야 한다.

 

     case WM_CHAR:

        if (wParam == 8)

            return 0;

          szChar[0]=(BYTE)wParam;

 

WM_CHAR에서 입력받은 문자가 BS일 경우 그냥 리턴하여 문자가 출력되지 않도록 하였다. 그 외의 문자는 평상시대로 처리하면 된다. 편집기능은 일단 완성되었고 마지막으로 한글 조립중에 열리는 조립창이 다소 눈에 거슬리므로 이 창은 숨기도록 한다. 조립이 시작되는 시점인 WM_IME_STARTCOMPOSITION 메시지를 받았을 때 이 메시지를 DefWindowProc으로 보내지 않고 그냥 리턴하면 된다.

 

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

{

     ....

     case WM_IME_STARTCOMPOSITION:

          return 0;

 

조립창을 다시 보이도록 하고 싶다면 return 0; break;로 바꾸기만 하면 되는데 조립중의 문자가 작업영역에 바로 보이기 때문에 굳이 조립창을 보여줄 필요가 없는 것 같다. 여기까지 한글의 입력 및 캐럿 처리, 이동, 삽입, 삭제까지 완성하였다.