. 찾기 대화상자

찾기 기능은 조건에 맞는 문자열이 문서의 어디쯤에 있는지 찾아 주는 기능이며 텍스트 편집기라면 당연히 제공해야 하는 필수 기능이다. 아주 짧은 문서라면 몰라도 한 페이지 이상의 문서에서 육안으로 문자열을 찾는 것은 부정확할 뿐만 아니라 무척 귀찮은 일이다. ApiEdit buf에 문서의 모든 내용을 가지고 있으며 저장하고 있는 데이터가 단순한 문자열 포맷이므로 별 어려움 없이 사용자가 원하는 문자열을 찾아줄 수 있다.

찾기 기능은 조건에 맞는 문자열이 존재하는지를 조사하기 위해서도 사용되지만 그보다는 신속한 이동을 위해 더 많이 사용된다. 함수명이나 변수명 등을 직접 입력함으로써 편집하고자 하는 곳으로 빠르게 이동할 수 있다. 특히 프로그래밍 소스같이 중복되지 않는 명칭을 다루는 문서에서 이 기능은 어떤 방법보다 더 빠르고 정확한 이동 방법이 된다. 원하는 편집 부분을 명시적으로 지정하는 능동적이고 직접적인 이동 방법이라고 할 수 있다.

검색 기능을 작성하기 위해 이전 프로젝트를 복사하여 Dangeun7 프로젝트를 만들도록 하자. 검색 관련 명령들은 검색 메뉴에 모두 있으며 대부분의 명령에 단축키가 배정되어 있다. 리소스에는 검색 메뉴와 검색 관련 대화상자들이 이미 작성되어 있으므로 코드만 추가하면 된다. 먼저 Dangeun.cpp에 검색에 필요한 전역변수들을 선언한다.

 

HWND g_FindDlg;

CHistory arFind[4];

DWORD FindFlag;

 

찾기를 하는 중에 대화상자를 닫지 않고도 검색된 텍스트를 편집할 수 있어야 하므로 찾기/바꾸기 대화상자는 모델리스형으로 열려야 한다. 모델리스형 대화상자는 그 생명이 전역적이기 때문에 대화상자의 핸들도 전역으로 선언되어야 하며 g_FindDlg 가 찾기 대화상자의 핸들이다. arFind는 검색에 사용된 문자열 히스토리를 저장하는 CHistroy 객체 배열이며 4가지 종류의 히스토리를 관리한다. arFind[0]는 찾은 문자열, arFind[1]은 바꾸기 문자열이며 나머지는 파일 찾기에서 검색 대상 파일과 검색 폴더 저장에 사용된다. arFind[0] 객체가 사용자가 검색한 문자열의 목록을 기억한다는 것을 알아 두자. OnCreate에서 히스토리 객체들을 다음과 같이 초기화한다.

 

