.파일 열기

다음은 저장된 문서를 읽어오는 함수를 작성해 보자. 저장과는 달리 매번 파일 이름을 입력받아야 하므로 함수에서 모든 동작을 처리할 있다. 파일을 여는 방법이 다양하다면, 예를 들어 드래그 & 드롭이나 MRU 등의 기능이 제공된다면 파일명을 얻는 함수, 파일을 읽는 함수를 따로 만들어야 한다.

 

void Open()

{

   FileHeader Header;

   OPENFILENAME OFN;

   TCHAR lpstrFile[MAX_PATH]="";

   HANDLE hFile;

   DWORD dwRead;

   int idx;

 

   if (ConfirmSave() == IDCANCEL) {

      return;

   }

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

   OFN.lStructSize = sizeof(OPENFILENAME);

   OFN.hwndOwner=hWndMain;

   OFN.lpstrFilter="ApiDrawFile(*.adr)\0*.adr\0Every File(*.*)\0*.*\0";

   OFN.lpstrFile=lpstrFile;

   OFN.nMaxFile=256;

   OFN.lpstrDefExt="adr";

   if (GetOpenFileName(&OFN)==FALSE) {

      return;

   }

 

   hFile=CreateFile(OFN.lpstrFile,GENERIC_READ,0,NULL,

      OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

   if (hFile==INVALID_HANDLE_VALUE) {

      MessageBox(hWndMain,"파일을 없습니다.","에러",MB_OK);

      return;

   }

 

   ReadFile(hFile,&Header,sizeof(Header),&dwRead,NULL);

   if (strcmp(Header.szHeader,"ApiDraw File") != 0) {

      MessageBox(hWndMain,"ApiDraw 파일이 아닙니다.","에러",MB_OK);

      CloseHandle(hFile);

      return;

   }

   if (Header.version != 100) {

      MessageBox(hWndMain,"버전이 1.0 아닙니다.","에러",MB_OK);

      CloseHandle(hFile);

      return;

   }

 

   FreeDoc();

   InitDoc();

 

   arNum=Header.arNum;

   if (arNum >= arSize) {

      arSize=arNum+arGrowBy;

      arObj=(DObject **)realloc(arObj,sizeof(DObject *)*arSize);

   }

   for (idx=0;idx<arNum;idx++) {

      arObj[idx]=(DObject *)malloc(sizeof(DObject));

      ReadFile(hFile,arObj[idx],sizeof(DObject),&dwRead,NULL);

      if (arObj[idx]->Type >= DT_TEXT && arObj[idx]->Type <= DT_META) {

          arObj[idx]->Text=(TCHAR *)malloc(arObj[idx]->Len);

          ReadFile(hFile,arObj[idx]->Text,arObj[idx]->Len,&dwRead,NULL);

      }

   }

   CloseHandle(hFile);

   InvalidateRect(hWndMain,NULL,TRUE);

   ChangeCaption(OFN.lpstrFile);

}

 

파일을 열기 전에 먼저 ConfirmSave 호출하여 편집하던 파일부터 처리한다. 함수 호출에 의해 사용자는 미저장 문서를 저장하거나 버릴 것이며 만약 취소를 선택하면 파일 열기 동작도 취소해야 한다. 미저장 문서를 처리했으면 파일 열기 공통 대화상자로 읽을 파일명을 입력받고 파일을 읽을 준비를 한다. 과정에서 다수의 에러 처리문이 들어가는데 사용자가 파일 열기 대화상자에서 취소를 했거나 지정한 파일을 없을 때는 미련없이 리턴해야 한다. 취소시는 굳이 메시지 박스를 필요가 없지만 파일 오픈 실패시는 메시지 박스로 실패했음을 알리는 것이 좋다. 예제에서는 단순히 실패 사실만 알려 주었지만 가급적이면 어떤 이유로 실패했는지도 상세한 정보를 제공하는 것이 바람직하다.

파일 열기에 성공했으면 먼저 헤더를 읽어 보고 처리 가능한 파일인지를 먼저 확인해야 한다. ApiDraw문서는 파일 선두에 반드시 "ApiDraw File"이라는 문자열이 기록되어 있어야 하는데 문자열이 없으면 ApiDraw 파일이 아니다. 또한 설사 맞다고 하더라도 버전이 1.0 아니면 문서도 역시 읽을 없다. 장래에 ApiDraw 확장되어 1.5 2.0 곡선, 다각형, JPG 등의 객체들을 처리할 있다면 ApiDraw 현재 버전은 문서를 제대로 처리할 없을 것이다.

ApiDraw 다음 버전도 똑같은 확장자 adr 사용할 것이므로 이런 계산이 처음부터 들어가야 한다. 현재의 프로그램이 미래에 만들어질 상위 버전의 문서를 열지 못하는 것은 당연한 일이지만 최소한 에러 처리는 제대로 해야 하는 것이다. ApiDraw 상위 버전은 아마도 1.0 호환성이 있거나, 자료 구조의 변화로 인해 직접 읽을 없더라도 변환 필터를 제공하게 것이다. 헤더 정보가 프로그램과 맞지 않을 경우 파일을 닫고 즉시 리턴한다.

오픈한 파일이 처리 가능한 파일임이 확인되었으면 FreeDoc, InitDoc 함수를 순서대로 호출하여 문서를 재초기화한다. 이전에 편집하던 문서의 도형들은 모두 삭제될 것이며 arObj배열은 초기 크기만큼 할당될 것이다. 함수를 호출하는 시점에 주의하도록 하자. 파일을 문서를 재초기화할 필요가 분명히 있으므로 Open 함수의 선두에서 함수들을 호출할 수도 있는데 이는 현명하지 못하다. 파일을 읽을 있다는 사실이 완전히 확정되기 전에는 이전 문서를 일단은 가지고 있어야 사용자의 취소나 에러 상황에 대비할 있다.

레코드를 읽어올 때는 AppendObject 일일이 호출하지 않고 필요한 메모리를 미리 확장 할당한 한꺼번에 읽어들이도록 했다. 객체의 수가 대단히 많을 때는 매번 재할당해가며 배열에 추가하는 것이 비효율적이기 때문이다. 오브젝트마다 DObject 구조체를 할당하고 객체를 읽어 들이며 텍스트, 비트맵, 메타인 경우는 관련 데이터도 별도의 메모리를 할당해서 읽어들였다. 저장할 객체의 데이터 길이만큼 정확하게 저장했으므로 읽을 때도 길이만큼만 읽어들이면 오차없이 레코드를 순서대로 읽을 있다.

모든 레코드를 읽어 들였으면 새로 읽은 파일명을 타이틀 바에 출력하고 화면을 무효화하여 새로 읽은 문서를 출력한다. OnPaint 새로 만들어진 arObj 내용을 그대로 화면으로 출력할 것이다. 다음은 개의 도형과 하나의 비트맵으로 구성된 가상의 문서를 파일로 출력하는 예를 보인 것이다.

arObj 배열에 입체적으로 구조체와 이미지 데이터 등이 전개되어 있는데 데이터들을 파일에 선형적으로 기록했다. 읽을 들일 때는 반대로 선형의 파일로부터 읽어들여 메모리에 입체적으로 전개하면 원래 문서가 복원된다. 파일 입출력 함수들이 모두 작성되었으며 이제 메뉴 선택시 적절한 함수만 호출하면 된다.

 

LRESULT Main_OnCommand(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

   switch(LOWORD(wParam)) {

   case IDM_FILE_NEW:

      New();

      break;

   case IDM_FILE_OPEN:

      Open();

      break;

   case IDM_FILE_SAVE:

      Save();

      break;

   case IDM_FILE_SAVEAS:

      SaveAs();

      break;

   ....

 

명령의 액셀러레이터와 버튼도 동일한 ID 정의되어 있으므로 사용자는 편한 방법으로 명령을 선택할 있다. 파일 입출력과 관련된 마지막 작업은 미저장 문서를 확인하는 것이다. 편집중에 프로그램을 종료할 문서의 저장 여부를 사용자에게 물어 보아야 한다. 메인 윈도우가 WM_CLOSE 메시지를 받았을 미저장 문서를 처리한다.

 

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

{

....

#ifndef _DEBUG

   case WM_QUERYENDSESSION:

   case WM_CLOSE:

      if (ConfirmSave() == IDCANCEL) {

          return TRUE;

      } else {

          break;

      }

#endif

   }

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

}

 

ConfirmSave라는 함수가 이미 작성되어 있으므로 함수만 호출하면 된다. 사용자가 함수에서 취소를 누르면 프로그램의 종료를 거부해야 하는데 이때는 WM_CLOSE DefWindowProc으로 가지 못하도록 막기만 하면 된다. WM_QUERYENDSESSION 메시지는 운영체제를 종료할 보내지는데 이때도 미저장 문서를 확인해야 한다. 운영체제가 종료될 때는 빠른 종료를 위해 프로세스를 일일이 종료하지 않기 때문에 별도의 메시지를 받아야 한다.

메시지에 대한 처리 코드는 _DEBUG 매크로가 정의되어 있지 않을 때만 컴파일하도록 하여 개발중에는 미저장 문서를 확인하지 않도록 했다. 개발중에는 테스트를 위해 임시 문서를 많이 만들어 보는데 문서는 따로 저장하지 않아도 상관이 없다. 이런 문서에 대해서도 일일이 저장을 질문하면 개발자가 무척 피곤해질 것이다. 디버깅중의 문서는 저장 대상이 아니라 단순한 테스트 대상일 뿐이다.