. 일반

일반(General) 페이지는 설정 대화상자의 첫 번째 페이지이며 프로그램 전반에 걸쳐 적용되는 옵션들을 보여주고 변경하도록 한다. 대화상자의 모양은 다음과 같다.

옵션 이름이 한글로 되어 있으니 의미는 쉽게 알 수 있을 것이다. 일반 페이지의 옵션은 ApiEdit와는 전혀 상관이 없으며 호스트에만 적용되는 옵션들이다. 이 설정값을 저장할 변수들은 SOption에 이미 선언되어 있지만 아직 이 옵션들의 적용 코드는 작성되어 있지 않다. 이 페이지를 관리하는 대화상자 프로시저를 다음과 같이 작성한다.

 

BOOL bEditByCode=FALSE;

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

{

     HWND hCon;

 

     switch(iMessage)

     {

     case WM_INITDIALOG:

          hCon=GetDlgItem(hDlg,IDC_MAXMRU);

          SendMessage(hCon,CB_ADDSTRING,0,(LPARAM)"5");

          SendMessage(hCon,CB_ADDSTRING,0,(LPARAM)"10");

          SendMessage(hCon,CB_ADDSTRING,0,(LPARAM)"15");

          SendMessage(hCon,CB_ADDSTRING,0,(LPARAM)"20");

          SendMessage(hCon,CB_ADDSTRING,0,(LPARAM)"25");

          SendMessage(hCon,CB_ADDSTRING,0,(LPARAM)"30");

 

          SendDlgItemMessage(hDlg,IDC_DEFEXT,EM_LIMITTEXT,12,0);

          return TRUE;

     case WM_NOTIFY:

          switch (((LPNMHDR)lParam)->code) {

          case PSN_SETACTIVE:

label_reload:

              g_StartPage=0;

              switch (NewOption.StartAction) {

               case 0:

                   CheckRadioButton(hDlg,IDC_START1,IDC_START4,IDC_START1);

                   break;

              case 1:

                   CheckRadioButton(hDlg,IDC_START1,IDC_START4,IDC_START2);

                   break;

              case 2:

                   CheckRadioButton(hDlg,IDC_START1,IDC_START4,IDC_START3);

                   break;

              case 3:

                   CheckRadioButton(hDlg,IDC_START1,IDC_START4,IDC_START4);

                   break;

              }

 

              CheckDlgButton(hDlg,IDC_EXPLORERPOPUP,

                   NewOption.bExplorerPopup ? BST_CHECKED:BST_UNCHECKED);

              CheckDlgButton(hDlg,IDC_MAXFIRSTCHILD,

                   NewOption.bMaxFirstChild ? BST_CHECKED:BST_UNCHECKED);

              CheckDlgButton(hDlg,IDC_ALLOWMULTI,

                   NewOption.bAllowMulti ? BST_CHECKED:BST_UNCHECKED);

              CheckDlgButton(hDlg,IDC_WATCHCHANGE,

                   NewOption.bWatchChange ? BST_CHECKED:BST_UNCHECKED);

              CheckDlgButton(hDlg,IDC_RELOADNOASK,

                   NewOption.bReloadNoAsk ? BST_CHECKED:BST_UNCHECKED);

              CheckDlgButton(hDlg,IDC_RELOADPROJECT,

                   NewOption.bReloadProject ? BST_CHECKED:BST_UNCHECKED);

 

              SendDlgItemMessage(hDlg,IDC_MAXMRU,CB_SETCURSEL,NewOption.MaxMru,0);

              bEditByCode=TRUE;

               SetDlgItemText(hDlg,IDC_DEFEXT,NewOption.DefExt);

              bEditByCode=FALSE;

              AdjustDlgUI(0,hDlg);

              return TRUE;

          case PSN_APPLY:

              ApplyNow();

              return TRUE;

          case PSN_KILLACTIVE:

              if (IsDlgButtonChecked(hDlg, IDC_START1))

                   NewOption.StartAction=0;

              if (IsDlgButtonChecked(hDlg, IDC_START2))

                   NewOption.StartAction=1;

              if (IsDlgButtonChecked(hDlg, IDC_START3))

                   NewOption.StartAction=2;

              if (IsDlgButtonChecked(hDlg, IDC_START4))

                   NewOption.StartAction=3;

 

              NewOption.bExplorerPopup=IsDlgButtonChecked(hDlg,

                   IDC_EXPLORERPOPUP) == BST_CHECKED ? 1:0;

              NewOption.bMaxFirstChild=IsDlgButtonChecked(hDlg,

                   IDC_MAXFIRSTCHILD) == BST_CHECKED ? 1:0;

              NewOption.bAllowMulti=IsDlgButtonChecked(hDlg,

                   IDC_ALLOWMULTI) == BST_CHECKED ? 1:0;

              NewOption.bWatchChange=IsDlgButtonChecked(hDlg,

                   IDC_WATCHCHANGE) == BST_CHECKED ? 1:0;

              NewOption.bReloadNoAsk=IsDlgButtonChecked(hDlg,

                   IDC_RELOADNOASK) == BST_CHECKED ? 1:0;

              NewOption.bReloadProject=IsDlgButtonChecked(hDlg,

                   IDC_RELOADPROJECT) == BST_CHECKED ? 1:0;

 

              NewOption.MaxMru=SendDlgItemMessage(hDlg,IDC_MAXMRU,CB_GETCURSEL,0,0);

              GetDlgItemText(hDlg,IDC_DEFEXT,NewOption.DefExt,12);

              return TRUE;

          }

          break;

     case WM_COMMAND:

          switch (LOWORD(wParam)) {

          case IDC_WATCHCHANGE:

              AdjustDlgUI(0,hDlg);

          case IDC_START1:

          case IDC_START2:

          case IDC_START3:

          case IDC_START4:

          case IDC_EXPLORERPOPUP:

          case IDC_MAXFIRSTCHILD:

          case IDC_ALLOWMULTI:

          case IDC_RELOADNOASK:

              PropSheet_Changed(GetParent(hDlg),hDlg);

              return TRUE;

          case IDC_MAXMRU:

              switch (HIWORD(wParam)) {

              case CBN_SELCHANGE:

                   PropSheet_Changed(GetParent(hDlg),hDlg);

                   break;

              }

              return TRUE;

          case IDC_DEFEXT:

              switch (HIWORD(wParam)) {

              case EN_CHANGE:

                   if (bEditByCode==FALSE) {

                        PropSheet_Changed(GetParent(hDlg),hDlg);

                   }

                   break;

              }

              return TRUE;

          case IDC_BTNDEFAULT:

              if (MessageBox(hDlg,"모든 설정 상태를 처음 설치 상태로 초기화하시겠습니까?",

                   "알림",MB_YESNO)==IDYES) {

                   NewOption.Init();

                   PropSheet_Changed(GetParent(hDlg),hDlg);

                   goto label_reload;

              }

              return TRUE;

          }

          break;

     }

     return FALSE;

}

 

