5.대화상자

.대화상자

WTL에서 대화상자를 프로그래밍하는 방법은 다소 독특하다. 기본적인 개념은 MFC와 비슷하지만 개발툴의 지원이 없으므로 약간의 수작업이 들어 가야 한다. 간단한 예제를 만들어 보자.

 

: WTLDialog

대화상자는 껍데기를 정의하는 템플릿과 동작을 정의하는 대화상자 프로시저로 구성된다. 먼저 모양을 정의하기 위해 리소스 뷰에서 빈 대화상자를 추가한다. 확인, 취소 버튼만을 가지는 썰렁한 대화상자가 만들어질 것이다. 더 많은 컨트롤들을 배치할 수 있지만 실습중이므로 디폴트대로 만들기로 하자.

다음은 이 대화상자의 동작을 정의하는 클래스를 만들어야 한다. MFC에서는 더블클릭만 하면 클래스 마법사가 나타나 대화상자 클래스를 척척 만들어준다. WTL에서도 마찬가지로 더블클릭하면 마법사가 나타나지만 이 마법사는 MFC용 마법사이며 생성하는 코드도 MFC 코드이다. WTL 프로젝트에 MFC 코드를 생성해 넣었으니 당연히 컴파일 안된다. 아무 생각없이 더블클릭했다가는 귀찮아지므로 주의하도록 하자.

결론적으로, WTL에서는 대화상자 클래스를 만드는 마법사 기능이 제공되지 않는다. 따라서 개발자가 몸으로 떼우는 수밖에 없다. 다행히 비슷한 코드가 AboutDlg.h, cpp에 있으므로 Ctrl+C, Ctrl+V 신공으로 가져와 조금 뜯어 고치면 된다. MyDlg.h, MyDlg.cpp 파일을 추가하고 AboutDlg.h 파일의 내용을 그대로 복사해서 MyDlg.h에 붙인 후 명칭을 수정한다.

 

class CMyDlg : public CDialogImpl<CMyDlg>

{

public:

     enum { IDD = IDD_DIALOG1 };

 

     BEGIN_MSG_MAP(CMyDlg)

 

CAboutDlg라는 클래스 이름을 CMyDlg로 바꾸고 리소스 ID는 IDD_DIALOG1로 바꾼다. 구현 파일의 멤버 함수 소속도 CMyDlg로 바꾼다. 인클루드하는 헤더 파일 이름도 바꿔야 한다.

 

#include "MyDlg.h"

 

LRESULT CMyDlg::OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

....

 

이런 작업 절차는 따로 설명하지 않더라도 많이 해 봐서 잘 할 것이다. 뷰의 WM_LBUTTONDOWN 함수에서 이 대화상자를 호출한다. 대화상자 객체를 선언하고 DoModal만 호출하면 된다.

 

#include "MyDlg.h"

LRESULT CWTLDialogView::OnLButtonDown(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     CMyDlg dlg;

 

     dlg.DoModal();

 

     return 0;

}

 

대화상자가 워낙 썰렁해서 확인, 취소 버튼을 누르는 것 외에는 별로 할 게 없다. 대화상자의 목적에 맞게 대화상자 안에 컨트롤을 배치하고 컨트롤의 명령 핸들러를 작성해야 한다.

마법사의 지원이 없다 보니 대화상자 클래스를 정의하는 절차가 조금 원시적일 뿐이지 개념은 MFC와 똑같다.

.대화상자 프로젝트

프로젝트에 대화상자를 추가하는 것은 어렵지만 대화상자를 메인 윈도우로 가지는 프로젝트를 생성하는 것은 아주 쉽다. 마법사가 대화상자 기반의 프로젝트를 지원하므로 마법사를 부려 먹기만 하면 된다. 대화상자 자체가 메인 윈도우가 될 때는 대화상자 기반으로 프로젝트를 생성한다.

 

: DlgBase

응용 프로그램 형태 옵션에서 Dialog Based를 선택하고 바로 아래쪽의 Modal Dialog도 같이 선택한다.

모달 대화상자로 만들지 않으면 모델리스가 된다. 대화상자가 메인인데 모달이나 모델리스나 별 차이가 없어 보이며 사실상 차이가 거의 없다. 특별한 이유가 없다면 모달로 만드는 것이 더 편리하며 코드도 간단하다. 그럼에도 불구하고 왜 모델리스로 만드는 것을 지원하는가 하면 WTL의 구조상 모달로 만들어 버리면 자동화된 UIUpdate가 안되기 때문이다.

UI 갱신은 아이들 타임에 수행되는데 모달로 실행하면 운영체제가 모든 것을 처리해 버리므로 아이들 타임을 얻을 방법이 없다. UI 갱신을 할 필요가 없으면 모달로 만들고 그렇지 않다면 모델리스로 만들어야 한다. 그렇다면 MFC의 경우는 어떨까? MFC도 마찬가지 문제가 있는데 그래서 MFC는 모든 대화상자를 모델리스로 만들고 부모를 얼려 버림으로써 모달인 척 흉내를 내는 것이다. 즉 MFC는 아예 모달 대화상자라는 것을 지원하지도 않는다. 여러분들은 지금까지 속고 산 것이다.

마법사가 프로젝트를 어떻게 만들어 놓았는지 소스를 들여다 보자. _tWinMain의 소스는 다음과 같이 간단해진다. 일부 필요한 초기화를 한 후 대화상자 객체를 생성하여 DoModal만 호출하면 그만이다. 대화상자가 메인이므로 대화상자만 띄우면 응용 프로그램은 더 할 일이 없다.

 

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR /*lpstrCmdLine*/, int /*nCmdShow*/)

