. bLineEnd

자동개행 기능은 편집기의 필수 기능이며 사용자에게 무척 편리한 기능이지만 메모리상의 물리적인 한 줄이 화면상에 여러 줄로 출력되므로 논리적으로 문제가 있다. 화면에 출력된 모양이 메모리에 저장되어 있는 원래의 문서 모습과는 다른 것이 문제의 근원이다. 자동개행이란 좀 다르게 얘기하면 억지 개행이며 원래 개행되지 않아야 할 곳에서 강제로 개행을 시키는 것이다. 원래의 모습과 다르게 문서를 출력하므로 부작용이 생긴다. 어떤 부작용이 있는지 알아 보고 문제를 해결하기 위해 OnCreate에 다음 임시 코드를 작성해보자.

 

 

BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     ....

     memset(buf,0,65536);

     lstrcpy(buf,"동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라 만세 "

          "무궁화 삼천리 화려 강산 대한 사람 대한으로 길이 보전하세");

 

buf에 초기 문자열을 복사해놓았는데 이렇게 되면 실행하자마자 이 문자열들이 문서에 입력되어 있을 것이다. 매번 문자열을 타이프하기가 귀찮기도 하지만 항상 같은 데이터로 테스트를 해 볼 수 있으므로 편리하며 또 문제의 현상과 수정 후의 동작을 일관성있게 비교해 볼 수 있다. 이 코드는 임시 코드이므로 다 사용한 후에는 삭제하도록 하자. 이제 예제를 실행해보자.

과연 멋지게 자동개행된다. 캐럿이 좌상단의 첫 위치에 있는데 이 상태에서 End키를 눌러 줄의 끝으로 가 보도록 하자. 줄의 끝이라면 자 다음이므로 이 위치로 캐럿이 가야 할 것이다. 그런데 얼씨구, 그렇게 되지 않고 캐럿이 다음 줄 처음인 자 앞으로 간다. 이게 도대체 어떻게 된 것일까? VK_END의 코드를 보면 현재 줄의 제일 끝 열로 이동하도록 되어 있지만 다음 줄의 처음으로 이동을 한 것이다. VK_END의 코드가 잘못된 것도 아니고 GetOffFromRC가 틀린 것도 아니며 원인은 다른 곳에 있다.

이번에는 첫 줄의 끝 단어인 하느님이에서 자 앞에 캐럿을 두고 오른쪽으로 계속 이동해보자. 캐럿이 이동하는 순서는 다음 그림과 같다.

자 앞에서 오른쪽으로 이동하면 캐럿은 자의 뒤로 이동하는데 이 위치는 곧 자의 앞이기도 하다. 자 다음이라는 표현과 자 앞이라는 표현은 결국 같은 표현이다. 자 앞에서 오른쪽으로 이동하면 캐럿은 자의 앞으로 이동한다. 이 경우는 줄이 바뀌었기 때문에 자의 뒤라고 표현할 수 없다. 자의 다음 위치인 자는 자동개행에 의해 줄이 바뀌었으므로 어떻게 하더라도 자 뒤로 캐럿이 갈 수가 없다. 왼쪽으로 이동할 때도 마찬가지로 자 앞에서 왼쪽으로 가면 자 뒤로 가는 것이 아니라 자의 앞으로 간다.

캐럿을 좌우로 이동시키는 VK_LEFT, VK_RIGHT의 코드는 정상적으로 작성되어 있으며 이 코드가 정상이라는 것은 메모리상의 이동을 확인해보면 된다. 오른쪽 그림에서 보다시피 메모리상의 오프셋 이동은 지극히 정상적이며 2바이트씩 제대로 이동하고 있다. 문제의 핵심은 자동개행된 경우 줄의 끝과 다음 줄의 처음이 메모리상의 동일 오프셋이라는 점이다.

