. 스크롤바 초기화

여기까지 작성한 후 실행하면 수직 스크롤바가 나타나지만 화살표 버튼을 클릭하거나 썸을 드래그해도 아직 동작은 하지 않을 것이다. 왜냐하면 스크롤바를 만들기만 했지 범위나 위치, 페이지 크기 등의 정보가 초기화되지 않았기 때문이다. 스크롤바 정보는 문서의 내용이 바뀔 때마다 갱신해야 하므로 이 작업을 전담할 함수를 작성하였다.

 

void UpdateScrollInfo()

{

     SCROLLINFO si;

     RECT crt;

     int i, MaxLength;

     int s,e;

 

     GetClientRect(hWndMain,&crt);

     yMax=(GetRowCount()+crt.bottom/LineHeight/2)*LineHeight;

 

     si.cbSize=sizeof(SCROLLINFO);

     si.fMask=SIF_ALL | SIF_DISABLENOSCROLL;

     si.nMin=0;

     si.nMax=yMax;

     si.nPage=(crt.bottom/LineHeight)*LineHeight;

 

     if (si.nMax < (int)si.nPage) {

          yPos=0;

     }

     si.nPos=yPos;

     SetScrollInfo(hWndMain, SB_VERT, &si, TRUE);

 

     if (nWrap == 0) {

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

               GetLine(i,s,e);

               if (s==-1) {

                   break;

               }

               MaxLength=max(MaxLength,(e-s));

          }

 

          xMax=int(MaxLength*FontWidth*1.5);

          si.nMax=xMax;

          si.nPage=crt.right;

          si.nPos=xPos;

          SetScrollInfo(hWndMain, SB_HORZ, &si, TRUE);

     }

}

 

이 예제는 픽셀 단위로 스크롤을 한다. 텍스트에디터는 줄간이 일정하기 때문에 줄단위로 스크롤을 하며 그래서 스크롤 단위를 줄번호로 해도 무방하다. 그러나 이렇게 하면 당장은 문제가 없지만 줄번호는 모든 프로그램에 일반적인 것이 아니므로 확장성에 대단히 불리하다. 만약 이 예제를 만들다가 문단에 줄간을 지정할 수 있는 워드프로세서로 업그레이드 하고 싶다면 스크롤 구조를 다 뜯어 고쳐야 할 것이다. 그래서 확장성을 고려하여 줄단위보다는 픽셀 단위로 스크롤하도록 하였다.

이 함수가 해야 할 가장 중요한 일은 문서의 길이에 맞게 yMax를 결정하는 것인데 이 값을 결정하는 식이 좀 요상하게 생겼다. 문서의 길이만큼 스크롤 범위를 설정하려면 GetRowCount()로 총 줄 수를 구하고 여기에 줄간인 LineHeight를 곱하면 된다. 예를 들어 총 줄 수가 10줄이고 줄간이 24라면 y축 스크롤 범위는 240이면 된다.

그러나 yMax를 이렇게 구하지 않고 여기에 crt.bottom/LineHeight/2라는 약간의 값이 더 추가되어 있다. 이 식의 의미는 화면에 들어가는 줄 수의 절반이라는 뜻이다. 왜 스크롤 범위에 이 값을 더하는가 하면 문서를 끝까지 스크롤했을 때 위쪽 절반은 문서의 마지막 부분이 보이고 아래쪽 절반은 여백이 보이도록 하기 위해서이다. 화면당 줄 수가 8이라고 할 때 다음 두 그림을 비교해보자.

 

왼쪽은 yMax를 문서 길이에 맞춘 것이며 오른쪽은 문서 길이를 화면의 절반만큼 늘려준 것이다. 이 길이가 너무 꼭 맞으면 일단 답답해보이고 문서 끝임을 단번에 알 수 없다. 끝으로 내렸을 때 반쯤 여백이 보이면 여기가 문서의 끝이라는 것을 확실하게 알 수 있어 좋고 보기에도 시원하다. 이 두 가지 방법 외에 끝까지 스크롤 했을 때 문서를 모두 다 올려버리는 방식도 있고 마지막 한 줄만 보여주어 여백을 최대한 많이 보여주는 방식도 있다.

어떤 방식이든지 스크롤 범위에 영향을 줄 뿐이지 기능적인 차이가 있는 것은 아니며 편집상의 문제가 있는 것도 아니다. 이 범위를 결정하는 것은 옳고 그름의 문제가 아니라 취향의 문제일 뿐이다. 내가 보기에 절반 여백이 가장 보기 좋다고 판단했기 때문에 이 방법을 선택한 것뿐이다. 아마 여러분들도 왼쪽 그림보다는 오른쪽 그림이 더 마음에 들 것이다.

스크롤 페이지(si.nPage)의 길이는 일반적으로 작업영역의 높이로 정하면 되는데 단, 정확한 길이가 되기 위해서는 줄간의 배수가 되어야 한다. (crt.bottom/LineHeight)*LineHeight 식의 의미는 crt.bottom 값을 취하되 이 값이 줄간의 배수가 되도록 내림한다는 뜻이다. 나누기를 하고 또 곱하기를 하면 원래값이 될 것 같지만 나누기에 의해 일부 값이 잘려 나가게 되므로 차이가 있다.