int OnCreate(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

     ....

     ccs.hWindowMenu=GetSubMenu(GetMenu(hWnd),5);

     ccs.idFirstChild=IDM_WINDOWCHILD;

     g_hMDIClient=CreateWindowEx(WS_EX_CLIENTEDGE,"MDICLIENT", NULL,

          WS_CHILD | WS_CLIPCHILDREN |    WS_VISIBLE,

          0,0,0,0,hWnd,(HMENU)NULL, g_hInst, (LPSTR)&ccs);

     SetTimer(hWnd,100,1,NULL);

 

    arFind[0].Init(256,50);

    arFind[1].Init(256,20);

    arFind[2].Init(256,20);

    arFind[3].Init(256,20);

     ....

 

한 번에 검색할 수 있는 문자열은 최대 255바이트까지로 제한하였다. 찾기 히스토리가 가장 많이 사용되므로 50개까지 기억하도록 했으며 나머지는 그다지 자주 사용되지 않으므로 20개까지만 기억한다. 더 많은 히스토리를 기억하고 싶다면 이 초기값을 늘려주고 사용자가 직접 변경할 수 있도록 하고 싶다면 옵션에 포함시키면 된다.

FindFlag는 대소문자 구분, 단어 단위로, 검색 방향 등을 지정하는 검색 옵션이다. 검색 옵션들은 모두 BOOL형이므로 옵션마다 별도의 변수를 만들 필요없이 정수형 변수의 각 비트별로 옵션을 기억하도록 하였다. ApiEdit.h FindFlag를 구성하는 다음 플래그들을 정의한다. 플래그의 의미는 이름만 봐도 쉽게 알 수 있을 것이다.

 

#define AE_FIND_UP 0x1

#define AE_FIND_WHOLEWORD 0x2

#define AE_FIND_MATCHCASE 0x4

#define AE_FIND_WRAP 0x8

#define AE_FIND_CLOSE 0x10

#define AE_FIND_RECURSIVE 0x20

#define AE_FIND_SHORTPATH 0x40

 

이 플래그들은 Dangeun이 검색 옵션을 설정할 때도 사용하며 ApiEdit가 실제 검색을 할 때도 사용하므로 ApiEdit.h 헤더 파일에 정의해야 한다. 찾기 대화상자는 다음과 같이 디자인되어 있다.

찾을 내용을 입력받는 콤보박스와 문자열 검색에 필요한 옵션들을 입력받을 수 있는 체크박스들이 배치되어 있다. 다섯 개의 체크박스들은 AE_FIND_* 매크로 상수와 차례대로 대응된다. 콤보박스는 검색 문자열 히스토리를 가지며 콤보 리스트에서 이전 검색 문자열을 재 검색할 수 있다. 운영체제가 제공하는 찾기 공통 대화상자가 있기는 하지만 다룰 수 있는 옵션이 제한적이기 때문에 직접 대화상자를 만들었다. 다음은 찾기 대화상자의 프로시저를 작성한다. 검색 히스토리를 콤보박스로 읽어오는 유틸리티 함수도 같이 만들도록 하자.

 

void RefillHistory(HWND hCombo, CHistory &Hist)

{

     int i;

 

     SendMessage(hCombo,CB_RESETCONTENT,0,0);

     for (i=0;i<Hist.num;i++) {

          SendMessage(hCombo,CB_ADDSTRING,0,(LPARAM)Hist.Get(i));

     }

     SendMessage(hCombo,CB_SETCURSEL,0,0);

}

 

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

{

     TCHAR szTemp[256];

 

     switch(iMessage)

     {

     case WM_INITDIALOG:

          if (FindFlag & AE_FIND_UP) {

              CheckDlgButton(hDlg,IDC_FIND_UP,BST_CHECKED);

          }

 

          if (FindFlag & AE_FIND_WHOLEWORD) {

              CheckDlgButton(hDlg,IDC_FIND_WHOLEWORD,BST_CHECKED);

          }

 

          if (FindFlag & AE_FIND_MATCHCASE) {

              CheckDlgButton(hDlg,IDC_FIND_MATCHCASE,BST_CHECKED);

          }

 

          if (FindFlag & AE_FIND_WRAP) {

              CheckDlgButton(hDlg,IDC_FIND_WRAP,BST_CHECKED);

          }

 

          if (FindFlag & AE_FIND_CLOSE) {

              CheckDlgButton(hDlg,IDC_FIND_CLOSE,BST_CHECKED);

          }

 

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

          SendMessage(GetDlgItem(hDlg,IDC_FIND_WHAT), 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);

                   }

                   break;

              case CBN_SELCHANGE:

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

              }

              break;

          case IDCANCEL:

              DestroyWindow(hDlg);

              break;

          case IDC_BTNFIND:

              if (IsDlgButtonChecked(hDlg,IDC_FIND_UP)) {

                   FindFlag |= AE_FIND_UP;

              } else {

                   FindFlag &= ~AE_FIND_UP;

              }

 

              if (IsDlgButtonChecked(hDlg,IDC_FIND_WHOLEWORD)) {

                   FindFlag |= AE_FIND_WHOLEWORD;

              } else {

                   FindFlag &= ~AE_FIND_WHOLEWORD;

              }

 

              if (IsDlgButtonChecked(hDlg,IDC_FIND_MATCHCASE)) {

                   FindFlag |= AE_FIND_MATCHCASE;

              } else {

                   FindFlag &= ~AE_FIND_MATCHCASE;

              }

 

              if (IsDlgButtonChecked(hDlg,IDC_FIND_WRAP)) {

                   FindFlag |= AE_FIND_WRAP;

              } else {

                   FindFlag &= ~AE_FIND_WRAP;

              }

 

              if (IsDlgButtonChecked(hDlg,IDC_FIND_CLOSE)) {

                   FindFlag |= AE_FIND_CLOSE;

              } else {

                   FindFlag &= ~AE_FIND_CLOSE;

              }

 

              GetDlgItemText(hDlg,IDC_FIND_WHAT,szTemp,255);

              if (lstrlen(szTemp)) {

                   arFind[0].Add(szTemp);

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

              }

 

              switch (LOWORD(wParam)) {

              case IDC_BTNFIND:

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

                   if (FindFlag & AE_FIND_CLOSE) {

                        DestroyWindow(hDlg);

                   }

                   break;

              }

              return TRUE;

          }

          return FALSE;

     }

     return FALSE;

}

 

