. 설정 저장 및 복구

설정값을 저장할 수 있는 곳은 INI 파일이나 레지스트리 둘 중 하나인데 INI 파일도 시스템 레지스트리를 건드리지 않는다는 점에서 나쁜 선택은 아니지만 Win32 환경에서는 레지스트리에 정보를 저장하는 것이 여러 모로 유리하다. 레지스트리 입출력은 비교적 쉬운 기술에 속하므로 이 실습은 전혀 어렵지 않을 것이다. 다음 함수들은 정수와 문자열에 대한 레지스트리 입출력 기능을 사용하기 쉽도록 만들어 놓은 것이다. Util.h에 함수 원형과 매크로 상수들을 선언한다.

 

#define SHLM HKEY_LOCAL_MACHINE

#define SHCU HKEY_CURRENT_USER

#define SHCR HKEY_CLASSES_ROOT

 

UINT SHRegReadInt(HKEY hKey, LPCTSTR lpKey, LPCTSTR lpValue, INT nDefault);

BOOL SHRegReadString(HKEY hKey, LPCTSTR lpKey, LPCTSTR lpValue, LPCTSTR lpDefault,

   LPTSTR lpRet, DWORD nSize);

BOOL SHRegWriteInt(HKEY hKey, LPCTSTR lpKey, LPCTSTR lpValue, UINT nData);

BOOL SHRegWriteString(HKEY hKey, LPCTSTR lpKey, LPCTSTR lpValue, LPCTSTR lpData);

 

자주 사용되는 레지스트리 키에 대해 좀 더 짧은 이름의 매크로 상수를 정의해두었다. 함수의 코드는 Util.cpp에 작성한다.

 

UINT SHRegReadInt(HKEY hKey, LPCTSTR lpKey, LPCTSTR lpValue, INT nDefault)

{

     HKEY key;

     DWORD dwDisp;

     UINT Result;

     DWORD Size;

     if (RegCreateKeyEx(hKey, lpKey,0,NULL,

          REG_OPTION_NON_VOLATILE, KEY_READ,NULL,&key,&dwDisp)

          !=ERROR_SUCCESS)

          return 0;

     Size=sizeof(LONG);

     if (RegQueryValueEx(key, lpValue, 0, NULL,(LPBYTE)&Result, &Size)

          !=ERROR_SUCCESS)

          Result=nDefault;

     RegCloseKey(key);

     return Result;

}

 

BOOL SHRegReadString(HKEY hKey, LPCTSTR lpKey, LPCTSTR lpValue, LPCTSTR lpDefault,

   LPTSTR lpRet, DWORD nSize)

{

     HKEY key;

     DWORD dwDisp;

     DWORD Size;

     if (RegCreateKeyEx(hKey, lpKey,0,NULL,

          REG_OPTION_NON_VOLATILE, KEY_READ,NULL,&key,&dwDisp)

          !=ERROR_SUCCESS)

          return FALSE;

     Size=nSize;

     if (RegQueryValueEx(key, lpValue, 0, NULL,(LPBYTE)lpRet, &Size)

          !=ERROR_SUCCESS) {

          strcpy(lpRet, lpDefault);

          return FALSE;

     }

     RegCloseKey(key);

     return TRUE;

}

 

BOOL SHRegWriteInt(HKEY hKey, LPCTSTR lpKey, LPCTSTR lpValue, UINT nData)

{

     HKEY key;

     DWORD dwDisp;

     if (RegCreateKeyEx(hKey, lpKey,0,NULL,

          REG_OPTION_NON_VOLATILE, KEY_WRITE,NULL,&key,&dwDisp)

          !=ERROR_SUCCESS) {

          return FALSE;

     }

     if (RegSetValueEx(key, lpValue,0,REG_DWORD,(LPBYTE)&nData,sizeof(UINT))

          !=ERROR_SUCCESS)

          return FALSE;

     RegCloseKey(key);

     return TRUE;

}

 

BOOL SHRegWriteString(HKEY hKey, LPCTSTR lpKey, LPCTSTR lpValue, LPCTSTR lpData)

