. 탭 출력

OnChar에서 입력해놓은 탭문자를 출력하는 일은 OnPaint에서 해야 한다. 문자열을 출력하다가 탭을 만나면 그 위치에서 탭의 폭을 계산해서 폭만큼 여백을 만들면 된다. OnPaint에서 이런 작업까지 같이 하려면 출력코드가 점점 더 복잡해질 것이며 혼자서 이런 처리를 다 하기가 어려워진다. 그래서 OnPaint를 도와줄 서브함수를 작성하였다.

 

int DrawLine(HDC hdc, int Line)

{

     int s,e;

     int x;

     int len;

     int nowoff;

 

     GetLine(Line,s,e);

     if (s == -1)

          return 0;

 

     x=0-xPos;

     nowoff=s;

     for (;;) {

          for (len=0;;) {

               if (buf[nowoff+len] == ‘\t’) {

                   if (len == 0)

                        len=1;

                   break;

               }

               if (nowoff+len == e) {

                   break;

               }

               len++;

          }

 

          DrawSegment(hdc,x,Line*LineHeight-yPos,nowoff,len,(nowoff+len==e));

 

          nowoff+=len;

          if (nowoff == e)

               return 1;

     }

}

 

이 함수는 두 번째 인수로 전달된 Line 줄을 출력하는 일을 하는데, DC 핸들은 OnPaint에서 전달한다. 먼저 GetLine 함수를 호출하여 줄의 범위를 구하는데 이때 만약 s -1이면, 즉 존재하지 않는 줄이면 0을 리턴하여 OnPaint가 출력을 그만둘 수 있도록 해야 한다. 정상적으로 출력했으면 1을 리턴하면 된다.

줄의 처음 오프셋 s부터 시작해서 문자를 모아 출력할 조각(Segment)을 만들되 탭을 만나면 조각을 끊는다. 그래서 문자열은 탭문자를 기준으로 조각이 나누어지며 한 줄은 여러 개의 조각으로 쪼개져 각각 따로 출력된다. 만약 탭이 없는 문장이라면 줄 전체는 통째로 하나의 조각이 될 것이다. 다음 문장은 가운데 탭문자에 의해 3개의 조각으로 나누어진다.

루프에서 nowoff 변수는 현재 모으고 있는 조각의 선두 오프셋인데 최초 줄의 선두 오프셋인 s로 초기화되며 한 조각을 출력할 때마다 조각의 길이만큼 증가한다. 조각을 모으는 안쪽 for 루프에서 len 0으로 초기화되고 탭을 만나거나 또는 줄 끝에 이를 때까지 len을 증가시키며 조각을 구성하는 문자를 하나씩 수집한다. 이때 한글, 영문은 별도로 구분할 필요가 없으며 IsDBCS 함수를 쓸 필요 없이 코드를 바로 읽어도 상관없다. 왜냐하면 한글, 영문은 조각을 나누는 구분자가 아니기 때문이다. 오로지 탭문자와 줄 끝 조건만이 조각을 나눈다.

첫 번째 루프를 돌 때 탭이 있는까지 조각을 모은 후 탭(\t)을 만나게 되는데 이때 길이 len 5의 값을 가질 것이다. 루프를 빠져 나와 수집된 조각을 출력하고 nowoff는 다음 오프셋으로 이동한다. 다시 조각 수집 루프로 돌아오면 또 \t를 만나게 되고 이때 len 0이다. 길이가 0이라는 것은 조각을 모으자 마자, 즉 조각의 처음이 탭이라는 뜻이며 이때는 탭문자만 하나의 조각으로 만들어 출력한다. 그래서 len을 강제로 1로 만들었다. 탭을 출력한 후 다시 루프를 돌며 문장입니다 조각을 모아 출력하고 줄 끝을 만나면 이 함수가 종료된다.

