. 스크롤 메시지 처리

이제 스크롤 기능 구현을 위해 스크롤 관련 메시지를 처리하도록 하자. 다음은 수평스크롤 메시지인 WM_HSCROLL 메시지 처리 함수이다.

 

void OnHScroll(HWND hWnd, HWND hwndCtl, UINT code, int pos)

{

     int xInc;

     RECT crt;

     SCROLLINFO si;

 

     GetClientRect(hWnd,&crt);

     xInc=0;

     switch (code) {

     case SB_LINEUP:

          xInc=-FontHeight;

          break;

     case SB_LINEDOWN:

          xInc=FontHeight;

          break;

     case SB_PAGEUP:

          xInc=-(crt.right-crt.left);

          break;

     case SB_PAGEDOWN:

          xInc=crt.right-crt.left;

          break;

     case SB_THUMBTRACK:

          si.cbSize=sizeof(SCROLLINFO);

          si.fMask=SIF_TRACKPOS;

          GetScrollInfo(hWnd,SB_HORZ,&si);

          xInc=si.nTrackPos-xPos;

          break;

     default:

          break;

     }

 

     xInc=max(-xPos, min(xInc, xMax-xPos));

     xPos=xPos+xInc;

     ScrollWindow(hWnd, -xInc, 0, NULL, NULL);

     SetScrollPos(hWnd, SB_HORZ, xPos, TRUE);

}

 

아주 기본적인 코드이므로 스크롤 기법 자체에 대해서는 설명을 하지 않기로 하고 특징적인 몇 가지만 보고 넘어가도록 하자. 수평으로 줄단위 스크롤할 때의 크기는 FontHeight로 되어 있는데 이 점이 좀 이상하게 생각될 수도 있다. 수평스크롤이므로 폰트의 평균폭인 FontWidth를 사용해야 할 것 같은데 이 값은 너무 작아 스크롤 속도가 느려서 사용하지 않는다. 어차피 스크롤 단위라는 것도 임의의 선택값일 뿐이다.

SB_THUMBTRACK 에서 현재 스크롤 위치를 구할 때는 반드시 GetScrollInfo 함수로 32비트의 스크롤 값을 구해 사용해야 한다. 이 프로그램의 스크롤 단위는 픽셀이며 문서가 대단히 길 수 있기 때문에 wParam의 상위 워드로 전달되는 16비트 값으로는 부족하다. 32비트 스크롤 위치값은 40억까지 가능하므로 거의 무한대의 문서까지도 스크롤할 수 있다. 다음은 조금 더 복잡한 수직스크롤 코드를 보도록 하자.

 

void OnVScroll(HWND hWnd, HWND hwndCtl, UINT code, int pos)

{

     int yInc;

     RECT crt;

     SCROLLINFO si;

     int LinePerPage;

 

     GetClientRect(hWnd,&crt);

     LinePerPage=(crt.bottom/LineHeight)*LineHeight;

     yInc=0;

     switch (code) {

     case SB_LINEUP:

          yInc=-LineHeight;

          break;

     case SB_LINEDOWN:

          yInc=LineHeight;

          break;

     case SB_PAGEUP:

          yInc=-LinePerPage;

          break;

     case SB_PAGEDOWN:

          yInc=LinePerPage;

          break;

     case SB_THUMBTRACK:

          si.cbSize=sizeof(SCROLLINFO);

          si.fMask=SIF_TRACKPOS;

          GetScrollInfo(hWnd,SB_VERT,&si);

          yInc=si.nTrackPos-yPos;

          break;

     default:

          break;

     }

 

     yInc=max(-yPos, min(yInc, yMax-yPos-LinePerPage));

     yInc=yInc-(yInc % LineHeight);

     yPos=yPos+yInc;

     ScrollWindow(hWnd, 0, -yInc, NULL, NULL);

     SetScrollPos(hWnd, SB_VERT, yPos, TRUE);

}

 

줄단위 스크롤크기는 두말할 필요도 없이 줄간인 LineHeight이다. 페이지단위 스크롤크기는 LinePerPage이되 페이지당 줄 수의 배수로 계산했다. SB_THUMBTRACK에서는 마찬가지로 32비트의 스크롤 위치값을 구해 사용한다. 이 코드에서 좀 어려운 코드라면 yInc의 상하한 값을 결정하는 코드인데 수직스크롤시 yPos의 가능한 하한값은 0이며 상한값은 yMax-LinePerPage이다. 아무리 위로 스크롤해도 음수 위치로 가서는 안되며 또한 아무리 아래로 스크롤해도 문서 끝을 넘어서는 안된다.