옵션 대화상자가 처리하는 메시지는 별로 많지 않으며 각 메시지마다 해야 할 일도 일정하게 정해져 있다. 일반 대화상자뿐만 아니라 이후의 모든 대화상자도 이 메시지들을 같은 방식으로 처리하므로 이 대화상자만 잘 분석하면 이후의 대화상자도 쉽게 이해할 수 있을 것이다.

 

WM_INITDIALOG

이 메시지는 대화상자가 초기화될 때 보내지며 대화상자의 컨트롤을 초기화한다. 주로 콤보박스에 초기 항목 문자열을 삽입하거나 에디트 컨트롤의 입력 문자수를 제한하는 등의 처리를 한다. 일반 대화상자에서는 최근 파일 개수 콤보박스에 선택 가능한 값들을 문자열로 초기화하는 처리와 기본 확장자 에디트에 최대 12바이트까지만 입력을 제한하는 처리를 하고 있다. 이외에 대화상자 동작에 필요한 초기값을 구한다거나 미리 필요한 정보를 계산해놓는 등의 동작을 하기도 한다.

PSN_SETACTIVE 통지 메시지

이 메시지는 페이지가 선택될 때 보내지며 옵션값을 컨트롤에 보여주는 일을 한다. 참조할 옵션값은 전역 옵션의 사본인 NewOption이다. NewOption StartAction 변수값을 읽어 어떤 옵션이 선택되어 있는지 라디오 버튼으로 보여주며 탐색기 팝업설정(bExplorerPopup), 첫 차일드 최대화(bMaxFirstChild) 등의 옵션값에 따라 체크박스를 선택하거나 비선택한다.