{

     hRes = _Module.Init(NULL, hInstance);

 

     int nRet = 0;

     // BLOCK: Run application

     {

          CMainDlg dlgMain;

          nRet = dlgMain.DoModal();

     }

 

     _Module.Term();

     return nRet;

}

 

실행하면 대화상자가 바로 나타날 것이다. 이 대화상자가 메인이며 이 안에서 하고자하는 일을 하면 된다. 대화상자에 버튼 하나와 에디트 하나를 배치해 보자.

두 컨트롤의 ID는 모두 디폴트대로 사용한다. 버튼의 Click 이벤트 핸들러를 다음과 같이 작성한다. 메뉴의 핸들러를 만들 때와 마찬가지로 속성창의 이벤트 페이지에서 핸들러를 만들면 된다.

메시지 맵에 엔트리가 추가되고 함수 원형도 선언된다. 구현 파일에 만들어진 본체에 다음 코드를 작성한다.

 

#include "atlmisc.h"

LRESULT CMainDlg::OnBnClickedButton1(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)

{

     CEdit Edit=GetDlgItem(IDC_EDIT1);

     TCHAR buf[128];

 

     Edit.GetWindowText(buf,128);

     SetWindowText(buf);

 

     return 0;

}

 

CEdit의 객체를 선언하고 GetDlgItem으로 읽어서 대입해 버리면 에디트 객체가 된다. 객체에 윈도우 핸들을 대입하면 Attach 함수를 호출하도록 재정의되어 있으며 Attack 함수는 이 핸들을 래핑한다. 또는 생성자로 핸들을 넘겨도 된다.

 

CEdit Edit(GetDlgItem(IDC_EDIT1));

 

에디트에 입력된 내용을 읽어 대화상자의 타이틀 바로 출력해 보았다.

객체가 파괴되어도 핸들은 파괴되지 않으므로 Detach를 굳이 호출할 필요는 없다. 핸들을 래핑하고 있는 지역 객체만 사라질 뿐이다.

.DDX/DDV

컨트롤에 입력된 값을 읽거나 쓰는 DDX 기능도 지원하며 값의 유효한 범위를 점검하는 DDV 기능도 제공된다. MFC와 개념은 동일하지만 개발툴의 지원이 없으므로 모든 코드를 수작업으로 작성해야 한다. 앞 예제의 에디트 박스에 입력된 텍스트를 DDX 기능으로 읽어 보자. CMainDlg에 다음 코드를 추가한다.

 

#include <atlmisc.h>

#include <atlddx.h>

class CMainDlg : public CDialogImpl<CMainDlg>, public CWinDataExchange<CMainDlg>

{

public:

     enum { IDD = IDD_MAINDLG };

 

     BEGIN_DDX_MAP(CMainDlg)

          DDX_TEXT(IDC_EDIT1,m_Text)

     END_DDX_MAP()

 