일반적으로 A A보다 크지 않은 B의 가장 가까운 배수가 되도록 하고 싶으면 A=A/B*B하면 된다. A 11이고 B 3이면 가장 가까운 배수는 9가 되어야 하는데 11 3으로 나누면 3이 되고 다시 3을 곱하면 9가 된다. 11/3은 정수 나눗셈이기 때문에 3.6666이 되는 것이 아니라 3이 되기 때문이다. 근접배수를 찾는 이 식은 아주 일반적인 공식이며 A=A-A%B와 동일하다. 다 알고 있는 너무 쉬운 내용을 괜히 다룬 건 아닌가 모르겠다.

수직 스크롤바의 정보를 갱신하기 직전에 if (si.nMax < (int)si.nPage)라는 조건문이 들어가 있는데 이 조건문에 대한 설명은 잠시 보류하기로 한다. 아직 스크롤 기능이 다 만들어진 상태가 아니기 때문에 이 조건문이 없을 때 어떤 문제가 있는지 지금 보여 줄 수 없다. 일단 기능을 다 만들고 난 다음에 문제점과 해결책을 모색해보기로 하자.

다음은 수평 범위를 설정하는 코드를 보자. 수평 범위는 nWrap 0일 때만 의미가 있다. 자동개행 기능이 동작중이면 화면 오른쪽으로 문자가 벗어날 일이 없으며 따라서 수평으로 스크롤할 필요도 없다. 수직 범위는 문서의 총 줄수를 기준으로 해서 결정하는데 비해 수평 범위는 가장 긴 줄의 길이를 기준으로 해야 한다.

이 값은 미리 계산된 것이 없으므로 모든 줄에 대해 루프를 돌면서 누가누가 더 긴가 놀이를 한판 해야 한다. MaxLength 0으로 초기화하고 가장 긴 e-s를 이 변수에 대입하였다. 이렇게 구해진 길이에 평균 문자폭을 곱하고 다시 1.5를 곱해 xMax값을 결정한다. 예를 들어 가장 긴 줄이 100바이트이고 문자의 평균폭은 8픽셀일 때 xMax 1200이 될 것이다.

보다시피 xMax계산은 대충 했으며 그럴 수밖에 없다. 우선은 문자의 평균폭이라는 개념 자체가 부정확하다. 가변폭 폰트는 각 문자의 폭이 다르며 줄의 길이가 제일 길다고 해서 반드시 제일 폭이 넓은 줄이라고 할 수도 없다. 실제로 i 20개 있는 줄은 W 10개 있는 줄보다 더 짧다. 평균폭이란 어디까지나 확률적 평균일 뿐이므로 문자의 구성에 따라 평균폭보다 더 넓은 줄이 분명히 있을 수 있다. 또한 탭은 옵션 설정에 따라 폭이 아주 커질 수 있으며 그래서 안전하게 1.5정도를 곱한 것이다.

이렇게 되면 일반적으로 xMax는 과대 평가되는 경향이 있으며, 오른쪽으로 여백이 많이 남게 될 것이다. 그러나 모자라는 것은 문제가 될 수 있어도 남는 것은 전혀 문제가 되지 않는다. 수직스크롤과는 달리 정확할 필요가 없으며 수평 스크롤바로 오른쪽으로 이동할 수만 있으면 된다. 수평 스크롤바를 오른쪽 끝으로 드래그해서 가장 긴 줄의 끝을 반드시 찾고 말 거야 이런 생각을 하는 사용자는 없으며 실무에서 이런 경우도 없다.

정확하게 xMax를 계산하고자 한다면 물론 전혀 어려운 일이 아니다. 모든 줄의 폭을 GetTextExtentPoint32 함수로 조사한 후 그 중 가장 너비가 넓은 줄의 폭을 xMax로 취하면 된다. 약간의 여유분을 줘도 좋고 말이다. 그런데 이렇게 하면 엄청나게 느려진다는 반대 급부가 있다. 득보다는 실이 훨씬 더 많으며 속도 저하를 무릅쓰고 이런 정확함을 바라는 사람도 없기 때문에 그냥 대충 계산하는 것이다. 아니! 컴퓨터 과학을 한다는 사람이 어떻게 대충이라는 말을 하는지 의아해 할지도 모르겠지만 여러분들이 쓰고 있는 프로그램들을 자세히 관찰해보면 이런 대충이 정말 많다는 것을 알게 될 것이다.

스크롤 바의 정보를 갱신하는 함수를 작성했는데 이 함수는 문서의 내용이 변경되거나 또는 정렬 상태가 바뀔 때마다 호출되어야 한다. Insert, Delete, OnSize, OnCreate, SetWrap에서 이 함수를 호출하도록 하자.

 

void Insert(int nPos, TCHAR *str)

{

     ....

     bLineEnd=FALSE;

    UpdateScrollInfo();

}

 

void Delete(int nPos, int nCount)

{

     ....

     memmove(buf+nPos, buf+nPos+nCount, movelen);

    UpdateScrollInfo();

}

 

void OnSize(HWND hWnd, UINT state, int cx, int cy)

{

     if (state != SIZE_MINIMIZED) {

        UpdateScrollInfo();

          if (GetFocus()==hWnd) {

               SetCaret();

          }

     }

}

 

BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     ....

    UpdateScrollInfo();

 

     return TRUE;

}

 

void SetWrap(int aWrap)

{

     ....

    UpdateScrollInfo();

     SetCaret();

}

 

SetCaret 함수에서 키보드 스크롤을 위해 스크롤 정보를 참조하므로 이 함수는 SetCaret보다 먼저 호출되어야 한다.