. 바꾸기

바꾸기는 조건에 맞는 문자열을 찾아 다른 문자열로 변경하는 기능이다. 일단 문자열을 찾아야 바꾸기를 할 수 있으므로 찾기 기능의 확장이라고 생각할 수 있다. 대화상자의 모양은 다음과 같다.

기능이 유사하기 때문에 대화상자도 비슷하다. 검색 완료 후 대화상자 닫기 옵션이 사라졌고 대신 바꿀 내용 콤보박스와 바꾸기, 모두 바꾸기 버튼이 추가되었다. 대화상자 프로시저는 찾기 대화상자와 공유한다. 옵션을 처리하는 부분들이 비슷하기 때문에 굳이 따로 프로시저를 만들 필요가 없다. 찾기 대화상자 프로시저에 몇 가지 코드만 추가하면 된다.

 

BOOL CALLBACK FindDlgProc(HWND hDlg,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

     switch(iMessage)

     {

     case WM_INITDIALOG:

          ....

          RefillHistory(GetDlgItem(hDlg,IDC_FIND_WHAT),arFind[0]);

        RefillHistory(GetDlgItem(hDlg,IDC_FIND_TO),arFind[1]);

          SendMessage(GetDlgItem(hDlg,IDC_FIND_WHAT), CB_LIMITTEXT, (WPARAM)255, 0);

        SendMessage(GetDlgItem(hDlg,IDC_FIND_TO), CB_LIMITTEXT, (WPARAM)255, 0);

          SendMessage(hDlg,WM_COMMAND,MAKEWPARAM(IDC_FIND_WHAT,CBN_EDITCHANGE),0);

          return TRUE;

     case WM_COMMAND:

          switch (LOWORD(wParam))

          {

          case IDC_FIND_WHAT:

              switch (HIWORD(wParam)) {

              case CBN_EDITCHANGE:

                   if (GetWindowTextLength(GetDlgItem(hDlg,IDC_FIND_WHAT)) == 0) {

                        EnableWindow(GetDlgItem(hDlg,IDC_BTNFIND),FALSE);

                   } else {

                        EnableWindow(GetDlgItem(hDlg,IDC_BTNFIND),TRUE);

                   }

               SendMessage(hDlg,WM_COMMAND,MAKEWPARAM(IDC_FIND_TO,CBN_EDITCHANGE),0);

                   break;

              case CBN_SELCHANGE:

                   PostMessage(hDlg,WM_COMMAND,MAKEWPARAM(IDC_FIND_WHAT,CBN_EDITCHANGE),0);

                   break;

              }

              break;

        case IDC_FIND_TO:

           switch (HIWORD(wParam)) {

           case CBN_EDITCHANGE:

               if (GetWindowTextLength(GetDlgItem(hDlg,IDC_FIND_TO)) == 0 ||

                   GetWindowTextLength(GetDlgItem(hDlg,IDC_FIND_WHAT)) == 0) {

                   EnableWindow(GetDlgItem(hDlg,IDC_BTNREPLACE),FALSE);

                   EnableWindow(GetDlgItem(hDlg,IDC_BTNREPLACEALL),FALSE);

               } else {

                   EnableWindow(GetDlgItem(hDlg,IDC_BTNREPLACE),TRUE);

                   EnableWindow(GetDlgItem(hDlg,IDC_BTNREPLACEALL),TRUE);

               }

               break;

           case CBN_SELCHANGE:

               PostMessage(hDlg,WM_COMMAND,MAKEWPARAM(IDC_FIND_TO,CBN_EDITCHANGE),0);

               break;

           }

           break;

          case IDCANCEL:

              DestroyWindow(hDlg);

              break;

          case IDC_BTNFIND:

        case IDC_BTNREPLACE:

        case IDC_BTNREPLACEALL:

              ....

           GetDlgItemText(hDlg,IDC_FIND_TO,szTemp,255);

           if (lstrlen(szTemp)) {

               arFind[1].Add(szTemp);

               RefillHistory(GetDlgItem(hDlg,IDC_FIND_TO),arFind[1]);

           }

 

              switch (LOWORD(wParam)) {

              case IDC_BTNFIND:

                   SendMessage(GetParent(hDlg),WM_USER+2,1,0);

                   if (FindFlag & AE_FIND_CLOSE) {

                        DestroyWindow(hDlg);

                   }

                   break;

           case IDC_BTNREPLACE:

               SendMessage(GetParent(hDlg),WM_USER+2,2,0);

               break;

           case IDC_BTNREPLACEALL:

               SendMessage(GetParent(hDlg),WM_USER+2,3,0);

               break;

              }

              return TRUE;

          }

          return FALSE;

     }

     return FALSE;

}

 

