. 문단 관리 함수

들여쓰기(Indent) 기능은 줄 앞에 탭이나 공백을 삽입하여 적당히 여백을 띄워 주는 기능이며 소스 코드를 작성할 때 주로 많이 사용된다. 문서 작성중에 자동으로 들여쓰기를 할 수도 있고 이미 작성된 문서의 들여쓰기를 조정할 수도 있다. C 프로그래밍을 하고 있는 사람이라면 들여쓰기의 개념에는 아주 익숙할 것이며 다음 절에서 실습해 볼 것이다.

들여쓰기는 항상 문단 단위로 동작하며 엔터코드로 끊어진 문단의 선두에 대해서만 적용된다. 줄단위로 들여쓰기를 하는 것이 아니므로 자동개행된 줄의 선두에는 들여쓰기가 적용되지 않는다. 들여쓰기 함수가 문서를 조작하는 단위가 문단이므로 들여쓰기 기능을 구현하기 전에 먼저 문단을 관리할 수 있는 함수의 집합을 정의할 필요가 있다. ApiEdit.h에 다음 멤버함수의 원형을 선언한다.

 

class CApiEdit

{

     ....

     void GetParaFromOff(int nPos, int &pr, int &pc);

     int GetOffFromPara(int pr, int pc);

     int GetParaFirstLine(int nPara);