설정 대화상자의 컨트롤은 라디오 버튼, 콤보박스, 에디트, 체크박스 등이므로 옵션 변수값을 이 컨트롤로 출력한다고 생각하면 된다. 한 페이지에 표시되는 옵션값들이 많기 때문에 이 메시지에 많은 양의 코드가 작성되지만 대부분 비슷비슷한 코드들이라 결코 어렵지는 않다.

이 메시지에서 하는 또 다른 중요한 일은 g_StartPage 전역변수를 현재 페이지 번호로 바꾸는 것이다. g_StartPage는 프로퍼티 시트가 열릴 때 보여질 시작 페이지인데 마지막으로 열었던 페이지를 그대로 다시 열기 위해 이 변수값을 변경한다. 예를 들어 색상 페이지에서 색상을 바꾸었다면 다음 번 프로퍼티 시트를 열 때도 사용자가 색상 페이지를 원할 확률이 높으므로 이 페이지를 열어 주도록 하였다. 각 페이지마다 g_StartPage에 자신의 페이지 인덱스를 대입하는데 일반 페이지는 0번이므로 g_StartPage 0을 대입하였다.

PSN_KILLACTIVE 통지 메시지

이 메시지는 페이지가 선택 해제될 때, 즉 다른 페이지로 옮겨 간다거나 프로퍼티 시트가 닫힐 때 보내진다. PSN_SETACTIVE에서 옵션값으로 컨트롤을 초기화했다면 이 메시지에서는 반대로 컨트롤값을 옵션에 대입해야 한다. 즉 페이지가 선택될 때 옵션값을 컨트롤로 읽고 페이지를 떠날 때 변경된 값을 다시 옵션에 저장하는 것이다.

그래서 이 메시지의 코드는 PSN_SETACTIVE 메시지와는 반대로 작성되어 있다. 라디오 버튼 중 어떤 버튼이 체크되어 있는지를 조사하여 NewOption StartAction 변수값을 변경하고 체크박스의 체크 상태에 따라 bExplorerPopup, bMaxFirstChild 등의 BOOL형 값을 변경한다. 일반 페이지의 이 메시지 처리코드를 보면 어떤 일을 하는지 쉽게 알 수 있을 것이다. 컨트롤의 값을 NewOption으로 다시 읽어들이는 아주 단순한 코드들이다.

이 메시지에서 해야 할 또 다른 중요한 일은 값의 유효성을 점검하는 것이다. 일반 페이지에는 그런 값이 없지만 보기 페이지의 경우 글꼴의 크기를 1200포인터 같은 엄청난 값으로 설정한다거나 줄간을 -38로 설정한다면 이 값을 그대로 받아들일 수 없다. 사용자들이 유효하지 못한 옵션을 입력했을 때는 메시지박스로 사용자의 실수를 알려주고 제대로 값을 입력하도록 해야 하며 대화상자가 닫히지 않도록 해야 한다.

PSN_APPLY 통지 메시지

이 메시지는 사용자가 적용 버튼을 클릭했을 때 전달된다. 또는 확인 버튼을 클릭하여 대화상자를 닫을 때도 전달되는데 이 메시지를 받았다는 것은 사용자가 변경한 옵션을 즉시 적용하라는 명령을 내린 것이다. 이 처리는 각 페이지마다 할 수 없으므로 ApplyNow라는 별도의 함수에서 일괄적으로 처리하도록 하였다. ApplyNow가 변경된 옵션을 어떻게 적용하는지는 잠시 후 따로 분석해보도록 하자.

WM_COMMAND

이 메시지는 잘 알다시피 컨트롤이 부모 윈도우에게 자신의 어떤 변화를 알리는 통지 메시지이다. 콤보박스의 선택 상태가 변경되었다거나 체크박스의 체크 상태가 변경되었을 때 컨트롤이 부모 윈도우에게 이 메시지를 보낸다.

