. 파일 열기

파일 입출력함수 선언

ApiEdit는 텍스트 입출력을 위한 멤버함수만을 제공하며 직접 파일을 다루지는 않는다. 파일을 관리하는 일은 호스트 프로그램이 할 일이므로 Dangeun에 파일 입출력 코드를 작성해보자. 파일 입출력을 위해 다음 함수를 Dangeun.cpp에 추가한다. 일단 원형부터 선언하도록 하자.

 

void Open();

BOOL OpenFromFile(TCHAR *Path);

BOOL OpenFileToChild(HWND hChild, TCHAR *Path);

BOOL Save(HWND hChild);

BOOL SaveAs(HWND hChild);

BOOL SaveToFile(HWND hChild,TCHAR *Path);

int ConfirmSave(HWND hChild);

HWND FindChildWithFile(TCHAR *path);

BOOL TestNeedActive(WORD ID);

 

New 함수는 이미 작성했으므로 New 함수 다음에 원형을 선언하면 된다. OnCommand 에서는 각 메뉴항목이 선택될 때 대응되는 함수를 호출하였다.

 

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

{

    HWND hActive;

 

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

     switch(LOWORD(wParam)) {

     case IDM_FILE_NEW:

          New();

          break;

    case IDM_FILE_OPEN:

        Open();

        break;

    case IDM_FILE_SAVE:

        Save(hActive);

        break;

    case IDM_FILE_SAVEAS:

        SaveAs(hActive);

        break;

    case IDM_FILE_CLOSE:

        SendMessage(hActive,WM_CLOSE,0,0);

        break;

     ....

 

저장의 경우 활성창을 대상으로 하므로 WM_MDIGETACTIVE 메시지로 활성창의 핸들을 hActive에 조사하여 이 핸들을 Save, SaveAs 함수의 인수로 전달하였다.

유틸리티 함수

그럼 Dangeun.cpp에 파일 입출력함수들의 본체를 작성해보자. 먼저 상대적으로 간단한 두 개의 유틸리티 함수를 작성한다.

 

int ConfirmSave(HWND hChild)

{

     int result=IDNO;

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

     TCHAR Mes[MAX_PATH+64];

 

     if (pSi->Ae.GetModified()) {

          wsprintf(Mes,"%s 파일이 변경되었습니다. 저장하시겠습니까?",pSi->NowFile);

          result=MessageBox(g_hFrameWnd,Mes,"알림",MB_YESNOCANCEL);

          if (result == IDCANCEL) {

              return IDCANCEL;

          }

          if (result == IDYES) {

              if (Save(hChild) == FALSE)

                   return IDCANCEL;

              else

                   return IDYES;

          }

     }

     return result;

}

 

HWND FindChildWithFile(TCHAR *path)

{

     HWND hChild;

     SInfo *pSi;

 

     hChild=GetWindow(g_hMDIClient,GW_CHILD);

     while (hChild) {

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

          if (lstrcmp(pSi->NowFile,path)==0) {

              return hChild;

          }

          hChild=GetWindow(hChild,GW_HWNDNEXT);

     }

     return NULL;

}

 

ConfirmSave 함수는 hChild창의 파일이 저장되었는지를 조사하며 만약 저장되지 않았다면 사용자에게 질문을 하고 저장하도록 한다. 편집창을 닫거나 아니면 프로그램을 종료할 때 이 함수가 호출되어 미보관 문서가 있는지 확인하고 저장할 기회를 제공하게 된다. 파일이 변경되지 않았으면 이미 저장이 완료된 것이므로 아무 일도 하지 않으며 변경되었다면 다음 메시지박스를 통해 저장할 것인지 질문을 한다.

이 질문에 대해 사용자는 세 가지 선택을 할 수 있는데 아니오를 선택하면 IDNO, 취소를 선택하면 IDCANCEL을 리턴한다. IDYES를 선택했을 때의 처리는 조금 복잡한데 일단 Save를 호출하여 문서를 저장하도록 한다. Save 함수 내에서 저장이 완료되면 IDYES를 리턴하고 실패하면 IDCANCEL을 리턴하는데 이때 실패의 원인은 여러 가지가 있을 수 있다. 파일명을 묻는 질문에 취소를 선택할 수도 있고 이미 존재하는 파일을 덮어 쓰지 않겠다고 답변을 했을 수도 있고 아니면 디스크 용량 부족이나 액세스 거부로 저장에 실패할 수도 있다.

이 함수의 리턴값은 세 가지이지만 호출측의 입장에서 보면 IDNO IDYES는 동일한 리턴값이며 IDCANCEL을 리턴했는가 아닌가만 관심을 가진다. 이 함수는 이름 그대로 저장(Save)할 것인지 아닌지를 확실히(Confirm) 처리하도록 하는 동작을 하는데 를 눌러 저장을 했건 아니오를 눌러 편집 내용을 버렸건 이 두 선택에 대한 처리는 이미 이 함수 내에서 결정이 난 것이다. 호출측에서는 오직 사용자가 취소를 선택했는가 아닌가를 보고 창을 닫을 것인지, 프로그램을 무사히 종료할 것인지를 결정할 뿐이다.

FindChildWithFile 함수는 이미 이 문서가 열려 있는지 조사하고, 열려 있다면 이 문서를 편집하고 있는 차일드의 핸들을 구한다. MDI 클라이언트의 모든 차일드를 순회하면서 현재 편집하고 있는 파일이 인수로 전달된 path 파일인지를 조사하였다. 같은 이름을 가진 창이 발견되면 이 창의 핸들을 리턴하고 모든 차일드를 순회한 결과 path 파일을 편집하고 있는 창이 없다면 NULL을 리턴한다.

파일을 여는 함수는 먼저 이 함수를 호출하여 이미 열려 있는 파일인 경우 새로 윈도우를 만들지 않고 해당 윈도우를 활성화시켜 주기만 하면 된다. 만약 이 검사를 하지 않고 무조건 파일을 열어 버린다면 같은 파일을 두 번, 세 번 열 수도 있으며 이는 잠재적으로 편집 내용을 덮어쓸 위험을 유발하게 된다.

파일 읽기 함수

다음은 파일 읽기 함수들이다. Dangeun.cpp의 아래쪽에 코드를 작성하자.

 

void Open()

{

     OPENFILENAME OFN;

     TCHAR *lpstrFile;

     TCHAR Dir[MAX_PATH];

     TCHAR Path[MAX_PATH];

     TCHAR *p;

 

     memset(&OFN, 0, sizeof(OPENFILENAME));

     OFN.lStructSize = sizeof(OPENFILENAME);

     OFN.hwndOwner=g_hFrameWnd;

     OFN.lpstrFilter="모든 파일(*.*)\0*.*\0텍스트 파일\0*.txt\0당근 프로젝트(*.dgp)\0*.dgp*\0";

     lpstrFile=(TCHAR *)malloc(100000);

     lpstrFile[0]=0;

     OFN.lpstrFile=lpstrFile;

     OFN.nMaxFile=100000;

     OFN.Flags=OFN_EXPLORER | OFN_ALLOWMULTISELECT;

 

     if (GetOpenFileName(&OFN)) {

          p=lpstrFile;

          lstrcpy(Dir,p);

          p=p+lstrlen(Dir)+1;

          if (*p==0) {

              OpenFromFile(Dir);

          } else {

              for (;*p;) {

                   wsprintf(Path,"%s\\%s",Dir,p);

                   p=p+lstrlen(p)+1;

                   OpenFromFile(Path);

              }

          }

     } else {

          if (CommDlgExtendedError()==FNERR_BUFFERTOOSMALL) {

              MessageBox(g_hFrameWnd,"한 번에 파일을 너무 많이 선택하셨습니다.",

                   "알림",MB_OK);

          }

     }

     free(lpstrFile);

}

 

BOOL OpenFromFile(TCHAR *Path)

{

     HWND hChild;

     SInfo *pSi;

     BOOL bNew=TRUE;

     TCHAR Mes[512];

 

     hChild=FindChildWithFile(Path);

     if (hChild) {

          SendMessage(g_hMDIClient,WM_MDIACTIVATE,(WPARAM)hChild,0);

          return TRUE;

     }

 

     if (g_ChildNum==1) {

          bNew=FALSE;

          hChild=GetWindow(g_hMDIClient,GW_CHILD);

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

          if (pSi->Ae.GetModified() || strncmp(pSi->NowFile,"이름없음",8)) {

              bNew=TRUE;

          }

     }

    

     if (bNew) {

          hChild=New();

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

     }

 

     if (OpenFileToChild(hChild,Path)==FALSE) {

          if (bNew) {

              SendMessage(g_hMDIClient,WM_MDIDESTROY,(WPARAM)hChild,0);

          }

          wsprintf(Mes,"%s 파일을 열 수 없습니다. "

              "다른 프로그램이 사용중인지 확인하십시오.",Path);

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

          return FALSE;

     }

     return TRUE;

}

 

BOOL OpenFileToChild(HWND hChild, TCHAR *Path)

{

     SInfo *pSi;

     HANDLE hFile;

     DWORD dwRead,dwSize;

     TCHAR *TextBuf;

 

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

 

     hFile=CreateFile(Path,GENERIC_READ,0,NULL,

          OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

     if (hFile==INVALID_HANDLE_VALUE) {

          return FALSE;

     }

 

     pSi->Ae.InitDoc();

     dwSize=GetFileSize(hFile,NULL);

     if (dwSize > 30*1048576) {

          if (MessageBox(g_hFrameWnd, "이 파일의 크기는 30M가 넘습니다. 정말로 여시겠습니까?",

              "질문",MB_YESNO)==IDNO) {

              CloseHandle(hFile);

              return FALSE;

          }

     }

     TextBuf=(TCHAR *)malloc(dwSize+1);

     ReadFile(hFile,TextBuf,dwSize,&dwRead,NULL);

     TextBuf[dwRead]=0;

     pSi->Ae.SetText(TextBuf);

     CloseHandle(hFile);

     free(TextBuf);

     pSi->Ae.SetModified(FALSE);

     lstrcpy(pSi->NowFile,Path);

     SetWindowText(hChild,Path);

 

     return TRUE;

}

 

Open 함수는 IDM_FILE_OPEN 메뉴항목과 일대일로 대응되는 메뉴항목 처리 함수라고 생각하면 된다. 메뉴가 선택된 후의 모든 처리는 이 함수가 담당하며 리턴값을 넘겨줄 필요는 없으므로 void형이다. 파일열기 대화상자를 보여주고 사용자가 읽을 파일을 선택하면 그 파일명을 OpenFromFile 함수로 전달한다. 파일을 선택하지 않고 취소했으면 아무 일도 할 필요가 없으며 그냥 리턴하면 된다. 사용자로부터 파일명을 입력받고 OpenFromFile 함수를 호출하며 이 함수의 처리 결과를 점검한 후 에러 메시지를 보여주는 것이 Open 함수가 하는 일의 전부이다.

복수 개의 파일을 선택하는 옵션을 적용했기 때문에 파일명을 추출하는 부분이 조금 복잡해보일 수도 있는데 API 정복 13-2-마 항에 복수 개의 파일 선택에 관해 자세하게 설명되어 있으므로 참조하기 바란다.

OpenFromFile 함수는 인수로 전달된 Path 파일을 여는데 두 가지 중요한 처리를 하고 있다. 첫 번째는 이미 열린 파일인가 아닌가를 점검해보고 편집중인 파일이면 해당 차일드를 활성화시키기만 한다. 함수 선두에서 FindChildWithFile을 불러 보고 이 함수가 NULL이 아닌 값을 리턴하면 이 파일은 이미 열려 있는 것이므로 WM_MDIACTIVATE 메시지를 보내 이 창을 활성화시킨다.

기본창 처리

두 번째 중요한 처리는 기본으로 만들어준 빈 윈도우를 우아하게 처리하는 것이다. Dangeun MDI 프로그램이므로 새로 파일을 연다고 해서 편집하던 파일을 닫을 필요가 없으며 새로 차일드 윈도우를 하나 더 만들기만 하면 된다. 메모장 같은 SDI 프로그램은 새로 파일을 열려면 먼저 편집하던 파일을 닫아야 하지만 MDI 프로그램은 필요한 만큼 윈도우를 만들 수 있으므로 윈도우를 만들고 파일을 읽어오면 된다. 하지만 예외가 하나 있는데 프로그램이 시작되면서 기본적으로 만들어준 윈도우인 경우는 새로 차일드를 만들지 않고 이 윈도우에 파일을 연다.

Dangeun은 시작 직후에 이름없음1이라는 기본 윈도우를 생성하는데 이는 어디까지나 사용자의 편의와 테스트 목적을 위해 미리 만들어 놓은 것이지 사용자가 명시적으로 연 것이 아니므로 다른 파일이 열리면 굳이 이 윈도우를 유지할 필요가 없다. 사용자가 기본 윈도우를 전혀 편집하지 않은 상태에서 다른 파일을 읽었다면 빈 차일드는 사라져야 한다.

, 사용자가 빈 윈도우에 무엇인가 입력을 했거나 저장을 했다면 이때는 윈도우를 유지해야 한다. 예제에서는 현재 열린 차일드가 하나뿐이고 이 차일드가 편집되지 않았으며 파일명이 여전히 이름없음으로 되어 있을 때만 이 윈도우에 파일을 읽어온다. 그 외의 경우는 항상 New 함수를 호출하여 새로운 윈도우를 만들도록 하였다.

만약 이 상황이 잘 이해가 되지 않는다면 워드를 실행해보도록 하자. 최초 문서1이라는 빈 문서 윈도우를 하나 열어 준다. 이 문서를 편집하지 않은 상태에서 다른 문서 파일을 열면 문서1은 사라지고 새로 읽은 문서 파일을 편집하기 시작할 것이다. 만약 기본적으로 열리는 문서1에 한 글자라도 입력을 한 상황에서 다른 문서 파일을 열면 문서1은 계속 유지되고 새로운 창에 문서 파일이 열린다.

기본적으로 만들어 준 문서1이라는 윈도우는 사용자가 문서를 새로 작성할 것으로 예상하고 서비스 차원에서 미리 만들어 놓은 것인데 사용자가 따로 파일을 열었다면 이 예상이 빗나간 것이다. 그래서 문서1은 더 이상 필요가 없으며 닫아야 한다. Dangeun도 이와 동일한 방법으로 윈도우를 관리한다.

OpenFileToChild 함수는 hChild Path 파일을 읽어준다. Ae.InitDoc을 호출하여 문서를 초기화하고 파일의 크기를 조사하여 그 크기에 널종료 문자를 위한 1을 더한 만큼 버퍼를 할당하고 파일의 내용을 읽는다. 그리고 SetText 함수를 호출하여 파일 내용을 버퍼에 전송한다. 아주 일반적인 파일 입출력 문장뿐이므로 이해하기 어렵지 않을 것이다. 새로 읽은 파일이므로 변경플래그는 리셋되고 hChild NowFile에 새 파일명을 적어넣고 창의 캡션을 바꾸는 정도의 추가 처리도 같이 하고 있다.

OpenFileToChild 함수는 성공할 경우 TRUE를 리턴하며 실패할 경우 FALSE를 리턴한다. OpenFromFile 함수는 이 함수가 FALSE를 리턴할 경우 새로 만든 창을 파괴하고 에러 메시지를 출력한다. MDI의 차일드를 파괴할 때는 DestroyWindow 함수를 사용해서는 안되며 반드시 WM_MDIDESTROY 메시지를 보내야 한다.

파일 크기 제한

OpenFileToChild는 파일을 열기 전에 파일 용량을 확인하여 30MB를 넘을 경우 이 파일을 정말로 열 것인지 사용자에게 확인을 한다. 사용자가 이 질문에 예라고 대답을 할 때만 파일을 열어 주고 그렇지 않으면 실패로 간주하고 FALSE를 리턴하도록 되어 있다. Dangeun은 편집 용량이 이론적으로 무한대이므로 굳이 파일의 용량을 제한할 필요까지는 없다. 큰 파일을 열면 비록 느려지기는 하겠지만 사용자가 그렇게 하겠다면 굳이 말릴 필요는 없는 것이다.

그럼에도 불구하고 30MB의 용량에 대해 귀찮게 질문을 하는 이유는 사용자의 확실한 열기 요청이 아니라 대개의 경우 실수에 의한 열기 요청일 확률이 높기 때문이다. 전반적으로 시스템 성능이 향상되고 기억 장치의 용량이 커지다 보니 650MB Mpeg 파일들이 흔하게 작성되고 DVD의 경우 GB 단위의 파일도 존재한다. 영화 자막 파일을 연다는 것이 실수로 Avi 파일을 선택했다거나 할 경우를 얼마든지 가정해 볼 수 있는데 이럴 때 사용자의 진짜 의도를 확인해 볼 필요가 있는 것이다.

만약 이런 확인을 하지 않고 무조건 파일을 열어 버린다면 그 결과는 예상외로 심각하다. 650MB 파일을 실수로 열 경우 Dangeun 650MB의 메모리를 할당하려고 할 것이다. 운영체제는 응용 프로그램의 메모리 요구를 결코 거절하지 않는다. 100GB 같은 터무니없는 양이 아니라면 원칙적으로 운영체제는 응용 프로그램이 요구한 양을 할당하도록 되어 있으며 요구한 양의 메모리를 내 주기 위해 모든 조치를 취하게 된다. 마치 부모가 귀여운 자식의 요구를 거절하지 못하는 것처럼 말이다.

응용 프로그램이 아빠. 650MB, 그러면 운영체제는 그래! 좀 기다려봐. 내 무슨 수를 쓰더라도 650MB 만들어 주지. 이때부터 불행이 시작되는 것이다. 운영체제는 다른 프로그램이 사용하는 모든 물리 메모리를 비우고 페이징 파일의 남는 공간을 정리하여 650MB를 만들어내기 위해 안간힘을 쓰며 최선을 다한다. 만약 그래도 650MB 용량을 확보하지 못할 경우 하드디스크의 가상 메모리 공간을 늘려(사용자에게 묻지도 않고) 결국은 650MB를 만들어 내기 위해 극단의 조치까지도 서슴지 않는다.

운영체제의 이 시도는 시스템 상황에 따라 실패할 수도 있고 성공할 수도 있다. 512MB 램을 가진 시스템은 거의 100% 성공하며 256MB 정도의 램이 장착된 시스템이라면 성공할 확률이 아주 높으며 그 이하라면 실패할 경우도 있다. 그러나 성공이든 실패든 양쪽 다 문제가 된다. 설사 성공한다 하더라도 한 프로그램이 650MB를 쓰고 있는 상황이라면 다른 프로그램들이 어떻게 돌아갈지, 그 이후의 시스템 상황이 어찌 될지는 안 봐도 뻔하다.

실패할 경우 에러 메시지가 뜨기는 하지만 최종적으로 실패 판단을 하기까지 약 10분 정도가 소요된다. 실행중인 프로그램이 많고 CPU가 느리다면 30분이 걸릴 수도 있다. 페이징 파일을 늘리는 작업은 아무리 운영체제라 하더라도 보통 일이 아닌 것이다. 메모리 할당중인 프로세서는 작업 관리자로 죽이지도 못하며 사용자는 10분 동안 메모리 할당 성공 또는 실패를 기다리거나 아니면 전원을 내리는 수밖에 없다. 결국 이 상태는 다운 상태와 거의 유사한 상태가 되고 만다. 이 상황이 실감이 나지 않으면 다른 편집기로 또는 당근으로 650MB 파일을 열어 직접 테스트해보아라. 정말로 다운과 똑같아지며 실제로 다운될 수도 있다.

사용자의 의도가 100MB 1GB든 어쨌든 이 파일을 보고 싶다면 시스템의 모든 메모리를 동원하여 보여주어야겠지만 실수일 경우는 그 결과가 황당해지므로 좀 귀찮더라도 확인을 하도록 했다. 30MB라는 크기는 시스템에 무리가 갈 가능성이 있는 최소 크기로 계산된 것이며 CPU성능이나 물리 메모리의 양에 따라 더 크게 잡을 수도 있다.

정리

사용자가 파일/열기 항목을 선택하면 Open 함수가 호출된다. 이 함수는 파일열기 대화상자를 보여주고 읽을 파일명을 선택하면 OpenFromFile 함수를 호출한다. OpenFromFile 함수는 이미 열린 파일인지, 빈 윈도우가 있는지를 보고 빈 윈도우를 재사용하든지 아니면 새 윈도우를 만든 후 OpenFileToChild 함수를 호출한다. OpenFileToChild 함수는 실제로 파일을 열어 윈도우에 출력하고 몇 가지 잡스러운 처리까지 담당하고 있다. 결국 이 세 함수가 연속적으로 호출되어야만 파일이 제대로 열리며 한 군데라도 실패하면 파일은 열리지 않는다.

그렇다면 파일열기 기능을 Open 함수에 다 작성하지 왜 이렇게 세 함수로 기능을 분할해놓았는지 의문이 들 것이다. 파일열기 동작을 각 단계별로 이렇게 나누어 놓은 이유는 파일을 여는 방법이 다양하기 때문이다. 메뉴를 통해 열 때는 파일열기 공통 대화상자부터 시작하지만 쉘 오픈이나 명령행으로 파일명을 전달받는 경우는 파일명을 이미 알고 있기 때문에 OpenFromFile 함수를 호출하면 된다. 또한 이미 열린 파일이 아니고 대상 차일드를 알고 있다면 OpenFileToChild 함수를 호출해야 한다.

차후에 네트워크 지원, FTP 지원, 프로젝트 기능을 작성하면 파일을 여는 방법은 점점 더 다양해지며 그때마다 호출해야 할 함수가 달라지기 때문에 이렇게 함수들이 분할되어 있다.