. 삽입 및 삭제시 무효영역 관리

문서가 변경될 때 어떤 부분이 다시 그려져야 하는지 보자. 가급적이면 꼭 필요한 부분만 다시 그려야 하므로 최소한 좁은 영역만 그리도록 해야 한다. 일단 다음 그림을 보아라.

첫 줄의 자에 캐럿이 있는데 이 상태에서 <Del>키를 누르면 이 글자가 삭제되고 그 자리로 자가 오게 될 것이다. 따라서 첫 번째 줄은 반드시 다시 그려야 한다. 하지만 두 번째 줄 이하는 첫 번째 줄의 변화와 전혀 상관이 없으므로 다시 그릴 필요가 없다. 자 앞에 다른 글자를 입력할 때도 마찬가지로 첫 번째 줄만 다시 그려주면 된다. 하지만 다음과 같은 경우는 좀 다르다.

네 번째 줄의 자에 캐럿이 있는데 이 상태에서 <Del>키를 누르면 아랫줄의 자가 윗줄로 올라와야 하고 이후의 줄들도 연속적으로 한 칸씩 위로 이동되어야 한다. 이때는 삽입, 삭제가 일어난 줄 뿐만 아니라 그 이후의 줄들도 다시 그려져야 하므로 편집된 줄 이하의 모든 줄이 무효영역이 되어야 한다. 위 그림의 경우 네 번째 줄 무궁화~ 이후부터 다시 그려야 하며 그 이전 줄은 다시 그릴 필요가 없다. 문서의 한 부분을 편집했을 때 그 이후 부분은 어떤 변화가 있을 수도 있고 없을 수도 있지만 그 이전 부분은 변하지 않을 것이다. 과연 항상 그럴까? 다음의 경우를 보자.

 

한글은 글자 정렬되어 있고 영문은 단어 정렬되어 있으며 P자에 캐럿이 있다. 이 상태에서 P를 삭제하면 Reaint라는 단어가 윗줄로 올라가 붙는다. RePaint는 윗줄 여백에 안 들어가기 때문에 강제 개행되었지만 한 글자가 삭제됨으로써 정렬 상태가 바뀐 것이다. 보다시피 편집된 줄의 이전 줄도 변화가 있을 수 있다. , 이 경우도 같은 문단 내에서만 변화가 있을 뿐 세 번째 줄 이전은 전혀 변화가 없다.

정리하자면 문서편집에 의해 무효화되어야 할 가장 작고도 안전한 영역은 편집된 위치가 속한 문단 이후부터 화면 아래까지이다. 예로 보인 그림은 편집 화면이 작아서 별 속도 향상 효과가 없을 것처럼 보이지만 문서를 입력할 때 캐럿은 대부분 화면 아래쪽에 있으므로 이 정도로만 무효영역을 좁게 만들어도 대단한 속도 향상 효과가 있다. 다음 함수는 무효영역 설정을 위한 도우미 함수이다.

 

int FindParaStart(int nPos)

{

     int n;

 

     if (nPos==0) {

          return 0;

     }

 

     for (n=nPos-1;n!=0;n--) {

          if (buf[n]==‘\r’ && buf[n+1]==‘\n’) {

               n+=2;

               break;

          }

     }

 

     return n;

}

 

nPos를 주면 이 오프셋이 속해 있는 문단의 선두 오프셋을 찾아준다. 즉 이 함수가 찾아 주는 오프셋 이후부터 무효화하면 안전하다. 그럼 이제 문서편집시에 무효영역을 실제로 적용해보도록 하자. 먼저 삭제할 때인 VK_DELETE, VK_BACK의 코드를 다음과 같이 수정한다.

 

     case VK_DELETE:

          ....

        Invalidate(FindParaStart(off));

          SetCaret();

          return;

     case VK_BACK:

          ....

        Invalidate(FindParaStart(off));

          SetCaret();

          return;

 

큰 변화는 없고 InvalidateRect(hWnd,NULL,TRUE)Invalidate(FindParaStart(off))로 바뀌기만 했다. 작업영역 전체를 다 무효화시키는 것이 아니라 삭제된 오프셋이 속한 문단의 첫 줄 이후부터 화면 끝까지 무효화된다. 앞에서 설명한대로 편집이 일어난 위치에서 변화가 생길 가능성이 있는 가장 좁은 영역을 무효영역으로 설정한 것이다.