자동개행된 줄은 메모리상의 오프셋과 화면상의 좌표가 반드시 1:1로 대응되지 않는다. 대부분의 오프셋은 대응되는 화면 위치가 하나밖에 없지만 자동개행에 의해 잘려진 곳은 대응되는 화면상의 위치가 두 군데 있다. 여기뿐만 아니라 삼천리 사이, 보전하세 사이도 마찬가지 경우이다.

VK_END는 줄 끝의 오프셋을 제대로 찾는다. 하지만 이 오프셋과 대응되는 화면상의 위치를 제대로 찾지 못한다. 마찬가지로 좌우로 이동할 때 이동할 오프셋은 정확하게 찾았지만 줄의 제일 끝으로는 캐럿이 갈 수가 없다.

이제 문제가 무엇인가를 확실히 알았다. 대부분의 경우 문제의 원인을 알면 해결 방법을 찾는 것은 그보다 쉽다. 어떻게 이 문제를 해결할 수 있을지 실습을 계속 진행하기 전에 먼저 고민을 해보아라. 여러분은 이 문제를 과연 어떻게 해결할 것인가? 귀찮다고 생각하지 말고 재미있는 퍼즐을 푼다는 기분으로 생각을 해보아라. 충분한 생각을 한 후 이 책의 해결방법이 여러분들이 생각한 것과 같은지 비교해 본다면 훨씬 더 재미있게 공부할 수 있을 것이다.

한 오프셋에 대해 두 개의 후보 좌표가 있기 때문에 근본적으로 오프셋만으로 화면의 절대 위치를 구하는 것은 불가능하다. 이 결정을 하기 위한 정보가 부족한 것이다. 그래서 두 후보 좌표 중 어떤 좌표를 취할 것인가를 지정하는 별도의 플래그를 만들어야 한다. 둘 중 하나를 선택하는 문제이므로 플래그는 BOOL형이면 충분하다.

이 플래그는 두 후보 좌표 중 어떤 좌표를 취할 것인가를 지정하며 캐럿을 이동할 때마다 항상 관리해야 한다. 이제 코드를 작성하면서 계속 연구해보도록 하자. 플래그의 이름은 bLineEnd로 정했으며 이 값이 TRUE이면 캐럿 위치는 줄 끝(LineEnd)이고 아니면 다음 줄의 처음이 된다. 전역변수를 선언하고 OnCreate에서 FALSE로 초기화한다.

 

 

 

BOOL bLineEnd;

 

BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     ....

     bLineEnd=FALSE;

 

이 변수의 직접적인 영향을 받는 함수는 오프셋으로부터 행렬 위치를 찾는 GetRCFromOff 함수이다. bLineEnd 플래그의 상태에 따라 이 함수가 행렬을 계산하는 방법이 달라진다. 다음 코드를 추가한다.

 

void GetRCFromOff(int nPos, int &r, int &c)

{

     ....

               if (nPos == e) {

                   if (buf[e] == 0 || buf[e] == ‘\r’ || bLineEnd == TRUE) {

                        break;

                   }

               }

          }

     }

     c=nPos-s;

}

 

nPos가 줄의 끝이고 bLineEnd TRUE이면 이 오프셋을 현재 줄의 끝으로 인정한다. 그렇지 않으면 이 오프셋은 현재 줄에 포함되지 않으며 자연히 다음 줄의 처음이 된다.

, bLineEnd가 행렬 계산에 영향을 미치는 경우는 자동개행된 상태에서 오프셋이 줄의 끝인 경우에만 국한된다. 그 외의 경우는 이 플래그가 행렬 계산에 영향을 미치지 말아야 하며 그럴 필요도 없다. 그래서 이 조건 점검은 nWrap 0이 아니고 nPos==e인 경우에만 수행한다.

