. GetLine

여러 줄을 입력하고 출력할 수 있는 ApiEdit예제에서 가장 핵심 함수를 꼽으라면 단연 GetLine 함수이다. 분석은 천천히 해보기로 하고 일단 다음과 같이 코드를 입력한다. GetLine 함수는 GetLineSub라는 서브함수 하나를 거느리고 있다. GetLineSub는 오로지 GetLine에서만 호출하는 일종의 개념적인 지역 함수이다.

 

int GetLineSub(TCHAR *&p)

{

     for (;;p++) {

          if (*p == ‘\r’)

               return 1;

 

          if (*p == 0)

               return 0;

     }

}

 

void GetLine(int Line, int &s, int &e)

{

     TCHAR *p;

     int i;

 

     p=buf;

 

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

          if (i==Line)

               break;

 

          if (GetLineSub(p)==0) {

               s=-1;

               e=-1;

               return;

          }

 

          p+=2;

     }

 

     s=p-buf;

     GetLineSub(p);

     e=p-buf;

}

 

텍스트 버퍼인 buf는 일차원의 연속된 메모리 공간이므로 특정 위치의 줄을 찾기가 쉽지 않은데 GetLine이 바로 이 일을 대신한다. 이 함수는 인수로 준 Line 줄에 대해 시작 오프셋과 끝 오프셋(=줄의 범위)을 조사한다. 원형은 다음과 같다.

 

void GetLine(int Line, int &s, int &e);

 

알고 싶은 줄번호를 첫 번째 인수 Line에 넘겨주면 그 줄의 시작 오프셋 s와 끝 오프셋 e를 조사한다. 조사된 값 두 개를 리턴해야 하므로 s, e는 레퍼런스여야 한다. 예를 들어 세 번째 줄의 범위를 알고 싶다고 한다면 GetLine(2,s,e)를 호출한 후 s e값을 보면 된다. 줄번호는 Zero Base이므로 세 번째 줄이 3번 줄이 아니라 2번 줄임을 명심하도록 하자. 다음과 같이 4줄이 입력되어 있는 상황에서 세 번째 줄의 정보를 구했다.

 

s는 시작 문자 K의 오프셋인 10이 조사되며 e는 끝 문자 A 다음의 위치 15가 리턴된다. 줄의 끝 위치를 왜 끝 문자 위치로 하지 않고 그 다음 위치로 계산하는가 하면 이렇게 하면 줄의 길이를 e-s로 간단하게 구할 수 있기 때문이다. 이렇게 되면 줄 마지막 문자의 위치는 영문인 경우 e-1, 한글인 경우 e-2가 된다. 만약 e를 줄 끝 문자의 오프셋으로 계산하도록 했다고 해보자.

이렇게 하면 끝 문자 A의 오프셋은 e로 쉽게 구할 수 있지만 줄의 길이는 e-s+1(마지막 문자가 한글이면 e-s+2)이 되어야 한다. 이렇게 해도 별 문제가 없을 것 같아 보이지만 절대로 그렇지 않다. 이 방식은 아주 큰 문제가 있는데 아무 문자도 없는 빈 줄은 어떻게 처리되겠는가 생각해보자. 빈 줄의 길이는 의심할 여지없이 0이 되어야 하는데 e-s+1=0이 되려면 왼쪽 그림처럼 s=e+1이 되어야 한다.

시작이 끝보다 더 뒤쪽에 있는 말도 안되는 상황이 발생하는 것이다. e가 끝 문자 다음이 되어야 빈 줄의 경우 s e가 같은 위치로 조사되고 그래야 e-s=0이 되며 이것이 훨씬 더 자연스럽다. 어떤 범위 A~B를 표현할 때 시작위치 A는 포함되고 끝 위치 B는 포함되지 않는 것이 원칙적이며 더 일반적이다. A~B사이의 범위 r이란 A<=r<B를 의미한다. 한 예로 다음 코드를 보자.

 

Rectangle(hdc,10,10,100,100);

 

사각형을 그리는 코드이며 시작점은 (10,10)이고 끝 점은 (100,100)인데 실제로 사각형은 (10,10)-(99,99)까지 그려진다. 왜냐하면 범위의 원칙에 따라 끝점 (100,100)은 제외되기 때문이다. 끝점이 제외되지 않으면 아주 심각한 문제가 여기 저기서 터져 나오게 된다.

그럼 이제 GetLine이 어떻게 줄의 범위를 조사하는지 코드를 분석해보자. GetLineSub 함수는 아주 간단하다. 인수로 전달받은 p번지 줄의 끝을 찾아 주는데 줄의 끝은 엔터코드(\r) 또는 문서의 끝(0) 둘 중 하나다. p에 다시 찾은 위치를 리턴해야 하므로 p는 포인터형의 레퍼런스로 전달한다.