조각을 찾는 루프에서 if (len == 0) len=1이라는 조건문은 다른 문자열 조각을 검색하는 중에 탭문자를 만난 것인지 아니면 진짜 탭을 만난 것인지를 판단한다. 전자의 경우 탭을 만났다는 사실은 앞쪽 문자열 모으기를 완성한다는 뜻이고 후자의 경우는 탭만으로 하나의 조각을 이루어 출력하도록 한다는 뜻이다. 탭을 만났는데 그때 len 0이라는 말은 다른 문자열의 구분자로서의 탭이 아니라 당장 적용해야 할 탭이라는 뜻이다. 이런 식으로 루프를 돌며 조각을 모으고 각 조각을 출력하면 한 줄 출력이 완료된다.

DrawLine은 조각을 나누는 일만 하고 조각을 화면으로 출력하는 일은 DrawSegment 서브함수가 담당한다.

 

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

{

     if (buf[SegOff] == ‘\t’) {

          x= (x/TabSize+1)*TabSize;

     } else {

          TextOut(hdc,x,y,buf+SegOff,len);

          if (ignoreX == FALSE) {

               x+=GetCharWidth(hdc,buf+SegOff,len);

          }

     }

}

 

여섯 개나 되는 인수를 받는데 각 인수에 대해 먼저 정리하자. hdc는 출력할 DC이고 x, y는 조각을 출력할 화면상의 좌표이다. 조각을 출력하고 난 후에 다음 조각을 위해 수평좌표를 방금 출력한 조각의 길이만큼 증가시켜 돌려줘야 하므로 x는 레퍼런스로 전달받는다. SegOff len은 조각의 시작 오프셋과 길이이다. 마지막 인수 ignoreX x위치를 정말 갱신할 것인가를 지정하는데 잠시 후에 다시 살펴보자.

이 함수는 전달받은 조각이 탭이 아니면 기존 OnPaint와 동일하게 문자열을 출력한다. 문자열을 출력한 후에는 출력한 문자열의 폭을 x에 더하되 단 ignoreX TRUE일 때는 그럴 필요가 없다. 세 개의 조각을 출력할 때 이 함수는 세 번 호출되며 각 호출에서의 동작은 다음과 같다.

조각을 출력하고 난 후에는 다음 조각 출력위치를 알려주기 위해 x위치를 조각의 끝으로 이동시켜 놓아야 한다. 그러나 마지막 조각 출력 후는 더 이상 출력할 조각이 없기 때문에 이 동작을 하지 않아도 된다. 불필요하게 x 좌표를 갱신하지 않도록 함으로써 출력속도를 높일 수 있다. 탭문자가 없는 문장은 조각이 하나밖에 없다. 일반적인 경우가 그럴 것이며 따라서 줄 전체가 마지막 조각이다. 이때 x 좌표 갱신을 생략하면 속도상의 큰 이점이 있다. GetTextExtentPoint32 함수는 아주 느린 함수이며 가급적이면 호출을 자재해야 한다.

DrawLine에서 DrawSegment를 호출할 때 ignoreX인수는 논리식 (nowoff+len==e)의 평가값으로 전달된다. 이 논리식은 조각의 끝이 줄의 끝과 같은가를 묻고 있으며 즉, 지금 출력할 조각이 마지막인가를 조사한다. DrawSegment는 이 논리식의 결과에 따라 x 좌표를 갱신할 것인가 무시할 것인가를 결정한다.

다음은 조각이 탭문자인 경우를 보자. 탭문자는 현재 x위치에서 다음 탭위치로 이동시키면 되는데 이 동작은x=(x/TabSize+1)*TabSize 수학식 하나로 간단히 해결된다. 부쩍 자주 등장하는 공식인데 A=A/B*B A B의 근접배수로 내림하는 것에 비해 A=(A/B+1)*B A B의 근접배수로 올림하는 것이다.

x=(x/TabSize+1)*TabSize는 현재 x위치에서 다음 TabSize의 배수로 가라는 명령이며 바로 이 수학식이 탭 처리의 핵심이다. TabSize 32일 때 0~31까지는 32가 되고 32~63까지는 64가 될 것이다. 탭위치에 정확하게 걸린 경우도 다음 탭위치로 보내는 것에 대해서는 조금 다른 생각이 들 수도 있는데 32 위치에 있을 때는 그냥 둘 것인가 아니면 64로 보내는 것이 맞는가? 결론은 보내는 것이 맞다.