     CString m_Text;

 

CString을 선언하기 위해 atlmisc.h를 인클루드하고 DDX 기능을 사용하기 위해 atlddx.h를 인클루드한다. 주의할 것은 두 헤더 파일의 순서가 바뀌면 안된다는 점이다. atlddx.h에서 CString 타입을 참조하므로 CString이 먼저 정의되어야 한다. 아니면 atlbase.h를 인클루드하기 전에 _WTL_USE_CSTRING을 정의하여 CString을 전방 선언해야 한다. 어떤 순서로 인클루드를 하든 알아서 조정을 하거나 아니면 필요한 파일을 자동으로 인클루드하든가 해야지 참 구질 구질하다.

다음은 CMainDlg 선언문의 상속 리스트에 CWinDataExchange 클래스를 적는다. 이 클래스가 DDX를 제공하는 믹스인이므로 상속을 받아야 한다. 다음은 DDX 맵을 작성한다. 형식은 MFC의 유사하되 손으로 직접 작성해야 하므로 무척 번거롭다. BEGIN_DDX_MAP과 END_DDX_MAP 사이에 컨트롤과 변수의 짝을 짓는 다음 매크로들을 작성한다. 다행히 매크로 이름은 MFC와 거의 같다.

 

매크로

설명

DDX_TEXT

에디트 박스를 문자열과 연결한다. CString, BSTR 또는 정적으로 할당된 배열 등을 연결할 수 있다.

DDX_INT

에디트 박스를 정수(int)와 연결한다.

DDX_UINT

에디트 박스를 부호없는 정수(uint)와 연결한다.

DDX_FLOAT

에디트 박스를 실수(float, dluble)와 연결한다.

DDX_CHECK

체크 박스를 int나 bool 변수와 연결한다.

DDX_RADIO

라디오 그룹을 int와 연결한다.

DDX_CONTROL

대화상자내의 차일드 컨트롤을 객체와 연결한다.

DDX_TEXT_LEN

텍스트를 연결하되 길이에 제한을 둔다.

DDX_INT_RANGE

정수를 연결하되 값의 범위에 제한을 둔다.

 

이 예제에서는 에디트에 입력한 문자열을 읽고자 하므로 CString 타입의 m_Text 멤버를 선언한 후 IDC_EDIT1과 연결했다. 값 교환이 필요할 때는 다음 함수를 호출한다. MFC의 UpdateData 함수와 기능상 동일한 함수이다.

 

BOOL DoDataExchange(BOOL bSaveAndValidate = FALSE, UINT nCtlID = -1)

 

첫 번째 인수가 TRUE이면 컨트롤의 값을 멤버 변수로 읽어오고 FALSE이면 반대로 멤버 변수의 값을 컨트롤로 보낸다. 두 번째 인수는 값 교환 대상 컨트롤의 ID이되 -1을 지정하면 DDX 맵에 있는 모든 컨트롤이 대상이 된다. MFC에 비해 특정 컨트롤 하나에 대해서만 값을 교환할 수 있다는 점이 다르다. OnInitDialog의 끝에 m_Text를 초기화하고 이 값을 에디트로 대입한다.

 

LRESULT CMainDlg::OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     m_Text = _T("Sample");

     DoDataExchange();

 

     return TRUE;

}

 

이렇게 초기화하면 실행 직후에 에디트에 Sample이라는 문자열이 입력되어 있을 것이다. 버튼을 누를 때는 반대로 에디트의 값을 다시 읽어 보자.

 

LRESULT CMainDlg::OnBnClickedButton1(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)

{

     DoDataExchange(true);

     SetWindowText(m_Text);

     return 0;

}

 

코드는 짧아졌지만 동작은 동일하다. 다음은 DDV 기능을 작성해 보자. MFC는 DDX와 DDV가 별개의 매크로로 정의되지만 WTL에서는 DDX에 인수를 추가하는 형식으로 되어 있다. DDX 맵을 다음과 같이 수정한다.

 

BEGIN_DDX_MAP(CMainDlg)

     DDX_TEXT_LEN(IDC_EDIT1,m_Text,10)

END_DDX_MAP()

 

