. 예제의 변형

Ime1 예제는 비록 한 줄밖에 입력을 할 수 없고 이미 입력한 문자를 수정할 수도 없지만 그래도 완벽하게 동작한다. 그런데 결과코드를 보면 이해가 가겠지만 왜 꼭 저런 식으로 코드를 작성해야 하는지, 좀 다른 방식으로 메시지를 처리할 수는 없을까 하는 생각이 들기도 할 것이다. Ime1 예제의 코드에 일단 의심을 가져 보고 코드를 다양하게 변형시켜 보면서 실습을 계속 해보도록 하자. 만약 Ime1 예제를 100% 이해했고 다른 방식의 코드는 보고 싶지 않다면 이 실습은 건너 뛰어도 좋다.

사본 만들기

Ime1 예제를 수정하여 약간 다르게 동작하는 예제를 만들어 보려고 한다. Ime1 예제는 그대로 두고 이 예제를 계속 수정해보고 싶은 것이다. 이럴 때는 똑같이 동작하는 다른 이름의 예제를 만든 후 코드를 수정해보면 되나 동일한 예제를 다시 만드는 것은 아주 귀찮은 작업이다. 앞으로도 계속 이전 예제에 코드를 추가한 예제를 만들게 될 텐데 그때마다 처음부터 프로젝트 만들고, 소스 만들고, imm32.lib를 연결시키는 작업을 반복한다는 것은 너무 비효율적이다.

그래서 좀 더 쉬운 방법으로 프로젝트 폴더를 통째로 복사한 후 폴더 이름만 바꾸고 개발을 계속하는 편법을 쓰기로 한다. 같은 프로젝트의 사본을 만든 후 변형해보는 것이다. 방법은 다음과 같다. Ime1 프로젝트의 사본 Ime1a를 만드는 과정이다.

탐색기로 폴더를 복사하여 이름만 바꾸면 된다. 프로젝트가 열려 있는 상태에서는 제대로 복사되지 않으므로 반드시 원본 폴더의 프로젝트를 닫은 상태에서 복사해야 한다. 이렇게 되면 원본과 사본의 코드는 물론 프로젝트 이름도 동일하므로 폴더의 이름만으로 프로젝트를 구분해야 한다. 사본의 프로젝트 이름이 Ime1이라도 이 프로젝트의 폴더명이 Ime1a이면 이 프로젝트를 Ime1a로 칭하므로 앞으로 책을 읽는데 착오가 없기 바란다.

비주얼 C++ 7.0을 사용하고 있다면 작성하던 프로젝트의 사본 프로젝트는 처음 열었을 때 한 번 다시 빌드(Build All)하는 것이 좋다. 비주얼 C++ 7.0은 소스파일의 절대경로를 저장하는 맹점이 있고 종속성 관리에 약간의 버그가 있는 것 같다. 6.0은 오랜 기간 동안 안정화를 해왔기 때문에 이런 문제가 거의 없다.

Ime1 프로젝트를 계속 만들지 않고 새로 프로젝트의 사본을 만드는 이유는 실습의 단계를 명확하게 구분하기 위해서이다. 혹시 실습중에 잘못된 코드를 작성했다면 프로젝트를 지우고 다시 이전 단계의 소스를 가져오기만 하면 된다. 중간쯤의 프로젝트를 다시 실습해보고 싶다면 바로 이전 단계로 쉽게 돌아갈 수 있다. 이후의 실습 단계도 마찬가지이며 큰 기능별로 매번 프로젝트를 새로 만들 것이다. CD-ROM의 예제들은 이런 식으로 실습 단계별로 작성되어 있다.

Ime1a

앞에서 설명한대로 Ime1 예제를 복사하여 Ime1a 예제를 만들어 보도록 하자. 복사한 사본이므로 코드는 완전히 동일한데 제일 위에 있는 다음 한 줄만 수정하도록 하자. 꼭 이 문자열을 고칠 필요는 없지만 그래도 프로젝트를 바꾸었으므로 타이틀바에 나타나는 이름도 바꾸는 것이 좋을 것 같다. 앞으로의 실습에서도 이 문자열은 알아서 바꾸기 바란다. 물론 귀찮으면 바꾸지 않아도 상관없다.

 

