. 최근 파일 기능 구현

여기까지 MRU 구현을 위한 두 개의 클래스를 다 만들었다. 필요한 모든 기능을 클래스에 캡슐화했으므로 이 기능을 사용하는 것은 별로 어렵지 않다. Dangeun.cpp CMru 클래스를 사용하여 최근 파일 기능을 구현해보자. 먼저 전역 객체 하나를 선언한다.

 

CMru Mru;

 

생성자에서는 아무 일도 하지 않으며 현재 시점에서는 아무런 초기화도 하지 않는다. 파일 목록 출력을 위한 팝업메뉴는 파일 메뉴 아래 최근 파일이라는 이름으로 만들어져 있다. 최근 파일 항목에는 Popup 속성이 주어져 있으며 이 메뉴 아래 비어있음 항목은 Grayed 속성을 주어 흐리게 보이도록 만들었다. 이 항목은 목록이 비어 있을 때 디폴트로 보여질 안내 문장이다. CMru ArrangeMenu 함수는 등록된 파일 개수가 0일 때 팝업메뉴를 전혀 건드리지 않기 때문에 비어있음 항목이 그대로 보인다. OnCreate에서 이 객체가 동작하기 위한 정보를 전달하고 MRU 목록을 읽어온다.

 

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

{

     ....

     DragAcceptFiles(hWnd,TRUE);

    Mru.InitMRU(KEY"MRU",GetSubMenu(GetSubMenu(GetMenu(hWnd),0),7),

        45000,Option.GetMaxMru());

    Mru.LoadMRU();

     return 0;

}

 

레지스트리 키는 이 프로그램의 루트키 아래에 MRU 서브키를 주었으며 최근 파일 팝업메뉴 핸들을 전달하였다. 최근 파일 항목은 디폴트로 선택되는 hMenu1 메인메뉴의 0번째 팝업인 파일의 일곱 번째에서 찾을 수 있다. 메뉴항목의 시작 ID 45000으로 주었는데 충분히 큰 값을 줌으로써 다른 메뉴항목과 중복되지 않도록 하였다. ID이후 Mru.MaxMru개까지의 ID는 다른 용도로 사용되지 않도록 관리해야 한다. OnDestroy에서는 종료하기 전에 MRU 목록을 레지스트리에 저장한다.

 

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

{

     ....

     PostQuitMessage(0);

     SavePosition(hWnd,KEY"Position");

    Mru.SaveMRU();

}

 

파일을 열 때는 새로 열리는 파일을 목록에 등록한다. 파일을 여는 시점은 Open 함수도 있지만 파일을 드래그해서 열 수도 있고 또 MRU에 의해 열릴 수도 있으므로 OpenFromFile 함수에서 파일열기에 성공한 후에 등록하는 것이 좋다. AddMRU 함수만 호출하면 나머지 목록 관리는 CMru 클래스와 그 졸병 클래스인 CHistory가 알아서 처리할 것이다.

 

BOOL OpenFromFile(TCHAR *path)

{

     ....

    Mru.AddMRU(Path);

     return TRUE;

}

 

파일을 저장할 때도 MRU에 등록한다. 이미 존재하는 파일을 저장할 때는 MRU에 등록할 필요가 없으며 새 파일을 저장할 때만 MRU에 등록하면 되므로 SaveAs 함수에 등록 코드를 작성한다.

 

 

BOOL SaveAs(HWND hChild)

{

     ....

    Mru.AddMRU(OFN.lpstrFile);

     lstrcpy(pSi->NowFile,OFN.lpstrFile);

     SetWindowText(hChild,pSi->NowFile);

     return TRUE;

}

 

팝업메뉴에서 파일 목록을 선택할 때 선택된 파일을 여는 동작은 CMru 클래스가 할 수 없으며 프로그램이 직접 해야 한다. WM_COMMAND에서 메뉴항목 선택시 파일을 읽어온다. MRU 메뉴항목은 활성창이 없어도 선택할 수 있으므로 TestNeedActive 함수를 수정하여 MRU 시작 ID에서 MaxMru까지는 FALSE를 리턴하도록 하였다.

 

BOOL TestNeedActive(WORD ID)

{

     ....

    if (ID >= Mru.MenuID && ID < Mru.MenuID+Mru.MaxMru) {

        return FALSE;

    }

     return TRUE;

}

 

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

