. 조립중인 문자 보여주기

그럼 이제 IME 메시지를 활용하여 조립중인 한글을 보여주는 예제를 만들어 보도록 하자. 메모장에서 사용하고 있는 에디트 컨트롤을 처음부터 작성한다고 생각하면 된다. 이 실습은 생각보다 복잡하므로 반드시 소스를 직접 입력해 가면서 왜 저런 코드가 필요할까 고민을 해 볼 필요가 있다. 그렇지 않으면 결과는 볼 수 있어도 동작방식에 대한 이해를 할 수 없기 때문에 응용이 어려워진다. 반드시 컴파일러를 띄워 놓고 실습을 같이 해보도록 하자. 첫 번째 예제의 소스는 다음과 같다. 꼭 필요한 코드만 넣었으므로 다행히 그다지 길지 않다.

Ime1이라는 이름으로 새 프로젝트를 생성하고 이 프로젝트에 Ime1.cpp를 추가한 후 다음 소스를 입력하면 된다. 직접 입력하기가 귀찮으면 CD-ROM의 예제를 열어서 복사해 오도록 하되 그렇다고 하더라도 프로젝트는 반드시 직접 만들어 보기 바란다. imm32.lib와 연결하는 것을 잊지 말도록 하자.

 

#include <windows.h>

 

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);

HINSTANCE g_hInst;

HWND hWndMain;

LPCTSTR lpszClass=TEXT("Ime1");

 

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

       ,LPSTR lpszCmdParam,int nCmdShow)

{

     HWND hWnd;

     MSG Message;

     WNDCLASS WndClass;

     g_hInst=hInstance;

    

     WndClass.cbClsExtra=0;

     WndClass.cbWndExtra=0;

     WndClass.hbrBackground=(HBRUSH)(COLOR_WINDOW+1);

     WndClass.hCursor=LoadCursor(NULL,IDC_IBEAM);

     WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);

     WndClass.hInstance=hInstance;

     WndClass.lpfnWndProc=(WNDPROC)WndProc;

     WndClass.lpszClassName=lpszClass;

     WndClass.lpszMenuName=NULL;

     WndClass.style=CS_HREDRAW | CS_VREDRAW;

     RegisterClass(&WndClass);

 

     hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,

          CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,

          NULL,(HMENU)NULL,hInstance,NULL);

     ShowWindow(hWnd,nCmdShow);

     hWndMain=hWnd;

    

     while(GetMessage(&Message,0,0,0)) {

          TranslateMessage(&Message);

          DispatchMessage(&Message);

     }

     return (int)Message.wParam;

}

 

#include <imm.h>

TCHAR *buf;

BOOL bComp;

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

{

     HDC hdc;

     PAINTSTRUCT ps;

     HIMC hImc;

     TCHAR szChar[3], *szComp;

     int i;

     int len;

 

     switch(iMessage) {

     case WM_CREATE:

          bComp=FALSE;

          buf=(TCHAR *)malloc(65536);

          memset(buf,0,65536);

          return 0;

     case WM_CHAR:

          szChar[0]=(BYTE)wParam;

          szChar[1]=0;

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

              lstrcat(buf,szChar);

          }

          bComp=FALSE;

          InvalidateRect(hWnd,NULL,TRUE);

          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) {

                    buf[lstrlen(buf)-2]=0;

              }

              if (len == 0) {

                   bComp=FALSE;

              } else {

                   bComp=TRUE;

              }

              lstrcat(buf,szComp);

              ImmReleaseContext(hWnd,hImc);

              free(szComp);

              InvalidateRect(hWnd,NULL,TRUE);

          }

          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) {

              buf[lstrlen(buf)-2]=0;

          }

          lstrcat(buf,szChar);

          bComp=FALSE;

          InvalidateRect(hWnd,NULL,TRUE);

          return 0;

     case WM_PAINT:

          hdc=BeginPaint(hWnd,&ps);

          TextOut(hdc,0,0,buf,lstrlen(buf));

          EndPaint(hWnd,&ps);

          return 0;

     case WM_DESTROY:

          PostQuitMessage(0);

          free(buf);

          return 0;

     }

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

}

 

