. 탭 대화상자

이번에는 바탕화면 등록 정보 윈도우처럼 탭 컨트롤안에 대화상자를 넣어 탭 페이지를 만들어 보자. 대화상자는 리소스 편집기로 쉽게 컨트롤을 배치할 수 있으므로 탭 컨트롤과 함께 사용하면 아주 많은 옵션들을 그룹별로 나누어 입력받을 수 있으며 페이지 단위로 빠르게 전환할 수 있다는 장점도 있다.

여기서 한가지 분명히 해 둘 것은 탭 컨트롤은 페이지 관리에 대해서는 어떠한 기능도 제공하지 않는다는 점이다. 대화상자를 배치할 수 있는 넓다란 표시 영역을 제공할 뿐이지 탭을 바꾼다고 해서 페이지를 같이 바꾸어 준다거나 대화상자 자체를 탭의 차일드로 둘 수 있는 것도 아니다. 다만 탭은 선택이 바뀌었을 때 부모 윈도우로 TCN_SELCHANGE 통지 메시지를 보내 주어 페이지를 교체해야 할 시점을 알려 주기만 할 뿐이며 대화상자를 관리하는 모든 작업은 탭 컨트롤의 부모 윈도우가 해야 한다.

먼저 페이지를 구성할 대화상자를 리소스에 작성한다. 이 대화상자들은 탭의 표시 영역안에 나타나야 하므로 타이틀 바와 경계선을 가지지 않아야 하며 반드시 WS_CHILD 스타일을 주어야 한다. 필요한 페이지 수만큼 대화상자를 만들 수 있으며 크기는 가급적이면 동일한 것이 좋으나 실행중에 최대 크기에 맞출 수 있으므로 꼭 그럴 필요는 없다. 대화상자안에 컨트롤은 필요한만큼 원하는대로 배치할 수 있다.

 

메인 윈도우는 이 대화상자들 중 최대 크기만큼 탭 컨트롤의 표시 영역을 설정하고 탭과 대응되는 대화상자를 모델리스로 생성하여 표시 영역에 보여 주면 된다. 각 페이지안에서 일어나는 일은 물론 개별 대화상자 프로시저에서 처리해야 한다. 다음 예제는 두 개의 페이지를 가지는 탭 컨트롤을 생성한다. 이번에는 WinMain까지 전체 소스를 보였다.

 

#include <windows.h>

 

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);

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

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

HINSTANCE g_hInst;

HWND hWndMain;

LPCTSTR lpszClass=TEXT("TabDlg");

 

#include <commctrl.h>

#include "resource.h"

HWND hTab;

HWND hTabDlg;

 

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

     ,LPSTR lpszCmdParam,int nCmdShow)

{

   HWND hWnd;

   MSG Message;

   WNDCLASS WndClass;

   g_hInst=hInstance;

  

   WndClass.cbClsExtra=0;

   WndClass.cbWndExtra=0;

   WndClass.hbrBackground=(HBRUSH)(COLOR_BTNFACE+1);

   WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);

   WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);

   WndClass.hInstance=hInstance;

   WndClass.lpfnWndProc=(WNDPROC)WndProc;

   WndClass.lpszClassName=lpszClass;

   WndClass.lpszMenuName=NULL;

   WndClass.style=CS_HREDRAW | CS_VREDRAW;

   RegisterClass(&WndClass);

 

   hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW | WS_CLIPSIBLINGS,

      CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,

      NULL,(HMENU)NULL,hInstance,NULL);

   ShowWindow(hWnd,nCmdShow);

  

   while(GetMessage(&Message,0,0,0)) {

      if (!IsWindow(hTabDlg) || !IsDialogMessage(hTabDlg,&Message)) {

          TranslateMessage(&Message);

          DispatchMessage(&Message);

      }

   }

   return (int)Message.wParam;

}

 

typedef struct { 

  WORD      dlgVer;

  WORD      signature;

  DWORD     helpID;

  DWORD     exStyle;

  DWORD     style;

  WORD      cDlgItems;

  short     x;

  short     y;

  short     cx;

  short     cy;

//  sz_Or_Ord menu;

//  sz_Or_Ord windowClass;

//  WCHAR     title[titleLen];

//  WORD     pointsize;

//  WORD     weight;

//  BYTE     italic;

//  BYTE     charset;

//  WCHAR    typeface[stringLen]; 

} DLGTEMPLATEEX;

 

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

