. PgUp, PgDn

다음은 <PgUp>, <PgDn>키에 대한 처리를 작성해보자. 이 키를 누르면 페이지단위로 상하이동을 하게 되며 스크롤바의 몸통을 누른 것과 같은 효과가 나타난다. 그래서 보통 이 키로 페이지 스크롤을 할 때는 SB_PAGEUP(DOWN) 메시지를 보내주는 방법을 많이 사용하며 논리적으로 큰 무리가 없다. 이 방법대로 작성한 <PgUp>의 코드는 다음과 같다.

 

 

     int oldypos;

     int yInc;

     ....

     case VK_PRIOR:

          GetRCFromOff(off,r,c);

          oldypos=yPos;

          SendMessage(hWnd,WM_VSCROLL,MAKELONG(SB_PAGEUP,0),0);

          yInc=(yPos-oldypos)/LineHeight;

          r+=yInc;

          off=GetXPosOnLine(r,PrevX);

          SetCaret(FALSE);

          return;

 

먼저 r에 현재 줄번호를 조사해놓고 SB_PAGEUP 메시지를 보내면 위로 스크롤이 될 것이다. 스크롤된 결과 과연 몇 줄이나 스크롤되었는지 조사하기 위해 스크롤 전후의 yPos 값을 조사하여 이동된 줄수만큼 r에 더하는데 이 경우 위로 올라갔으므로 이동줄수는 음수이고 현재줄보다 더 위로 올라갈 것이다. 이동된 줄에서 GetXPosOnLine을 호출하여 새 오프셋을 찾았다. <PgUp> 이동도 일종의 상하이동이므로 PrevX는 건드리지 말아야 하며 SetCaret의 인수는 반드시 FALSE여야 한다.

이 코드로 테스트를 해보면 과연 잘 스크롤된다. 이런 방법은 일반적으로는 문제가 없으며 그래픽 프로그램이라면 이대로 쓸 수 있다. 그러나 텍스트에디터는 캐럿이 있기 때문에 <PgUp>키와 SB_PAGEUP의 동작이 완전히 일치하지 않는다. 어떤 경우 차이점이 있는가하면 다음과 같은 상태일 때이다.

캐럿이 여섯 번째 줄 끝에 있고 하나도 스크롤되어 있지 않은 상태인데 이 상태에서 SB_PAGEUP 메시지는 더 스크롤될 곳이 없기 때문에 아무 일도 하지 않는다. 하지만 <PgUp>은 스크롤은 하지 않더라도 캐럿은 제일 윗줄로 올려야 할 필요가 있다.

SB_PAGEUP은 보여줄 페이지를 위로 이동하라는 뜻이므로 캐럿은 항상 그 자리에 두고 화면만 이동시킨다. 반면 <PgUp>은 편집할 곳을 위로 이동하라는 뜻이며 캐럿이 화면과 함께 움직인다. 뿐만 아니라 가급적이면 스크롤한 후에도 캐럿의 화면상 위치가 일정하기를 바란다. 사용자는 <PgUp>을 누를 때 캐럿 위치를 보고 있으며 화면상의 세 번째 줄에서 <PgUp>을 눌렀을 경우 스크롤한 후에도 여전히 세 번째 줄에 캐럿이 있기를 기대한다.

편집 위치와 보여주는 위치가 각각 다를 수 있는 텍스트 편집기에서 두 명령은 의미가 약간 다른 것이다. 그래서 SB_PAGEUP 메시지를 대신 보내는 편리한 방법은 사용할 수 없으며 사용자가 기대하는 대로 동작하도록 하기 위해 코드를 다시 작성했다.

 

 

     int oldr;

     int yInc;

     RECT crt;

 

     GetClientRect(hWnd,&crt);

     ....

     case VK_PRIOR:

          GetRCFromOff(off,r,c);

          oldr=r;

          r-=crt.bottom/LineHeight;

          r=max(r,0);

          yInc=-(oldr-r)*LineHeight;

          yInc=max(-yPos, yInc);

          yPos=yPos+yInc;

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

          SetScrollPos(hWnd, SB_VERT, yPos, TRUE);

 

          off=GetXPosOnLine(r,PrevX);

          SetCaret(FALSE);

          return;

 

화면당 줄수만큼 위로 이동시키되 단, 음수줄로 가지는 않도록 하였다. 이때 스크롤해야 할 거리 yInc는 이동한 줄 수에 줄간을 곱해 구하되 단, yPos가 음수가 되지 않도록 하였다. ScrollWindow로 스크롤시키고 GetXPosOnLine으로 새 오프셋을 찾으면 된다. 이 코드로 테스트해보면 앞에서 설명한대로 <PgUp>이 잘 동작할 것이다. 항상 화면상의 위치가 일정하며 더 이상 스크롤할 곳이 없어도 화면 첫 줄로 이동한다.