이 예제는 두 개의 전역변수를 선언하고 있다. buf는 입력된 문자열을 저장할 버퍼인데 WM_CREATE에서 65536크기(64KB)로 할당하였으며 내용을 모두 0으로 초기화하였다. 더 큰 문서를 편집하고자 한다면 별도의 메모리관리가 필요하겠지만 간단한 예제이므로 일단 이 정도 크기면 충분하다. bComp 변수는 현재 한글이 조립중인가 아닌가를 기억하는데 이 값이 TRUE이면 한글을 조립하고 있는 중이고 FALSE이면 조립이 완료되었거나 영문을 입력하고 있는 중이다. 이 변수는 문자입력중에 수시로 참고하고 관리해야 하는 아주 중요한 변수이다. 최초 실행될 때는 한글조립중이 아니므로 WM_CREATE에서 FALSE로 초기화한다.

WndProc에서는 여섯 개의 메시지를 처리하는데 WM_CREATE는 버퍼 할당 및 초기화를 하고 WM_PAINT는 버퍼의 내용을 화면으로 무조건 출력한다. WM_DESTROY는 버퍼를 해제하고 프로그램을 종료하는 일을 하는데 세 메시지는 아주 평이하므로 별다른 관심을 보일 필요가 없다. 이 프로그램이 문자입력을 위해 처리하는 메시지는 세 가지뿐이다. 먼저 가장 쉬운 WM_CHAR 메시지의 코드를 분석해보자. 이 메시지는 한글이 아닌 영문, 숫자를 입력할 때만 발생하는데 wParam은 항상 1바이트이다.

wParam으로 전달된 문자코드를 버퍼 뒤에 붙이기만 하면 되는데 szChar 버퍼에 길이 1의 문자열을 만든 후 buf 뒤에 이 문자열을 누적시켰다. 물론 wParam 문자를 buf의 끝에 바로 대입해도 별 상관은 없지만 일관성과 확장성을 위해 임시 문자열 버퍼를 사용하였다. LOWORD(lParam)은 키입력 반복횟수인데 이 횟수만큼 문자를 누적시킨다. 통상 반복횟수는 1이므로 무시해도 별 상관은 없지만 예제의 완성도를 높이기 위해 일단 포함시켰다.

영문, 숫자가 입력되었다는 것은 한글조립중이 아니라는 뜻이므로 bComp 변수는 FALSE로 바꾼다. 문자를 버퍼에 누적시킨 후 화면을 무효화하여 다시 그리도록 하면 입력된 문자가 보일 것이다. 여기까지는 DefIme 예제와도 거의 동일하다.

이 예제의 가장 어려운 부분은 WM_IME_COMPOSITION 메시지 처리 부분인데 이 메시지는 한글을 조립중일 때, 또는 조립중인 문자가 완성될 때 보내진다. 조립중인 문자만 처리하기 위해 lParam GCS_COMPSTR 플래그가 설정된 경우만 처리하며 그 외의 경우는 DefWindowProc으로 처리를 넘겨 디폴트 처리되도록 하였다. 조립중인 문자를 보여주려면 먼저 조립된 문자의 코드를 구해야 한다. 이때는 다음 함수를 사용한다.

 

LONG ImmGetCompositionString(HIMC hIMC, DWORD dwIndex, LPVOID lpBuf, DWORD dwBufLen);

 

첫 번째 인수 hIMC는 입력 컨텍스트의 핸들인데, ImmGetContext 함수로 구한 핸들을 전달하면 된다. 두 번째 인수 dwIndex는 구하고자 하는 정보를 지정하는데, GCS_COMPSTR이면 조립중인 문자코드를 구하며, GCS_RESULTSTR이면 완성된 문자코드를 구한다. 조사된 정보는 세 번째 인수 lpBuf에 리턴된다. 네 번째 인수 dwBufLen lpBuf의 길이를 지정하되 단 이 값이 0이면 실제 정보를 구하는 것이 아니라 필요한 버퍼의 크기를 리턴한다. 그래서 조립중인 문자를 구할 때는 다음과 같이 코드를 작성하는 것이 원칙이다.

 