{

     HKEY key;

     DWORD dwDisp;

     if (RegCreateKeyEx(hKey, lpKey,0,NULL,

          REG_OPTION_NON_VOLATILE, KEY_WRITE,NULL,&key,&dwDisp)

          !=ERROR_SUCCESS)

          return FALSE;

     if (RegSetValueEx(key, lpValue,0,REG_SZ,(LPBYTE)lpData,strlen(lpData)+1)

          !=ERROR_SUCCESS)

          return FALSE;

     RegCloseKey(key);

     return TRUE;

}

 

레지스트리에 대한 일반적인 이론과 이 함수들에 대한 설명 및 구체적인 사용방법은 API 정복 26장을 참고하기 바라며 여기서는 사용만 하기로 한다. 이 함수들을 사용하면 레지스트리 입출력을 간편하게 할 수 있다. Dangeun.cpp의 선두에 이 프로그램의 정보를 저장할 레지스트리 키를 정의한다.

 

#define KEY "Software\\Miyoungsoft\\Dangeun\\1.0\\"

 

Dangeun의 루트 레지스트리 키이며 설정 대화상자에 있는 대부분의 설정값들이 이 키 아래에 저장된다. 마이크로소프트의 권고안대로 루트키에는 회사명, 제품명, 버전명을 명시하였다. 만약 당근이 새로운 버전으로 업그레이드되면 루트 레지스트리의 키를 바꾸게 되며 따라서 한 시스템에 여러 버전의 당근이 서로 방해받지 않고 동시에 실행될 수 있다. 레지스트리로부터 값을 입출력하는 일은 SOption 구조체의 Load, Save 두 멤버함수가 한다. 다음 코드를 작성하도록 하자.

 

void SOption::Load(TCHAR *Key)

