. 문자열 드래그

다음은 선택영역을 드래그하여 문자열을 이동 또는 복사하는 기능을 만들어보자. 이 기능은 클립보드 조작없이 마우스만으로 문장을 재배치할 수 있어 소스 코드 편집시에 아주 유용하다. 선택영역을 드래그하면 문자열이 이동되고 <Ctrl>키를 누른 채로 드래그하면 복사된다. 문자열 드래그 기능은 블록선택이나 줄단위 선택과는 또 다른 모드이므로 모드를 구분하기 위한 전역변수가 필요하다. 다음 변수를 선언한다.

 

BOOL bDragSel;

 

BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     ....

     bDragSel=FALSE;

 

이 변수가 TRUE이면 문자열 드래그 모드가 되며 마우스 메시지 처리 함수(구체적으로 OnMouseMove)들은 이 변수값에 따라 동작이 달라진다. OnCreate에서 이 변수를 FALSE로 초기화한다. 문자열 드래그의 시작은 마우스 왼쪽 버튼을 클릭한 시점인 OnLButtonDown에서 처리한다.

 

void OnLButtonDown(HWND hWnd, BOOL fDoubleClick, int x, int y, UINT keyFlags)

{

     int toff;

     BOOL bShift, bControl;

     int r,c;

     int nr;

     int SelFirst, SelSecond;

 

     bShift=((GetKeyState(VK_SHIFT) & 0x8000) != 0);

     bControl=((GetKeyState(VK_CONTROL) & 0x8000) != 0);

 

    if (IsInSelection(x+xPos, y+yPos)) {

        bDragSel=TRUE;

        SetCapture(hWnd);

        bCapture=TRUE;

        return;

    }

 

     if (x < MarginWidth) {

     ....

 

현재 커서 위치가 선택영역 안이면 즉, 선택영역에서 마우스 버튼을 클릭했으면 bDragSel TRUE로 변경하여 문자열 드래그가 시작되었음을 기록해놓는다. 드래그중에 마우스 메시지를 계속 받아야 하므로 커서는 캡처해야 한다. 드래그중의 동작은 OnMouseMove에서 처리한다.

 

void OnMouseMove(HWND hWnd, int x, int y, UINT keyFlags)

{

     ....

    BOOL bControl;

    int sx,sy;

 

     if (bCapture == FALSE) {

          return;

     }

 

    bControl=((GetKeyState(VK_CONTROL) & 0x8000) != 0);

 

    if (bDragSel) {

        if (x > frt.left && x < frt.right && y > frt.top && y < frt.bottom) {

           toff=GetOffFromXY(x+xPos,y+yPos);

           GetXYFromOff(toff,sx,sy);

           SetCaretPos(sx-xPos,sy-yPos);

           if (bControl) {

               SetCursor(hCCopy);

           } else {

               SetCursor(hCMove);

           }

        } else {

           SetCursor(hCNoDrop);

        }

    } else {

          OldOff=off;

          ....

     }

 

     bInstTimer=FALSE;

     ....

 

문자열 드래그중이고 포맷팅영역 안이면 캐럿의 위치를 마우스 커서 위치로 옮겨 드롭할 자리를 보여준다. 이때 SetCaret을 호출할 수는 없으며 반드시 SetCaretPos 함수로 직접 캐럿을 옮겨야 한다. 왜냐하면 SetCaret은 전역변수 off 값을 기준으로 캐럿을 옮겨주는데다 캐럿을 옮겨주는 작업 외에 강제 스크롤 기능 등 여러 가지 처리를 같이 하고 있기 때문이다. bDragSel일 때의 캐럿이동은 드롭 위치를 보여주는 임시적인 이동에 불과하다.

커서이동중에 커서의 모양도 바꿔주는데 <Ctrl>키가 눌러져 있으면 복사커서를 보여주고 그렇지 않으면 이동커서를 보여준다. 만약 포맷팅영역 밖으로 커서가 나갔으면 이 상태에서는 드롭할 수 없으므로 드롭 금지 커서를 보여주어야 한다.

OnMouseMove는 드롭될 자리를 보여주고 <Ctrl>키의 상태에 따라 적절히 커서만 바꾸면 된다. 문자열 복사나 이동처리는 버튼을 놓을 때인 OnLButtonUp에서 모두 처리한다. 지금까지 별로 하는 일이 없었던 OnLButtonUp에 대량의 코드가 추가되었다.

 

 

void OnLButtonUp(HWND hWnd, int x, int y, UINT keyFlags)

{

     BOOL bControl;

     int SelLen;

     int toff;

 

     bCapture=FALSE;

     bSelLine=FALSE;

     ReleaseCapture();

     KillTimer(hWnd,1);

 

     bControl=((GetKeyState(VK_CONTROL) & 0x8000) != 0);

 

     if (bDragSel) {

          bDragSel=FALSE;

          if (IsInSelection(x+xPos, y+yPos)==TRUE) {

              off=GetOffFromXY(x+xPos,y+yPos);

              SetCaret();

              SelStart=SelEnd=0;

              Invalidate(-1);

          } else {

              if (x > frt.left && x < frt.right && y > frt.top && y < frt.bottom) {

                   SelLen=abs(SelStart-SelEnd);

                   toff=GetOffFromXY(x+xPos,y+yPos);

                   CopyString(bControl,min(SelStart,SelEnd),toff,SelLen);

 

                   SelStart=toff;

                   SelEnd=SelStart+SelLen;

                   off=SelEnd;

                   SetCaret();

                   Invalidate(-1);

              } else {

                   SetCaret();

              }

          }

     }

}

 

이 함수가 드롭을 처리하려면 문자열 드래그 상태여야 하므로 모든 코드는 if (bDragSel) { 블록에 작성되어 있다. 드롭된 위치가 어디쯤인가에 따라 이 함수의 동작이 결정되는데 선택영역 안이라면 문자열 이동은 취소된다. 드래그를 시작한 곳에 다시 드롭했으므로 이동할 문자열과 대상 위치가 동일하여 옮길 필요가 없게 된 셈이다. 이때는 캐럿을 드롭한 자리로 옮겨 주고 선택영역은 해제한다. 굳이 선택영역을 해제할 필요까지야 있겠는가 생각될지도 모르겠지만 여기서 선택을 해제하지 않으면 선택영역을 단순 클릭했을 때도 선택이 그대로 남아있게 된다.

선택영역 안이 아니라면 포맷팅영역 안인지를 검사해 본다. 만약 포맷팅영역이 아니라면 드롭할 수 없는 곳에, 예를 들어 윈도우 바깥에서 마우스 버튼을 놓은 것이므로 이때도 드롭을 취소해야 한다. , 이 경우는 선택을 풀 필요도 없고 캐럿을 드롭한 곳으로 옮기지도 않으며 단순히 SetCaret만 호출하여 캐럿을 원래 있던 자리(SelEnd)로 다시 보내 주기만 하면 된다. 문자열 드래그 자체를 완전히 취소하고 드래그를 시작하기 전의 상태로 돌아가는 것이다.

포맷팅영역 안이고 선택영역 외부에 드롭되었다면 여기서 드롭 처리를 한다. <Ctrl>키의 상태에 따라 문자열 이동 또는 복사를 하는데 여기서 직접 하지 않고 CopyString이라는 문자열 이동 함수를 호출한다. 이 함수는 다음과 같이 작성한다.

 

void CopyString(BOOL bCopy, int from, int &to, int len)

{

     TCHAR *t;

     int orito=to;

 

     t=(TCHAR *)malloc(len+1);

     lstrcpyn(t,buf+from,len+1);

     Insert(to,t);

 

     if (bCopy==FALSE) {

          if (to > from) {

              Delete(from,len);

               to-=len;

          } else {

              Delete(from+len,len);

          }

     }

     free(t);

}

 

첫 번째 인수로 이동인지 복사인지를 전달받는데 이 인수가 TRUE이면 복사이다. 두 번째 인수는 대상 문자열의 시작 오프셋, 세 번째 인수는 이동 또는 복사할 오프셋이며 4번째 인수는 대상 문자열의 길이이다. 이동이라는 동작은 복사 후 삭제와 같으므로 복사 동작을 먼저 처리한다. 임시버퍼 t에 드래그된 문자열 길이만큼 메모리를 할당한 후 문자열을 복사하고 Insert 함수를 호출하여 드롭된 위치에 문자열을 삽입하기만 하면 복사는 아주 간단하게 이루어진다.

일단 복사를 먼저 해놓고 bCopy인수값을 점검하여 이동인지 확인해 본다. 만약 이 인수가 FALSE이면 복사된 원본 문자열을 삭제하면 된다. 삭제 동작은 드롭된 위치에 따라 달라지는데 원래 문자열이 있던 자리보다 더 뒤쪽에 드롭되었다면 SelStart~SelEnd 사이를 삭제하면 된다. 반대로 원래 문자열보다 더 앞쪽에 드롭되었다면 삭제할 대상 문자열의 자리가 이동되었으므로 오프셋을 잘 조정해야 한다.

위 그림에서 이동 단어를 드래그하여 드래그 앞에 드롭했다고 해보자. 이동 이 드롭된 곳으로 복사되면서 동시에 드롭된 위치 이후의 문자들은 삽입된 길이만큼 뒤로 밀린다. 이때 드래그되기 전의 문자열 위치를 가지고 있는 SelStart SelEnd도 그만큼 뒤로 이동할 것이다. 그래서 원래 선택된 문자열을 제대로 삭제하려면 선택의 시작점에서 선택영역의 길이만큼 더해야 한다.

이동, 복사가 완료된 후에는 드롭된 문자열을 선택한 상태로 만들어 드롭된 문자열임을 확실하게 보여주고 다음 작업을 쉽게 하도록 한다. 드롭된 자리 toff가 새로운 선택의 시작점인데 뒤쪽으로 드롭된 경우는 toff도 선택영역의 길이만큼 조정될 수 있다. 그래서 CopyString 함수의 세 번째 인수는 레퍼런스형으로 전달되어 조정된 후의 오프셋 값을 다시 돌려 주도록 하였다. 드롭 관련 코드가 좀 복잡해보이지만 하나씩 케이스를 따져 가며 점검해보면 어렵지 않게 이해가 될 것이다.

이 실습을 하면서 문자열 복사와 이동처리에 클립보드를 사용하면 되지 않을까 하는 생각을 한 사람도 분명히 있을 것이다. 드래그한 문자열을 클립보드에 복사하거나 잘라낸 후 드롭한 곳에 붙여넣기를 하면 아주 깔끔하다. 클립보드 관련 코드는 이미 다 작성되어 있으므로 코드는 더 작성할 필요도 없고 메시지만 순서대로 보내주면 될 것 같다. 기술적으로 충분히 가능하고 코드도 더 쉽다.

하지만 이런 발상은 가능하다고는 해도 절대로 바람직하지 않다. 왜냐하면 클립보드는 시스템 전체를 통틀어 단 하나밖에 없는 소중한 시스템 자원이고 이 클립보드에 대한 소유권은 전적으로 사용자에게 있기 때문이다. 사용자의 명시적인 명령이 없는 한 어떠한 프로그램도 클립보드를 건드려서는 안된다.

이런 시나리오를 생각해보자. 사용자는 작업중에 중요한 SQL문을 클립보드에 잘라 넣었다. 그리고 이 SQL문을 붙여넣을 자리로 이동한 후 보니 그 자리에 쓸데 없는 문장이 있어 밑으로 드래그해서 이동했다. 그리고 SQL문을 붙여넣었는데 이때 삽입되는 문장은 SQL문이 아니라 방금 드래그한 쓸데 없는 문장이라면 어떻게 되겠는가? 아마 ApiEdit는 당장 하드디스크에서 감쪽같이 사라질 것이다.

클립보드는 온전히 사용자의 것이며 사용자만 사용할 권한이 있다. 프로그램이 내부적인 자료 이동이나 다른 프로그램과 통신하기 위한 IPC 방법으로 클립보드를 사용했다면 그것은 정말로 주제넘은 짓을 한 것이다. 문자열 드래그가 정말 유용한 기능인 가장 큰 이유는 클립보드를 경유하지 않고 편집을 할 수 있다는 점이며 사용자들은 이 사실을 이미 알고 있다. 그 기대에 부응하기 위해 CopyString이라는 함수를 애써 만든 것이다. 여기서 만든 CopyString 함수는 문서를 변경하는 주요 함수 중 하나로서 Insert, Delete 만큼이나 중요한 역할을 하게 된다.

문자열 드래그를 위한 마지막 처리는 커서를 제대로 관리하는 것이다. <Ctrl>키를 누르면 문자열이 복사된다는 것을 확실하게 보여주어야 하며 그래서 OnMouseMove에서 <Ctrl>키의 상태를 일일이 체크하여 커서를 변경하고 있다. 하지만 이 처리만으로는 커서 관리가 충분하지 않은데 커서가 움직이지 않는 동안에도 <Ctrl>키의 상태가 변할 수 있기 때문이다. 어떤 경우라도 커서의 모양을 제대로 보여주고 싶으면 OnKey에서 <Ctrl>키의 누름과 놓음을 점검해서 커서를 바꿔야 한다.

 

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

{

     ....

    if (bDragSel) {

        if (vk==VK_CONTROL) {

           if (fDown) {

               SetCursor(hCCopy);

           } else {

               SetCursor(hCMove);

           }

        }

        return;

    }

 

     if (fDown==FALSE)

          return;

 

VK_CONTROL키가 눌러졌으면 복사 모양의 커서로 변경하고 그렇지 않으면 이동 모양의 커서로 변경하였다. 메시지크래커가 WM_KEYDOWN, WM_KEYUP을 같은 함수로 보내 주기 때문에 fDown 값을 한 곳에서 점검하여 커서 상태를 변경할 수 있게 되었다. 키를 놓을 때의 메시지도 받아야 하므로 이 코드는 반드시 if (fDown == FALSE) 조건문 이전에 있어야 한다. 이제 커서가 정지된 상태에서도 <Ctrl>키를 누르면 커서모양이 바뀔 것이다.

OnKey에서 <Ctrl>키를 누르거나 뗄 때마다 커서모양을 바꿔 준다고 해서 OnMouseMove의 커서 변경 코드를 삭제해서는 안된다. 왜 안되는지는 코드를 삭제해보면 알 수 있다.