개행문자와 문서의 끝은 리턴값으로 구분하는데 개행문자에 의해 줄이 끝났으면 1을 리턴하고 문서의 끝으로 인해 줄의 끝을 찾았으면 0을 리턴한다.

GetLine 함수는 먼저 포인터 변수 p를 버퍼의 선두에 맞춤으로써 문서의 처음부터 검색을 시작하며 원하는 줄을 찾을 때까지 p를 증가시켜 나간다. 제어변수 i는 현재 찾고 있는 줄번호이며 초기값은 0이다. 루프에서는 GetLineSub 함수로 p 이후 줄 끝을 찾고 엔터코드를 건너뛰기 위해 p 2 증가시키는데 이렇게 하면 p는 다음 줄의 처음으로 이동되어 있을 것이다. 이 위치가 찾고 있는 줄이면 즉, 현재 진행중인 i Line과 같으면 루프를 탈출하는데 이때 찾은 번지 p가 바로 원하는 줄의 시작위치가 된다.

번지를 오프셋으로 바꾸기 위해 p-buf 연산하여 s에 대입하며 시작위치를 구한 것이다. 줄의 끝 위치는 이 시작위치에서 다시 한 번 GetLineSub를 호출하면 되고 e도 역시 오프셋으로 바꾸어 대입한다. 가상의 문서에서 GetLine 2번째(=세 번째) 줄을 어떻게 찾는지 그림으로 그려 보았다.

루프의 중간에 있는 조건문은 검색중에 문서 끝을 만났는지 검사한다. 루프 안에서 원하는 줄까지 가기 위해 GetLineSub를 호출하고 있는데 이 중간에 0을 만났다는 것은 곧 해당 줄이 없다는 얘기다. 예를 들어 10번째 줄을 찾고자 하는데 7번째 줄에서 문서가 끝난 경우이다. 이때 GetLine s e -1을 대입한 후 리턴한다. 호출 측에서는 GetLine 호출 후 s -1인가 아닌가 점검해보아야 하는데 만약 s -1이라면 니가 찾는 줄은 없어라는 뜻으로 해석해야 하며 그에 합당한 조치를 취해야 한다.

ApiEdit는 문서 전체를 단일 버퍼에 저장하는 구조를 가지고 있지만 GetLine 함수에 의해 각 줄을 개별적으로 다룰 수 있는 능력을 가지게 되었다. 특정 줄에 대해서 어떤 처리를 하려면 GetLine 함수로 그 줄의 범위를 조사할 수 있고 s~e사이를 줄단위 버퍼인 것처럼 다루면 된다.

줄의 범위를 찾을 수 있게 되었으므로 이제 여러 줄이 입력된 문서를 화면으로 출력할 수 있다. 한 줄만 입력받을 때 OnPaint TextOut 호출 하나로 문서를 모두 출력할 수 있었다. 그러나 여러 줄을 출력할 때는 각 줄단위로 나누어 출력해야 한다. 첫 번째 줄 출력, 두 번째 줄 출력, 세 번째 줄 출력,... 이런 식으로 문서 끝까지 출력하면 된다. OnPaint 함수는 다음과 같이 수정된다.

 

void OnPaint(HWND hWnd)

{

     HDC hdc;

     PAINTSTRUCT ps;

     int l,s,e;

 

     hdc=BeginPaint(hWnd,&ps);

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

          GetLine(l,s,e);

          if (s == -1)

               break;

          TextOut(hdc,0,l*LineHeight,buf+s,e-s);

     }

     EndPaint(hWnd,&ps);

}

 

제어변수 l이 출력할 줄번호인데 0부터 증가하면서 각 줄의 시작위치 s와 끝 위치 e를 구한 후 TextOut으로 출력하였다. 이때 출력위치는 줄번호에 줄간(LineHeight)을 곱한 좌표가 되며 출력할 길이는 줄의 길이인 e-s가 된다. 루프를 돌면서 각 줄에 대해 GetLine을 호출하는데 s -1이 되는 순간에 . 다 그렸군. 그럼 끝내 버리자하고 그리기를 중지한다. OnPaint GetLine과 합작하여 buf를 각 줄별로 토막토막 내고 대응되는 화면 위치에 출력하기를 문서 끝까지 반복하는 것이다. 여기까지 예제를 작성하고 실행해보자.

<Enter>키를 누를 때 다음 줄로 잘 내려가며 여러 줄을 입력해도 화면에 잘 출력된다. 여러 줄 입력과 출력에는 성공했는데 아직 캐럿은 계속 첫 번째 줄에만 머물러 있다. 왜냐하면 캐럿이 첫 줄 이상은 아직 인식하지 못하기 때문이다.