. 커서모양 변경

마우스와 키보드, 마진영역을 총 동원해서 각종 방법으로 선택영역을 만드는 코드를 구현했다. 선택영역은 주로 클립보드 처리를 위해 사용되지만 마우스로 직접 드래그해서 문자열을 복사하거나 옮길 수도 있다. 이때 사용자에게 자신이 어떤 작업을 하고 있는지를 보여주기 위해 커서모양을 바꾸는 것이 좋다. 이 상태에서 마우스 버튼을 놓으면 문자열이 이동되는지 복사되는지를 커서모양으로 확인할 수 있도록 하는 것이다.

문자열 드래그 기능을 넣기 전에 먼저 선택영역에서 커서모양을 바꿔보자. I자 모양의 커서는 이 자리에서 문자열을 입력할 수 있다는 뜻이므로 이동이 가능하다는 뜻의 화살표 모양으로 바꾸는 것이 더 직관적이다. 이왕 커서모양을 바꾸는 김에 마진영역에서의 커서모양도 같이 변경할 것이다.

선택영역에서 커서를 다르게 보이도록 하고 싶으면 특정 좌표가 선택영역 안에 있는지 아닌지를 조사해야 한다. 다음 함수는 인수로 전달된 x, y 좌표에 대해 선택영역 안인지 조사한다. 이때 인수로 전달되는 x, y는 문서상의 픽셀 좌표이다.

 

BOOL IsInSelection(int x,int y)

{

     int toff;

     int r,c;

     int ss,se;

     int SelFirst, SelSecond;

     int x1,y1,x2,y2;

     RECT rt;

     POINT pt;

 

     if (SelStart == SelEnd) {

          return FALSE;

     }

 

     SelFirst=min(SelStart,SelEnd);

     SelSecond=max(SelStart,SelEnd);

 

     toff=GetOffFromXY(x,y);

     GetRCFromOff(toff,r,c);

 

     if (SelFirst > pLine[r].End || SelSecond <= pLine[r].Start) {

          return FALSE;

     }

 

     if (SelFirst < pLine[r].Start) {

          ss=pLine[r].Start;

     } else {

          ss=SelFirst;

     }

 

     if (SelSecond > pLine[r].End) {

          se=pLine[r].End;

     } else {

          se=SelSecond;

     }

 

     se=GetPrevOff(se);

 

     GetXYFromOff(ss,x1,y1);

     GetXYFromOff(se,x2,y2);

     if (IsDBCS(se)) {

          x2 += GetCharWidth(buf+se,2);

     } else {

          x2 += GetCharWidth(buf+se,1);

     }

 

     SetRect(&rt,x1,y1,x2,y1+LineHeight);

     pt.x=x;

     pt.y=y;

     if (PtInRect(&rt,pt)) {

          return TRUE;

     } else {

          return FALSE;

     }

}

 

아주 간단한 작업인 것 같은데 이 함수의 길이가 예상보다 길다. 커서가 있는 현재 위치의 오프셋은 GetOffFromXY로 구할 수 있고 선택영역의 범위는 SelStat, SelEnd로 알 수 있다. 하지만 오프셋을 비교해서 선택영역 내부인지 알아내는 방법은 두 가지 점에서 문제가 있다.

첫 번째로 GetOffFromXY가 조사하는 오프셋은 글자의 경계를 기준으로 하지 않고 글자폭의 절반을 기준으로 하기 때문에 정확한 영역조사가 되지 않는다. 두 번째로 설사 글자의 경계에서 오프셋을 조사하는 함수를 만든다 하더라도 선택영역의 중간에 있는 줄 끝 여백 때문에 오프셋을 비교하는 방법은 쓸 수 없다.

그림에서 바로 여기라고 칭한 저기가 문제다. 이 점의 오프셋을 조사하면 이 줄의 끝인 개행코드의 오프셋이 조사될 것이고 이 오프셋은 분명히 SelStart SelEnd 사이에 있지만 그렇다고 선택영역 안의 좌표라고 할 수는 없다. 글자가 없는 부분을 드래그해서 옮길 수는 없기 때문이다. 그래서 오프셋 비교를 해서는 안되면 픽셀 좌표끼리 비교를 해야 한다.