다음은 문자를 삽입할 때의 무효영역 관리를 보자. 삭제는 한 군데서 일어나지만 삽입은 여러 곳에서 일어난다. 세 군데가 있다.

 

void OnChar(HWND hWnd, TCHAR ch, int cRepeat)

{

     ....

     Invalidate(FindParaStart(off-lstrlen(szChar)));

     SetCaret();

}

 

LRESULT OnImeChar(HWND hWnd, WPARAM wParam, LPARAM lParam)

{

     ....

     Invalidate(FindParaStart(off-lstrlen(szChar)));

     SetCaret();

     return 0;

}

 

LRESULT OnImeComposition(HWND hWnd, WPARAM wParam, LPARAM lParam)

{

     ....

          Invalidate(FindParaStart(off-len));

          SetCaret();

     }

 

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

}

 

보다시피 InvalidateRect(hWnd,NULL,TRUE)를 좀 더 지능적인 Invalidate로 바꾼 것에 불과하다. 삽입할 때의 기준 위치는 삽입 후의 오프셋이 아니라 삽입하기 전의 오프셋이 기준이다. 만약 삽입 후의 오프셋을 기준으로 한다면 줄 중간에서 엔터코드가 입력되었을 때 잘려 내려간 줄은 제대로 출력되지만 윗줄은 갱신되지 않을 것이다.

명시적인 삽입과 삭제 외에 문서를 편집하는 부분으로 잘라내기와 붙여넣기가 있다. 이 두 부분도 작업영역 전체를 무효화할 필요없이 변화가 있는 부분만 무효화했다.

 

     case IDM_AE_CUT:

          if (SelStart != SelEnd) {

               SendMessage(hWnd,WM_COMMAND,MAKEWPARAM(IDM_AE_COPY,0),0);

               DeleteSelection();

            Invalidate(FindParaStart(off));

               SetCaret();

          }

          return;

     case IDM_AE_PASTE:

          if (IsClipboardFormatAvailable(CF_TEXT)) {

               DeleteSelection();

               OpenClipboard(hWnd);

               hmem=GetClipboardData(CF_TEXT);

               ptr=(TCHAR *)GlobalLock(hmem);

               Insert(off,ptr);

               GlobalUnlock(hmem);

               CloseClipboard();

            Invalidate(FindParaStart(off));

               off += lstrlen(ptr);

               SetCaret();

          }

          return;

 

잘라내기의 경우 DeleteSelection 함수에 의해 선택영역의 첫 부분이 현재 위치가 되어 있으므로 현재 위치 이후부터 무효화를 하면 된다. 붙여넣기를 할 때는 붙여넣기 전의 현재 위치부터 무효화시켜야 한다.

문서를 변경하는 함수들을 수정하여 무효화되는 영역을 가급적이면 좁게 설정하고 있는데 자칫 잘못 계산하면 부작용이 발생할 수 있다. 무효영역을 좁게 하다 보면 반드시 그려야 할 부분이 무효영역에서 제외되버리는 경우가 있는데 이렇게 되면 화면의 출력 내용이 문서의 상태를 정확하게 보여주지 못하게 된다. 앞서 작성한 함수들은 자신이 변경한 부분에 대해서는 안전한 범위의 무효영역을 정확하게 설정하고 있지만 예외 코드에 의해 무효영역의 불일치가 발생하는 경우가 있다. 어떤 경우인가 하면 삭제에 의해 강제 스크롤될 때이다.

 

void CApiEdit::UpdateScrollInfo()

{

     ....

     if (si.nMax < (int)si.nPage) {

          yPos=0;

        Invalidate(-1);

     }

 

이 코드는 ApiEdit3에서 작성했던 예외 처리코드인데 삭제 후 스크롤바가 디스에이블되면 강제로 위로 스크롤시켜 문서 전체가 보이도록 한다. 이 경우 Delete 함수가 찾은 무효영역은 스크롤을 미처 계산하지 못한 것이므로 전체 무효화를 할 필요가 있다. 예외적인 상황을 처리하고 있기 때문에 무효영역 설정에 대해서도 역시 예외처리를 해야 한다.