GetRCFromOff bLineEnd를 참고하여 행렬 계산을 하도록 했는데 그렇다면 이 함수가 제대로 동작하기 위해 bLineEnd가 정확한 상태를 가지도록 값을 적시에 바꿔줘야 할 것이다. 이 값을 바꾸는 시점은 캐럿을 이동할 때마다 인데 대체로 수평이동시에만 값을 변경하면 된다. 좌우이동시 이 플래그의 값이 어떻게 조정되는지 알아보되 이 논리는 다소 복잡하므로 일단 순서도를 그려보자. 다음 순서도는 오른쪽으로 이동(VK_RIGHT)할 때의 흐름도이다.

결코 간단한 순서도가 아닌데 일단 이 순서도를 잘 분석해보자. 이 순서도에서 현재의 자동개행 상태인 nWrap은 전혀 참고되지 않는다. 대신 줄의 끝이고 엔터코드가 아니라는 조건이 자동개행 상태를 알아내는 조건으로 사용된다. 줄의 끝인데도 엔터코드가 아니면 바로 그 자리가 자동개행된 자리인 것이며 여기서 bLineEnd를 관리해야 한다. bLineEnd는 가급적이면 FALSE값을 가지도록 되어 있으며 자동개행된 줄의 끝에서만 TRUE가 된다.

자 다음에서 오른쪽으로 이동할 경우 이 순서도상의 흐름은 ①②③④⑥⑨⑪이 될 것이다. 현재 위치가 줄의 끝이 아니며 한 칸 오른쪽으로 이동한 후에도 여전히 줄의 끝이 아니기 때문이다. bLineEnd FALSE가 되며 대부분의 경우 이 순서대로 실행된다.

자 다음에서 오른쪽으로 이동할 경우는 ①②③④⑥⑩⑪의 순서대로 실행된다. 오른쪽으로 이동한 결과인 자 다음 위치는 자동개행한 줄의 끝이기 때문이다. 이때 캐럿이 자의 뒤에 위치할 수 있도록 bLineEnd TRUE로 바꿔야 한다. 그래야만 GetRCFromOff가 이 위치를 첫 번째 줄의 마지막 열로 계산한다.

자 다음에서 오른쪽으로 이동하면 ①②③⑤⑦⑪의 순서대로 실행된다. 자 위치는 줄의 끝이며 이 위치는 엔터코드에 의해 강제로 개행된 곳이 아니다. , 자동개행에 의해 잘려져 있는 곳이다. 이때는 오프셋은 변경하지 않고 bLineEnd FALSE로 바꾸기만 하면 된다. 비록 오프셋은 그 자리에 있지만 플래그가 바뀌었기 때문에 캐럿은 자의 뒤에서 자의 앞으로 이동하게 되며 SetCaret에 의해 다음 줄로 내려간다.

순서도를 그려 보고 실제 상황에서 이 순서도가 제대로 동작하는지를 점검해보았는데 이제 이 순서도를 코드로 작성해보자. VK_RIGHT의 코드를 다음과 같이 다시 작성한다.

 

     int s,e;

     ....

     case VK_RIGHT:

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

               GetRCFromOff(off,r,c);

               GetLine(r,s,e);

               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;

                   }

               }

               SetCaret();

          }

          return;

 

순서도를 그대로 옮긴 것이므로 따로 분석할 필요는 없을 것이다. 논리가 확실하게 만들어졌으면 코드 작성은 그야 말로 식은 죽 먹기이며 순서도보다 오히려 코드가 더 짧다. 다음은 왼쪽으로 이동하는 경우의 순서도를 작성해보자. 왼쪽 이동은 이동후의 조건 점검이 필요없기 때문에 오른쪽 이동에 비해서는 상대적으로 간단하다.

자 앞에서 왼쪽으로 이동하면 bLineEnd TRUE가 되며 실제 오프셋 변화는 없다. 오른쪽 이동과 비슷한 논리이다. 이 순서도를 코드로 옮기면 다음과 같이 된다.

 

     case VK_LEFT:

          if (off > 0) {

               GetRCFromOff(off,r,c);

               GetLine(r,s,e);

               if (off==s) {

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

                        off=GetPrevOff(off);

                        bLineEnd=FALSE;

                   } else {

                        bLineEnd=TRUE;

                   }

               } else {

                   off=GetPrevOff(off);

                   bLineEnd=FALSE;

               }

               SetCaret();

          }

          return;

 

