. 탭과 공백

(Tab) 문자와 공백(Space)은 둘 다 보이지 않으며 칸을 띄우기 위한 용도로 사용한다는 점에서 동일하다. 하지만 여백의 크기가 다르다는 점에서 분명히 용도가 다른데 공백은 단어 사이를 띄우고 일정한 폭을 가지는데 비해 탭은 정해진 배수 위치까지 띄우고 시작위치에 따라 폭이 가변적이다. 탭은 주로 들여쓰기로 문서의 구조를 표현하는 소스 코드 편집시에 많이 사용된다.

들여쓰기는 공백으로 할 수도 있고 탭으로 할 수도 있다. 일반적으로 탭이 더 큰 여백을 표현하기 때문에 문서 크기가 작고 여러 단계의 들여쓰기를 쉽게 표현할 수 있어 소스 코드를 작성할 때는 탭을 사용하는 것이 더 좋다. 하지만 편집기마다 탭의 크기를 정하는 기준이 달라서 호환성에 불리하고 글꼴에 따라 문서의 모양이 달라지는 문제점이 있다. 공백은 여백의 크기가 작아 여러 개의 공백을 반복적으로 사용해야 하지만 문서의 모양이 일정하다는 장점이 있다.

들여쓰기를 공백으로 표현할 것인가 탭으로 표현할 것인가는 사용자의 취향과 작성하는 문서의 종류에 따라 달라진다. 그래서 탭키를 누를 때 어떤 문자를 삽입할 것인지를 옵션으로 선택할 수 있도록 하는 것이 좋다. 이 옵션을 위해 bSpaceForTab 옵션을 새로 선언한다.

 

class CApiEdit

{

     ....

     BOOL bSpaceForTab;

     BOOL bAutoIndent;