{

   TCITEM tie;

   int Sel;

   HRSRC hRc;

   HGLOBAL hGlb;

   DLGTEMPLATEEX *dTem;

   RECT trt={0,0,0,0};

 

   switch(iMessage) {

   case WM_CREATE:

      hWndMain=hWnd;

      InitCommonControls();

 

      hTab=CreateWindow(WC_TABCONTROL,"",WS_CHILD | WS_VISIBLE

          | WS_CLIPSIBLINGS,

          0,0,0,0,hWnd,(HMENU)0,g_hInst,NULL);

 

      tie.mask=TCIF_TEXT;

      tie.pszText="one";

      TabCtrl_InsertItem(hTab,0,&tie);

      tie.pszText="two";

      TabCtrl_InsertItem(hTab,1,&tie);

 

      // 첫번째 대화상자의 크기 구함

      hRc=FindResource(NULL,MAKEINTRESOURCE(IDD_DIALOG1),RT_DIALOG);

      hGlb=LoadResource(NULL,hRc);

      dTem=(DLGTEMPLATEEX *)LockResource(hGlb);

      trt.right=dTem->cx;

      trt.bottom=dTem->cy;

 

      // 두번째 대화상자의 크기 구하고 값을 취함

      hRc=FindResource(NULL,MAKEINTRESOURCE(IDD_DIALOG2),RT_DIALOG);

      hGlb=LoadResource(NULL,hRc);

      dTem=(DLGTEMPLATEEX *)LockResource(hGlb);

      trt.right=max(trt.right,dTem->cx);

      trt.bottom=max(trt.bottom,dTem->cy);

 

      // 대화상자 단위를 픽셀 단위로 바꾼다.

      hTabDlg=CreateDialog(g_hInst,MAKEINTRESOURCE(IDD_DIALOG1),hWnd,Dlg1Proc);

      MapDialogRect(hTabDlg,&trt);

      DestroyWindow(hTabDlg);

 

      // 표시 영역을 윈도우 크기로 바꾼다.

      TabCtrl_AdjustRect(hTab,TRUE,&trt);

 

      // 윈도우 좌표를 원점으로 옮긴다.

      OffsetRect(&trt,-trt.left,-trt.top);

 

      // 계산된 크기만큼 컨트롤 크기 변경

      SetWindowPos(hTab,NULL,trt.left,trt.top,trt.right,trt.bottom,SWP_NOZORDER);

 

      hTabDlg=CreateDialog(g_hInst,MAKEINTRESOURCE(IDD_DIALOG1),hWnd,Dlg1Proc);

      return 0;

   case WM_NOTIFY:

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

      case TCN_SELCHANGE:

          if (hTabDlg) {

             DestroyWindow(hTabDlg);

          }

          Sel=TabCtrl_GetCurSel(hTab);

          if (Sel==0) {

             hTabDlg=CreateDialog(g_hInst,MAKEINTRESOURCE(IDD_DIALOG1),hWnd,Dlg1Proc);

          } else {

             hTabDlg=CreateDialog(g_hInst,MAKEINTRESOURCE(IDD_DIALOG2),hWnd,Dlg2Proc);

          }

          break;

      }

      return 0;

   case WM_DESTROY:

      PostQuitMessage(0);

      return 0;

   }

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

}

 

// 대화상자는 반드시 차일드 스타일이어야 하며 경계선을 가지지 않는다.

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

{

   RECT prt;

   switch(iMessage)

   {

   case WM_INITDIALOG:

      // 탭의 표시 영역 상단으로 이동하며 형제중에 제일 위로 올라간다.

      GetWindowRect(hTab,&prt);

      TabCtrl_AdjustRect(hTab,FALSE,&prt);

      ScreenToClient(hWndMain,(LPPOINT)&prt);

      SetWindowPos(hDlg,HWND_TOP,prt.left,prt.top,0,0,SWP_NOSIZE);

      SendDlgItemMessage(hDlg,IDC_COMBO1,CB_ADDSTRING,0,(LPARAM)"짜장면");

      SendDlgItemMessage(hDlg,IDC_COMBO1,CB_ADDSTRING,0,(LPARAM)"탕수육");

      SendDlgItemMessage(hDlg,IDC_COMBO1,CB_ADDSTRING,0,(LPARAM)"갈비탕");

      SendDlgItemMessage(hDlg,IDC_COMBO1,CB_ADDSTRING,0,(LPARAM)"육계장");

      SendDlgItemMessage(hDlg,IDC_COMBO1,CB_SETCURSEL,0,0);

      return TRUE;

   // EndDialog 호출할 필요가 없다.

   case WM_COMMAND:

      switch (LOWORD(wParam))  {

      case IDC_BUTTON1:

          MessageBox(hDlg,"버튼을 눌렀습니다","알림",MB_OK);

          return TRUE;

      }

      return FALSE;

   }

   return FALSE;

}

 

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