RefillHistory 함수는 콤보박스의 핸들과 히스토리 객체를 주면 히스토리에 기록된 문자열을 콤보박스에 넣어준다. 찾기 대화상자의 경우 찾을 내용 콤보박스에 arFind[0] 히스토리를 대입하였다. 최초 히스토리가 비어 있으므로 아무것도 대입되지 않지만 검색 기능을 사용하면 한 번 검색한 문자열이 히스토리에 저장되며 콤보박스를 통해 다시 선택할 수 있게 된다. 이 함수는 찾기/바꾸기, 파일 찾기/파일 바꾸기 등에 공통적으로 사용된다.

WM_INITDIALOG에서 검색 옵션인 FindFlag의 각 비트를 점검하여 검색 옵션 컨트롤을 초기화하여 사용자에게 현재 검색 옵션을 보여준다. 예를 들어 AE_FIND_MATCHCASE 옵션이 선택되어 있으면 영문 대/소문자 구분 체크박스를 체크할 것이다. 히스토리 객체의 폭이 255로 제한되어 있으므로 검색식을 입력받는 콤보박스도 최대 255문자까지만 입력받을 수 있도록 제한하였다.

WM_COMMAND에서는 컨트롤의 통지 메시지를 처리하는데 찾을 내용 콤보박스가 편집될 때 입력된 문자열의 길이를 보고 찾기 버튼을 관리한다. 찾을 내용이 있어야만 검색을 시작할 수 있으므로 검색 문자열의 길이가 0이면 찾기 버튼을 사용 금지시켰다. 콤보박스의 선택 상태가 변경될 때나 대화상자가 초기화될 때도 이 처리를 해야 한다.

찾기 버튼을 클릭하면 컨트롤에 설정된 옵션들을 FindFlag 변수로 다시 읽어들이고 찾을 문자열은 arFind[0] 배열의 첫 번째 배열요소에 기억된다. 만약 이 문자열이 히스토리에 없으면 배열의 처음에 삽입되며 이미 있으면 제일 위로 올라갈 것이다. 이런 목록 관리 기능은 CHistroy::Add 함수에 의해 처리되며 MRU를 작성할 때 이미 살펴 보았었다. 이후부터 arFind[0].Get(0)로 사용자가 입력한 검색식을 구할 수 있다.

찾을 내용과 검색 옵션을 arFind[0], FindFlag 전역변수에 기록한 후 메인 윈도우로 WM_USER+2 메시지를 보내주되 이때 wParam 1의 값을 가진다. 메인 윈도우는 이 메시지를 받았을 때 전역변수값을 참조하여 검색을 할 것이다.

