. 문자폭 배열

이 프로그램을 느리게 만드는 첫 번째 주범은 바로 GetCharWidth 함수가 수시로 호출해 대는 GetTextExtentPoint32 함수이다. 이 함수는 문자열의 폭과 높이를 계산하는데 내부적으로 아주 복잡한 처리를 한다. DC에 어떤 폰트가 선택되어 있는지 보고 폰트로부터 글자를 읽어와 각 문자의 폭을 합산한 후 그 결과를 리턴한다. 정확한 계산을 위해서 글꼴의 크기나 속성 등을 일일이 참고해야 하므로 느릴 수밖에 없다. 게다가 이 함수는 정렬, 이동시마다 항상 호출되기 때문에 호출 빈도도 아주 높다.

그렇다고 해서 문자의 폭을 구하지 않고서는 편집기를 만들 수가 없다. 가변폭 폰트는 각 글자마다 폭이 다르기 때문에 고정된 값을 사용할 수도 없고 말이다. 하지만 명쾌한 해결책은 있다. 문자의 폭은 필요하고 각 문자의 폭은 가변적이지만 실행중에 문자폭이 변하지는 않는다는 점에 착안하면 이 함수의 호출 횟수를 대폭 줄일 수 있다.

어떻게 하는가 하면 문자의 폭을 미리 구해놓고 필요할 때 구해놓은 값을 사용하는 것이다. 이렇게 하면 매번 GetTextExtentPoint32 함수를 호출하지 않고도 필요한 문자폭을 정확하게 얻을 수 있다. 각 문자의 폭을 저장하기 위해 다음 배열을 전역으로 선언한다.

 

int arChWidth[128];

BYTE *arHanWidth;

 

arChWidth는 영문과 기호의 폭을 저장할 배열이며 arHanWidth는 한글의 폭을 저장할 배열이다. 한글은 가변폭인 경우도 있고 고정폭인 경우도 있으므로 상황에 따라 배열크기가 달라지며 따라서 고정크기의 배열이 아닌 포인터로 선언되었다. 이 배열에 문자폭을 미리 계산하는 다음 함수를 작성하자.

 

void PrepareCharWidth(HDC hdc)

{

     int i;

     TCHAR Char[3]={0,};

     SIZE sz;

 

     for (i=1;i<128;i++) {

          Char[0]=i;

          GetTextExtentPoint32(hdc, Char, 1, &sz);

          arChWidth[i]=(BYTE)sz.cx;

     }

     arChWidth[0]=arChWidth[32];

 

     if (arHanWidth) {

          free(arHanWidth);

     }

     arHanWidth=(BYTE *)malloc(1);

     lstrcpy(Char,"");

     GetTextExtentPoint32(hdc, Char, 2, &sz);

     arHanWidth[0]=(BYTE)sz.cx;

}

 

1번 문자부터 차례대로 폭을 검사하여 arChWidth 배열에 그 폭을 기록해놓는다. 문자의 높이는 문자별로 다른 것이 아니므로 따로 저장해놓을 필요가 없다. 여기서 i 0부터 시작하는 것이 아니라 1부터 시작함을 유의하도록 하자. 0번 문자 즉 NULL종료 코드는 실제 문자가 아니기 때문에 폭을 계산할 수 없다. 그렇다고 해서 이 값을 아무렇게나 내버려둘 수는 없으므로 공백과 같은 폭을 가지는 것으로 간주하였다. NULL 문자의 폭은 차후에 덮어쓰기 모드에서 실제로 사용된다.

한글 문자의 폭은 대표적으로 자에 대해서만 폭을 계산하여 arHanWidth[0]에 저장해두었다. 시스템 폰트의 한글은 고정폭이며 대부분의 한글 폰트는 고정폭므로 이 배열의 크기는 일단 1이면 된다. 한글이 가변폭인 경우는 차후에 따로 처리하도록 하자. 이 함수는 OnCreate에서 딱 한 번만 호출한다.

 

BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     ....

    PrepareCharWidth(hdc);

     ReleaseDC(hWnd,hdc);

     ....

 

문자폭을 구하기 위해서는 DC의 핸들이 필요하므로 ReleaseDC로 핸들을 해제하기 전에 이 함수를 호출한다. 이 함수가 할당한 arHanWidth 배열은 비록 크기는 작지만 메모리를 차지하고 있으므로 종료시에 해제해야 한다.

 

void OnDestroy(HWND hWnd)

{

     PostQuitMessage(0);

     free(buf);

    free(arHanWidth);

}

 

