. 좌우 이동

커서이동키 메시지 처리 부분이 <Ctrl>, <Shift>키와 조합됨에 따라 코드에 많은 변화가 필요하다. <Ctrl>키와 <Shift>키를 각각 누를 수도 있고 둘 다 누른 채로 커서키를 조작할 수도 있으므로 미리 이 점을 고려해야 한다. 각각의 키 루틴에서 <Ctrl>키와 <Shift>키의 상태를 매번 조사하는 것은 무척 번거로우므로 OnKey 함수의 선두에서 이 두 키의 상태를 미리 조사해놓도록 하자.

 

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

{

     ....

     BOOL bShift, bControl;

     int OldOff;

 

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

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

 

키의 눌림 상태는 GetKeyState 함수로 조사할 수 있다. <Ctrl>키의 눌림 여부는 bControl변수에 저장했으며 <Shift>키의 눌림 여부는 bShift에 저장했다. 키 루틴에서는 이 두 변수값만 보면 어떤 키가 눌러져 있는지 쉽게 알 수 있다. OldOff 변수는 선택영역 확장시 이동 전의 위치를 저장하기 위한 변수이다. 선택영역 관리를 위해 다음 두 개의 함수를 더 준비한다.

 

void ClearSelection()

{

     if (SelStart != SelEnd) {

          SelStart=SelEnd=0;

          InvalidateRect(hWndMain,NULL,TRUE);

     }

}

 

void ExpandSelection(int Start, int End)

{

     if (SelStart==SelEnd) {

          SelStart=Start;

          SelEnd=End;

     } else {

          SelEnd=End;

     }

     InvalidateRect(hWndMain,NULL,TRUE);

}

 

ClearSelection 함수는 선택영역이 있을 경우 SelStart SelEnd를 모두 0으로 만들어 선택영역을 제거한다. 이때 화면에 그려져 있는 반전 블록을 지워야 하므로 작업영역 전체를 무효화시켜 주었다. 이 함수와 동일한 코드가 이미 OnLButtonDown에 작성되어 있는데 중복된 코드이므로 ClearSelection 호출로 바꾸도록 하자.

 

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

{

     ClearSelection();

     ...

 

ExpandSelection 함수는 인수로 전달된 Start End사이로 선택영역을 확장하는데 선택되어 있지 않으면 새로 선택영역을 만들어 주고 이미 선택된 상태라면 SelStart는 그대로 두고 SelEnd만 늘려줌으로써 선택영역을 확장한다. 임의의 두 오프셋 Start~End사이를 추가로 더 선택하고 싶다면 이 함수를 호출하면 된다.

여러 가지 유틸리티 함수들을 만들어 두었는데 이제 진짜 키보드 선택 코드를 작성해보자. 다음은 왼쪽(VK_LEFT) 이동시의 처리코드이다. 군데군데 코드가 많이 삽입되었다.

 

     case VK_LEFT:

          if (off > 0) {

               GetRCFromOff(off,r,c);

               GetLine(r,s,e);

 

            OldOff=off;

            if (bControl) {

               off=GetPrevWord(off);

            } else {

                   if (off==s) {

                        if (buf[GetPrevOff(off)]==‘\r’) {

                             off=GetPrevOff(off);

                             bLineEnd=FALSE;

                        } else {

                             bLineEnd=TRUE;

                        }

                   } else {

                        off=GetPrevOff(off);

                        bLineEnd=FALSE;

                   }

            }

 

            if (bShift) {

               ExpandSelection(OldOff,off);

            } else {

               if (SelStart != SelEnd) {

                   off=min(SelStart, SelEnd);

               }

            }

               SetCaret();

          }

 

        if (!bShift) {

            ClearSelection();

        }

          return;

 

선택영역 확장을 위해 OldOff 변수에 이동 전의 오프셋을 미리 저장해두었다. bControl이면 이전 단어로 캐럿을 옮기고 그렇지 않으면 원래 루틴대로 이전 문자만 찾는다. <Ctrl>키의 눌림 여부에 따라 이동할 위치가 달라지기는 하지만 어쨌든 전역변수 off는 이동할 위치로 이미 이동되었다.

다음은 bShift 상태에 따라 선택영역을 확장한다. 만약 <Shift>키가 눌러져 있다면 ExpandSelection 함수를 호출하여 OldOff~off 사이, 즉 이동 전의 위치와 이동 후의 새 위치까지를 추가로 더 선택한다. ExpandSelection 함수가 선택 존재 여부에 따라 새로 선택을 만들든지 아니면 확장하므로 그 전에 선택영역이 있었는가 없었는가는 여기서 신경쓸 필요가 없다.

선택영역이 있었는데 <Shift>키가 눌러지지 않았다면 선택을 해제하고 캐럿만 이동한다. 이때 캐럿이 이동해야 할 위치는 현재 위치 off를 기준으로 하지 않고 블록의 시작점을 기준으로 한다. 즉 선택영역이 있는 상태에서 왼쪽으로 이동할 때는 현재 위치를 완전히 무시하고 블록의 왼쪽으로 캐럿을 보내는 것이다. 왜 이렇게 하는가 하면 선택이 있는 상태에서 사용자는 현재 캐럿이 있는 위치를 보지 않고 블록 전체를 캐럿처럼 취급하기 때문이다.

그렇다고 해서 선택을 풀 때 현재 위치의 이전 문자로 가는 것이 꼭 틀린 것은 아니다. 선택을 하고 있는 중에도 캐럿은 계속 깜박이는데 이때 캐럿을 여전히 현재 위치로 해석한다면 이전 문자로 가는 것이 옳다. 하지만 이 예제처럼 선택중의 캐럿을 선택의 방향을 명시하는 용도로 해석한다면 꼭 이전 문자로 가지 않아도 무방하다.

이런 문제는 논리를 따지는 것보다 사용자들이 어떤 식으로 캐럿을 조작하는가와 어떤 방식을 더 좋아하는가를 따져 보는 것이 더 합리적이다. 이 예제는 블록의 왼쪽으로 가는 방식을 적용했다. 어떤 편집기들은 블록의 처음을 현재 위치로 가정하고 블록의 왼쪽으로 가면서 동시에 다시 한 칸 더 왼쪽으로 가기도 한다. 아주 사소한 차이이지만 편집기의 동작들이 천차만별로 달라질 수 있는 여지가 많음을 알 수 있다.

<Shift>키가 눌러지지 않았으면 단순한 캐럿이동이므로 선택영역은 해제해야 한다. 이 문장은 if (off > 0) { } 블록의 밖에서 따로 처리하고 있는데 왜냐하면 off 0인 상태, 즉 문서의 처음에서 왼쪽으로 이동할 때 이동은 하지 못하더라도 선택영역은 해제해야 하기 때문이다.

이 함수는 크게 두 부분으로 나누어진다. 첫 부분은 <Ctrl>키를 처리하는데 <Ctrl>키의 상태에 따라 이전 문자나 또는 이전 단어로 오프셋을 옮겨준다. 두 번째 부분은 <Shift>키를 처리하며 첫 번째 부분이 옮겨놓은 위치까지 선택을 확장하거나 아니면 단순 이동한다. 이미 선택이 되어 있으면 선택을 해제하는 일까지 한다. 새 오프셋을 계산하는 부분과 선택을 확장하는 부분이 분리되어 있으므로 <Shift>키와 <Ctrl>키를 동시에 눌러도 새 오프셋까지 선택이 잘 확장된다. 만약 <Shift>키를 <Ctrl>키보다 먼저 처리하려고 한다면 구조가 무척 복잡해질 것이다. 또 초보자들은 이 함수를 다음과 같이 설계하는 실수를 하기도 한다.

 

if (bShift) { }

if (bControl) { }

if (bShift && bControl) { }

if (!bShift && !bControl) { }

 

이렇게 각각의 경우를 나누면 중복되는 코드가 많아져 효율적이지 못하며 코드를 수정하기도 무척 번거로워진다. 한마디로 별로 좋은 설계가 아니다. VK_LEFT Ctrl, Shift 처리뿐만 아니라 bLineEnd라는 아주 복잡한 변수를 다루고 있기 때문에 자칫 잘못 설계하면 당장의 동작은 물론이고 이후의 확장에도 큰 걸림돌이 된다. 다음은 오른쪽 이동을 작성해보자.

 

     case VK_RIGHT:

          if (off < (int)lstrlen(buf)) {

               GetRCFromOff(off,r,c);

               GetLine(r,s,e);

 

            OldOff=off;

            if (bControl) {

               off=GetNextWord(off);

            } else {

                   if (off==e) {

                        if (buf[e]==‘\r’) {

                             off=GetNextOff(off);

                        }

                        bLineEnd=FALSE;

                   } else {

                        off=GetNextOff(off);

                        if (off==e && buf[off]!=‘\r’) {

                             bLineEnd=TRUE;

                        } else {

                             bLineEnd=FALSE;

                        }

                   }

            }

 

            if (bShift) {

               ExpandSelection(OldOff,off);

            } else {

               if (SelStart != SelEnd) {

                   off=max(SelStart, SelEnd);

               }

            }

               SetCaret();

          }

 

        if (!bShift) {

            ClearSelection();

        }

          return;

 

왼쪽 이동과 완전히 동일한 구조이며 사용하는 논리도 역시 동일하다. 다만 선택을 풀 때 블록의 처음으로 가는 것이 아니라 블록의 끝으로 간다는 점과 <Ctrl>키를 누를 때 다음 단어를 찾는다는 점이 좀 다를 뿐이다. 다음은 Home, End의 코드를 작성해보자.

 

     case VK_HOME:

          GetRCFromOff(off,r,c);

        OldOff=off;

        if (bControl) {

            off=0;

        } else {

            off=GetOffFromRC(r,0);

        }

          bLineEnd=FALSE;

        if (bShift) {

            ExpandSelection(OldOff,off);

        } else {

            ClearSelection();

        }

          SetCaret();

          return;

     case VK_END:

          GetRCFromOff(off,r,c);

        OldOff=off;

        if (bControl) {

            off=lstrlen(buf);

        } else {

            off=GetOffFromRC(r,1000000);

        }

          if (buf[off]!=‘\r’ && buf[off]!=0) {

               bLineEnd=TRUE;

          }

        if (bShift) {

            ExpandSelection(OldOff,off);

        } else {

            ClearSelection();

        }

          SetCaret();

          return;

 

<Shift>키에 대한 처리는 좌우이동과 동일하다. <Ctrl>키에 대한 처리도 비교적 간단한데 <Ctrl+Home>은 문서의 처음으로 가라는 명령이므로 off 0으로 만들면 되고, <Ctrl+End>는 문서의 끝으로 가라는 명령이므로 버퍼의 끝으로 이동하면 된다.