TCHAR *szComp;

int len;

 

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

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

ImmGetCompositionString(hImc,GCS_COMPSTR,szComp,len);

szComp[len]=0;

 

// 조사한 정보 사용

 

free(szComp);

 

lpBuf인수에 NULL을 주고 이 버퍼의 길이를 0으로 지정하여 필요한 버퍼 크기를 먼저 구한다. 그리고 조사된 크기에 널문자를 위한 1을 더한 만큼 메모리를 할당한 후 다시 이 함수를 호출하여 원하는 코드를 조사하였다. 이때 ImmGetComposition 함수는 실제로 조사된 바이트 수를 리턴한다. 물론 할당한 메모리는 정보를 사용한 후 해제해야 한다.

한글의 경우 한 음절은 무조건 2바이트이므로 굳이 이런 번거로운 방법을 쓸 필요없이 공백과 널문자의 길이까지 고려한 4바이트 크기의 고정 길이 버퍼를 사용하면 충분하다. 하지만 조사되는 정보의 길이가 가변적이라면 동적으로 메모리를 할당하는 방법이 원칙적으로 옳은 방법이며 이렇게 메모리를 할당한 후 사용하더라도 실행시간의 낭비는 거의 없다.

조립중인 문자의 코드를 구한 후 buf 끝에 누적시키되 무조건 덧붙여서는 안된다. 이 메시지로 전달된 코드는 완성된 문자가 아니므로 이전의 조립중인 문자를 먼저 제거한 후 버퍼에 붙여야 한다. 예를 들어 자가 입력된 상태에서 ㄴ을 입력하면 자가 되는데 이렇게 되려면 자를 먼저 제거하고 자를 붙여야 한다. 즉 조립중인 문자는 누적시키는 것이 아니라 교체하는 것이다.

그래서 bComp TRUE인 경우, 즉 한글을 조립하고 있을 때는 buf의 뒤쪽 2바이트를 제거한 후 새 조립 코드를 buf에 붙여 기존의 조립중인 코드를 새 코드로 대체한다. 예제에서 조립중인 문자를 제거하는 코드인 buf[lstrlen(buf)-2]=0;을 주석으로 처리한 후 한글을 입력해보면 이 코드가 왜 필요한가 알 수 있을 것이다.

대한민국을 입력했는데 조립중의 임시 코드까지 모두 출력되어 버렸다. 한글은 영문, 숫자와는 달리 하나의 키입력으로 한 문자가 완성되지 않고 연속적으로 들어오는 키로부터 조립을 해 나가므로 완성되기 전의 문자는 계속 교체되어야 하며 최종적으로 완성된 문자만 버퍼에 붙여야 한다.

조립중인 문자를 처리중이므로 bComp TRUE로 변경하여 아직 조립중임을 기록해놓아야 하는데 조립중에도 bComp FALSE로 변경해야 하는 특수한 경우가 있다. IME는 조립중인 문자에 대해 <BS>키로 음소를 지울 수 있도록 하는데 만약 <BS>로 지워 나가다가 입력한 한 음절을 다 지워버리게 되면 이때는 조립이 취소된 것이므로 bComp FALSE로 변경해야 한다. 예를 들어 한글까지 입력한 후 한라로 변경하려고 <BS>키를 누른다고 해보자.