     int GetParaLastLine(int nPara);

 

함수의 이름을 보면 어떤 값으로부터 무엇을 찾아내는지 쉽게 알 수 있을 것이다. 예를 들어 GetParaFirstLine Para First Line Get한다. 함수의 이름에서 볼 수 있다시피 이 함수들은 GetOffFromRC, GetXYFromOff 류의 함수와 비슷한 기능을 수행하며 그 단위가 줄이 아닌 문단이라는 점만 다르다. ApiEdit 컨트롤의 핵심이 되는 아주 중요한 함수들임에도 불구하고 이 함수의 작성 시기가 많이 늦어진 것 같다. 코드는 다음과 같다.

 

int CApiEdit::GetParaFirstLine(int nPara)

{

     int Upper,Lower;

     int r;

 

     Lower=0;

     Upper=TotalLine-1;

     if (nPara < 0 || nPara > pLine[Upper].nPara) {

          return -1;

     }

 

     for (;;) {

          r=(Upper+Lower)/2;

 

          if (pLine[r].nPara == nPara) {

              return r-pLine[r].nLine;

          }

 

          if (pLine[r].nPara > nPara) {

              Upper=r-1;

          } else {

              Lower=r+1;

          }

     }

}

 

int CApiEdit::GetParaLastLine(int nPara)

{

     int r;

 

     r=GetParaFirstLine(nPara+1);

     if (r==-1) {

          return TotalLine-1;

     } else {

          return r-1;

     }

}

 

void CApiEdit::GetParaFromOff(int nPos, int &pr, int &pc)

{

     int r,c;

 

     GetRCFromOff(nPos,r,c);

     r=r-pLine[r].nLine;

 

     pr=pLine[r].nPara;

     pc=nPos-pLine[r].Start;

}

 

int CApiEdit::GetOffFromPara(int pr, int pc)

{

     int r,re;

 

     r=GetParaFirstLine(pr);

     if (r==-1) {

          return -1;

     }

     re=GetParaLastLine(pr);

 

     if (pLine[r].Start+pc <= pLine[re].End) {

          return pLine[r].Start+pc;

     } else {

          return -1;

     }

}

 

가장 기본이 되는 함수는 GetParaFirstLine 함수인데 이 함수는 인수로 문단번호를 주면 이 문단에 속한 첫 번째 줄을 찾아준다. 이 검색에 참조되는 정보는 물론 정렬정보인 pLine인데 pLine은 각 줄별로 문단(nPara)과 문단 내의 줄번호(nLine)를 저장하고 있다. 줄번호로부터 문단번호를 찾는 것은 pLine[r].nPara만 읽으면 되므로 아주 쉽지만 반대로 문단번호로 줄번호를 찾으려면 쉽지 않다.

다행히 pLine 배열은 문단번호가 오름차순으로 정렬되어 있으므로 이분 검색을 사용할 수 있다. 일단 문단에 속한 줄번호를 찾으면 그 문단 내에서 nLine 0인 줄, 즉 첫 번째 줄의 번호를 쉽게 구할 수 있다. 모두 15줄로 구성된 다음 문서를 예로 들어 이 함수가 1번 문단의 첫 번째 줄을 찾는 동작을 관찰해보자.

TotalLine 15이므로 마지막 줄의 번호는 14가 된다. 첫 단계에서 검색 대상은 중간 지점인 7번 줄이 되는데 이 줄의 문단값을 보니 2로 되어 있으며 찾고자 하는 1번 문단 보다는 더 뒤의 문단인 것이다. 그래서 Upper의 다음 단계는 R1보다 하나 작은 6이 된다.

두 번째 R2값은 6의 절반인 3이 되며 이 줄의 문단번호를 조사해보니 찾고자 하는 1번 문단과 같다. 3번 줄이 찾는 문단에 속한 줄이라는 것을 알 수 있으며 문단은 이미 찾은 것이다. 문단을 찾은 후에는 그 문단의 첫 줄을 찾는데 R2에서 시작하여 nLine 0이 될 때까지 거꾸로 거슬러 올라가면 된다. R2 nLine은 현재 2로 정의되어 있으므로 R2 1번 문단의 세 번째 줄이다. 즉 이 줄의 두 칸 위에 문단 첫 줄이 있다는 얘기다.

마지막으로 R2의 줄번호에서 이 줄의 nLine을 뺀 값을 계산해서 리턴하면 1번 문단의 첫 번째 줄을 찾게 되는 것이다. 줄번호는 항상 그 줄의 nLine보다 크기 때문에 r-pLine[r].nLine은 항상 0 이상이라는 것이 보장되므로 배열범위를 벗어날 위험은 절대로 없다.

이 함수는 검색을 시작하기 전에 nPara가 과연 제대로 전달된 값인지 검사를 하는데 nPara 0 이상이어야 하고 또한 마지막 줄의 문단번호보다 클 수는 없으므로 이 범위를 벗어나면 줄번호로 -1을 리턴하여 에러가 발생했음을 알려준다. 호출측에서 nPara 인수만 정확하게 전달한다면 이런 에러 처리는 굳이 필요치 않다. 하지만 이 에러 처리를 교묘하게 이용하면 다른 루틴들이 좀 더 간단해지는 효과가 있는데 GetParaLastLine 함수에서 이 에러 상황을 활용하는 예를 볼 수 있다.

문단의 마지막 줄은 그 다음 문단의 첫 줄 바로 위에 있는 줄이다. 그래서 nPara의 마지막 줄은 nPara+1문단의 첫 줄-1로 구한다. , 만약 nPara가 문서의 마지막 문단일 경우는 이 단순한 계산이 통하지 않는데 왜냐하면 nPara+1이 존재하지 않기 때문이다. 이 경우 GetParaFirstLine -1이라는 값을 리턴하는데 그렇다면 nPara의 마지막 줄은 TotalLine-1로 간단하게 구할 수 있다. 그렇지 않다면 nPara+1 문단의 첫 줄에서 1을 빼준 줄번호를 리턴한다.

오프셋으로부터 문단번호를 구하는 방법도 아주 간단하다. 둘 다 pLine 배열에 있는 정보이지만 서로 연관되어 있지 않기 때문에 직접적으로 구할 수 없으며 줄번호를 통해 간접적으로 서로를 구해야 한다. 전달된 오프셋 nPos로부터 이 오프셋이 있는 줄번호 r을 먼저 찾는다. 그리고 pLine[r].nPara를 읽으면 이 값이 바로 nPos가 속한 문단이다.

문단 내의 칸 번호인 pc nPos에서 문단의 첫 줄 시작 번지를 빼면 된다. 그래서 r을 문단의 첫 줄로 올린 후 nPos-pLine[r].Start를 계산했다. 문단번호와 문단 칸 번호로부터 오프셋을 구하는 방법은 지극히 간단해서 다음 한 줄이면 구할 수 있다.

 

return pLine[GetParaFirstLine(pr)].Start+pc

 

문단 첫 줄의 시작 오프셋과 문단 내의 칸 번호를 더해 리턴하기만 하면 된다. , pc가 이 문단 범위를 벗어난 칸 번호인 경우를 위해 에러 처리를 할 필요가 있다. 문단의 길이는 100자밖에 안되는데 문단 내의 200칸 오프셋을 구할 수는 없기 때문이다. 그래서 pc가 이 문단 범위에 있는지 조사하기 위해 문단의 끝줄이 필요하고 pc가 문단 첫 줄의 시작과 끝줄의 마지막 위치에 있는지 조사하였다. GetOffFromPara 함수는 이런 에러 처리 때문에 약간의 조건 검사문이 들어갔다.

이 함수들이 완성됨으로써 ApiEdit는 성질이 다른 여러 단위간을 쉽게 전환할 수 있게 되었다. 오프셋과 줄, 문단, 좌표를 자유롭게 전환할 수 있으며 하나의 값을 알면 중간값을 통해 다른 값을 쉽게 구할 수 있다. 예를 들어 마우스로 찍은 좌표가 어느 문단에 속하는지 알고 싶으면 좌표로부터 오프셋을 구하고 오프셋으로부터 문단을 구하면 된다.

이 함수들을 통칭한다면 GetAFromB라고 할 수 있는데 이 함수들이 단위간의 변환을 하기 때문에 ApiEdit의 다른 코드들이 간단해질 수 있다. 이 함수가 제대로 동작하는지 테스트 해보기 위해 다음 임시 코드를 작성해보자.

 

void CApiEdit::OnLButtonDown(HWND hWnd, BOOL fDoubleClick, int x, int y, UINT keyFlags)

{

     ....

    TCHAR szTemp[256];

    int pr,pc;

    GetParaFromOff(off,pr,pc);

    wsprintf(szTemp,"pr=%d, pc=%d, s=%d, e=%d",pr,pc,

        GetParaFirstLine(pr),GetParaLastLine(pr));

    SetWindowText(GetParent(hWnd),szTemp);

 

     SetCapture(hWnd);

     bCapture=TRUE;

     SetCaret();

}

 

마우스로 클릭한 부분의 문단번호, 문단 내의 칸번호와 문단의 첫 줄, 끝줄을 타이틀바에 출력하도록 했다. 이런 중요한 함수들은 여러 곳에서 사용되므로 정확하게 동작하는지 정밀하게 테스트해 볼 필요가 있으며 이런 테스트를 할 때는 디버거를 활용하는 것보다 아예 테스트 코드를 작성해서 실시간으로 확인해보는 것이 좋다. 물론 테스트가 끝난 후 이 코드는 삭제해야 한다.