     BOOL bBlockIndentWithTab;

 

bSpaceForTab TRUE이면 탭키를 누를 때 탭문자 대신 공백을 삽입한다. 나머지 두 옵션도 탭과 관련된 옵션인데 잠시 후 실습할 예정이므로 미리 선언해놓았다. SetDefaultSetting에서 이 옵션을 FALSE로 초기화하여 탭키는 탭문자를 입력하도록 한다.

 

void CApiEdit::SetDefaultSetting()

{

     ....

     bSpaceForTab=FALSE;

     bAutoIndent=TRUE;

     bBlockIndentWithTab=TRUE;

 

외부에서 이 값을 TRUE로 바꾸면 탭을 누를 때 탭과 같은 폭의 공백 문자들을 대신 삽입할 것이다. 이 옵션은 문자를 입력받는 OnChar에서 처리한다. 다음과 같이 코드를 추가한다.

 

void CApiEdit::OnChar(HWND hWnd, TCHAR ch, int cRepeat)

{

    TCHAR szChar[256];

     int i;

     BOOL bPrevSel;

     ....

     if (ch == ‘\r’) {

          szChar[0]=‘\r’;

          szChar[1]=‘\n’;

          szChar[2]=0;

     } else {

          szChar[0]=ch;

          szChar[1]=0;

     }

 

    if (ch==‘\t’) {

        if (bSpaceForTab) {

           for (i=0;i<TabWidth;i++) {

               szChar[i]=‘ ‘;

           }

           szChar[i]=0;

        }

    }

     ....

 

탭 입력시 TabWidth만큼의 공백으로 szChar를 채우고 이 문자열을 문서에 대신 삽입하도록 하였다. bSpaceForTab FALSE이면 아무런 처리도 하지 않고 탭문자가 그대로 삽입되도록 내버려 둔다. 탭의 폭이 클 경우를 대비하여 szChar의 길이를 3에서 256으로 충분히 길게 늘려 주었다. 그래서 탭키 한 번의 입력으로 공백을 최대 255개까지 입력할 수 있다.

입력할 때 탭을 공백으로 바꿀 수 있는 것처럼 이미 입력된 탭을 같은 폭의 공백으로 바꾸거나 반대로 공백을 탭으로 바꿀 수도 있다. 탭과 공백은 비슷한 용도로 사용되기 때문에 언제든지 서로 전환이 가능한 문자이다. , 공백 전환 기능을 사용하면 이미 작성된 문서를 자신이 읽기 편하게 바꿔서 읽을 수 있다. 탭과 공백을 상호 전환하는 다음 멤버함수를 추가하도록 하자.

 

void CApiEdit::ConvertSpaceTab(BOOL bToSpace)

{

     int SelFirst, SelSecond;

     TCHAR *src,*dest;

     TCHAR *s,*d;

     int len;

     int nDiff;

     int i;

     BOOL bFind;

 

     SelFirst=min(SelStart,SelEnd);

     SelSecond=max(SelStart,SelEnd);

     len=SelSecond-SelFirst;

     if (len==0) {

          return;

     }

 

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

     lstrcpyn(src,buf+SelFirst,len+1);

     for (s=src,i=0;*s;s++) {

          if (*s==‘\t’)

              i++;

     }

     dest=(TCHAR *)malloc(len+1+i*(TabWidth-1));

 

     s=src;

     d=dest;

     nDiff=0;

     if (bToSpace) {

          while (*s) {

              if (*s==‘\t’) {

                   for (i=0;i<TabWidth;i++) {

                        *d++=‘ ‘;

                   }

                   nDiff+=(TabWidth-1);

                   s++;

              } else {

                   *d++=*s++;

              }

          }

     } else {

          while (*s) {

              bFind=TRUE;

              for (i=0;i<TabWidth;i++) {

                   if (s[i]==0 || s[i]!=‘ ‘) {

                        bFind=FALSE;

                        break;

                   }

              }

 

              if (bFind) {

                   *d++=‘\t’;

                   s+=TabWidth;

                   nDiff+=(-TabWidth+1);

              } else {

                   *d++=*s++;

              }

          }

     }

     *d=*s;

     SelSecond += nDiff;

 

     StartUndoGroup();

     Delete(SelFirst,lstrlen(src));

     Insert(SelFirst,dest);

     EndUndoGroup();

 

     if (SelStart < SelEnd) {

          off=SelEnd=SelSecond;

     } else {

          SelStart=SelSecond;

     }

 

     Invalidate(SelFirst);

     SetCaret();

     free(src);

     free(dest);

}

 

전체적인 구조는 대소문자를 변환하는 ChangeCase 함수와 유사하다. 차이점이라면 변환 후 길이가 변할 수 있다는 점인데 탭을 공백으로 바꾸면 길이가 늘어날 것이고 공백을 탭으로 바꾸면 길이가 줄어들 것이다. 그래서 변환하기 전의 원본 문자열을 저장할 src 버퍼와 변환된 후의 결과 문자열을 저장할 dest 버퍼가 각각 필요하다. src 버퍼의 길이는 선택영역의 길이에 널 종료문자분을 더한 만큼이고 dest버퍼의 길이는 원본 길이에 늘어날 수 있는 최대 문자 수를 더한 만큼이다.

원본 문자열에 몇 개의 탭이 있는지 계산한 후 이 값에 TabWidth-1을 곱하면 변환 후의 버퍼가 얼마나 늘어날지 미리 예측할 수 있다. 탭문자 하나를 공백으로 변환하면 TabWidth-1만큼 길이가 늘어난다. bToSpace 인수는 변환 방법을 지정하는데 이 값이 TRUE이면 탭을 공백으로 바꾸고 FALSE이면 반대로 공백을 탭으로 바꾼다.

먼저 탭을 공백으로 바꾸는 코드를 보자. 코드를 한 번 읽어보면 이해가 될 정도로 쉽다. src의 처음부터 끝까지 한문자씩 읽어 dest에 기록하되 탭문자를 만나면 탭 대신 TabWidth만큼의 공백으로 대신 채워 넣으면 된다. 공백을 탭으로 바꾸는 방법은 이에 비해 조금 더 복잡한데 공백을 무조건 탭으로 바꾸는 것이 아니라 TabWidth개 만큼의 연속적인 공백이 있을 때만 이 공백들을 하나의 탭문자로 바꾼다. 만약 TabWidth개 이하의 공백이 발견되면 이 문자들은 탭으로 변환되지 않고 그대로 공백으로 남겨진다.

src의 모든 문자에 대해 공백과 탭을 변환한 후 마지막으로 src의 널 종료문자까지 dest로 복사함으로써 변환을 마친다. 이렇게 변환된 결과를 다시 문서에 삽입하는 방법은 대소문자 변환시와 동일하다. 선택 시작점에서 src길이만큼 Delete하고 그 자리에 dest Insert하면 된다. 물론 두 편집 동작은 그룹으로 묶어야 한다.

이 함수가 ChangeCase 함수와 다른 점이라면 변환 후의 길이가 달라질 수 있기 때문에 선택영역의 길이도 같이 관리해야 한다는 점이다. 예를 들어 10자를 선택해놓고 변환한 결과 16자로 늘어났다면 선택영역도 같이 16자 길이로 늘어나야 한다. 변환에 의해 비록 선택영역의 내용이 바뀌기는 하지만 선택영역은 여전히 문서의 같은 부분을 가리킬 수 있도록 해야 한다. 다음 그림을 보자. 이 그림에서 -는 공백 문자이다.

 

두 개의 탭으로 들여쓰기가 되어 있는 문자열에서 앞쪽의 탭과 wh까지 선택한 상태에서 탭을 공백으로 변환하였다. 변환 전에 선택영역의 길이는 4바이트였는데 변환 후에는 탭이 4개의 공백으로 늘어나므로 선택영역은 총 10바이트가 된다. 변환 후의 선택영역은 위쪽 그림처럼 당연히 10바이트로 같이 늘어나야 한다. 아래 그림처럼 원래의 길이를 그대로 유지하면 변환 전의 선택영역과는 다른 문자열을 가리키게 된다.

단순히 가리키는 영역만 달라진다면 보기에 좀 안 좋은 것 빼고는 별 상관이 없겠지만 한글의 경계에 걸치게 되면 프로그램이 죽을 수도 있다. \tA대한민국에서 \tA대한까지 선택해놓고 변환한다면 어떻게 되는가 계산해보아라. 끔찍할 것이다. 그래서 이 함수는 변환 중에 선택영역의 길이가 증감되는 정도를 nDiff변수에 철저하게 계산한다. 탭이 공백으로 바뀌면 nDiff는 증가하고 공백이 탭으로 바뀌면 nDiff는 감소하며 모든 변환이 끝난 후 SelSecond nDiff만큼 이동시킴으로써 선택영역을 조정하고 있다.

선택영역이 있는 상태에서 문서를 변환할 때 왜 선택영역의 길이를 관리해야 하는지는 쉽게 이해가 될 것이며 이 함수에서 사용하는 알고리즘도 무리없이 이해가 될 것이다. 앞으로의 편집 함수들도 똑같은 이유로 변환시에 선택영역의 길이를 관리하되 그 방법은 상황에 따라 조금씩 달라진다. 이후 이 문제에 대해서는 별도의 상세한 설명을 하지 않으므로 알아서 분석해보기 바란다.