. 버그 수정

프로그램 개발과정이란 버그와의 끊임없는 전쟁이라고 표현해도 좋을 정도로 대부분의 시간을 디버깅에 할애해야 한다. 당근 개발과정에서도 물론 수많은 버그들이 발견되었고 발견되는 족족 이전 프로젝트로 돌아가 다시 수정해 왔다. 여기서 수정할 버그들은 베타2 이후에 발견된 것들인데 간단한 버그들이므로 이전 프로젝트를 수정하지 말고 마지막 프로젝트만 수정하기로 한다.

첫 번째 버그는 자동개행 상태로 시작하지 않고 마진이 보이는 상태일 때 캐럿이 마진 안에 위치한다는 점이다. 이 버그는 초기화 과정의 특수함으로 인해 발생한 것인데 SetCaret에서 캐럿을 포맷팅영역 안으로 넣어주는 코드에 문제가 있었다. 캐럿이 수평 포맷팅영역을 벗어나면 화면의 중앙으로 옮기도록 되어 있는데 이 코드에 작업영역의 폭이 0이 아니라는 조건이 필요하다.

 

void CApiEdit::SetCaret(BOOL bUpdatePrevX/*=TRUE*/, BOOL bScrollToCaret/*=TRUE*/)

{

    RECT crt;

     ....

     GetXYFromOff(toff,x,y);

    GetClientRect(hWnd,&crt);

    if (bScrollToCaret && crt.right) {

          SendNotify(AEN_MOVE);

          if (nWrap==0) {

              if ((x+caretwidth > xPos+frt.right) || (x < xPos+MarginWidth)) {

                   xPos=max(0,x-frt.right/2);

                   bScroll=TRUE;

              }

          }

 

당근의 차일드에서 CApiEdit 객체를 생성한 직후에 SetSetting 함수를 호출하여 컨트롤의 여러 가지 설정상태를 조정한다. 이때 SetWrap(0) 호출문에서 SetCaret을 호출하여 캐럿의 초기 위치를 지정하는데 이때는 아직 WM_SIZE 메시지가 처리되지 않았기 때문에 frt가 포맷팅영역을 제대로 표현하지 못하는 상태이다. 이 상태대로 캐럿 위치와 수평스크롤 위치를 조정하다 보면 xPos MarginWidth로 초기화되어 마치 마진폭만큼 오른쪽으로 스크롤되어 있는 것으로 착각하게 된다. 그래서 캐럿이 마진 안쪽, 즉 수평좌표 0에 나타나는데 포맷팅영역이 조사되지 않았을 때는 캐럿이 있는 곳으로 스크롤 하지 않도록 하였다. 작업영역의 크기를 구해보고 폭이 0이면 아직 WM_SIZE를 처리하지 않은 것으로 판단한다.

두 번째 버그는 파일탭의 이미지 관리 버그인데 이름없음 1을 새로 만들어 편집하면 이 파일이 수정되었으므로 빨간색으로 표시될 것이다. 이 상태에서 다른 파일을 열면 OpenFromFile에서 New 함수를 호출하고 DGChildProc WM_CREATE에서 새로 만들어진 창에 이름없음 1이라는 파일명을 붙여준다. NewChild에서만 g_NewNo를 증가시키도록 해놓았기 때문에 새로 열리는 창의 이름이 일시적으로 이름없음 1이 된 후 초기화가 완료된 후 OpenFileToChild 함수에 의해 정확한 파일명을 다시 받게 된다.

이 과정에서 새로 열리는 창의 SetText 함수가 AEN_CHGMODI 통지 메시지를 보내면 SetStatusText 함수에서 이름없음 1 창을 찾아 이미지를 파란색으로 변경하게 된다. , FindFileTab 함수가 임시적으로 붙여진 이름으로부터 대상 탭을 잘못 찾는 것이다. 이 문제는 근본적으로 pSi->NowFile에 파일명을 붙여주는 시기를 잘못 선택했기 때문에 발생한다. NewChild 함수에 다음 코드를 추가한다.

 

HWND NewChild()

{

     HWND hChild;

     SInfo *pSi;

 

     g_NewNo++;

     hChild=New();

     pSi=(SInfo *)GetWindowLong(hChild,0);

    wsprintf(pSi->NowFile,"이름없음 %d",g_NewNo);

    SetWindowText(hChild,pSi->NowFile);

     AddFileTab(pSi->NowFile);

     return hChild;

}

 

새로 만들어진 창에 파일명을 붙이는 시기를 창이 완전히 만들어지고 난 다음으로 이동시켰다. DGChildProc WM_CREATE에서 pSi->NowFile에 파일명을 대입하고 윈도우의 캡션을 변경하는 코드는 삭제한다. 대신 pSi->NowFile에 빈 문자열을 대입하여 대응되는 탭을 찾지 못하도록 하였다.

 

LRESULT CALLBACK DGChildProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

     switch(iMessage) {

     case WM_CREATE:

          ....

          pSi->Ae.Create(0,0,0,0,WS_CHILD | WS_VISIBLE,1,hWnd);

          SetSetting(pSi->Ae);

        wsprintf(pSi->NowFile,"");

          return 0;

 

문서창에 편집파일의 이름을 주는 코드는 새 문서창을 만드는 NewChild 함수와 기존 문서를 완전히 열었을 때인 OpenFileToChild에만 있도록 했다. 이제 임시적인 이름으로부터 탭을 잘못 찾는 경우가 없어질 것이다.

세 번째 버그는 단어 찾기 함수인 GetNowWord에 있는데 이 함수는 앞쪽 구분자를 찾고 구분자 다음 문자부터 다음 구분자까지를 단어로 검색한다. 단 문서 처음과 끝, 구분자가 연속인 경우에 대해서는 앞쪽 구분자에서 다음 문자로 이동하지 않고 다음 구분자를 찾도록 하였다. 이 코드는 대개의 경우 문제가 없지만 아주 특별한 경우에 제대로 동작하지 않는다. 어떤 경우인가 하면 문서 처음에 구분자가 있고 단어가 이어지는 경우, 구체적인 예를 들자면 #include 라는 문자열로 시작될 때이다.

이때 include를 더블클릭해도 단어로 선택되지 않는데 앞쪽으로 구분자 #을 찾은 후 이 자리에서 다음 문자로 이동하지 않고 다음 구분자를 찾기 때문이다. 그래서 문서의 처음이고 첫 문자가 구분자이면 일단 한 칸 다음으로 이동한 후 단어를 찾도록 하였다. if문의 nPos!=0 조건에 첫 문자가 구분자인지 조사하는 조건을 OR로 연결한다. 즉 문서 처음이더라도 그 위치의 문자가 구분자이면 일단 구분자를 건너 뛰도록 함으로써 구분자 다음의 단어를 선택하도록 했다.

 

void CApiEdit::GetNowWord(int nPos, int &s, int &e)

{

     for (;;nPos--) {

          if (IsDelimiter(nPos)==TRUE || nPos==0) {

              break;

          }

     }

 

    if ((nPos != 0 || IsDelimiter(nPos)) &&

          nPos != doclen && IsDelimiter(nPos+1)==FALSE) {

          nPos++;

     }

     s=nPos;

 

네 번째 버그는 비교적 간단한 실수에서 비롯되었다. UpdateLineInfo 함수는 정렬한 결과 총 줄 수가 만줄이 넘어가거나 또는 만줄 이하로 줄어들 때 마진 폭을 동적으로 조정하기 위해 WM_SIZE 메시지를 다시 보내는데 이때 GetXYFromOff가 참조하는 off가 무효이기 때문에 무한 루프로 빠져드는 문제가 있다. 이 문제를 해결하려면 일단 UpdateLineInfo 함수가 완전히 실행을 마친 후 다시 정렬해야 하므로 SendMessage 대신 PostMessage를 사용해야 한다.

다섯 번째 버그도 사소한 실수로 인해 발생한 것인데 버그라기보다는 코드의 오타라고 보는 것이 오히려 맞을 것 같다. 덮어쓰기 모드일 때 SetCaret 함수에서 캐럿을 현재 위치의 문자폭으로 계산하는데 현재 위치는 off가 아니라 한글 조립중일 때의 위치를 고려한 toff여야 한다.

 

void CApiEdit::SetCaret(BOOL bUpdatePrevX/*=TRUE*/, BOOL bScrollToCaret/*=TRUE*/)

{

     ....

     if (bOvr) {

          if (IsDBCS(toff)) {

              if (buf[toff] == '\r') {

                   caretwidth=arChWidth[' '];

              } else {

               caretwidth=GetCharWidth(buf+toff,2);

              }

          } else {

           caretwidth=GetCharWidth(buf+toff,1);

          }

     }

 

여섯 번째 버그는 IsDBCS 멤버함수에 있는데 이 함수를 다음과 같이 수정한다. IsDBCSLeadByte 함수를 사용하지 않고 최상위 비트가 1인지 보도록 했다.

 

inline BOOL CApiEdit::IsDBCS(int nPos)

{

     return ((buf[nPos] & 0x80) != 0/*IsDBCSLeadByte(buf[nPos])*/

          || (buf[nPos]==‘\r’ && buf[nPos+1]==‘\n’));

}

 

두 방식간의 차이점은 거의 없으며 0x80으로 시작되는 문자를 2바이트 문자로 볼 것인가 아니면 1바이트 문자로 볼 것인가만 다르다. IsDBCSLeadByte 함수는 0x80 1바이트로 조사하는데 이 동작이 한글 문서와는 맞지 않는 경우가 있다. 조합형 문서를 컨버팅할 경우 0x80문자가 나타날 수 있는데 이때 이 문자를 2바이트로 조사하지 않으면 한글의 경계에 걸치기 때문에 다운될 위험이 있다. 통상의 경우 0x80코드는 텍스트 파일에 나타나지 않기 때문에 큰 차이를 느끼기는 어렵다.