이 함수에서 눈여겨 볼 코드는 끝에서 4번째에 있는 yInc=yInc-(yInc % LineHeight);문장이다. OnHScroll에는 대응되는 코드가 없는데 수직스크롤에는 반드시 이 코드가 있어야 한다. 왜 그런가는 아직 스크롤 결과를 확인할 수 없으므로 OnPaint 코드까지 작성한 후 논해보자. 스크롤된 결과는 OnPaint 함수에서 적용한다. 다음과 같이 TextOut 함수의 문자열 출력 좌표에 xPos, yPos를 빼면 된다.

 

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-xPos,l*LineHeight-yPos,buf+s,e-s);

     }

     EndPaint(hWnd,&ps);

}

 

스크롤된 만큼 더하는 것이 아니라 빼주는 것임을 유의하자. 이 함수 외에도 스크롤에 영향을 받을만한 함수로 GetXYFromOff가 있을 것 같은데 이 함수는 수정할 필요가 없다. 이 함수가 조사하는 XY 좌표값은 화면상의 좌표가 아니라 문서상의 좌표이기 때문에 스크롤 상태와는 무관하다. GetRCFromOff 함수가 구하는 행렬값도 마찬가지 이유로 스크롤 상태와는 전혀 무관하다.

이제 실행해보면 수직 스크롤바를 이용해서 아래위로 이동할 수 있다. 스크롤바의 버튼을 클릭하면 한 줄씩 스크롤되고 몸통을 누르면 페이지단위로 스크롤될 것이다. 그럼 이제 잠시 전에 보류해두었던 yInc=yInc-(yInc % LineHeight); 코드의 존재 가치에 대해 알아 보기 위해 이 줄을 주석 처리하고 실행해보아라. 버튼을 클릭하면 한 칸씩 스크롤 되는 것은 동일한데 썸을 드래그할 때는 완전히 픽셀 단위로 스크롤될 것이다.

SB_THUMBTRACK은 썸의 현재 위치를 임의의 위치로 옮겼을 때 발생하며 따라서 줄의 경계 따위는 완전히 무시한다. 그래서 위 그림처럼 화면상의 첫 줄이 반쯤만 보이는 현상이 생기게 되는 것이다. 이렇게 픽셀 단위로 스크롤하면 아주 미려하게 스크롤되어 보기는 좋을지 몰라도 사용하기는 오히려 불편하다. 제일 끝 줄이 반쯤 보이는 것은 상관없지만 첫 줄이 저렇게 반만 보이면 좀 이상하지 않은가?

화면의 처음에 줄이 반쯤 걸쳐 있으면 여러 가지로 복잡한 논리적 예외가 발생하며 이 예외를 처리하려면 다량의 코드가 추가로 요구된다. 그래서 편집기들은 이런 상태를 허용하지 않으며 어떻게 하든 화면 제일 위의 줄은 제대로 보여야 하는 것이다. 리스트박스나 트리 뷰 같은 표준 컨트롤들의 동작을 관찰해보면 어떤 경우라도 첫 번째 항목이 일부만 보이도록 내버려 두지 않는다. 그만큼 이 문제가 골치 아프다는 뜻이다.

워드프로세서의 경우는 줄마다 간격이나 높이가 다를 수 있기 때문에 불가피하게 첫 줄이 반쯤 걸치는 경우가 있다. 그러나 워드프로세서도 불가피한 경우에만 이런 현상을 허용할 뿐 스크롤이나 키보드 이동시에는 가급적이면 첫 줄이 다 보이도록 하기 위해 안간힘을 쓴다.

yInc=yInc-(yInc % LineHeight); 코드는 스크롤 증감값을 줄간의 배수로 강제로 내림함으로써 이런 현상을 방지하는 아주 중요한 역할을 하며 절대로 생략할 수 없다. ApiEdit의 주요 함수들은 yPos는 항상 줄간의 배수임을 가정하고 있는데 이 가정이 틀려지면 엉뚱한 동작을 하게 된다. 예를 들어 GetXYFromOff 함수는 문서상의 줄번호 r에 줄간 LineHeight를 곱해 y 좌표를 구하는데 이 계산식은 yPos가 줄간의 배수임을 가정하고 있는 것이다.