WM_INITDIALOG에서는 arFind[1] 목록을 IDC_FIND_TO 콤보박스에 대입한다. 바꾸기, 모두 바꾸기 버튼은 찾을 내용과 바꿀 내용이 모두 입력되어 있어야 사용 가능하다. 그래서 IDC_FIND_TO 콤보박스의 길이와 IDC_FIND_WHAT 콤보박스의 길이가 모두 0보다 커야만 사용 허가 된다. IDC_FIND_WHAT이 편집될 때도 이 점검을 해야 한다.

바꾸기, 모두 바꾸기 버튼을 클릭하면 컨트롤에 설정된 옵션과 찾을 내용, 바꿀 내용을 읽어들인다. 읽은 결과는 arFind[0], arFind[1] 목록의 첫 항목과 FindFlag에 저장될 것이다. 찾기 기능과 마찬가지로 메인 윈도우로 WM_USER+2 메시지를 보내되 wParam은 각각 2,3으로 전달하였다. WM_USER+2는 찾기/바꾸기 대화상자로부터 메인 윈도우로의 요청을 전달하는 메시지이며 wParam은 다음과 같이 정의되었다.

 

wParam

명령

1

찾기

2

바꾸기

3

모두 바꾸기

 

메인 윈도우의 OnUser2 함수는 wParam값에 따라 찾기 및 바꾸기를 하면 된다. 메뉴의 바꾸기 항목을 선택하면 바꾸기 대화상자를 보여준다.

 