컨트롤이 자신의 변경 사실을 통지했다는 것은 곧 사용자에 의해 옵션의 변화가 있었다는 뜻이므로 프로퍼티 시트의 적용 버튼을 활성화시켜야 한다. 이때는 대화상자의 부모 윈도우, 즉 프로퍼티 시트에게 PSM_CHANGED 메시지를 보내거나 이 메시지를 보내주는 PropSheet_Changed 매크로 함수를 호출하면 된다. 라디오 버튼이나 체크박스 같은 버튼 컨트롤은 BN_CLICKED 통지 메시지만 보내기 때문에 무조건 적용 버튼을 활성화해야 하며 에디트는 EN_CHANGE를 받았을 때, 콤보박스는 CBN_SELCHANGECBN_EDITCHANGE를 받았을 때 적용 버튼을 활성화하면 된다.

또한 이 메시지에서는 각 컨트롤이 요구하는 처리를 해야 한다. 예를 들어 색상선택 버튼을 클릭하면 색상선택 대화상자를 보여준다거나 권장옵션 버튼을 클릭하면 권장옵션으로 돌아가야 한다. 옵션의 종속성 관리도 이 메시지에서 처리한다. 예를 들어 다른 프로그램에 의한 변경 감시 옵션이 선택되어 있지 않으면 변경된 파일 발견시 질문없이 다시 읽기 옵션은 선택할 수가 없으므로 사용 금지시켜야 한다. 이런 종속성 처리는 대화상자가 초기화될 때는 물론이고 각 컨트롤을 조작할 때마다 반복적으로 해야 하므로 AdjustDlgUI라는 별도의 함수가 담당하도록 했다.

AdjustDlgUI

이 함수는 설정 대화상자들이 공유하는 보조 함수이며 각 페이지별로 컨트롤의 종속성을 관리한다. 여러 번 호출되어야 하기 때문에 함수로 만들어 두었다.

 

void AdjustDlgUI(int Page,HWND hDlg)

{

     switch (Page) {

     case 0:

          if (IsDlgButtonChecked(hDlg,IDC_WATCHCHANGE) == BST_CHECKED) {

              EnableWindow(GetDlgItem(hDlg,IDC_RELOADNOASK),TRUE);

          } else {

              EnableWindow(GetDlgItem(hDlg,IDC_RELOADNOASK),FALSE);

          }

          break;

     case 1:

          if (SendDlgItemMessage(hDlg,IDC_FONTFACE,CB_GETCURSEL,0,0)==0) {

              EnableWindow(GetDlgItem(hDlg,IDC_FONTSIZE),FALSE);

          } else {

              EnableWindow(GetDlgItem(hDlg,IDC_FONTSIZE),TRUE);

          }

 

          if (IsDlgButtonChecked(hDlg,IDC_MARGIN) == BST_CHECKED) {

              EnableWindow(GetDlgItem(hDlg,IDC_LINENUM),TRUE);

          } else {

              EnableWindow(GetDlgItem(hDlg,IDC_LINENUM),FALSE);

          }

          break;

     case 5:

          if (IsDlgButtonChecked(hDlg,IDC_PRTFONTSCREEN) == BST_CHECKED) {

              EnableWindow(GetDlgItem(hDlg,IDC_PRTFONTFACE),FALSE);

              EnableWindow(GetDlgItem(hDlg,IDC_PRTFONTSIZE),FALSE);

          } else {

              EnableWindow(GetDlgItem(hDlg,IDC_PRTFONTFACE),TRUE);

              if (SendDlgItemMessage(hDlg,IDC_PRTFONTFACE,CB_GETCURSEL,0,0)==0) {

                   EnableWindow(GetDlgItem(hDlg,IDC_PRTFONTSIZE),FALSE);

              } else {

                   EnableWindow(GetDlgItem(hDlg,IDC_PRTFONTSIZE),TRUE);

              }

          }

          break;

     }

}

 

각 페이지별로 종속성이 있는 컨트롤의 상태를 조사하여 종속되는 컨트롤의 상태를 변경한다. 종속성이 있는 페이지는 0,1,5페이지뿐이다. 1번 페이지의 경우 시스템 폰트가 선택되어 있을 때는 폰트 크기를 조정할 수 없도록 하며 마진이 보이는 상태에서만 줄번호 보기 옵션을 변경할 수 있다.