이제 프로그램이 시작될 때 각 문자의 폭을 구해 arChWidth 배열과 arHanWidth 배열에 그 폭을 기록해놓았으므로 GetCharWidth 함수는 매번 GetTextExtentPoint32 함수를 호출할 필요없이 이 배열에서 문자의 폭을 읽을 수 있다. 실시간으로 문자의 폭을 구하는 것과 배열값을 읽는 것은 속도상 비교가 되지 않을 정도로 차이가 많이 난다. 또한 DC로부터 문자폭을 구하는 것이 아니므로 GetCharWidth 함수는 DC의 핸들을 전달받을 필요가 없어졌다. 이 함수는 다음과 같이 수정된다.

 

int GetCharWidth(TCHAR *ch, int len)

{

     if (len==1) {

          return arChWidth[*ch];

     } else {

          return arHanWidth[0];

     }

}

 

폭을 구할 문자 길이가 1이면 arChWidth 배열에서 폭을 구하고 길이가 2이면 arHanWidth에서 폭을 구해 리턴하면 된다. arChWidth는 문자코드를 배열의 첨자로 사용하고 있으므로 전달된 문자의 값으로부터 별다른 여분의 계산없이 바로 배열값을 읽을 수 있다.

이 함수가 이렇게 바뀜으로써 GetCharWidth를 호출하는 모든 함수들도 수정되어야 한다. 모두 여섯 개의 함수가 이 함수를 호출하고 있는데 SetCaret, GetLineSub, GetXYFromOff, GetXPosOnLine, GetOffFromXY, DrawSegment 등이다. 대표적으로 SetCaret 함수를 수정해보자.

 

void SetCaret(BOOL bUpdatePrevX/*=TRUE*/, BOOL bScrollToCaret/*=TRUE*/)

{

     int toff;

     int caretwidth;

     int x,y;

     RECT crt;

     int ty;

     BOOL bScroll=FALSE;

 

     if (bComp) {

          toff=off-2;

        caretwidth=GetCharWidth(buf+toff,2);

     } else {

          toff=off;

          caretwidth=2;

     }

     ....

 

지역변수 hdc가 삭제되었으며 GetDC 호출문과 ReleaseDC 호출문도 삭제되었다. DC를 생성하고 해제하는 코드도 꽤 시간을 많이 소모하므로 GetDC만 제거해도 속도상의 많은 이득을 볼 수 있다. GetCharWidth 함수 호출문에도 hdc 인수를 빼야 한다. 나머지 함수들도 동일한 방법으로 수정하도록 하되 DrawSegment 함수는 수정하는 방법이 좀 다르다. 상기의 함수들은 한 문자의 폭만 구하지만 이 함수는 문자열의 폭을 구하므로 배열을 곧바로 읽을 수는 없다. 그래서 문자열의 폭을 구하는 별도의 추가 함수를 만들 필요가 있다.

 

int MyGetTextExtent(TCHAR *text, int len)

{

     TCHAR *p=text;

     int acwidth;

 

     for (acwidth=0;p-text!=len;) {

          if (IsDBCS(p-buf)) {

               acwidth+=GetCharWidth(p,2);

               p+=2;

          } else {

               acwidth+=GetCharWidth(p,1);

               p++;

          }

     }

     return acwidth;

}

 

이 함수는 text 문자열의 폭을 구해 리턴하는데 내부적으로 GetCharWidth 함수를 호출하여 미리 구해놓은 배열을 참조하고 있으므로 속도는 확실히 빨라진다. DrawSegment에서 이 함수를 호출하도록 수정하자.

 

void DrawSegment(HDC hdc, int &x, int y, int SegOff, int len, BOOL ignoreX, COLORREF fore, COLORREF back)

{

     ....

          if (ignoreX == FALSE) {

            x+=MyGetTextExtent(buf+SegOff,len);

          }

     }

}

 

이제 GetTextExtentPoint32 함수는 유일하게 PrepareCharWidth 함수에서 최초 실행시 딱 한 번만 호출되며 그 외의 나머지 부분에서는 이 함수가 미리 구해놓은 결과만을 사용한다. 과연 얼마나 빨라졌는지 컴파일한 후 다시 속도 측정을 해보도록 하자. 결과는 시스템마다 다르겠지만 어차피 상대적인 측정이므로 절대적인 시간은 중요하지 않다. 다음은 나의 노트북에서 테스트한 결과이다.

 

테스트

1

2

3

4

5

6

최초

14.29

15.4

28.89

37.29

46.52

78.03

문자폭 계산

2.92

2.87

5.76

7.10

8.72

15.14

 

배열 하나를 도입했을 뿐인데 속도는 평균 다섯 배 정도 개선되었다. 문장을 입력해보면 체감할 수 있을 만큼 빨라져 있을 것이다. 놀라운 개선 효과이기는 하지만 아직도 여전히 느리기는 마찬가지다.