{

   RECT prt;

   switch(iMessage)

   {

   case WM_INITDIALOG:

      SetWindowPos(hDlg,HWND_TOP,100,100,0,0,SWP_NOSIZE);

      GetWindowRect(hTab,&prt);

      TabCtrl_AdjustRect(hTab,FALSE,&prt);

      ScreenToClient(hWndMain,(LPPOINT)&prt);

      SetWindowPos(hDlg,HWND_TOP,prt.left,prt.top,0,0,SWP_NOSIZE);

      SetWindowText(GetDlgItem(hDlg,IDC_EDIT1),"에디트 컨트롤이다");

      return TRUE;

   }

   return FALSE;

}

 

두 개의 전역 변수가 선언되어 있는데 hTab은 탭 컨트롤이고 hTabDlg는 현재 보이는 페이지의 대화상자 핸들이다. 탭 페이지를 만드는 모든 코드는 WM_CREATE에 있으므로 이 코드를 분석해 보도록 하자.

WM_CREATE에서 해야할 가장 중요한 일은 탭 컨트롤의 크기를 결정하는 것이다. 탭 컨트롤은 포함할 대화상자를 충분히 표시할 수 있는 충분한 표시 영역을 가져야 한다. 이때 표시 영역이란 탭 컨트롤에서 탭이 출력되는 부분과 경계선을 제외한 영역이며 탭 컨트롤 자체의 크기보다는 더 좁다. 표시 영역과 탭 컨트롤 윈도우의 크기는 다음 함수로 계산한다.

 

VOID TabCtrl_AdjustRect( HWND hwnd, BOOL fLarger, RECT FAR *prc);

 

fLarge가 TRUE이면 prc에 주어진 표시 영역으로부터 필요한 윈도우 크기를 구하고 fLarge가 FALSE이면 반대로 윈도우 크기로부터 표시 영역 크기를 구한다. 대화상자의 크기가 400*300이라면 이 크기에 탭이 출력될 높이 및 경계선의 두께를 더하여 윈도우 크기를 계산할 수 있으며 반대로 윈도우가 500*400일 때 표시 영역은 얼마나 되는지를 계산할 수 있다. 이 계산에는 시스템에 정의된 경계선의 두께, 탭 컨트롤에 지정된 폰트의 크기 등 많은 요소들이 참조될 것이다.

리소스에 정의되어 있는 대화상자의 크기를 구하기 위해서는 대화상자 템플리트를 직접 읽어 템플리트의 cx, cy를 조사해야 한다. 비주얼 C++ 6.0이상은 대화상자 템플리트를 DLGTEMPLATEEX 구조체로 작성하는데 이 구조체는 표준 헤더 파일에는 포함되어 있지 않으므로 필요할 때 직접 선언한 후 사용해야 한다. 이 예제에서는 cx, cy값만 알면 되므로 나머지 멤버는 주석 처리하였다. 이 멤버까지 포함하려면 여분의 헤더까지 포함시켜야 하므로 여러 모로 귀찮아진다.

템플리트에서 대화상자의 폭과 높이를 구한 후 이 중 가장 큰 값을 취하여 가장 큰 대화상자가 표시될 수 있도록 해야 한다. 대화상자의 크기 단위는 DLU로 되어 있으므로 MapDialogRect 함수로 픽셀값으로 바꾸어야 하며 대화상자의 실제 글꼴을 참조해야 하므로 일단 대화상자를 먼저 만든 후 계산하고 다시 파괴하였다. 이 계산 방법에 대해서는 13장에서 자세하게 다룬 바 있으므로 참조하기 바란다.

대화상자의 최대 크기를 픽셀값으로 조사했으면 이 대화상자가 들어갈만한 표시 영역을 가지는 탭 컨트롤 윈도우의 크기를 TabCtrl_AdjustRect 함수로 계산한다. 예를 들어 최대 대화상자의 크기가 250*200이었다면 이 대화상자를 표시할 수 있는 탭 컨트롤은 254*225 정도가 될 것이다. 이렇게 계산된 크기로 탭 컨트롤의 크기와 위치를 변경하였으며 첫번째 페이지의 대화상자를 CreateDialog 함수로 생성하였다. 여기까지 초기화가 완료되면 다음과 같은 모습으로 실행된다.