제일 복잡한 좌우이동은 완료했고 나머지 캐럿이동 루틴을 수정하도록 하자. Home, End의 코드는 비교적 간단하고 직관적으로 이해가 될 것이다.

 

     case VK_HOME:

          GetRCFromOff(off,r,c);

          off=GetOffFromRC(r,0);

        bLineEnd=FALSE;

          SetCaret();

          return;

     case VK_END:

          GetRCFromOff(off,r,c);

          off=GetOffFromRC(r,1000000);

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

            bLineEnd=TRUE;

        }

          SetCaret();

          return;

 

줄의 처음으로 이동할 때 bLineEnd는 무조건 FALSE가 됨은 너무나 당연하다. 줄의 끝으로 이동한 후에는 그 위치가 엔터나 문서 끝에 의한 물리적인 줄의 끝인지를 볼 필요가 있다. 그렇지 않다면 즉, 줄의 끝이긴 한데 엔터코드나 문서 끝이 아니라면 bLineEnd TRUE로 바꿔야 한다. 그래야 다음 줄의 처음으로 캐럿이 내려가지 않는다.

캐럿을 이동하는 시점은 꼭 커서이동키를 사용할 때만은 아니다. 문자열을 입력하는 중에도 캐럿은 이동하는데 이때 bLineEnd는 무조건 FALSE로 바꾸면 된다.

 

void Insert(int nPos, TCHAR *str)

{

     ....

     memcpy(buf+nPos,str,len);

    bLineEnd=FALSE;

}

 

입력할 때의 캐럿은 다음 삽입될 위치를 가리키는 역할을 하므로 설사 자동개행된 줄 끝인 상태에서 다음 줄 처음으로 이동한다고 해서 전혀 이상할 것이 없다. <BS>키로 입력한 문장을 지울 때도 bLineEnd의 관리가 필요할 것 같은 생각이 들지 모르겠는데 그렇지 않다. 실제로 테스트해보면 <BS> 입력시는 아무 것도 할 필요가 없다. 여기까지 코드를 작성한 후 실행해보면 좌우이동 및 <Home>, <End>키 입력시 줄 끝을 제대로 찾아갈 것이다.

마지막으로 bLineEnd를 관리해야 할 시점은 아래위로 이동할 때이다. 상하이동할 때는 bLineEnd를 관리할 필요가 거의 없다. 그러나 아주 특수한 상황에서 약간의 문제가 있을 수 있어 예외 처리가 필요하다. 여기까지 작업한 상태에서 예제를 실행하고 보우하사 다음을 엔터로 끊은 후 다음과 같이 알파벳 a~z까지를 두 번 입력해보자. 두 알파벳은 엔터로 개행된 것이 아니고 공백 하나로 끊어져 있는 것인데 영문은 단어 정렬하므로 두 줄로 나누어 출력되어 있다.

 

캐럿을 첫 번째 줄 자 다음에 두고 아래로 두 번 내려 보자. 두 번 내렸으니까 세 번째 줄 z다음으로 가기를 기대하겠지만 오른쪽 그림처럼 네 번째 줄 처음으로 이동한다. 조금만 생각해보면 왜 이런 동작을 하는지 금방 알 수 있다. 캐럿이 처음부터 줄 끝에 있지 않았으므로 bLineEnd FALSE인데 이 상태에서는 세 번째 줄의 마지막 오프셋이 네 번째 줄의 처음으로 인식되는 것이다.

다음은 캐럿을 다섯 번째 줄 자 다음에 두고 위로 올라가 보자. 위로 한 칸 올리면 네 번째 줄의 끝인 z로 이동한다. 그리고 한 번 더 위로 올라가면 세 번째 줄 끝으로 가지 않고 네 번째 줄 처음 a로 이동한다. 이유는 앞의 경우와 동일하다.