LPCTSTR lpszClass=TEXT("Ime1a");

 

WM_IME_COMPOSITION 메시지는 조립중일 때도 보내지지만 문자가 완성될 때도 보내진다. ImeMsg 예제를 실행해서 확인해보면 알겠지만 조립중일 때는 GCS_COMPSTR 플래그가 설정되고 완성되었을 때는 GCS_RESULTSTR 플래그가 설정된다. 그렇다면 굳이 완성문자 처리를 위해 WM_IME_CHAR 메시지를 처리할 필요없이 GCS_RESULTSTR 플래그를 받았을 때 처리해도 되지 않을까?

과연 그런지 테스트해보기 위해 Ime1a 프로젝트의 소스를 다음과 같이 변형해보자. WM_IME_CHAR 메시지 처리 부분은 과감하게 제거하였으며 WM_IME_COMPOSITION 메시지 처리 부분을 수정하였다.

 

     case WM_IME_COMPOSITION:

          hImc=ImmGetContext(hWnd);

          if (lParam & GCS_COMPSTR) {

               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;

               }

          }

          if (lParam & GCS_RESULTSTR) {

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

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

               ImmGetCompositionString(hImc,GCS_RESULTSTR,szComp,len);

               szComp[len]=0;

               if (bComp) {

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

               }

               bComp=FALSE;

          }

          lstrcat(buf,szComp);

          ImmReleaseContext(hWnd,hImc);

          free(szComp);

          InvalidateRect(hWnd,NULL,TRUE);

          if (lParam & GCS_COMPSTR) {

               break;

          } else {

               return 0;

          }

 

GCS_RESULTSTR 플래그가 설정되었을 때 완성문자를 조립하도록 했다. 물론 이 경우도 DefWindowProc으로 제어가 가지 않도록 하여 WM_CHAR 메시지는 막아야 한다. 이 예제는 Ime1 예제와 마찬가지로 제대로 동작하며 논리적으로 큰 무리가 없다. 여기서 논리적으로 무리가 없다는 표현은 지금 당장 그렇다는 것이지 예제가 확장되거나 일본어나 중국어까지 처리하고자 한다면 달라질 수도 있다는 뜻이다. 적어도 한글 윈도우즈에서만큼은 Ime1이나 Ime1a나 동일하다.

Ime1b

Ime1 예제는 WM_IME_COMPOSITION에서 현재 조립중인 문자의 코드를 구하기 위해 ImmGetCompositionString 함수를 사용하고 있다. 그런데 메시지 레퍼런스를 보면 조립중인 문자의 코드가 wParam으로 전달된다고 되어 있다. 그렇다면 Ime1 예제는 시스템이 이미 구해서 전달한 코드를 이중으로 다시 조사하고 있다는 말이 되는데 그렇게 하지 말고 wParam의 코드를 사용해보도록 하자. Ime1 예제의 사본을 만들어 Ime1b 프로젝트를 만들고 다음과 같이 코드를 수정한다.

 

     case WM_IME_COMPOSITION:

          if (lParam & GCS_COMPSTR) {

               if (bComp) {

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

               }

               bComp=TRUE;

 

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

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

               szChar[2]=0;

               lstrcat(buf,szChar);

               InvalidateRect(hWnd,NULL,TRUE);

          }

          break;

 

ImmGetCompositionString 함수를 사용하지 않았으며 wParam으로 전달된 문자를 buf에 누적시켰다. 조립 문자열을 직접 구하지 않았으므로 입력 컨텍스트를 관리할 필요도 없고 임시버퍼를 할당할 필요도 없어졌다. 조립 문자열의 길이를 알 수 없기 때문에 wParam은 무조건 2바이트라고 가정하고 있는데 이 가정이 별 문제가 없는지 실행해보자. 문자열을 입력해보면 별 문제가 없으며 Ime1 예제와 마찬가지로 동작한다. 조립중인 문자는 한글이므로 2바이트가 맞다.