두 개의 페이지를 가지는 탭 컨트롤이 생성되어 있고 탭의 표시 영역에 첫번째 대화상자가 모델리스형으로 생성되어 있다. 마치 탭 컨트롤안에 대화상자가 포함되어 있는 것처럼 보이지만 사실 탭과 대화상자는 형제이지 부모 자식 관계가 아니며 둘 다 메인 윈도우의 통제를 받는다. 대화상자는 자신만의 대화상자 프로시저를 가지고 있어 버튼, 콤보 박스들로부터 전달되는 통지 메시지를 처리할 수 있다.

차일드로 생성된 대화상자이므로 어떤 일이 있더라도 EndDialog를 호출해서는 안된다. 메인 메시지 루프에는 IsDialogMessage가 포함되어 있어 차일드로 생성된 모델리스 대화상자의 메시지를 처리해 주므로 대화상자내에서 Tab키를 눌러 컨트롤간의 포커스를 이동할 수 있으며 디폴트 버튼이나 취소 버튼이 제대로 동작될 것이다.

다음은 탭의 선택이 변경될 때, 즉 one 페이지에서 two 페이지를 선택할 때 어떤 일이 일어나는지 보자. 이때 탭 컨트롤의 부모인 메인 윈도우로 TCS_SELCHANGE 통지 메시지가 전달되는데 이 메시지를 받았을 때 차일드로 생성되어 있는 모델리스 대화상자를 교체한다. 교체하는 방법은 아주 원론적인데 기존에 만들어져 있는 대화상자를 DestroyWindow 함수로 파괴하고 새로운 대화상자를 다시 생성하면 된다. 다음은 두번째 페이지가 활성화된 모습이다.

각 대화상자의 WM_INITDIALOG에서는 자신을 형제 중 제일 위쪽으로 옮겨주는 코드가 반드시 필요하다. 탭의 표시 영역에 대화상자가 나타나야 하는데 탭에 의해 대화상자가 가려져서는 안되기 때문이다. 여러번 강조하지만 탭과 대화상자는 형제 관계이며 운영체제는 형제간의 겹침에 대해서는 어떠한 처리도 하지 않으므로 자신이 직접 위로 올라오는 수밖에 없다.

탭 컨트롤의 one, two 탭을 눌러 보면 페이지가 빠른 속도로 교체될 것이다. 이 예제는 두 개의 페이지만 교체해 보았고 대화상자가 유용한 동작을 하지는 않지만 수십개의 페이지를 가진 탭 페이지를 만들더라도 방법은 동일하다. 이 예제는 사실 탭 컨트롤에 관련된 기술보다는 대화상자를 다루는 기술이 더 필요한 예제라 할 수 있다.

탭 컨트롤을 사용하면 이런 식으로 페이지를 겹쳐서 표시할 수 있다는 것을 보였는데 컨트롤의 지원이 미약하여 다소 실망스럽다고 느껴지지 않은가? 탭이 대화상자를 차일드로 가지고 탭 선택을 변경하면 직접 페이지를 교체해 준다면 좋을 것이다. 또한 차일드로 포함된 대화상자의 최대 크기를 자동으로 계산해 주고 활성화된 페이지를 위로 올려 주는 서비스를 해 준다면 무척 편할 것 같다.

여러분들이 이런 생각을 한다면 마이크로소프트도 당연히 그런 생각을 한다. 그래서 이럴 때 사용하라고 만들어 놓은 것이 바로 프로퍼티 시트(Property Sheet)이다. 프로퍼티 시트는 탭 컨트롤을 차일드로 가지며 대화상자를 자동으로 관리해 주는 여러 가지 서비스를 해 주는 공통 컨트롤의 일종이며 바로 앞절에서 자세하게 다룬 바 있다. 여러 개의 페이지를 가지는 대화상자가 필요하다면 프로퍼티 시트를 사용하는 것이 좋다. 다만 여러 개의 페이지를 가지는 대화상자 묶음이 여러 개 동시에 표시되어야 한다거나 탭 페이지 자체가 대화상자의 차일드가 되어야 할 때는 이 예제처럼 탭 컨트롤을 직접 다루어야 한다.