그럼 IsInSelection 함수를 처음부터 분석해보자. 우선 선택영역이 아예 없으면 더 볼 것도 없이 FALSE를 리턴하면 된다. 선택이 없는 상태에서야 어떤 점을 조사하든지 선택영역 안에 있을 리가 만무하다. 일단 선택영역이 있어야 조사의 의미가 있다.

(x, y) 좌표의 줄번호를 구해 r에 일단 대입해두었다. 그리고 이 줄의 범위가 선택영역에 포함되어 있는지 조사한다. 현재 줄이 선택영역의 시작보다 더 앞에 있거나 선택영역의 끝보다 더 아래에 있다면 나머지 좌표는 조사할 필요가 없다. 줄 전체가 선택영역 밖에 있으므로 줄 내의 어떤 좌표도 선택영역 안에 있을 리가 없는 것이다. 현재 줄의 한 지점이 선택영역 안에 있으려면 최소한 다음 세 가지 상태중 하나여야 한다.

현재 줄에서 선택이 시작되었거나(두 번째 줄) 아니면 앞 줄에서 시작해서 현재 줄에서 선택이 끝이 났다(네 번째 줄)면 현재 줄의 일부가 선택된 것이다. 아니면 세 번째 줄처럼 선택 시작은 현재줄보다 위에 있고 선택의 끝은 현재줄 아래에 있어 줄 전체가 선택되어 있어야 한다. 첫 번째 줄이나 다섯 번째 줄은 아예 선택영역에 걸쳐 있지도 않으므로 이 줄에 있다면 이 줄 내의 어떤 점도 선택영역 안에 있을 수가 없으므로 더 재 볼 것도 없이 FALSE를 리턴하면 된다.

선택의 끝점이 줄의 시작보다 더 앞에 있다는 조건에 <=연산자가 사용되었음을 주의한다. 설사 선택의 끝점이 줄의 시작과 같다고 해도 범위의 원칙에 따라 끝점은 선택에 포함되지 않기 때문에 이 줄에는 선택영역이 없는 것이다. 이 조건식에 < 연산자를 사용하면 바로 앞 줄에서 선택이 끝난 경우도 선택이 이 줄에 걸쳐 있는 것으로 잘못 판단하게 된다.

줄 내에 선택영역이 포함되어 있다는 것을 확인했다면 과연 어떤 부분이 선택영역인지 조사한다. ss, se 변수는 줄 내의 선택영역 오프셋을 대입받는 변수이다. ss를 계산하는 코드를 보자. 선택 시작점이 이 줄의 시작점보다 앞에 있다면 이 줄은 처음부터 선택되어 있는 것이므로(그림의 세 번째, 네 번째 줄) 줄의 처음이 ss가 되고 아니라면 줄의 중간에서 선택이 시작된 것이므로(그림의 두 번째 줄) SelFirst ss가 된다.

줄 내의 선택 끝점 se도 비슷한 방법으로 조사된다. 선택 끝점이 이 줄 끝보다 뒤에 있으면 이 줄은 끝까지 선택되어 있으므로(그림의 두 번째, 세 번째 줄) 줄 끝이 se가 되고 아니라면 줄 중간에서 선택이 끝난 것이므로(그림의 네 번째 줄) SelSecond se가 된다. 줄 내의 선택 오프셋을 구했으면 이 선택 오프셋으로부터 화면 좌표를 계산한다.

좌상단 좌표 (x1, y1) ss 오프셋으로부터 GetXYFromOff 함수를 호출하여 쉽게 구할 수 있다. 우하단 좌표는 조금 어려운데 se 오프셋의 좌표를 바로 사용할 수는 없고 se보다 한 칸 앞 문자의 좌표를 구한 후 이 문자의 폭을 다시 더해야 한다. 왜냐하면 se는 선택영역의 끝을 가리키고 있는 것이 아니라 선택영역 끝 다음 문자를 가리키고 있기 때문이다. 자동개행된 줄 끝까지 선택되어 있는 경우 se는 다음 줄의 처음을 가리키고 있고 이 점의 좌표를 조사하면 x 0(또는 MarginWidth)으로 조사되므로 선택영역 좌표가 제대로 조사되지 않는다.