AE_FIND_CLOSE 옵션은 검색을 마친 후 대화상자를 닫을 것인지를 기억하는데 이 옵션이 선택되어 있으면 DestroyWindow 함수로 찾기 대화상자를 직접 파괴한다. 선택되어 있지 않으면 찾기 대화상자는 닫히지 않고 그대로 유지된다. 계속 검색을 할 때는 이 옵션을 선택하지 말고 이동을 위해 검색을 하고 있으면 이 옵션을 선택하여 이동한 곳에서 바로 편집을 할 수 있도록 하는 것이 좋다.

찾기 대화상자는 메뉴에서 검색/찾기를 선택하거나 또는 단축키 <Ctrl+F>를 누를 때 나타난다. OnCommand에서 IDM_SEARCH_FIND 명령을 받았을 때 이 대화상자를 호출한다.

 

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

{

     ....

     case IDM_SEARCH_FIND:

          if (!IsWindow(g_FindDlg)) {

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

              ShowWindow(g_FindDlg,SW_SHOW);

          } else {

              SetFocus(g_FindDlg);

          }

          break;

 

모델리스 대화상자이므로 DialogBox 함수를 사용하지 않고 CreateDialog 함수로 만들기만 한다. g_FindDlg 핸들이 유효하면 이미 찾기 대화상자가 떠 있으므로 새로 대화상자를 만들 필요없이 SetFocus로 포커스만 넘겨주면 된다. 메시지 루프는 모델리스 대화상자의 메시지를 처리할 수 있도록 다음과 같이 수정되어야 한다.

 

     while(GetMessage(&Message,0,0,0)) {

        if (!IsWindow(g_FindDlg) || !IsDialogMessage(g_FindDlg,&Message)) {

              if (!TranslateMDISysAccel(g_hMDIClient, &Message)) {

                   if (!TranslateAccelerator(hWnd,hAccel,&Message)) {

                        TranslateMessage(&Message);

                        DispatchMessage(&Message);

                   }

              }

        }

     }

 

이 처리를 하지 않으면 찾기 대화상자에서 Tab, 커서이동키 등으로 컨트롤을 전환할 수 없다. 메시지 루프 모양이 좀 복잡해졌는데 메시지 큐에서 메시지를 꺼내면 IsDialogMessage 함수가 먼저 모델리스 대화상자를 위한 메시지인지 본다. 아니면 MDI액셀러레이터인지 보고 그것도 아니면 액셀러레이터 테이블에 등록된 키입력인지 본다. 이 모든 조건에 해당되지 않을 때 비로소 DispatchMessage 함수에 의해 윈도우 프로시저로 메시지를 보낸다.

메시지 큐에서 꺼낸 메시지는 여러 함수들에 의해 점검되고 처리되는데 이때 순서에 주의해야 한다. 어떤 함수를 먼저 호출하는가에 따라 메시지 처리 우선 순위가 결정되는데 모델리스 대화상자의 메시지를 처리하는 IsDialogMessage 함수가 최우선 순위를 가져야 한다. 만약 이 함수가 TranslateAccelerator 함수보다 뒤에 있으면 찾기 대화상자의 콤보박스에서 <Ctrl+C>, <Ctrl+V> 등의 클립보드 단축키가 작동하지 않는다.

여기까지 작성한 후 실행하면 대화상자가 나타나며 검색 옵션이나 검색 문자열을 입력할 수는 있지만 아직 검색은 되지 않는다. 검색 요청은 메인 윈도우가 직접 처리해야 한다. 이 대화상자의 임무는 현재 설정되어 있는 검색 옵션을 보여주고 사용자에게 검색 옵션과 검색 문자열을 입력받는 것뿐이다. 대화상자는 대화가 주목적이지 기능을 제공하는 것이 본분이 아니다. 파일열기 대화상자는 열고자 하는 파일의 경로를 입력받아 줄 뿐이지 파일을 직접 열어주는 것은 아니지 않는가? 대화상자가 대화 이외의 동작을 하려고 할 때 프로그램은 복잡해진다.