. 프로퍼티 시트

텍스트 편집기 정도의 프로그램이라면 사용자가 지정할 수 있는 설정값의 종류가 아주 많기 때문에 한 대화상자로 모든 설정상태를 보여주고 입력받기가 어렵다. 그래서 여러 개의 대화상자를 포개서 표현할 수 있는 프로퍼티 시트를 사용하기로 한다. 프로퍼티 시트는 많은 옵션을 표현할 수 있으며 옵션의 종류별로 그룹을 지을 수 있고 대화상자를 닫지 않고도 옵션을 바로 적용할 수 있는 등 여러 가지 장점이 있다. 설정 대화상자를 만들기 위해 필요한 전역변수를 Dangeun.cpp의 선두에 선언하도록 하자.

 

SOption Option, NewOption;

LOGFONT *arFont;

int FontNum;

int g_StartPage=0;

 

두 개의 옵션 구조체를 선언했는데 Option이 실제로 적용될 옵션값이며 NewOption Option 구조체의 사본, 즉 설정 대화상자에서 값을 변경하는 대상이다. 앞에서 설명했다시피 취소를 위해서는 옵션의 사본에 대해서 값을 바꾸어야 하며 사용자의 확인 명령이 떨어졌을 때 실제로 옵션을 변경해야 한다.

arFont는 폰트를 열거한 결과를 저장하기 위한 LOGFONT의 배열이며 실행중에 동적으로 할당된다. FontNum은 발견된 폰트의 개수이며 이는 곧 시스템에 설치된 폰트의 개수와 같다. 폰트 열거는 시간이 오래 걸리기 때문에 한 번만 열거한 후 계속 사용해야 하며 그래서 그 결과를 저장하기 위한 전역변수가 필요하다. g_StartPage는 프로퍼티 시트의 페이지 번호이며 최후로 열린 페이지 번호를 기억한다. 다음 번 설정 대화상자가 열릴 때는 이 변수가 가리키는 페이지가 열리게 된다. 초기값이 0이므로 최초 설정 대화상자가 열릴 때는 첫 번째 페이지를 보여준다.

다음은 설정 관련 함수들을 작성한다. 프로퍼티 시트는 여러 개의 대화상자로 구성되기 때문에 페이지수만큼 대화상자 프로시저가 필요하다. 나머지 함수는 설정 변경을 도와주고 적용하는 보조 함수들이다. 일단 Dangeun.cpp의 선두에 함수 원형을 선언하도록 하자.

 

void Config();

void DgEnumFonts();

int CALLBACK EnumFamCallBack(ENUMLOGFONT *, NEWTEXTMETRIC *, int, LPARAM);

int CALLBACK ConfigProc(HWND hwndDlg, UINT uMsg, LPARAM lParam);

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

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

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

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

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

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

void ApplyNow();

void AdjustDlgUI(int Page,HWND hDlg);

void SetSetting(CApiEdit &Ae);

void InitSysColor();

COLORREF& GetColorFromID(int ID);

 

이 함수들 중에 설정 대화상자를 처리하는 설정 시작 함수는 Config 함수이다. 이 함수에 의해 설정 대화상자가 만들어지고 화면으로 보여지게 된다.

 

void Config()