그래서 좀 번거롭지만 GetPrevOff 함수로 한 칸 앞으로 이동하는데 이때 조사된 오프셋이 선택영역의 마지막 문자 위치이다. 이 오프셋으로 GetXYFromOff를 호출하여 x2를 구하고 다시 문자폭을 더했다. ss se는 같은 줄 내에 소속된 오프셋이므로 y1 y2는 항상 같으며 선택영역의 우하단 좌표는 (x2, y1+LineHeight)가 된다. (x2, y2+LineHeight) 이렇게 적고 싶으면 물론 그렇게 해도 상관없다.

줄 사이의 여백은 반전 블록으로 표시되지는 않지만 이 영역에 커서가 있을 때도 선택영역으로 인정하였다. 그래서 상단 좌표 y1에서 하단 좌표를 구할 때 FontHeight를 더하지 않고 줄간인 LineHeight를 더했다. 만약 FontHeight를 더한다면 계산은 더 정확하겠지만 여러 줄이 선택된 상태에서 커서를 수직으로 이동할 때 커서가 반복적으로 바뀌기 때문에 눈에는 거슬린다. 그래서 줄간 여백도 선택영역에 포함되는 것으로 정책을 결정하였다.

커서 위치 판별을 위한 함수를 완성했다. 다음은 실행중에 사용할 커서를 미리 읽어 놓도록 하자. 커서모양이 수시로 변경되므로 필요할 때 읽는 것보다 미리 읽어 놓는 것이 속도상 유리하다. 다섯 개의 커서 핸들 변수를 준비하고 OnCreate에서 적당한 모양의 커서를 읽어 놓는다.

 

HCURSOR hCSel,hCCopy,hCMove,hCMargin,hCNoDrop;

 

BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     ....

     hCSel=LoadCursor(NULL,IDC_ARROW);

     hCNoDrop=LoadCursor(NULL,IDC_NO);

     hCCopy=LoadCursor(NULL,IDC_APPSTARTING);

     hCMove=LoadCursor(NULL,IDC_ARROW);

     hCMargin=LoadCursor(NULL,IDC_SIZENESW);

 

아직 리소스 작업을 할 수 있는 상황이 아니므로 비슷한 모양의 스톡 커서를 사용하기로 한다. 선택영역의 좌표를 구했으면 ptInRect 함수로 현재 좌표가 이 영역 안에 있는지 조사할 수 있고 그 결과를 리턴하면 된다. 이 함수가 조사하는 결과에 따라 커서의 모양을 실제로 변경시키는 작업은 OnSetCursor에서 한다.

 

BOOL OnSetCursor(HWND hWnd, HWND hwndCursor, UINT codeHitTest, UINT msg)

{

     POINT pt;

 

     GetCursorPos(&pt);

     ScreenToClient(hWnd,&pt);

 

     if (codeHitTest==HTCLIENT) {

          if (pt.x < MarginWidth) {

              SetCursor(hCMargin);

              return TRUE;

          }

 

          pt.x += xPos;

          pt.y += yPos;

          if (IsInSelection(pt.x, pt.y)) {

              SetCursor(hCSel);

              return TRUE;

          }

     }

 

     return FORWARD_WM_SETCURSOR(hWnd,hwndCursor,codeHitTest,msg,DefWindowProc);

}

 

커서의 위치를 구하고 작업영역 좌표로 바꾼 후 지금 커서가 어디에 있는가에 따라 다른 모양으로 보여준다. 마진영역에 있다면 왼쪽으로 기울어진 화살표를 보여주고 선택영역 안에 있다면 오른쪽으로 기울어진 화살표를 보여준다. 둘 다 아니라면 윈도우 클래스에 정의된 I자 모양의 커서가 사용될 것이다. 이 처리는 반드시 커서가 작업영역(HTCLIENT)에 있을 때만 해야 한다. 커서의 좌표값만으로 조건 점검을 하면 경계선이나 타이틀바에 커서가 있을 때조차 커서를 바꾸려고 들 것이다.