하지만 아직도 불만이 있다. 어차피 페이지단위로 스크롤하면 화면 전체가 바뀌므로 스크롤을 하는 것보다는 yPos만 바꾼 후 다시 그리는 것이 더 낫다. ScrollWindow 함수를 쓰지 않으면 yInc를 계산할 필요도 없어지고 yPos를 직접 변경하는 것이 훨씬 더 직관적이다. 한 번 더 코드를 수정해보자.

 

void OnKey(HWND hWnd, UINT vk, BOOL fDown, int cRepeat, UINT flags)

{

     ....

     int oldr;

     RECT crt;

 

     if (fDown==FALSE)

          return;

 

     GetClientRect(hWnd,&crt);

     ....

     case VK_PRIOR:

          GetRCFromOff(off,r,c);

          oldr=r;

          r-=crt.bottom/LineHeight;

          r=max(r,0);

          yPos=yPos-(oldr-r)*LineHeight;

          yPos=max(yPos,0);

          InvalidateRect(hWnd,NULL,TRUE);

          SetScrollPos(hWnd, SB_VERT, yPos, TRUE);

 

          off=GetXPosOnLine(r,PrevX);

          SetCaret(FALSE);

          return;

 

yPos 값을 스크롤된 거리만큼 직접 바꾸었고 0보다 더 큰 값을 가지도록 했다. 스크롤은 할 필요없으며 InvalidateRect로 전체 작업영역을 무효화해놓으면 OnPaint가 새로 변경된 yPos 위치에 그려줄 것이다. 이 코드가 논리적으로 가장 완벽하고 마음에 든다. <PgDn>의 코드도 같은 논리대로 작성하면 된다.

 

     case VK_NEXT:

          GetRCFromOff(off,r,c);

          oldr=r;

          r+=crt.bottom/LineHeight;

          r=min(r,GetRowCount()-1);

          yPos=yPos+(r-oldr)*LineHeight;

          yPos=max(0,min(yPos,yMax-(crt.bottom/LineHeight)*LineHeight));

          InvalidateRect(hWnd,NULL,TRUE);

          SetScrollPos(hWnd, SB_VERT, yPos, TRUE);

 

          off=GetXPosOnLine(r,PrevX);

          SetCaret(FALSE);

          return;

 

<PgUp>키를 처리하는 코드를 세벌이나 작성해 봤는데 보다시피 잘 될 것 같은 코드도 좀 더 살펴보면 문제가 있는 경우가 많다. 처음 생각했던 코드가 당장은 문제가 없지만 다른 관점에서 보면 말썽이 있어 수정하게 되고 이런 과정을 여러 번 거쳐야 제대로 된 코드를 얻을 수 있다. 그래서 프로그래머는 항상 자신의 코드를 의심하는데 게을러서는 안된다. 여러 번 수정했지만 끝까지 문제점을 찾지 못한 채로 넘어가게 되면 그것을 버그라고 한다.

모든 스크롤 관련 코드를 다 작성했다. 이제 보류해두었던 UpdateScrollInfo 함수의 다음 조건문이 왜 필요한지 예제를 실행해놓고 문제를 확인해보도록 하자.

 

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

          yPos=0;

     }

 

예제를 실행하면 OnCreate에서 대입한 초기 문자열이 화면에 나올 것이다. 문서의 제일 끝으로 이동한 후 BS Line3앞까지 모두 다 지우고 제일 아래로 스크롤하여 왼쪽 그림처럼 만들어보자.

 

위쪽에 두 줄이 더 있는데 스크롤되어 보이지 않는다. 이 상태에서 <BS>키로 계속 문자를 삭제하면 오른쪽 그림처럼 된다. 위에 숨어 있는 두 줄까지 다 합쳐도 세 줄밖에 안 남았고 화면에는 7줄까지 표시할 수 있으므로 수직 스크롤바는 디스에이블된다. 이 상태에서는 키보드로는 위로 올라갈 수 있지만 스크롤바를 클릭하여 위로 올라가지 못하는 문제가 있다.

그래서 스크롤 범위를 정할 때 이 상태인지 아닌지 점검한다. 조건문 si.nMax < (int)si.nPage는 곧 스크롤 범위가 페이지 높이보다 작으면이라는 뜻이고 이는 곳 스크롤바가 디스에이블될 조건이다. 이 상태가 되면 yPos를 강제로 0으로 맞추어 첫 줄이 보이도록 미리 강제 스크롤을 했다.