{

     PROPSHEETPAGE psp[6];

     PROPSHEETHEADER psh;

 

     NewOption=Option;

 

     psp[0].dwSize=sizeof(PROPSHEETPAGE);

     psp[0].dwFlags=PSP_DEFAULT;

     psp[0].hInstance=g_hInst;

     psp[0].pszTemplate=MAKEINTRESOURCE(IDD_GENERAL);

     psp[0].pfnDlgProc=(DLGPROC)GeneralDlgProc;

     psp[0].lParam=0;

 

     psp[1].dwSize=sizeof(PROPSHEETPAGE);

     psp[1].dwFlags=PSP_DEFAULT;

     psp[1].hInstance=g_hInst;

     psp[1].pszTemplate=MAKEINTRESOURCE(IDD_VIEW);

     psp[1].pfnDlgProc=(DLGPROC)ViewDlgProc;

     psp[1].lParam=0;

 

     psp[2].dwSize=sizeof(PROPSHEETPAGE);

     psp[2].dwFlags=PSP_DEFAULT;

     psp[2].hInstance=g_hInst;

     psp[2].pszTemplate=MAKEINTRESOURCE(IDD_COLOR);

     psp[2].pfnDlgProc=(DLGPROC)ColorDlgProc;

     psp[2].lParam=0;

 

     psp[3].dwSize=sizeof(PROPSHEETPAGE);

     psp[3].dwFlags=PSP_DEFAULT;

     psp[3].hInstance=g_hInst;

     psp[3].pszTemplate=MAKEINTRESOURCE(IDD_EDIT);

     psp[3].pfnDlgProc=(DLGPROC)EditDlgProc;

     psp[3].lParam=0;

 

     psp[4].dwSize=sizeof(PROPSHEETPAGE);

     psp[4].dwFlags=PSP_DEFAULT;

     psp[4].hInstance=g_hInst;

     psp[4].pszTemplate=MAKEINTRESOURCE(IDD_SYNTAX);

     psp[4].pfnDlgProc=(DLGPROC)SyntaxDlgProc;

     psp[4].lParam=0;

 

     psp[5].dwSize=sizeof(PROPSHEETPAGE);

     psp[5].dwFlags=PSP_DEFAULT;

     psp[5].hInstance=g_hInst;

     psp[5].pszTemplate=MAKEINTRESOURCE(IDD_PRINT);

     psp[5].pfnDlgProc=(DLGPROC)PrintDlgProc;

     psp[5].lParam=0;

 

     psh.dwSize=sizeof(PROPSHEETHEADER);

     psh.dwFlags=PSH_PROPSHEETPAGE | PSH_USECALLBACK;

     psh.hwndParent=g_hFrameWnd;

     psh.pszCaption="설정";

     psh.nPages=sizeof(psp)/sizeof(psp[0]);

     psh.nStartPage=g_StartPage;

     psh.ppsp=(LPCPROPSHEETPAGE)&psp;

     psh.pfnCallback=ConfigProc;

 

     if (arFont==NULL) {

          DgEnumFonts();

     }

 

     InitSysColor();

     PropertySheet(&psh);

 

     return;

}

 

#include <pshpack1.h>

typedef struct DLGTEMPLATEEX

{

    WORD dlgVer;

    WORD signature;

    DWORD helpID;

    DWORD exStyle;

    DWORD style;

    WORD cDlgItems;

    short x;

    short y;

    short cx;

    short cy;

} DLGTEMPLATEEX, *LPDLGTEMPLATEEX;

#include <poppack.h>

 

int CALLBACK ConfigProc(HWND hwndDlg, UINT uMsg, LPARAM lParam)

{

     switch (uMsg) {

     case PSCB_PRECREATE:

          if (((DLGTEMPLATEEX *)lParam)->signature==0xffff) {

              ((DLGTEMPLATEEX *)lParam)->style &= ~DS_CONTEXTHELP;

          } else {

              ((DLGTEMPLATE *)lParam)->style &= ~DS_CONTEXTHELP;

          }

          return TRUE;

     }

     return TRUE;

}

 

void DgEnumFonts()

{

     HDC hdc;

 

     hdc=GetDC(NULL);

     if (arFont==NULL) {

          arFont=(LOGFONT *)malloc(sizeof(LOGFONT)*500);

     }

     FontNum=0;

     EnumFontFamilies(hdc,NULL,(FONTENUMPROC)EnumFamCallBack,(LPARAM)NULL);

     ReleaseDC(NULL,hdc);

}

 

int CALLBACK EnumFamCallBack(ENUMLOGFONT FAR *lpelf, NEWTEXTMETRIC FAR *lpntm,

                                  int FontType, LPARAM lParam)

{

     if (FontNum < 500) {

          if (lpelf->elfLogFont.lfFaceName[0]!=‘@’ && FontType==TRUETYPE_FONTTYPE) {

              arFont[FontNum] = lpelf->elfLogFont;

              FontNum++;

          }

          return TRUE;

     }

     else {

          return FALSE;

     }

}

 

Config 함수는 여섯 개의 페이지를 가지는 프로퍼티 시트를 만든다. 프로퍼티 시트를 만들려면 PROPSHEETHEADER 구조체에 프로퍼티 시트의 정보를 채우고 페이지의 개수만큼 PROPSHEETPAGE 배열을 작성한 후 PropertySheet 함수를 호출하면 된다. 구조체가 좀 복잡해보여서 그렇지 별로 복잡하지는 않다. 프로퍼티 시트에 관한 자세한 내용은 API 정복 17장을 참조하기 바란다.

Config 함수는 프로퍼티 시트를 만드는 일 외에도 중요한 몇 가지 일을 한다. 우선 함수의 선두에서 NewOption=Option; 대입문으로 옵션 구조체의 사본을 NewOption에 작성하는데 이 구조체가 바로 설정의 대상이다. 현재 적용되고 있는 옵션의 복사본이므로 이 구조체를 어떻게 변경하더라도 다시 Option으로 대입만 하지 않으면 쉽게 취소할 수 있다.