BS를 세 번 눌러 자를 완전히 삭제한 후 자를 입력하려고 할 때 bComp TRUE로 되어 있으면 앞에 있는 이미 완성된 자까지도 조립중인 문자로 취급하여 삭제해 버린다. 그래서 BS로 조립이 취소된 경우는 다음 입력을 위해 조립중임을 해제하는 특별한 처리가 필요하다. ImmGetCompositionString 함수로 조립중인 코드를 조사한 결과 그 길이가 0이면 조립되던 문자가 완전히 삭제된 것이므로 이때 bComp FALSE로 바꾸고 그 나머지 경우는 bComp TRUE로 바꾸면 된다. 이 코드가 왜 필요한지 직접 보고 싶으면 bComp를 무조건 TRUE로 바꾼 후 테스트해보면 된다.

 

//            if (len == 0) {

//                bComp=FALSE;

//            } else {

                   bComp=TRUE;

//            }

 

음절을 완전히 지우지 않은 상태에서는 별 상관이 없지만 한 음절을 삭제하고 다시 입력하려고 하면 앞쪽 글자가 깨지게 된다. bComp 값까지 변경한 후에는 조사된 조립 코드를 buf에 붙이고 화면을 무효화하여 조립중인 문자를 보여주도록 했다. 이 메시지에서 조립중인 문자를 처리했다고 해서 return 0;로 리턴해버리면 안되며 반드시 DefWindowProc으로 보내야 한다. 왜냐하면 IME도 이 메시지를 받아야만 조립 윈도우를 갱신할 수 있으며 한 음절이 완성될 때 WM_IME_CHAR 메시지가 보내지기 때문이다. 그래서 이 예제의 WM_IME_COMPOSITION 메시지는 처리 후 break; 문을 사용하고 있다.

WM_IME_CHAR 메시지는 문자가 완성될 때 보내진다. 이 메시지로는 한글 코드만 오는 것이 아니라 한글조립 후에 곧바로 입력되는 공백, 숫자, 기호 등의 1바이트 코드에 대해서도 발생한다. 예를 들어 다음 문장을 입력했다고 해보자.

다음의 공백, 다음의 2, 다음의 공백에 대해서도 WM_IME_CHAR 메시지가 발생한다. 1바이트 문자도 전달될 수 있기 때문에 wParam으로 전달된 코드가 DBCS인지 SBCS인지 조사한 후 szChar에 문자열을 먼저 만들고 이 문자열을 buf에 누적시켜야 한다. 이때도 물론 한글을 조립중이었으면 조립중인 문자를 먼저 제거해야 한다.

WM_IME_CHAR 메시지의 제일 끝에 있는 return 0;는 절대로 생략할 수 없다. 이 메시지를 처리한 후 리턴하지 않고 DefWindowProc으로 보내 버리면 디폴트 처리에 의해 완성된 한글 문자를 WM_CHAR로도 보내게 된다. 이렇게 되면 완성된 문자를 두 곳에서 처리하게 되므로 그야 말로 엉망진창이 될 것이다. return 0; break;로 바꿔 놓고 테스트해보면 얼마나 엉망이 되는지 직접 확인해 볼 수 있다. 이 메시지를 처리한 후 return 0;를 하는 것은 완성 문자는 내가 잘 처리했으니 더 이상 신경쓰지 말라는 명확한 의사 표현이다. 그럼 이제 예제를 실행해보고 한글과 영문, 숫자 등을 입력해보자.

조립중인 문자를 <BS>키로 수정할 수 있으며 후보 윈도우를 통해 한자를 입력할 수도 있다. 하지만 앞쪽으로 이동하거나 이미 완성한 문자를 삭제하는 것은 아직 안된다. 이런 처리는 좀 더 많은 코드를 작성해야 가능하다.

Ime1 예제는 짧은 길이에 비해 굉장히 많은 의미를 가지고 있다. 텍스트 편집기를 만드는 첫걸음에 해당하는 중요한 예제이며 커스텀컨트롤이나 워드프로세서를 만들더라도 이 예제에 대한 정확한 이해가 필요하다. IME 메시지가 언제 어떤 정보를 가지고 발생하는지 ImeMsg 예제로 살펴 보고 각 메시지들이 Ime1 예제에서 어떻게 처리되길래 문자열이 제대로 조립되는지 보기 바란다.