{

     TCHAR szKey[MAX_PATH];

 

     lstrcpy(szKey,Key);

     lstrcat(szKey,"Setting");

 

     StartAction=SHRegReadInt(SHCU,szKey,"StartAction",0);

     bExplorerPopup=SHRegReadInt(SHCU,szKey,"bExplorerPopup",0);

     bMaxFirstChild=SHRegReadInt(SHCU,szKey,"bMaxFirstChild",0);

     MaxMru=SHRegReadInt(SHCU,szKey,"MaxMru",0);

     SHRegReadString(SHCU,szKey,"DefExt","",DefExt,12);

     Hangul=SHRegReadInt(SHCU,szKey,"Hangul",0);

     bInitFind=SHRegReadInt(SHCU,szKey,"InitFind",0);

     bInitNextFind=SHRegReadInt(SHCU,szKey,"InitNextFind",0);

     bShowOutput=SHRegReadInt(SHCU,szKey,"bShowOutput",0);

     OutputHeight=SHRegReadInt(SHCU,szKey,"OutputHeight",0);

     bShowToolBar=SHRegReadInt(SHCU,szKey,"bShowToolBar",0);

     bShowStatus=SHRegReadInt(SHCU,szKey,"bShowStatus",0);

     bShowFileWnd=SHRegReadInt(SHCU,szKey,"bShowFileWnd",0);

     bShowFileTab=SHRegReadInt(SHCU,szKey,"bShowFileTab",0);

     FileWndWidth=SHRegReadInt(SHCU,szKey,"FileWndWidth",0);

     bShowHidden=SHRegReadInt(SHCU,szKey,"bShowHidden",0);

     FilterIndex=SHRegReadInt(SHCU,szKey,"FilterIndex",0);

     bToolBarBig=SHRegReadInt(SHCU,szKey,"bToolBarBig",0);

     bToolBarText=SHRegReadInt(SHCU,szKey,"bToolBarText",0);

     bBrowseMode=SHRegReadInt(SHCU,szKey,"bBrowseMode",0);

     bSoundFindFail=SHRegReadInt(SHCU,szKey,"bSoundFindFail",0);

     bWatchChange=SHRegReadInt(SHCU,szKey,"bWatchChange",0);

     bReloadNoAsk=SHRegReadInt(SHCU,szKey,"bReloadNoAsk",0);

     bReloadProject=SHRegReadInt(SHCU,szKey,"bReloadProject",0);

 

     LineRatio=SHRegReadInt(SHCU,szKey,"LineRatio",0);

     bWrap=SHRegReadInt(SHCU,szKey,"bWrap",0);

     nWrap=SHRegReadInt(SHCU,szKey,"nWrap",0);

     HideSelType=SHRegReadInt(SHCU,szKey,"HideSelType",0);

     nShowCurLine=SHRegReadInt(SHCU,szKey,"nShowCurLine",0);

     bShowMargin=SHRegReadInt(SHCU,szKey,"bShowMargin",0);

     bShowLineNum=SHRegReadInt(SHCU,szKey,"bShowLineNum",0);

     bShowEnter=SHRegReadInt(SHCU,szKey,"bShowEnter",0);

     bShowTab=SHRegReadInt(SHCU,szKey,"bShowTab",0);

     bShowSpace=SHRegReadInt(SHCU,szKey,"bShowSpace",0);   

     TabWidth=SHRegReadInt(SHCU,szKey,"TabWidth",0);

     bNoFirstSpace=SHRegReadInt(SHCU,szKey,"bNoFirstSpace",0);

     RightWrap=SHRegReadInt(SHCU,szKey,"RightWrap",0);

     ColMark=SHRegReadInt(SHCU,szKey,"ColMark",0);  

     MarColor1=SHRegReadInt(SHCU,szKey,"MarColor1",0);

     MarColor2=SHRegReadInt(SHCU,szKey,"MarColor2",0);

     NumColor=SHRegReadInt(SHCU,szKey,"NumColor",0);

     MarkColor=SHRegReadInt(SHCU,szKey,"MarkColor",0);

     ShowTabType=SHRegReadInt(SHCU,szKey,"ShowTabType",0);

     ShowEnterType=SHRegReadInt(SHCU,szKey,"ShowEnterType",0);

     ShowSpaceType=SHRegReadInt(SHCU,szKey,"ShowSpaceType",0);

     CodeColor=SHRegReadInt(SHCU,szKey,"CodeColor",0);

     CurColor=SHRegReadInt(SHCU,szKey,"CurColor",0);

     cBack=SHRegReadInt(SHCU,szKey,"cBack",0);

     cFore=SHRegReadInt(SHCU,szKey,"cFore",0);

     cSelFore=SHRegReadInt(SHCU,szKey,"cSelFore",0);

     cSelBack=SHRegReadInt(SHCU,szKey,"cSelBack",0);

     bUseLineEnd=SHRegReadInt(SHCU,szKey,"UseLineEnd",0);

     bAllowDrag=SHRegReadInt(SHCU,szKey,"AllowDrag",0);

     CaretWidth=SHRegReadInt(SHCU,szKey,"CaretWidth",0);

     bHideCurLine=SHRegReadInt(SHCU,szKey,"HideCurLine",0);

     bCalcTabWithAvg=SHRegReadInt(SHCU,szKey,"bCalcTabWithAvg",0);

     UndoLimit=SHRegReadInt(SHCU,szKey,"UndoLimit",0);

     SHRegReadString(SHCU,szKey,"FaceName","",logfont.lfFaceName,32);

     logfont.lfPitchAndFamily=SHRegReadInt(SHCU,szKey,"PitchAndFamily",0);

     logfont.lfCharSet=SHRegReadInt(SHCU,szKey,"CharSet",0);

     logfont.lfHeight=SHRegReadInt(SHCU,szKey,"FontHeight",0);

     bSpaceForTab=SHRegReadInt(SHCU,szKey,"bSpaceForTab",0);

     bAutoIndent=SHRegReadInt(SHCU,szKey,"bAutoIndent",0);

     bBlockIndentWithTab=SHRegReadInt(SHCU,szKey,"bBlockIndentWithTab",0);

     bHomeToFirstChar=SHRegReadInt(SHCU,szKey,"bHomeToFirstChar",0);

     FindDlgPos=SHRegReadInt(SHCU,szKey,"FindDlgPos",0);

}

 