또한 Config 함수는 프로퍼티 시트를 띄우기 전에 폰트 열거를 하되 최초 호출시 딱 한 번만 한다. 전역변수 arFont는 자동으로 NULL로 초기화되는데 이 변수가 NULL일 때만 DgEnumFonts 함수를 호출한다. DgEnumFonts에서 arFont에 폰트 정보 500개를 저장할 수 있는 메모리를 할당하므로 두 번째 Config 함수가 호출될 때는 폰트 열거를 하지 않는다. 특별한 일이 없는 한 시스템의 폰트 구성이 바뀌지 않기 때문에 폰트 열거를 매번 할 필요는 없다.

대신 시스템에 폰트 목록이 바뀌면 다시 열거를 하도록 해야 한다. 새로운 폰트가 설치되거나 폰트가 제거될 때 메인 윈도우로 WM_FONTCHANGE 메시지가 전달되는데 이때 열거를 다시 하도록 하면 된다. OnFontChange 함수에서 arFont를 해제하고 NULL로 만들어 두면 다음 번 Config 함수가 호출될 때 다시 열거를 할 것이다.

 

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

{

          ....

          case WM_FONTCHANGE:OnFontChange(hWnd,wParam,lParam);return 0;

     }

     return(DefFrameProc(hWnd,g_hMDIClient,iMessage,wParam,lParam));

}

 

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

{

     if (arFont)

          free(arFont);

     arFont=NULL;

}

 

폰트 열거 중에 발견되는 모든 폰트는 arFont 배열에 저장하되 @으로 시작되는 폰트는 세로 폰트이므로 저장하지 않도록 했으며 트루타입 폰트만 사용하도록 했다. 열거되는 폰트 개수의 상한값은 500으로 되어 있는데 이 정도면 거의 대부분의 시스템에 충분한 값이다. 이렇게 열거된 폰트 정보는 ApiEdit의 폰트 선택에 사용되며 인쇄 폰트 선택에도 사용된다.

폰트 열거는 딱 한 번만 하므로 프로그램 시작시인 OnCreate에서 하는 것이 더 합당할 것 같다. 물론 그래도 되지만 굳이 Config 함수에서 폰트 열거를 하는 이유는 폰트 열거 속도가 굉장히 느리기 때문이다. OnCreate에서 폰트 열거를 한다면 프로그램이 시작되는 속도가 느려지며 사용자들이 직접적으로 체감할 수 있을 정도다. 특히 텍스트 뷰어로 당근을 활용한다면 실제 실행속도만큼 시작 속도도 중요한 의미를 가진다.

설정 대화상자는 매번 여는 것이 아닌데 시작 속도 감소를 희생해 가면서 쓰지도 않을 폰트 정보를 매번 작성할 필요가 없다. 그래서 실제로 폰트 정보가 필요한 첫 번째 시점인 Config 함수에서 폰트 열거를 하도록 했다. 설정 대화상자를 처음 열 때 속도가 조금 느려지겠지만 이 때의 속도 감소는 충분히 감수할만하다. 시작 속도는 곧 프로그램의 반응성이 되므로 어떤 프로그램이라도 OnCreate에서 폰트 열거를 하는 것은 아주 멍청한 짓이며 절대로 그렇게 해서는 안된다.

프로퍼티 시트는 타이틀바에 상황별 도움말 출력 버튼인 ? 버튼을 가진다. 이 버튼을 없애기 위해 ConfigProc 콜백 함수를 정의했으며 프로퍼티 시트가 생성되기 전에 DS_CONTEXTHELP 속성을 없애도록 했다. 상황별 도움말을 제공하면 물론 더 없이 좋겠지만 Dangeun은 아직 기능이 완성되지 않았기 때문에 일단 도움말에 대한 지원은 생각하지 않는다.

설정 대화상자는 여섯 개의 페이지로 구성되어 있는데 각 페이지의 대화상자 리소스는 이미 작성되어 있다. 리소스 편집기로 대화상자를 열어 보면 미리 그 모양을 구경할 수 있을 것이다. 설정 대화상자 컨트롤의 단축키 지정은 일부러 하지 않았다. 모든 컨트롤의 캡션에 &를 넣어 키보드로도 옵션을 선택할 수 있도록 하는 것이 원칙적이며 권장되는 바이나 키보드는 마우스보다 불편하고 또 대화상자가 지저분해지는 듯해서 의도적으로 생략했다.

설정 대화상자를 만들고 화면에 출력하는 코드는 완성되었다. 프로퍼티 시트 자체는 페이지를 담기 위한 껍데기에 불과하므로 옵션을 보여주거나 변경하는 코드를 가지지 않는다. 다음 항부터 각 페이지별로 옵션을 어떻게 조정하는지 개별적으로 코드를 작성해보도록 하자.