bEditByCode 전역변수

GeneralDlgProc 함수 바로 위에 bEditByCode라는 전역변수가 선언되어 있으며 FALSE로 초기화되어 있다. 이 변수가 필요한 이유는 에디트 표준 컨트롤이 사용자가 편집할 때나 코드가 SetDlgItemText 함수로 값을 변경할 때나 구분하지 않고 EN_CHANGE 통지 메시지를 보내기 때문이다. PSN_SETACTIVE에는 NewOption.DefExt 옵션값을 IDC_DEFEXT 에디트에 대입하는 코드가 있다. 이 대입문은 옵션을 변경한다는 뜻이 아니라 초기화를 한다는 뜻이므로 이때는 적용 버튼을 활성화해서는 안된다.

그래서 코드가 에디트의 텍스트를 변경할 때는 bEditByCode를 잠시 TRUE로 변경해놓고 텍스트를 대입하며 대입이 끝난 후에 이 변수를 다시 FALSE로 바꾸어 놓는다. WM_COMMAND에서는 에디트가 EN_CHANGE를 보냈을 때 사용자가 편집한 것인지 코드가 텍스트를 바꾼 것인지를 이 변수값으로 판단하여 코드가 바꾼 경우는 적용 버튼을 활성화하지 않도록 하였다. 이렇게 하지 않으면 대화상자가 열리자 마자 적용 버튼은 항상 활성화되어 있을 것이다.

이런 상태 구분을 위해 전역변수를 사용하는 것이 무척 이상해보고 마음에 안 들지 모르겠지만 에디트 컨트롤이 아무 때나 통지 메시지를 보내기 때문에 어쩔 수가 없다. 만약 이 대화상자가 프로퍼티 시트에 속한 대화상자가 아니라면 전역변수를 쓸 필요없이 PSN_SETACTIVE에서 에디트의 텍스트를 바꾼 후 적용 버튼을 비활성화시키는 방법을 사용할 수 있다. 그러나 프로퍼티 시트에서 이렇게 하면 다른 페이지에서 적용 버튼을 활성화시킨 경우를 무시하는 꼴이 되어 버려 그럴 수가 없다. 이 변수가 필요한 이유는 에디트 컨트롤의 특이한 특성과 프로퍼티 시트에 속한 대화상자라는 상황 때문이다.

권장옵션

일반 페이지에는 권장옵션 버튼이 있다. 여기서 말하는 권장옵션이란 곧 호스트가 지정하는 디폴트 옵션을 의미한다. 옵션을 마음대로 바꾼 후 최초 상태로 돌아가고 싶을 때 이 버튼을 사용하면 된다. 최초 상태로 돌아가는 것은 아주 쉽다. NewOption.Init를 호출하면 모든 옵션이 디폴트가 되며 이 상태에서 PSN_SETACTIVE로 점프해버리면 된다.

점프를 위해 goto 문을 사용했는데 goto 문은 구조화 프로그래밍 방식에서 가급적이면 사용하지 말아야 제어 구조로 알려져 있다. 하지만 과다하게 사용할 경우에 문제가 될 뿐이지 goto 문 자체가 코드를 엉망으로 만드는 것은 아니므로 지나치게 금기시할 필요까지는 없다. 이 예제의 경우 PSN_SETACTIVE의 코드를 별도의 함수로 분리해놓으면 goto 문을 쓰지 않고도 코드를 깔끔하게 유지할 수 있지만 함수 하나를 더 만드는 것보다는 차라리 goto를 쓰는 것이 더 간편하다. 꼭 필요할 때는 사용하되 약간의 주의만 기울이면 된다.

PSN_SETACTIVE NewOption의 값으로 컨트롤을 초기화하므로 이 페이지의 모든 컨트롤이 즉시 권장옵션 상태를 보여줄 것이다. NewOption이 초기화되면 다른 페이지에 있는 옵션들도 모두 초기화된다. , 이 경우도 변경된 것은 NewOption이지 Option이 아니므로 취소 버튼을 클릭하여 이전의 상태로 돌아갈 수 있다. 이 명령은 지금까지 설정한 모든 설정을 무효화시키는 동작이므로 안전상 사용자에게 한 번의 확인을 더 요구한다.