글자 수를 최대 10개만 입력받도록 했다. 10자를 넘어면 띵띵거리며 에러임을 알린다. 그러나 그 뿐이다. 메시지 박스를 연다거나 별다른 동작을 하지 않는다. 디폴트 동작을 수정하려면 OnDataValidateError 가상 함수를 재정의하여 원하는 동작을 하도록 코드를 작성해야 한다. CMainDlg에 다음 멤버 함수를 추가한다.

 

void OnDataValidateError(UINT nID, BOOL bSave, _XData data)

{

     CString Mes;

     Mes.Format(_T("%d 글자는 너무 길다. %d 이하로 "),

          data.textData.nLength,data.textData.nMaxLength);

     MessageBox(Mes,_T("알림"));

}

 

이 함수의 인수로 전달되는 _XData는 에러의 내용을 가지는 공용체이다. atlddx.h에서 그 정의를 볼 수 있다.

 

enum _XDataType

{

     ddxDataNull = 0,

     ddxDataText = 1,

     ddxDataInt = 2,

     ddxDataFloat = 3,

     ddxDataDouble = 4

};

 

struct _XTextData

{

     int nLength;

     int nMaxLength;

};

 

struct _XIntData

{

     long nVal;

     long nMin;

     long nMax;

};

 

struct _XFloatData

{

     double nVal;

     double nMin;

     double nMax;

};

 

struct _XData

{

     _XDataType nDataType;

     union

     {

          _XTextData textData;

          _XIntData intData;

          _XFloatData floatData;

     };

};

 

텍스트의 경우는 문자열 길이로, 정수나 실수는 값의 범위를 점검하므로 공용체로 정보를 넘긴다. 이 예제의 경우 길이에 대한 정보만 보면 되므로 textData의 멤버를 읽었다. 10자를 넘으면 다음 에러 메시지가 나타난다.

에러 메시지만 출력하고 거부는 하지 않는데 만약 강제로 입력 문자수를 제한하려면 에디트로 EM_LIMITTEXT를 전달하는 다른 방법을 사용해야 한다. 에러 메시지가 반말로 되어 있어 좀 기분 나쁠지 모르겠지만 그건 예제 만드는 놈 마음이다.

.컨트롤

WTL도 운영체제가 제공하는 모든 컨트롤을 클래스로 래핑하여 제공한다. MFC와 거의 유사한데 제공하는 컨트롤 종류에 큰 차이가 없고 이름까지도 같다. 컨트롤은 통상 대화상자 내에서 사용되는데 앞 예제에서 본 것처럼 GetDlgItem으로 컨트롤의 객체를 구한 후 사용하거나 아니면 좀 더 고급 방법으로는 DDX_CONTROL 매크로로 연결해 사용하면 된다.

어떤 방법으로든 컨트롤을 래핑한 객체를 얻었으면 이후부터 이 객체의 멤버 함수를 호출하여 컨트롤을 프로그래밍할 수 있다. 차일드 컨트롤이 보내는 통지 메시지 핸들러는 이벤트창에서 만들면 되고 컨트롤의 상태 관리는 UIUpdate 기능을 사용하면 될 것이다.

대화상자 내에서의 컨트롤 프로그래밍 방법은 특별히 더 설명할 게 없으므로 여기서는 뷰에서 런타임에 컨트롤을 만드는 방법에 대해 알아 보자. 뷰의 차일드로 리스트 박스를 생성하고 문자열 항목 몇개를 등록한다. 그리고 항목 선택이 바뀔 때의 통지 메시지도 처리해 볼 것이다.

 

: ListBox

문자열을 관리하려면 atlmisc.h 헤더 파일이 필요하다. 지금까지는 필요할 때 바로 인클루드하는 방식을 사용했으나 이는 예제이기 때문에 편의상 그런 것이고 정석대로 하자면 stdafx.h에 인클루드해야 한다.

 

#include <atlbase.h>

#include <atlapp.h>

#include <atlmisc.h>

 

뷰의 헤더 파일에 CListBox 타입의 멤버를 선언한다.

 

class CListBoxView : public CWindowImpl<CListBoxView>

{

public:

     DECLARE_WND_CLASS(NULL)

     CListBox m_List;

 

MFC에서와 마찬가지로 CListBox 자체가 리스트 박스인 것은 아니고 다만 리스트 박스를 래핑할 수 있는 객체일 뿐이다. 리스트 박스 컨트롤은 실행중에 생성해야 한다. 뷰의 OnCreate에서 리스트 박스를 생성한다. 그리고 리스트 박스에 문자열 항목을 추가한다.

 

LRESULT CListBoxView::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     CRect rt;