조립 코드를 구하기 위해 별도의 함수를 호출하지 않고 wParam값을 바로 읽었으며 메모리를 할당하지 않으므로 당연히 실행 시간이 더 빨라질 것이다. 하지만 그렇다고 해도 프로그램의 속도 향상에는 별로 기여하지 못한다. 왜냐하면 이 메시지는 사람이 키보드를 두드릴 때만 발생하며 사람이 키보드를 누르는 속도는 형편없이 느리기 때문이다. Ime1 Ime1b는 조립 문자열을 구하는 방법상의 차이만 있을 뿐 성능상의 차이는 전혀 없다.

Ime1c

이번에는 WM_IME_CHAR 메시지를 처리하지 않는 Ime1a예제를 변형하여 wParam의 조립 코드를 사용해보도록 하자. Ime1a 프로젝트를 복사하여 Ime1c 프로젝트를 만들고 코드를 다음과 같이 수정하였다.

 

     case WM_IME_COMPOSITION:

          if (bComp) {

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

          }

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

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

          szChar[2]=0;

          lstrcat(buf,szChar);

          InvalidateRect(hWnd,NULL,TRUE);

          if (lParam & GCS_COMPSTR) {

               bComp=TRUE;

               break;

          } else {

               bComp=FALSE;

               return 0;

          }

 

Ime1b예제와 마찬가지로 wParam에서 조립 코드를 읽었다. 그러나 직접 테스트해보면 알겠지만 이 예제는 제대로 동작하지 않는다. 한글조립중에 입력하는 숫자나 공백이 버퍼에 추가되지 않는 문제가 있어 띄어쓰기가 전혀 되지 않는다.

왜냐하면 조립중인 코드는 2바이트이지만 완성문자열은 2바이트 이상일 수도 있기 때문이다. 왜 그런지 테스트해보기 위해 Ime1a 예제의 다음 위치에 중단점을 설정하고 실행해보자.

 

     case WM_IME_COMPOSITION:

          ....

          if (lParam & GCS_RESULTSTR) {

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

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

               ImmGetCompositionString(hImc,GCS_RESULTSTR,szComp,len);

            szComp[len]=0;

 

한글 한 문자와 숫자를 같이 입력해보면 이 중단점에서 멈출 것이다. 예를 들어 3을 입력했다고 하자. 중단점에서 멈추었을 때 조사된 완성 코드를 보면 3이 조사되고 길이는 3이 된다. 자를 조립중인 상태에서 3을 누르면 자도 완성되지만 3자도 같이 완성되기 때문이다. ImmGetCompositionString 함수는 완성된 모든 문자를 한꺼번에 조사하므로 이 함수가 조사한 대로 buf에 덧붙이면 아무 문제가 없다. 그러나 wParam은 최종 코드만 전달하므로 완성문자를 다룰 때는 이 인수를 사용하지 말아야 한다.

Ime1 예제와 그 변형 프로젝트를 통해 IME 메시지를 처리하는 몇 가지 예를 실습해보았는데 메시지를 처리하는 방법이 여러 가지라는 것을 알 수 있다. 이것은 비단 IME에서만 그런 것이 아니라 어떤 메시지를 처리하나 마찬가지이다. 예를 들어 WM_PAINT 메시지에서 DC 핸들을 구하는 방법은 두 가지가 있고 WM_SIZE lParam으로 전달된 크기를 사용할 수도 있고 GetClientRect를 호출할 수도 있다. 문제를 푸는 방법은 항상 복수 개가 있으며 어떤 한 가지가 절대적인 정답이라고 말할 수는 없는 것이다.

다만 여러 가지 방법 중 현재 상황에 가장 어울리고 문제가 없는 한 가지 방법을 선택할 수 있을 뿐이다. IME 메시지를 처리하는 방법도 앞에서 예를 든 방법 외에 여러 가지가 더 있을 수 있으며 언어가 바뀌면 방법도 바뀌어야 한다. 다방면으로 이 예제들을 분석해 본 결과 Ime1 예제의 방식이 가장 문제가 없고 확장성에 유리한 것 같으므로 앞으로는 이 방식대로 IME 메시지를 처리할 것이다.