만약 32가 탭의 배수라고 하여 64가 되지 않고 그 자리에 있도록 처리한다면 같은 논리로 0에서 32로 가지 않을 것이다. 첫 번째 칸에서 탭을 입력했는데 여기 탭 자린데요하고 캐럿이 버티고 있는다면 좀 이상하지 않은가? x=(x/TabSize+1)*TabSize 공식은 정확하게 다음 탭위치를 찾아준다. 이 공식은 더 이상의 의심을 할 여지가 없을 정도로 정확한 것 같지만 아주 민감한 오류가 있다. 어떤 오류인지 다음 설명을 읽기 전에 직접 맞추어 보되 정답을 보지 않고 문제를 파악할 수 있다면 대단한 추리력을 가지고 있다고 자부해도 좋다.

이 공식은 x가 양수일 때는 항상 정확하지만 음수일 때는 틀린 위치를 찾아준다. 자동개행 상태가 아니면 수평스크롤이 가능해지며 문서의 오른쪽으로 스크롤하면 출력좌표가 왼쪽의 보이지 않는 음수영역이 될 수 있는데 이때는 A=(A/B+1)*B 공식이 통하지 않는다. 일반적인 경우의 증명을 해 볼 필요없이 특수한 경우 하나만 생각해보면 되는데 x -5라고 해보자. 그러면 다음 탭위치는 0이 되어야 하는데 그렇지 않고 32가 된다. 이 공식에서 피연산자인 x는 화면상의 좌표인데 xPos TabSize의 배수가 아닐 때는 이 공식이 적용되지 않는다.

그래서 x를 먼저 문서상의 x 좌표로 변경한 후에 이 공식을 적용하여 위치를 계산하고 계산된 결과를 다시 화면상의 x 좌표로 바꿔야 한다. 다음이 스크롤 상태까지 고려하여 수정된 DrawSegment 함수이다.

 

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

{

     int docx;

 

     if (buf[SegOff] == ‘\t’) {

          docx=x+xPos;

          docx=(docx/TabSize+1)*TabSize;

          x=docx-xPos;

     } else {

     ....

 

docx는 문서상의 x 좌표값이며 이 값을 공식에 대입하여 문서상의 다음 탭위치를 찾고 이 위치를 다시 화면상의 위치로 바꾸었다. 탭은 x 위치만 옮기고 문자는 출력하지 않는다. 어차피 탭도 공백이므로 자리를 비워 두기만 하면 된다.

한 줄을 출력하는 DrawLine 함수가 만들어졌고 DrawLine이 잘라놓은 조각을 출력하는 DrawSegment 함수가 만들어졌으니 이제 OnPaint는 다음과 같이 간단해진다.

 

void OnPaint(HWND hWnd)

{

     HDC hdc;

     PAINTSTRUCT ps;

     int l;

 

     hdc=BeginPaint(hWnd,&ps);

     for (l=0;;l++) {

          if (DrawLine(hdc,l) == 0)

               break;

     }

     EndPaint(hWnd,&ps);

}

 

첫 줄부터 루프를 돌며 졸병 함수들만 불러주면 되므로 정말 부담이 없어졌다. 하지만 조금만 더 지나면 OnPaint는 원래 있던 코드보다 훨씬 더 커지게 된다. 그만큼 문서 모양을 정확하게 출력한다는 것이 어렵다는 뜻인데 그래서 더 복잡해지기 전에 미리 보조 함수들을 작성하여 분업 체계를 만들어 놓았다.

여기서는 탭문자 출력을 위해 조각(Segment)이라는 개념을 도입했다. 조각이란 한 번에 같이 출력할 수 있는 성질이 같은 문자열의 집합으로 정의된다. 현재는 탭만이 조각을 구분하는 구분자가 되지만 앞으로 선택이나 문법강조 등에도 이 개념이 계속 적용될 것이다. 이렇게 한 번 개념을 잘 정리해놓으면 논리성이 강화되고 예제를 확장하기가 쉬워진다.