     rt.SetRect(10,10,200,200);

     m_List.Create(m_hWnd,rt,_T(""),WS_CHILD | WS_VISIBLE | WS_BORDER | LBS_NOTIFY,

          0,1234);

     m_List.AddString(_T("대한민국"));

     m_List.AddString(_T("북한"));

     m_List.AddString(_T("아르헨티나"));

     m_List.AddString(_T("스페인"));

     m_List.AddString(_T("남아프리카 공화국"));

 

     return 0;

}

 

Create 멤버 함수를 리스트 박스 컨트롤을 생성한다. 통지 메시지를 처리하려면 적당한 ID를 주어야 하는데 편의상 1234라는 상수 ID를 지정했다. 좀 더 형식성을 따진다면 #define으로 매크로 상수를 정의하는 것이 원칙적이다. 문자열 항목을 추가할 때는 AddString 멤버 함수를 호출한다. 말 안해도 뻔히 알겠지만 이 멤버 함수는 LB_ADDSTRING 메시지를 보낸다.

여기까지 작성한 후 실행하면 뷰의 차일드로 리스트 박스가 생성될 것이다. 그러나 통지 메시지를 처리하지 않기 때문에 선택을 바꾸어도 아무 반응이 없다. 선택된 항목의 문자열을 읽어 메인 윈도우의 타이틀 바에 출력해 보자. 메시지 맵에 엔트리를 추가하여 1234번으로부터 전달되는 통지 메시지는 OnListBox가 처리하도록 한다.

 

BEGIN_MSG_MAP(CListBoxView)

     MESSAGE_HANDLER(WM_PAINT, OnPaint)

     MESSAGE_HANDLER(WM_CREATE, OnCreate)

     COMMAND_ID_HANDLER(1234,OnListBox)

END_MSG_MAP()

 

대화상자에서는 속성창을 사용하면 되지만 런타임에 직접 만든 컨트롤이다 보니 속성창을 통해 핸들러를 추가할 수 없다. 헤더 파일에 이 함수의 원형을 선언한다. 원형은 바로 위에 주석으로 CommandHandler라는 이름의 함수가 선언되어 있으므로 이 함수 선언문을 복사한 후 이름만 바꾸면 된다. 구현 파일에 OnListBox의 본체를 작성한다.

 

LRESULT CListBoxView::OnListBox(WORD wNotifyCode, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)

{

     int idx;

     CString szItem;

     if (wNotifyCode == LBN_SELCHANGE) {

          idx=m_List.GetCurSel();

          m_List.GetText(idx,szItem);

          GetParent().SetWindowText(szItem);

     }

     return 0;

}

 

선택 항목의 인덱스를 구하고 텍스트를 읽어 메인 윈도우의 타이틀 바로 출력했다. 주의할 것은 뷰가 메인 윈도우가 아니라는 점이다. 리스트 박스는 뷰의 차일드로 생성되었으므로 메인 윈도우는 리스트 박스의 할아버지인 셈이다. 현재 이 코드가 작성되는 곳이 뷰의 멤버 함수이므로 메인 프레임을 구하기 위해 GetParent()를 호출해야 한다.

가장 전형적인 컨트롤이라 할 수 있는 리스트 박스에 대한 예제만 만들어 보았다. CButton, CComboBox, CCheckBox, CScrollBar 등의 표준 컨트롤들과 CListViewCtrl, CToolTipCtrl, CImageList 등의 공통 컨트롤 들이 제공된다. 이외에 MFC가 제공하지 않는 CLinkCtrl 같은 커스텀 컨트롤도 있고 CBitmapButton처럼 MFC와는 조금 다르게 동작하는 컨트롤도 있다.

사용하는 방법은 MFC에 준하므로 따로 장황하게 설명하지 않기로 한다. 워낙 수가 많다 보니 컨트롤 하나 하나에 대해 프로그래밍 하는 방법과 활용법을 일일이 소개하려면 책 한권을 써도 부족할 지경이다. 컨트롤의 일반적인 개념을 안다면 문서 또는 헤더 파일을 통해 얼마든지 사용할 수 있을 것이다.