. 휠 마우스 지원

다음은 휠 마우스를 지원하도록 해보자. 휠 마우스란 알다시피 위쪽에 버튼 두 개 외에 바퀴가 하나 더 달려 있어 이 바퀴를 만지작거리면 편리하게 스크롤할 수 있는 장비이다. 휠 마우스를 프로그래밍하려면 다음 매크로를 먼저 선언해야 한다.

 

#define _WIN32_WINNT 0x400

#define _WIN32_WINDOWS 0x401

#include <windows.h>

 

휠 마우스는 NT 4.0이후, 95초과부터 가능해졌지만 비주얼 스튜디오의 헤더 파일들은 아직도 윈도우즈 95에 환경이 맞추어져 있기 때문에 WM_MOUSEWHEEL 매크로 상수를 쓰기 위해서는 _WIN32_WINNT 0x400이상이거나 _WIN32_WINDOWS 0x400 초과여야만 한다. 그래서 목표 운영체제를 NT 4.0 이상 95 초과로 지정하기 위해 두 개의 매크로 상수를 정의하였다. 아니면 WM_MOUSEWHEEL 매크로 대신 상수 0x20a를 곧바로 사용해도 상관없다.

사용자가 휠을 굴리면 마우스 커서 아래의 윈도우에게 WM_MOUSEWHEEL 메시지를 보내주므로 이 메시지를 받아서 처리하면 된다. WndProc에 다음 코드를 추가한다.

 

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

     ....

     case WM_MOUSEWHEEL:

          OnMouseWheel(hWnd,wParam,lParam);

          return 0;

     }

     return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

다른 메시지는 모두 HANDLE_MSG 매크로로 깔끔하게 인수를 분리했지만 이 메시지에 대해서는 직접 case문으로 분기를 해서 OnMouseWheel을 부르도록 했는데 그 이유는 메시지크래커를 정의하는 windowsx.h에 이 메시지에 대한 매크로가 잘못 작성되어 있기 때문이다. 쉽게 말하자면 헤더 파일에 버그가 있고 이 버그를 피하기 위해 메시지크래커를 쓰지 않았다. 어떤 버그가 있는지는 직접 헤더 파일을 열어서 확인해보기 바란다.

WM_MOUSEWHEEL wParam은 하위 워드에 가상 키의 상태값을, 상위 워드에 휠이 굴러간 거리를 전달한다. 이 거리값은 120으로 정의되어 있는 WHEEL_DELTA의 배수값으로 표현되는데 이 값이 양수이면 휠이 사용자 반대쪽으로 전진한 것이며 음수값이면 사용쪽으로 전진한 것이다. 양수일 경우(120)는 위로 스크롤이며 음수일 경우(-120)는 아래로 스크롤시키면 된다. OnMouseWheel 함수를 다음과 같이 작성해보자.

 

void OnMouseWheel(HWND hWnd, WPARAM wParam, LPARAM lParam)

{

     UINT Lines;

     SystemParametersInfo(SPI_GETWHEELSCROLLLINES,0,&Lines,0);

 

     for (int i=0;i<(int)Lines;i++) {

          if ((short)HIWORD(wParam) == WHEEL_DELTA) {

              SendMessage(hWnd, WM_VSCROLL, SB_LINEUP, 0L);

          } else {

              SendMessage(hWnd, WM_VSCROLL, SB_LINEDOWN, 0L);

          }

     }

}

 

HIWORD(wParam)의 값에 따라 스크롤 메시지를 보내 주되 한 번 휠을 굴릴 때마다 한 줄씩 스크롤하는 것이 아니라 Lines줄씩 스크롤시킨다. Lines 값은 제어판의 설정상태에 따라 달라지는데 통상 3이며 한 번에 세 줄씩 스크롤된다. 이렇게만 작성하면 대부분의 휠 마우스와 함께 잘 동작한다. 그러나 WM_MOUSEWHEEL은 이후의 확장성을 위한 충분한 준비를 하고 있으므로 이 메시지의 원래 의도대로 제대로 코드를 작성하자면 이 정도로는 부족하며 미래의 휠 마우스에 대해서도 준비를 해야 한다.

WHEEL_DELTA라고 정의되어 있는 120이라는 값은 한 번 휠을 굴릴 때의 이동 거리인데 왜 1이나 -1이 아니라 120이라는 큰 값으로 되어 있는가 하면 현재 발표된 휠 마우스보다 더 정밀한 해상도의 휠 마우스를 지원하기 위해서이다. 휠은 딸깍거리며 걸리는 노치(Notch)가 있어서 이 노치에 걸릴 때만 메시지를 보내준다. 노치가 없는 휠 마우스의 바퀴는 걸리적거리는 것 없이 마치 자동차의 핸들처럼 부드럽게 돌아갈 것이다.