더 심각한 것은 이 상태에서 더 이상 위로 올라가지 못한다는 점이다. GetRCFromOff는 현재 4 0열에 있다고 보고하며 PrevX는 최초 자 다음 위치를 가리키고 있는데 이 상태에서 위로 이동하면 3행의 PrevX위치로 갈려고 할 것이고 3행의 마지막 문자 위치를 찾는다. 그런데 아직 bLineEnd는 여전히 FALSE이기 때문에 4행 첫 열로 다시 돌아오는 것이다. 결국 아무리 위로 올려 봐야 그 자리에서 맴돌기만 하고 캐럿은 움직이지 않는다. 이 버그는 bLineEnd PrevX의 절묘한 합작품이다.

보다시피 아주 특수한 경우에 문제가 있는데 이 특수한 경우란 자동개행된 줄보다 더 긴 줄이 아래위에 있고 더 긴 줄의 끝이 아닌 지점에서 상하이동을 할 때이다. 이런 경우는 사실 실제 상황에서 발생할 빈도가 아주 희박하다. 하지만 잘 발생하지 않더라도 무시할 수 없으므로 상하이동을 할 때도 bLineEnd를 적절히 갱신해야 하는데 이동한 결과 위치가 자동개행된 줄의 끝이면 bLineEnd TRUE로 바꿔야 한다.

이 작업은 VK_UP, VK_DOWN에서 해야 하지만 여기보다 더 좋은 장소는 GetXPosOnLine 함수이다. 어차피 상하이동을 할 때 X 좌표를 찾아야 하고 그때마다 이 함수가 호출되기 때문이다. 이 함수의 끝에 다음 코드를 작성한다.

 

int GetXPosOnLine(int r,int DestX)

{

     ....

     ReleaseDC(hWndMain,hdc);

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

        bLineEnd=TRUE;

    } else {

        bLineEnd=FALSE;

    }

     return p-buf;

}

 

PrevX로부터 찾은 오프셋이 줄의 끝이되 물리적인 줄의 끝이 아니라면 bLineEnd TRUE로 변경하여 줄 끝에 캐럿을 위치시키도록 하였다. 결국 논리는 좌우이동과 동일하다. 이렇게 하면 모든 문제가 해결된다. 앞으로 <PgUp>, <PgDn> 등의 상하이동 코드를 작성하더라도 이 함수만 호출하면 더 이상 bLineEnd를 조정할 필요는 없다.

 

이상으로 세 가지 기능을 추가한 ApiEdit2 예제 제작을 마친다. 여기서 구현한 세 가지 기능은 사실 정리된 결과를 보여서 그렇지 결코 쉬운 코드가 아니다. 아직 실습 초반인데 이런 까다로운 내용을 다루게 되어 조금 미안스러운 감이 있지만 워낙 기본 기능이라 초반에 넣지 않으면 갈수록 더 어려워지게 될 것 같아 여기서 구현했다. 다음에 다른 기능들이 먼저 들어간 후에는 더 복잡해지고 생각해야 할 것들이 많아져서 순서를 앞쪽에 둘 수 밖에 없었다.

이 예제는 조금 난이도가 있었고 앞으로 당분간은 그다지 어려운 코드가 나오지 않으니 안심하기 바란다. 만약 ApiEdit2의 세 기능 중 아직도 잘 이해가 되지 않는 기능이 있다면 잠시 보류해도 좋다. 다행히 다음 실습에 반드시 필요한 것은 아니므로 대충이라도 이해를 했다면 일단 넘어가고 천천히 시간을 두고 좀 더 연구해보기 바란다. 이 내용이 당장 이해가 되지 않는다고 해서 고민할 필요는 없다. 좀 긍정적인 표현으로 바꾸자면 이 내용을 다 이해했다면 앞으로의 실습들도 전혀 어렵지 않을 것이다.