void SOption::Save(TCHAR *Key)

{

     TCHAR szKey[MAX_PATH];

 

     lstrcpy(szKey,Key);

     lstrcat(szKey,"Setting");

 

     SHRegWriteInt(SHCU,szKey,"StartAction",StartAction);

     SHRegWriteInt(SHCU,szKey,"bExplorerPopup",bExplorerPopup);

     SHRegWriteInt(SHCU,szKey,"bMaxFirstChild",bMaxFirstChild);

     SHRegWriteInt(SHCU,szKey,"MaxMru",MaxMru);

     SHRegWriteString(SHCU,szKey,"DefExt",DefExt);

     SHRegWriteInt(SHCU,szKey,"Hangul",Hangul);

     SHRegWriteInt(SHCU,szKey,"InitFind",bInitFind);

     SHRegWriteInt(SHCU,szKey,"InitNextFind",bInitNextFind);

     SHRegWriteInt(SHCU,szKey,"bShowOutput",bShowOutput);

     SHRegWriteInt(SHCU,szKey,"OutputHeight",OutputHeight);

     SHRegWriteInt(SHCU,szKey,"bShowToolBar",bShowToolBar);

     SHRegWriteInt(SHCU,szKey,"bShowStatus",bShowStatus);

     SHRegWriteInt(SHCU,szKey,"bShowFileWnd",bShowFileWnd);

     SHRegWriteInt(SHCU,szKey,"bShowFileTab",bShowFileTab);

     SHRegWriteInt(SHCU,szKey,"FileWndWidth",FileWndWidth);

     SHRegWriteInt(SHCU,szKey,"bShowHidden",bShowHidden);

     SHRegWriteInt(SHCU,szKey,"FilterIndex",FilterIndex);

     SHRegWriteInt(SHCU,szKey,"bToolBarBig",bToolBarBig);

     SHRegWriteInt(SHCU,szKey,"bToolBarText",bToolBarText);

     SHRegWriteInt(SHCU,szKey,"bBrowseMode",bBrowseMode);

     SHRegWriteInt(SHCU,szKey,"bSoundFindFail",bSoundFindFail);

     SHRegWriteInt(SHCU,szKey,"bWatchChange",bWatchChange);

     SHRegWriteInt(SHCU,szKey,"bReloadNoAsk",bReloadNoAsk);

     SHRegWriteInt(SHCU,szKey,"bReloadProject",bReloadProject);

 

     SHRegWriteInt(SHCU,szKey,"LineRatio",LineRatio);

     SHRegWriteInt(SHCU,szKey,"bWrap",bWrap);

     SHRegWriteInt(SHCU,szKey,"nWrap",nWrap);

     SHRegWriteInt(SHCU,szKey,"HideSelType",HideSelType);

     SHRegWriteInt(SHCU,szKey,"nShowCurLine",nShowCurLine);

     SHRegWriteInt(SHCU,szKey,"bShowMargin",bShowMargin);

     SHRegWriteInt(SHCU,szKey,"bShowLineNum",bShowLineNum);

     SHRegWriteInt(SHCU,szKey,"bShowEnter",bShowEnter);

     SHRegWriteInt(SHCU,szKey,"bShowTab",bShowTab);

     SHRegWriteInt(SHCU,szKey,"bShowSpace",bShowSpace);

     SHRegWriteInt(SHCU,szKey,"TabWidth",TabWidth);

     SHRegWriteInt(SHCU,szKey,"bNoFirstSpace",bNoFirstSpace);

     SHRegWriteInt(SHCU,szKey,"RightWrap",RightWrap);

     SHRegWriteInt(SHCU,szKey,"ColMark",ColMark);

     SHRegWriteInt(SHCU,szKey,"MarColor1",MarColor1);

     SHRegWriteInt(SHCU,szKey,"MarColor2",MarColor2);

     SHRegWriteInt(SHCU,szKey,"NumColor",NumColor);

     SHRegWriteInt(SHCU,szKey,"MarkColor",MarkColor);

     SHRegWriteInt(SHCU,szKey,"ShowTabType",ShowTabType);

     SHRegWriteInt(SHCU,szKey,"ShowEnterType",ShowEnterType);

     SHRegWriteInt(SHCU,szKey,"ShowSpaceType",ShowSpaceType);

     SHRegWriteInt(SHCU,szKey,"CodeColor",CodeColor);

     SHRegWriteInt(SHCU,szKey,"CurColor",CurColor);

     SHRegWriteInt(SHCU,szKey,"cBack",cBack);

     SHRegWriteInt(SHCU,szKey,"cFore",cFore);

     SHRegWriteInt(SHCU,szKey,"cSelFore",cSelFore);

     SHRegWriteInt(SHCU,szKey,"cSelBack",cSelBack);

     SHRegWriteInt(SHCU,szKey,"UseLineEnd",bUseLineEnd);

     SHRegWriteInt(SHCU,szKey,"AllowDrag",bAllowDrag);

     SHRegWriteInt(SHCU,szKey,"CaretWidth",CaretWidth);

     SHRegWriteInt(SHCU,szKey,"HideCurLine",bHideCurLine);

     SHRegWriteInt(SHCU,szKey,"bCalcTabWithAvg",bCalcTabWithAvg);

     SHRegWriteInt(SHCU,szKey,"UndoLimit",UndoLimit);

     SHRegWriteString(SHCU,szKey,"FaceName",logfont.lfFaceName);

     SHRegWriteInt(SHCU,szKey,"PitchAndFamily",logfont.lfPitchAndFamily);

     SHRegWriteInt(SHCU,szKey,"CharSet",logfont.lfCharSet);

     SHRegWriteInt(SHCU,szKey,"FontHeight",logfont.lfHeight);

     SHRegWriteInt(SHCU,szKey,"bSpaceForTab",bSpaceForTab);

     SHRegWriteInt(SHCU,szKey,"bAutoIndent",bAutoIndent);

     SHRegWriteInt(SHCU,szKey,"bBlockIndentWithTab",bBlockIndentWithTab);

     SHRegWriteInt(SHCU,szKey,"bHomeToFirstChar",bHomeToFirstChar);

     SHRegWriteInt(SHCU,szKey,"FindDlgPos",FindDlgPos);

}

 