노치가 없는 휠 마우스는 휠을 자유 자재로 회전할 수 있으며 휠이 약간만 움직여도 WM_MOUSEWHEEL 메시지가 전달된다. 이때 이동 거리는 휠의 회전 정도에 따라 120보다 훨씬 더 작은 값이 될 수도 있고 큰 값이 될 수도 있다. WHEEL_DELTA라는 값은 노치가 있는 마우스의 노치 하나에 해당하는 이동 거리를 정의하는 값이며 어떤 동작(스크롤, )을 해야 할 경계값으로 정의된 것이다. 노치가 없는 마우스는 최소한 WHEEL_DELTA만큼 바퀴가 이동되었을 때만 해당 동작을 해야 한다. 그래서 이 메시지를 제대로 처리하려면 전달된 회전값의 누적분을 유지하면서 증감된 델타값만큼 스크롤해야 한다. 다음과 같이 이 함수를 수정해보자.

 

void OnMouseWheel(HWND hWnd, WPARAM wParam, LPARAM lParam)

{

     static int SumDelta=0;

     short zDelta;

     UINT Lines;

     int Unit;

    

     SystemParametersInfo(SPI_GETWHEELSCROLLLINES,0,&Lines,0);

     Unit=WHEEL_DELTA/Lines;

     zDelta=(short)HIWORD(wParam);

     SumDelta += zDelta;

     while (abs(SumDelta) >= Unit) {

          if (SumDelta > 0) {

              SendMessage(hWnd, WM_VSCROLL, SB_LINEUP, 0L);

              SumDelta-=Unit;

          } else {

              SendMessage(hWnd, WM_VSCROLL, SB_LINEDOWN, 0L);

              SumDelta+=Unit;

          }

     }

}

 

Lines는 시스템설정에 지정된 휠 스크롤 거리이며 Unit WHEEL_DELTA Lines로 나눈 값, 즉 한 번 스크롤이 발생하기 위한 휠의 이동 거리인데 통상 이 값은 40이 될 것이다. 스태틱 변수 SumDelta가 지금까지 전달된 델타값의 총 합이며 이 값의 절대값이 Unit 보다 더 커지면 부호 방향으로 스크롤시키고 누적값에서 스크롤된 만큼 감한다.

상기 두 함수의 동작은 현재까지는 완전히 동일하다. wParam으로 전달되는 zDelta값은 항상 120이나 -120이므로 한 번에 세 줄씩 아래위로 스크롤이 발생한다. 하지만 노치가 없는 휠 마우스가 발표된다면 달라진다. 휠을 미세하게 조정할 수 있다면 한 번에 세 줄씩 스크롤되는 것이 아니라 이동 거리를 정확하게 측정하여 한 줄씩 또는 한 픽셀 단위로 섬세하게 스크롤할 수 있을 것이다. 안타깝게도 아직 그런 휠 마우스를 구경해 본 바가 없어서 확인은 해보지 못했다.

이 함수는 휠을 이동을 스크롤 메시지로 보내 기존의 코드를 재활용하고 있다. 스크롤 코드는 완벽하게 작성되어 있으므로 메시지만 보내면 안전하게 스크롤된다. 그런데 스크롤 메시지를 세 번에 나누어서 전달하기 때문에 화면 갱신 속도가 느리다는 것이 좀 불만스러운데 ScrollWindow 함수는 스크롤된 만큼 비트맵 전송을 하고 새로 드러난 부분을 다시 그리기 때문에 굉장히 느리다. 속도 향상을 위해 스크롤 메시지를 쓰지 말고 이 함수가 한 번에 스크롤하도록 수정해보자.

 

void OnMouseWheel(HWND hWnd, WPARAM wParam, LPARAM lParam)

{

     static int SumDelta=0;

     short zDelta;

     UINT Lines;

     int Unit;

     int nScroll=0;

     int yInc;

     int LinePerPage;

 

     SystemParametersInfo(SPI_GETWHEELSCROLLLINES,0,&Lines,0);

     Unit=WHEEL_DELTA/Lines;

     zDelta=(short)HIWORD(wParam);

     SumDelta += zDelta;

     while (abs(SumDelta) >= Unit) {

          if (SumDelta > 0) {

              nScroll--;

              SumDelta-=Unit;

          } else {

              nScroll++;

              SumDelta+=Unit;

          }

     }

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

     yInc=LineHeight*nScroll;

     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);

}

 

이동해야 할 거리를 nScroll에 미리 구한 후 한꺼번에 스크롤하도록 변경했는데 스크롤 방식은 OnVScroll 함수에 있는 것과 동일하다. 이 코드를 최종 코드로 채택하도록 하자. 이제 ScrollWindow 함수가 한 번만 호출되므로 휠을 굴릴 때 화면이 갱신되는 속도가 거의 세 배 정도 빨라질 것이다.

이상으로 휠 마우스 지원 기능을 모두 작성했다. 그런데 운영체제 설계자들은 휠의 이동이 의미를 가지게 되는 값인 WHEEL_DELTA를 하필이면 120이라는 값으로 정의했을까 하는 의문이 들 것이다. 그 이유는 120이 적당한 크기의 해상도를 가지면서 가장 많은 약수를 거느린 수이기 때문인 것으로 추측된다. 1은 물론이고 2,3,4,5,6,8,10,12로 나누어 떨어지므로 휠의 이동 거리를 분할하기가 쉽다.