void OnCommand(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

     ....

     case IDM_SEARCH_REPLACE:

          if (!IsWindow(g_FindDlg)) {

              g_FindDlg=CreateDialog (g_hInst,MAKEINTRESOURCE(IDD_REPLACE),hWnd,FindDlgProc);

              ShowWindow(g_FindDlg,SW_SHOW);

          } else {

              SetFocus(g_FindDlg);

          }

          break;

 

대화상자를 보여주는 코드도 찾기 대화상자와 거의 동일하다. 대화상자 템플리트의 ID만 다를 뿐 대화상자 프로시저까지 동일하다. 찾기 동작의 주체가 CApiEdit 객체이듯이 바꾸기 동작도 CApiEdit가 직접 해야 한다. CApiEdit 클래스에 바꾸기 함수를 추가하도록 하자.

 

int CApiEdit::ReplaceText(int nPos, TCHAR *what, DWORD dwFlag, TCHAR *to)

{

     int len;

     TCHAR *pSel;

     TCHAR *pWhat;

     int Result;

 

     if (SelStart == SelEnd) {

          if (FindText(nPos,what,dwFlag) == TRUE) {

              Result=1;

          } else {

              Result=0;

          }

     } else {

          pWhat=(TCHAR *)malloc(lstrlen(what)+1);

          lstrcpy(pWhat,what);

 

          len=abs(SelStart-SelEnd);

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

          lstrcpyn(pSel,buf+min(SelStart,SelEnd),len+1);

 

          if ((dwFlag & AE_FIND_MATCHCASE) == 0) {

              CharUpper(pWhat);

              CharUpper(pSel);

          }

 

          if (lstrcmp(pWhat,pSel) != 0) {

              if (FindText(nPos,what,dwFlag) == TRUE) {

                   Result=1;

              } else {

                   Result=0;

              }

          } else {

              StartUndoGroup();

              DeleteSelection();

              Insert(off,to);

              off+=lstrlen(to);

              SetCaret();

              Invalidate(-1);

              FindText(off,what,dwFlag);

              Result=2;

              EndUndoGroup();

          }

          free(pWhat);

          free(pSel);

     }

     return Result;

}

 

선택영역이 없거나 있더라도 검색 대상 문자열이 아니면 바꾸기를 하지 않고 다음 찾기를 실행한다. 즉 이 함수는 선택된 텍스트가 검색 대상과 같을 때만 바꾸기를 하고 그렇지 않을 경우는 찾기와 동일한 동작을 한다. 바꾸기는 먼저 바꿀 대상을 선택한 후 다른 텍스트로 바꾸는 것이므로 바꿀 대상이 선택되어 있지 않으면 선택을 먼저 하는 것이 순서에 맞다.

바꿀 대상이 선택되어 있으면 문자열을 대체하는데 방법은 아주 원론적이다. DeleteSelection으로 선택된 텍스트를 먼저 삭제하고 바꿀 문자열을 그 위치에 삽입해 넣으면 감쪽같이 문자열이 대체될 것이다. 이때 삭제와 삽입은 바꾸기 동작 하나를 구성하는 세부 동작이므로 하나의 편집 동작으로 보아야 하며 그래서 두 개의 취소 레코드를 그룹으로 묶어 주었다. <Ctrl+Z>로 바꾸기를 취소하면 바꾸기 전의 상태로 정확하게 돌아갈 것이다.

바꾸기는 여러 번 반복적으로 실행할 경우가 많으므로 바꾸기를 한 후 다음 문장을 선택하여 계속 바꾸기를 할 수 있도록 했다. 이 함수는 바꾸기 동작의 결과를 리턴하는데 세 가지 리턴값을 가진다. 바꿀 대상이 선택되어 있지 않아 찾기만 했으면 1을 리턴하며 찾기와 바꾸기를 다 했으면 2를 리턴하고 검색에 실패하면 0을 리턴한다. 호스트는 이 리턴값을 보고 몇 개의 문자열이 대체되었는지 헤아리며 찾기 결과를 사용자에게 보여줄 것이다.

이 함수는 호스트의 OnUser2에서 호출된다. OnUser2는 찾기/바꾸기 대화상자로부터의 요청을 받아 wParam값에 따라 ApiEdit에게 요청을 전달하며 정확한 동작을 위한 몇 가지 처리를 한다. 코드는 다음과 같다.

 

void OnUser2(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

     HWND hActive;

     SInfo *pSi;

    int i;

    DWORD tFlag;

    TCHAR Mes[512];

 

     if (IsWindowEnabled(hWnd)==FALSE)

          return;

 

     hActive=(HWND)SendMessage(g_hMDIClient,WM_MDIGETACTIVE,0,NULL);

     if (hActive == NULL) {

          return;

     }

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

     switch (wParam) {

     case 1:

          if (pSi->Ae.FindText(-1,arFind[0].Get(0), FindFlag) == FALSE) {

              MessageBox(hWnd,"찾는 문자열이 없습니다.","알림",MB_OK);

          }

          break;

    case 2:

        if (pSi->Ae.ReplaceText(-1,arFind[0].Get(0), FindFlag,arFind[1].Get(0)) == 0) {

           MessageBox(hWnd,"찾는 문자열이 없습니다.","알림",MB_OK);

        }

        break;

    case 3:

        int Result;

        pSi->Ae.StartUndoGroup();

        for (i=0;;) {

           tFlag=FindFlag;

           tFlag &= ~AE_FIND_WRAP;

           Result=pSi->Ae.ReplaceText(-1,arFind[0].Get(0), tFlag,arFind[1].Get(0));

           UpdateWindow(pSi->Ae.hWnd);

           if (Result==0)

               break;

           if (Result==2)

               i++;

        }

        pSi->Ae.EndUndoGroup();

        if (i==0) {

           lstrcpy(Mes,"찾는 문자열이 없습니다");

        } else {

           wsprintf(Mes, " %d개의 \"%s\"문자열을 \"%s\"로 바꾸었습니다",

               i,arFind[0].Get(0), arFind[1].Get(0));

        }

        MessageBox(hWnd,Mes,"알림",MB_OK);

        break;

     }

}

 

한 번만 바꾸기를 할 때는 ReplaceText를 호출하고 그 결과를 사용자에게 알려 주기만 하면 된다. 모두 바꾸기를 할 때는 되돌리기(WRAP) 옵션을 잠시 꺼 두어야 한다. 왜냐하면 바꾼 결과 검색 대상 문자열이 아직도 남아 있을 수 있기 때문이다. 바보천재로 모두 바꾼다면 모든 바보가 천재가 됨으로써 문제가 없지만 바보안바보로 바꾼다면 바꾼 결과 여전히 바보라는 문자열이 남아 있으므로 WRAP 옵션이 켜진 상태에서는 무한루프에 빠져 버릴 것이다. 그래서 모두 바꾸기를 할 때는 사용자가 정한 옵션을 잠시 무시하고 문서 현재 위치에서부터 끝까지 한 번만 바꾸기를 수행해야 한다.

하나의 문자열이 대체될 때마다 UpdateWindow 함수를 호출하여 문서의 어디쯤을 바꾸고 있는 중인지를 보여주도록 하였다. 이렇게 하면 속도는 많이 느려지지만 사용자에게 문서변경 사실을 확실하게 보여 준다는 점에서 긍정적이다. 하지만 문서 크기가 커지면 한 단어를 바꿀 때마다 재정렬을 하므로 이럴 때는 UpdateWindow를 하지 않는 것이 더 좋다. 이 처리는 옵션으로 만들어 둘만하기도 한데 여기에 왜 UpdateWindow가 필요한지는 잠시 후 또 연구해보자.

모두 바꾸기는 전체 과정이 하나의 편집 동작이므로 편집 그룹을 구성하고 있다. ReplaceText 함수 내부에서도 편집 그룹을 구성하고 있지만 ApiEdit는 편집 그룹의 중첩을 허용하므로 안쪽 그룹은 무시된다. 모두 바꾼 후 단 한 번의 <Ctrl+Z>로 모두 바꾸기 취소를 할 수 있으니 참 멋진 일이다. 앞 장에서 힘들게 편집 그룹을 만들어 놓은 덕택이다.

기본적인 찾기/바꾸기 기능이 완성되었다. 다음 코드는 문서 분석 정보를 삭제하는 함수이다. 한참 전에 실습한 함수인데 왜 갑자기 이 함수를 다시 보는가 하면 바꾸기 기능과 관련이 있기 때문이다.

 

void CParse::DeleteParseInfo(int nLine)

{

     int l;

 

     for (l=nLine;;l++) {

          if (l>=ParseSize || pInfo[l].pUnit[0].pos == -1)

              break;

 

          memset(pInfo[l].pUnit,-1,sizeof(ParseUnit)*pInfo[l].UnitSize);

     }

}

 

이 함수는 분석 정보가 작성되어 있는 모든 행의 pUnit을 초기화함으로써 분석 정보를 무효화한다. 분석 정보 배열 제일 끝줄의 첫 유닛은 항상 pos -1이기 때문에 이 조건만으로 분석된 끝 줄을 찾을 수 있는데 루프를 탈출할 조건에 l>=ParseSize조건이 필요한 이유는 무엇일까? 화면에 한 번이라도 출력된 줄에 대해서는 분석 정보 배열이 할당되어 있으므로 사실 대부분의 경우 이 조건은 필요가 없다.

아주 특수한 경우 아직 할당도 안된 행의 분석 정보를 삭제해야 할 경우가 있는데 바로 모두 바꾸기를 할 때이다. 모두 바꾸기는 메모리만 변경하지 화면을 그리지 않으므로 OnPaint가 호출되지 않고 따라서 분석 정보가 갱신되지 않는다. 하지만 UpdateLineInfo 함수는 반복적으로 호출되므로 이 할당되지 않는 행의 분석 정보를 지우려고 시도하게 된다. l>=ParseSize 조건은 이런 상황일 때를 위한 추가된 것이며 모두 바꾸기에 UpdateWindow 호출이 있는 이유는 바꾸기를 한 모든 줄에 분석 정보를 위한 메모리를 할당해놓기 위해서이다.