기본 검색 기능은 완성되었지만 아직 서비스가 많이 부족하다. 더 편리하게 검색할 수 있도록 만들어야 하며 또한 찾기 기능이 들어감으로써 생긴 문제점들도 해결해야 한다.

. 대화상자의 위치

현재 찾기 대화상자의 위치에 대해서는 어떠한 통제도 하지 않기 때문에 디폴트 위치인 메인 윈도우의 왼쪽 위에 열린다. 이 위치는 문장을 편집하고 있는 영역이기 때문에 찾기 대화상자에 의해 글자가 가려져서 불편하다. 매번 왼쪽 위에 열리도록 하지 말고 사용자가 한 번 대화상자를 옮기면 그 위치를 기억하고 있다가 다음 번 열릴 때는 마지막 위치에서 열리는 것이 좋다. 대화상자 프로시저에 다음 코드를 추가한다.

 

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

{

    static RECT drt;

 

     switch(iMessage)

     {

     case WM_INITDIALOG:

        SetWindowPos(hDlg,HWND_NOTOPMOST,drt.left,drt.top,0,0,SWP_NOSIZE);

          ....

     case WM_COMMAND:

          ....

          case IDCANCEL:

           GetWindowRect(hDlg,&drt);

              DestroyWindow(hDlg);

              break;

          ....

              switch (LOWORD(wParam)) {

              case IDC_BTNFIND:

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

                   if (FindFlag & AE_FIND_CLOSE) {

                   GetWindowRect(hDlg,&drt);

                       DestroyWindow(hDlg);

                   }

 

RECT형의 정적 변수 drt를 선언하고 대화상자가 종료되기 전에 이 변수에 대화상자의 위치를 저장해놓는다. 다음 번 열릴 때는 WM_INITDIALOG에서 이 위치로 대화상자를 옮김으로써 사용자가 원하는 위치에 한 번만 옮겨 놓으면 계속 그 자리에서 열리도록 하였다. 하지만 정적 변수는 별다른 초기화를 하지 않으면 모두 0으로 초기화되기 때문에 최초 대화상자가 열릴 때는 화면 좌상단인 0,0에 열리게 된다.

처음 열릴 때 좀 더 좋은 위치를 찾아 보도록 하자. 가급적이면 사용자의 편집에 방해가 되지 않는 빈 영역에 열리는 것이 좋을 것 같다. 또는 메인 윈도우의 정 중앙에 열리는 것도 나쁘지 않은 것 같은데 최초 열리는 위치를 사용자가 선택할 수 있도록 해보자. SOption 구조체에는 이런 목적으로 FindDlgPos라는 변수가 선언되어 있으며 이 변수값에 따라 찾기 대화상자의 최초 위치를 결정한다. WM_INITDIALOG에 다음 코드를 더 작성한다.

 

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

{

     TCHAR szTemp[256];

     static RECT drt;

    RECT wrt;

    int dx,dy;

 

     switch(iMessage)

     {

     case WM_INITDIALOG:

        if (drt.left==0) {

           GetWindowRect(GetParent(hDlg),&wrt);

           GetWindowRect(hDlg,&drt);

           switch (Option.FindDlgPos) {

           case 0:

               dx=wrt.left+10;

               dy=wrt.top+10;

           default:

           case 1:

               dx=wrt.right-(drt.right-drt.left)-10;

               dy=wrt.top+10;

               break;

           case 2:

               dx=wrt.right-(drt.right-drt.left)-10;

               dy=wrt.bottom-(drt.bottom-drt.top)-10;

               break;

           case 3:

               dx=wrt.left+(wrt.right-wrt.left)/2-(drt.right-drt.left)/2;

               dy=wrt.top+(wrt.bottom-wrt.top)/2-(drt.bottom-drt.top)/2;

               break;

           }

           if (GetSystemMetrics(80/*SM_CMONITORS*/) <= 1) {

               if (dx+(drt.right-drt.left) > GetSystemMetrics(SM_CXSCREEN)) {

                   dx=GetSystemMetrics(SM_CXSCREEN)-(drt.right-drt.left);

               }

               if (dy+(drt.bottom-drt.top) > GetSystemMetrics(SM_CYSCREEN)) {

                   dy=GetSystemMetrics(SM_CYSCREEN)-(drt.bottom-drt.top);

               }

           }

           SetWindowPos(hDlg,HWND_NOTOPMOST,dx,dy,0,0,SWP_NOSIZE);

        } else {

           SetWindowPos(hDlg,HWND_NOTOPMOST,drt.left,drt.top,0,0,SWP_NOSIZE);

        }

 

          if (FindFlag & AE_FIND_UP) {

              CheckDlgButton(hDlg,IDC_FIND_UP,BST_CHECKED);

          }

          ....

 

drt.left 0일 경우 즉, 아직 위치가 저장되지 않은 처음 실행 상태인 경우는 FindDlgPos 옵션이 지정하는 위치로 이동한다. 이 값이 0이면 메인 윈도우의 좌상단, 1이면 우상단, 2면 우하단, 3이면 중앙에 열리도록 하였다. 이 옵션에 의해 찾기 대화상자의 일부가 화면을 벗어날 경우 화면 안쪽으로 강제로 이동시키되 단 멀티 모니터 시스템에서는 이 처리를 하지 않는다. 1번 위치인 오른쪽 위가 텍스트를 가장 적게 가릴 확률이 높으므로 무난한 것 같다. 그래서 FindDlgPos의 디폴트값은 1이다.

찾기 대화상자가 처음 열릴 때만 FindDlgPos 옵션이 적용되며 두 번째 열릴 때부터는 drt.left 0이 아니므로 사용자가 옮겨놓은 자리에 다시 열릴 것이다. 이 위치를 레지스트리에 저장할 수도 있으나 부모 윈도우를 기준으로 한 상대 좌표가 아니라 화면 좌표이기 때문에 별로 큰 의미가 없는 것 같다.

다음은 검색중에 대화상자의 위치를 옮기도록 해보자. 검색된 문자열은 블록으로 선택되어 표시되는데 이 문자열이 찾기 대화상자에 의해 가려져서는 안된다. 대화상자 뒤쪽에 검색된 문자열이 숨어서 보이지 않으면 사용자는 대화상자를 옮겨야만 검색결과를 볼 수 있다. 검색 후에 대화상자의 위치와 검색된 문자열의 위치를 비교해보고 대화상자가 검색결과를 가리고 있다면 위치를 강제로 옮겨서 검색결과를 볼 수 있도록 하는 것이 좋을 것 같다.

 

이런 서비스를 제공하려면 선택영역이 대화상자에 의해 가려졌는지 아닌지를 비교할 수 있어야 한다. CApiEdit에 선택영역의 좌표를 구하는 다음 함수를 추가한다.

 

void CApiEdit::GetSelRect(RECT &srt)

{

     int x1,y1,x2,y2;

 

     if (SelStart==SelEnd) {

          srt.left=-1;

          return;

     }

 

     GetXYFromOff(min(SelStart,SelEnd),x1,y1);

     GetXYFromOff(max(SelStart,SelEnd),x2,y2);

 

     x1-=xPos;

     y1-=yPos;

     x2-=xPos;

     y2-=yPos;

 

     if (y1==y2) {

          srt.left=x1;

          srt.top=y1;

          srt.right=x2;

          srt.bottom=y1+FontHeight;

     } else {

          srt.left=frt.left;

          srt.top=y1;

          srt.right=frt.right;

          srt.bottom=y2+FontHeight;

     }

}

 

선택영역은 한 줄에 있거나 아니면 자동개행된 줄인 경우 여러 줄에 나뉘어져 있을 수 있다. 여러 줄에 선택영역이 걸쳐 있는 경우 선택 블록의 모양은 다각형이 되는데 다각형의 모든 꼭지점을 정확하게 구할 필요까지는 없다. 이 함수는 선택영역을 모두 포함하는 최소한의 좁은 사각 영역을 구한다. 이때 구해지는 영역은 작업영역의 좌상단을 원점으로 하는 픽셀값이다. 즉 스크롤 상태는 고려하지 않는다.

검색 루틴에서는 문자열을 발견한 직후 검색한 블록과 대화상자의 현재 좌표를 비교해 봐야 한다. 이 작업은 대화상자가 직접 하되 그러기 위해서는 대화상자에게 블록의 좌표값를 전달해야 한다. OnUser2에서 검색에 성공한 후 GetSelRect 함수로 블록의 좌표를 구하며 대화상자의 좌표와 직접 비교할 수 있도록 화면 좌표로 변경 하였다. 이렇게 구한 영역의 포인터를 wParam에 대입하여 WM_USER+1을 찾기 대화상자에게 보내준다.

 

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

{

     HWND hActive;

     SInfo *pSi;

     int i;

     DWORD tFlag;

     TCHAR Mes[512];

    RECT srt;

 

     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);

        } else {

           if (IsWindow(g_FindDlg)) {

               pSi->Ae.GetSelRect(srt);

               ClientToScreen(pSi->Ae.hWnd,(LPPOINT)&srt);

               ClientToScreen(pSi->Ae.hWnd,(LPPOINT)&srt+1);

               SendMessage(g_FindDlg,WM_USER+1,(WPARAM)&srt,0);

           }

        }

          break;

     case 2:

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

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

        } else {

           pSi->Ae.GetSelRect(srt);

           ClientToScreen(pSi->Ae.hWnd,(LPPOINT)&srt);

           ClientToScreen(pSi->Ae.hWnd,(LPPOINT)&srt+1);

           SendMessage(g_FindDlg,WM_USER+1,(WPARAM)&srt,0);

        }

          break;

     case 3:

          ....

 

찾기, 바꾸기 명령이 성공한 직후에 대화상자로 WM_USER+1 메시지를 보낸다. 모두 바꾸기는 문서 전체를 대상으로 하므로 대화상자를 굳이 옮기지 않아도 상관없다. 대화상자는 WM_USER+1을 받았을 때 wParam으로 전달된 블록 영역과 자신의 영역을 비교한 후 자신이 블록을 가리고 있으면 자리를 옮긴다. 즉 메인 윈도우로부터 찾기 대화상자로 전달되는 WM_USER+1 메시지는 문자열이 잘 보일 수 있도록 비켜라는 명령이다.

 

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

{

     TCHAR szTemp[256];

     static RECT drt;

     RECT wrt;

     int dx,dy;

    RECT irt;

    int dHeight;

 

     switch(iMessage)

     {

     ....

    case WM_USER+1:

        wrt=*(LPRECT)wParam;

        GetWindowRect(hDlg,&drt);

        IntersectRect(&irt,&wrt,&drt);

        if (IsRectEmpty(&irt))

           return TRUE;

 

        InflateRect(&wrt,32,32);

        dHeight=drt.bottom-drt.top;

        if (wrt.top-dHeight > 0) {

           drt.top=wrt.top-dHeight;

           SetWindowPos(hDlg,HWND_NOTOPMOST,drt.left, drt.top,0,0,SWP_NOSIZE);

        } else if (wrt.bottom+dHeight < GetSystemMetrics(SM_CYSCREEN)) {

           drt.top=wrt.bottom;

           SetWindowPos(hDlg,HWND_NOTOPMOST,drt.left, drt.top,0,0,SWP_NOSIZE);

        }

        return TRUE;

     }

     return FALSE;

}

 

찾기 대화상자는 이 메시지를 받았을 때 자신의 영역을 drt에 구하고 wParam으로 전달된 블록 영역을 wrt에 구한 후 두 사각 영역의 교집합을 irt에 계산한다. irt가 비어 있으면, 즉 대화상자에 의해 블록이 가려져 있지 않으면 아무 것도 할 필요가 없다. 만약 블록과 대화상자의 영역이 겹쳐 있다면 대화상자를 이동시킨다.

먼저 블록의 위쪽으로 이동을 시도해보되 이동한 결과 대화상자의 일부가 화면에 보이지 않게 된다면 블록의 아래쪽으로 이동한다. 위쪽을 우선으로 이동하는 이유는 검색의 방향이 보통 아래쪽이기 때문이다. 아예 위쪽으로 올라가 버려야 다음 검색시에 겹치지 않을 확률이 높다. 대화상자와 선택 블록간에는 32픽셀만큼 여유를 두어 너무 밀착되지 않도록 하였다.

이 기능은 아주 작은 기능이고 직접 사용해보기 전에는 그 존재를 알 수 없다. 하지만 사용자가 검색결과를 잘 볼 수 있도록 자리를 비켜 준다는 것은 서비스 측면에서 사용자를 감동시킬만한 세심한 배려라고 할 수 있다. 아무리 작은 프로그램이라도 잘 찾아 보면 이런 서비스를 할만한 부분들이 항상 있게 마련이다. 기능 자체도 유용하지만 찾기 대화상자에 약간의 지능을 부여하여 최대한 불편하지 않도록 애쓴 흔적을 보임으로써 사용자에게 신뢰를 얻게 될 것이다. 개발자는 가끔 사용자의 입장에서 프로그램을 사용해 볼 필요가 있다.