코드량이 좀 많기는 하지만 모두 단순한 레지스트리 입출력문들일 뿐이다. SOption 구조체를 통째로 레지스트리에 저장한다고 볼 수 있다. 이런 큰 구조체는 각 멤버마다 일일이 키를 만들어 저장하는 방법 대신 이진 포맷(REG_BINARY)으로 저장하면 단 한 번의 함수 호출로 입출력할 수도 있다. 그러나 레지스트리 편집기로 직접 레지스트리를 편집하는 고급 사용자들을 위해, 그리고 개발중의 테스트 편의를 위해 옵션 하나당 하나의 키를 할당하는 것이 더 좋다. 모든 설정상태의 저장과 복구는 이 두 함수에서만 하므로 차후에 옵션이 늘어나거나 옵션의 포맷이 변경되더라도 이 두 함수만 수정하면 된다.

제어코드 보기 방식을 지정하는 ShowTabType, ShowEnterType, ShowSpaceType 옵션들은 좀 특수하게 다루어지고 있다. 이 값들도 레지스트리에 저장 및 복구를 하지만 설정 대화상자에는 이 값들을 보여주거나 변경할 수 있는 사용자 인터페이스가 없다. 그래서 사용자들은 이 옵션에 대해서는 마음에 드는 모양을 선택할 수 없으며 Dangeun의 디폴트 설정대로만 사용해야 한다. 다만 레지스트리 편집기로 직접 이 값을 변경하는 것은 허용된다.