{

    TCHAR Path[MAX_PATH];

    int idx;

 

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

     if (hActive==NULL) {

          if (TestNeedActive(LOWORD(wParam)))

              return;

     } else {

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

     }

 

    if (LOWORD(wParam) >= Mru.MenuID && LOWORD(wParam) < Mru.MenuID+Mru.MaxMru) {

        idx=LOWORD(wParam)-Mru.MenuID;

        Mru.GetFilePath(idx,Path);

        if (_access(Path,0) != 0) {

           wsprintf(Mes,"%s 파일을 찾을 수 없습니다. 최근 파일 목록에서 "

               "이 파일을 제거합니다.",Path);

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

           Mru.RemoveMRU(idx);

        } else {

           OpenFromFile(Path);

        }

        return;

    }

 

     switch(LOWORD(wParam)) {

     ....

 

메뉴 항목의 ID가 상수가 아니라 MenuID 이후 Mru.MaxMru까지의 범위이기 때문에 case문에 코드를 작성할 수는 없고 if문으로 범위 점검을 해야 한다. 이 범위내의 메뉴항목을 선택했으면 GetFilePath 함수로 선택한 파일의 경로를 구하고 OpenFromFile 함수로 파일을 열었다.

파일열기에 실패한 경우는 두 가지가 있다. 첫 번째는 파일이 디스크에서 삭제되었거나 이름이 변경된 경우인데 대상 파일이 사라진 것이므로 최근 파일 목록에서 제거하도록 해야 한다. 이 경우는 OpenFromFile을 호출하기 전에 파일이 제대로 있는지 미리 확인해보고 에러 점검을 한다. OpenFromFile 함수는 파일이 없는 경우뿐만 아니라 공유 위반, 베드 섹터, 액세스 거부 등 모든 경우에 FALSE를 리턴하기 때문에 이 함수의 리턴값만으로는 목록을 제거해야 할 시점인지 정확하게 판단할 수 없다.

두 번째는 다른 프로그램이 이 파일을 편집하고 있는 중인 공유 위반이나 기타 파일 입출력상의 문제가 발생한 경우인데 이 경우는 OpenFromFile 함수 내에서 이미 에러 처리를 하고 있으므로 여기서 처리하지 않아도 되며 일시적으로 파일을 읽지 못하는 상태이므로 최근 목록에서 파일을 삭제할 필요까지는 없다. 여기까지 작업한 후 실행해보면 MRU 목록이 메뉴에 나타날 것이다.

이 프로그램은 메뉴를 두벌 가지고 있고 열린 파일의 개수에 따라 메뉴가 교체되기 때문에 MRU 메뉴항목도 그때마다 조정해야 한다. 메뉴가 교체되는 시점은 DGChildProc WM_CREATE WM_DESTROY이므로 여기서 MRU 항목도 같이 관리하도록 한다.

 

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

{

     SInfo *pSi;

 

     switch(iMessage) {

     case WM_CREATE:

          g_ChildNum++;

          if (g_ChildNum == 1) {

              SendMessage(g_hMDIClient,WM_MDISETMENU,(WPARAM)hMenu1,

                   (LPARAM)GetSubMenu(hMenu1,5));

           Mru.ChangeMenu(GetSubMenu(GetSubMenu(hMenu1,0),17));

              DrawMenuBar(g_hFrameWnd);

          }

     ....

     case WM_DESTROY:

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

          delete pSi;

          g_ChildNum--;

          if (g_ChildNum==0) {

              SendMessage(g_hMDIClient,WM_MDISETMENU,(WPARAM)hMenu2,NULL);

           Mru.ChangeMenu(GetSubMenu(GetSubMenu(hMenu2,0),7));

              DrawMenuBar(g_hFrameWnd);

          }

          return 0;

     }

     return(DefMDIChildProc(hWnd,iMessage,wParam,lParam));

}

 

hMenu1에서는 파일 메뉴의 17번째가 최근 파일이고 hMenu2에서는 7번째가 최근 파일이다. ChangeMenu 함수로 정확한 위치를 가르쳐 주도록 했다. MRU 기능은 다른 프로젝트에서도 자주 사용되므로 이 클래스를 가져다 사용하면 큰 무리없이 MRU 기능을 붙일 수 있을 것이다. 반드시 독립된 팝업메뉴를 주어야 한다는 제약이 있기는 하지만 수정해서 사용하는 것도 그리 어렵지는 않을 것이다.

MRU의 크기는 SOption::MaxMru 변수가 기억하는데 디폴트 크기는 10이다. 설정 대화상자의 편집 페이지에서 이 값을 변경할 수 있도록 되어 있으므로 설정 관련 코드는 더 이상 작성할 필요가 없고 ApplyNow에서 크기 변경시의 처리만 하면 된다.

 

void ApplyNow()

{

     ....

    if (Option.MaxMru != NewOption.MaxMru) {

        Mru.ChangeMaxMru(NewOption.GetMaxMru());

    }

 

     hChild=GetWindow(g_hMDIClient,GW_CHILD);

     ....

 

MRU의 크기가 바뀌면 CMru::ChangeMaxMru 함수를 호출하여 크기를 변경하도록 했으며 이 함수는 CHistory::ChangeHeight 함수를 호출하여 arMru 배열의 크기를 동적으로 조정한다. CHistory가 크기 변경을 직접 지원하므로 프로그램을 다시 시작하지 않고도 MRU의 개수를 실시간으로 바꿀 수 있다.