자주 사용되지 않는 옵션은 이런 식으로 UI를 숨기는 정책을 사용할 수 있는데 운영체제도 이런 숨겨진 옵션을 많이 가지고 있다. 디폴트값이 무난해서 특별히 바꿀 필요가 없다거나 옵션의 의미를 설명하기 힘들 때, 또는 옵션 변경의 효과가 보안이나 전반적인 성능에 영향을 미칠 때는 차라리 옵션을 숨기는 것이 더 나을 수도 있다. 그래도 고정된 옵션(Hard Coding)보다는 일단 숨기고 직접 수정을 허용하는 것이 고급 사용자를 위해서 더 좋은 정책이다.

Load, Save 두 함수에 의해 SOption의 멤버들은 영구 정보로 그 격이 승격되며 영원한 생명을 보장받게 된다. 전역변수는 프로그램 실행중에 항상 그 값을 유지하는 긴 지속성을 가지는데 비해 이 정보들은 프로그램 설치 후 언인스톨될 때까지 그 값을 유지하는 영구 정보(Persistent)가 되는 것이다. 변수의 지속성이 전역변수보다도 훨씬 더 긴 셈이다.

레지스트리 입출력함수를 만들었으니 이제 옵션을 한 번만 바꿔 놓으면 이 설정상태를 그대로 저장했다가 다음 번 실행할 때 동일한 설정상태로 시작할 수 있다. 값을 저장하는 시점은 프로그램을 종료하기 직전인 OnDestroy이다.

 

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

{

    Option.Save(KEY);

 

     if (arFont)

          free(arFont);

     PostQuitMessage(0);

}

 

함수를 만들어 놨으니 불러 주기만 하면 된다. 전역 구조체 Option에 기억되어 있는 설정상태가 고스란히 레지스트리에 저장될 것이다. 프로그램이 명시적으로 종료될 때 외에도 설정을 강제로 저장해야 할 곳이 한 군데 더 있다.

 

BOOL OnQueryEndSession(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

     HWND hCloseChild;

     hCloseChild=GetWindow(g_hMDIClient,GW_CHILD);

     while (hCloseChild) {

          if (ConfirmSave(hCloseChild)==IDCANCEL)

              return TRUE;

          hCloseChild=GetWindow(hCloseChild,GW_HWNDNEXT);

     }

    OnDestroy(hWnd,0,0);

     return FALSE;

}

 

시스템이 종료될 때는 실행중인 응용 프로그램의 메인 윈도우를 일일이 파괴하지 않기 때문에 강제로 설정 저장 함수를 호출해야 한다. 물론 시스템 종료를 취소했으면 그럴 필요가 없다. 이렇게 저장된 설정상태를 다시 읽어오는 시점은 물론 OnCreate이다.

 

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

{

     ....

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

 

    if (SHRegReadInt(SHCU,KEY"Setting","StartAction",1000) == 1000) {

    } else {

        Option.Load(KEY);

    }

     ....

 

Option.Load 함수에 의해 이전의 설정상태를 읽어오도록 하였다. , 최초 실행시에는 저장된 설정정보가 없기 때문에 Load 함수를 호출해도 값을 읽을 수가 없다. StartAction의 값을 대표적으로 읽어보되 디폴트를 1000으로 지정하여 읽혀진 값이 그대로 1000이라면 레지스트리에 정보가 없는 것으로 판단한다. 이 경우는 아무 것도 하지 않으며 SOption::Init에서 초기화한 호스트 디폴트 설정을 그대로 사용하도록 하였다.

if문의 형태가 좀 마음에 안 들도록 되어 있는데 지금은 최초 실행시 할 일이 없지만 검색 기능, 문법강조 기능이 들어가면 이 블록에서 할 일이 있으므로 일단 이렇게 작성하도록 하자. 이제 설정상태를 바꾼 후 종료했다가 다시 실행해보면 변경된 설정이 그대로 복구될 것이다. 종료할 때 저장하고 다시 시작할 때 저장된 값을 읽어오므로 당연하다.