C++ 예제 (CMFCPropertyGridCtrl)

저는 C++로 GUI를 구현할 때 MFC(Microsoft Foundation Class)를 자주 사용합니다.

MFC는 MS가 windows API 함수들을 Wapper class형태로 구현해 놓은 것을 뜻합니다.

처음 프로그래밍을 배우던 시절 Visual Studio 6.0 을 사용하면서 습관화 되어 MFC가 익숙하지만 요즘은 Qt를 더 자주 사용하게 됩니다.

무료라는 점도 MFC를 더 자주 사용했던 이유중에 하나입니다.


입력값이 많은 프로그램 개발 시 문제


MFC를 사용하며 느끼는 불편함은 여러 가지지만, 특히 규모가 큰 프로젝트 진행 시 입,출력 값이 많다면 CEdit 컨트롤을 너무 많이 생성해야 하는 불편함이 존재합니다.

대화상자 편집기에서 직접 컨트롤을 배치하든, 소스코드에서 동적 할당해 사용하든 상관없이 말이죠.

[복잡한 입력값 처리 예시]

하나의 대화상자에 배치할 컨트롤이 너무 많아도 보기 싫고, 그렇다고 대화상자를 여러개 만들어도 사용하기가 불편합니다.

물론, DB or 파일에서 데이터를 읽어들여 사용하는 방법도 있겠으나, 가능하면 다양한 설정치를 소프트웨어에서 사용자가 쉽게 변경하고 변경치를 즉시 확인해가며 진행하는 방식 (예를 들면 시뮬레이션 분야 등) 에서는 답이 없습니다.

복잡한 UI 대안은?


이런 경우 MFC의 좋은 대안이 있습니다.

Visual Studio 2008 부터 (Feature Pack 설치 시) 지원되는 (이후 버전부터 기본적으로 지원) CMFCPropertyGridCtrl가 그 주인공입니다.

화면이 친숙한데, Visual Studio에 속성창으로 자주 사용되는 컨트롤 입니다.
 
[CMFCPropertyGridCtrl class]

 CEdit control 대비 장점을 살펴보자면,

1. CFrameWndEx 에 도킹 가능 (상속받은 CMainFrame도)

2. 여러 입력값의 이름과 값을 한줄에 편리하게 관리 (CStatic 이름 + CEdit 값 불편)

3. int, float, double, string, bool, color, font, file, path 등 다양한 입력지원

4. 속성이 많은 경우 수평, 수직 스크롤바 자동 지원

5. 입력값의 설명 삽입 가능

6. 나름 훌륭한 디자인


단점도 있습니다.

1. MS에서 제작된 클래스가 아니라 버그가 존재 (BCG Soft 제작)

2. 지속적인 업데이트 없음

3. 부실한 문서화


장, 단점에 대한 구체적인 설명은 아래 코드를 분석하며 살펴보겠습니다.

프로젝트 생성

1. VS 2017 MFC 프로젝트 생성



2. 응용프로그램 종류 : 단일 문서, 스타일 Visual studio



 3. 고급 기능 : 속성 도킹 창 체크



 4. 생성된 클래스 : CWinAppEx, CFrameWndEx  확인



소스코드


CMFCPropertyGridCtrl을 사용하기 위해 먼저 부모 역할(윈도우 창)을 하는 윈도우가 필요합니다.

위와 같이 프로젝트를 생성했다면 이미 만들어져 있습니다.

MainFrm.h

바로 CMainFrame에 선언된 CPropertiesWnd type 변수 m_wndProperties 입니다.
class CMainFrame : public CFrameWndEx
{
    
생략...

protected:  // 컨트롤 모음이 포함된 멤버입니다.   
    CPropertiesWnd    m_wndProperties;

생략...

};

UML 다이어그램으로 본 CMainFrame 입니다.

[UML, CMainFrame]

CPropertiesWnd 는 CDockablePane 에서 상속받은 클래스 입니다.

오늘의 주인공인 CMFCPropertyGridCtrl은 바로 이 도킹 가능한 창(CPropertiesWnd)에 생성되게 됩니다.

PropertiesWnd.h

8번 라인에 CMFCPropertyGridCtrl 타입 변수 m_wndPropList 가 존재합니다.
class CPropertiesWnd : public CDockablePane
{

생략...

protected:
    CFont m_fntPropList;    
    CMFCPropertyGridCtrl m_wndPropList;
    int m_id;

생략...
protected:
    LRESULT OnPropertyChanged(WPARAM wp, LPARAM lp);

    DECLARE_MESSAGE_MAP()

    void InitPropList();
    void SetPropListFont();
    void AddProperty(CMFCPropertyGridProperty& group, const CString& name, const COleVariant& value, LPCTSTR desc=NULL, const bool bSpin=false);
    void AddProperty(CMFCPropertyGridProperty& group, const CString& name, const COleVariant& value, const CStringArray& options, LPCTSTR desc = NULL);

};

UML 다이어그램으로 본 CPropertiesWnd 입니다.

멤버변수로 CMFCPropertyGridCtrl 타입 변수 m_wndPropList을 확인 할 수 있습니다.

[UML, CPropertiesWnd]

PropertiesWnd.cpp

윈도우 생성시 OnCreate() 함수에 의해 InitPropList() 함수가 호출됩니다.
void CPropertiesWnd::InitPropList()
{
    SetPropListFont();
    // 헤더 컨트롤 추가
    m_wndPropList.EnableHeaderCtrl(TRUE, _T("Name"), _T("Value"));
    // 아래쪽에 설명창 추가
    m_wndPropList.EnableDescriptionArea();
    // 닷넷 스타일 설정
    m_wndPropList.SetVSDotNetLook();
    // 초기값 변경시 굵게 표시
    m_wndPropList.MarkModifiedProperties();

    // 정수형 그룹 추가
    CMFCPropertyGridProperty* pGroup1 = new CMFCPropertyGridProperty(_T("정수"));    
    AddProperty(*pGroup1, _T("int"), (_variant_t)0l, _T("정수형"), false);
    AddProperty(*pGroup1, _T("int(spin)"), (_variant_t)10l, _T("정수형"), true);
    m_wndPropList.AddProperty(pGroup1);

    CMFCPropertyGridProperty* pSize = new CMFCPropertyGridProperty(_T("창 크기"), 0, TRUE);
    AddProperty(*pSize, _T("높이"), (_variant_t)250l, _T("창의 높이를 지정합니다."));
    AddProperty(*pSize, _T("너비"), (_variant_t)150l, _T("창의 너비를 지정합니다."));    
    m_wndPropList.AddProperty(pSize);

    // 실수형 그룹 추가
    CMFCPropertyGridProperty* pGroup2 = new CMFCPropertyGridProperty(_T("실수"));
    AddProperty(*pGroup2, _T("float"), (_variant_t)(float)0.0, _T("실수형"), false);
    AddProperty(*pGroup2, _T("double"), (_variant_t)(double)0.0, _T("실수형"), false);
    m_wndPropList.AddProperty(pGroup2);

    // 문자열, 부울 그룹 추가
    CMFCPropertyGridProperty* pGroup3 = new CMFCPropertyGridProperty(_T("문자열"));
    AddProperty(*pGroup3, _T("string"), (_variant_t)_T("글자"), _T("문자열"), false);
    AddProperty(*pGroup3, _T("bool"), (_variant_t)false, _T("문자열"), false);
    m_wndPropList.AddProperty(pGroup3);

    // 문자열, 부울 그룹 추가
    CMFCPropertyGridProperty* pGroup4 = new CMFCPropertyGridProperty(_T("그룹"));
    CStringArray str;
    str.Add(_T("Left"));
    str.Add(_T("Right"));
    str.Add(_T("Top"));
    str.Add(_T("Bottom"));
    AddProperty(*pGroup4, _T("Group"), (_variant_t)_T(""), str, _T("그룹"));    
    m_wndPropList.AddProperty(pGroup4);

    // 기타 폰트, 색상, 파일
    CMFCPropertyGridProperty* pGroup5 = new CMFCPropertyGridProperty(_T("기타"));

    // 폰트
    LOGFONT lf;
    CFont* font = CFont::FromHandle((HFONT)GetStockObject(DEFAULT_GUI_FONT));
    font->GetLogFont(&lf);
    _tcscpy_s(lf.lfFaceName, _T("맑은 고딕"));
    pGroup5->AddSubItem(new CMFCPropertyGridFontProperty(_T("글꼴"), lf, CF_EFFECTS | CF_SCREENFONTS, _T("창의 기본 글꼴을 지정합니다."), m_id++));

    // 색상
    CMFCPropertyGridColorProperty* pColorProp = new CMFCPropertyGridColorProperty(_T("창 색상"), RGB(210, 192, 254), nullptr, _T("창의 기본 색상을 지정합니다."), m_id++);
    pColorProp->EnableOtherButton(_T("기타..."));
    pColorProp->EnableAutomaticButton(_T("기본값"), ::GetSysColor(COLOR_3DFACE));
    pGroup5->AddSubItem(pColorProp);

    // 파일
    static const TCHAR szFilter[] = _T("음악 파일(*.mp3)|*.mp3|모든 파일(*.*)|*.*||");
    pGroup5->AddSubItem(new CMFCPropertyGridFileProperty(_T("음악파일"), TRUE, _T(""), _T("mp3"), 0, szFilter, _T("소리파일을 지정합니다."), m_id++));
    pGroup5->AddSubItem(new CMFCPropertyGridFileProperty(_T("경로"), _T("c:\\"), m_id++));
    
    m_wndPropList.AddProperty(pGroup5);
}

바로 이 함수에서 아래 창을 초기화하여 보여주게 됩니다.



사용 방법은

1. CMFCPropertyGridProperty 동적 생성
CMFCPropertyGridProperty* pGroup1 = new CMFCPropertyGridProperty(_T("정수"));

2. delete는 m_wndPropList 소멸자에서 자동 호출. (delete 필요없음)

3. 사용자 정의 함수 AddProperty() 에서 해당 속성을 생성하고, pGroup에 추가
AddProperty(*pGroup1, _T("int"), (_variant_t)0l, _T("정수형"), false);
AddProperty(*pGroup1, _T("int(spin)"), (_variant_t)10l, _T("정수형"), true);

4. AddProperty() 함수는 2개의 오버로딩 형태로 구현

그룹 생성후, 하나의 그룹에 여러 타입 속성(int, float 등)을 함께 추가하면 오류가 발생하는 버그가 존재합니다.

  • group : CMFCPropertyGridProperty 타입 변수
  • name : 이름 (라벨)
  • value : 값
  • desc : 해당 설명
  • bSpin : 스핀 컨트롤 여부 
첫번째 AddProperty() 함수 (입력값이 1개인 경우 사용)
void CPropertiesWnd::AddProperty(CMFCPropertyGridProperty& group,
    const CString& name,
    const COleVariant& value,
    LPCTSTR desc,
    const bool bSpin)
{
    CMFCPropertyGridProperty *p = new CMFCPropertyGridProperty(name, value, desc, m_id);


    switch (value.vt)
    {
    case VT_INT:
    case VT_I2:
    case VT_I4:
        p->EnableSpinControl(bSpin, -100, 100);
        break;
    case VT_R4:        
        break;
    case VT_R8:        
        break;
    case VT_BSTR:        
        break;
    case VT_BOOL:
        break;
    }
    
    //p->AllowEdit(true);    
    //p->Enable(true);    
    group.AddSubItem(p);
    m_id++;
}
두번째 AddProperty() 함수 (입력값 다수의 경우, 콤보박스 형태)
CStringArray로 여러가지 값 중 선택 입력 가능
void CPropertiesWnd::AddProperty(CMFCPropertyGridProperty & group,
    const CString & name,
    const COleVariant & value,
    const CStringArray & options,
    LPCTSTR desc)
{
    CMFCPropertyGridProperty *p = new CMFCPropertyGridProperty(name, value, desc, m_id);

    for (int i = 0; i < options.GetSize(); i++)    
        p->AddOption(options[i]);        
    
    p->SetValue((_variant_t)options[0]);
    
    group.AddSubItem(p);
    m_id++;
    
}

4. CMFCPropertyGridCtr에 추가
m_wndPropList.AddProperty(pGroup1);

5. 위 행동을 반복해 필요한 만큼 CMFCPropertyGridCtr에 추가

값 변경시 이벤트 처리 방법

CMFCPropertyGridCtrl 의 값 변경시 처리하는 방법입니다.

PropertiesWnd.h

OnPropertyChanged() 함수 선언
LRESULT OnPropertyChanged(WPARAM wp, LPARAM lp);

PropertiesWnd.cpp

BEGIN_MESSAGE_MAP 에 추가
BEGIN_MESSAGE_MAP(CPropertiesWnd, CDockablePane)
    생략...
    ON_REGISTERED_MESSAGE(AFX_WM_PROPERTY_CHANGED, &CPropertiesWnd::OnPropertyChanged)
END_MESSAGE_MAP()

OnPropertyChanged() 함수 정의
LRESULT CPropertiesWnd::OnPropertyChanged(WPARAM wp, LPARAM lp)
{
    // wp : CMFCPropertyGridCtrl ID
    // lp : pointer of CMFCPropertyGridProperty

    CMFCPropertyGridProperty* pProp = NULL;
    pProp = (CMFCPropertyGridProperty*)lp;

    if (!pProp)
        return 0L;

    _variant_t var = pProp->GetValue();

    int id = pProp->GetData();

    CString str;
    str = pProp->GetName();

    
    switch (var.vt)
    {
    case VT_UINT:
        str.Format(_T("Name(ID:%d):%s, Value:%d"), id, str, var.uintVal);
    case VT_INT:
        str.Format(_T("Name(ID:%d):%s, Value:%d"), id, str, var.intVal);
    case VT_I2:
        str.Format(_T("Name(ID:%d):%s, Value:%d"), id, str, var.iVal);
    case VT_I4:
        str.Format(_T("Name(ID:%d):%s, Value:%d"), id, str, var.lVal);
        break;
    case VT_R4:
        str.Format(_T("Name(ID:%d):%s, Value:%f"), id, str, var.fltVal);
        break;
    case VT_R8:
        str.Format(_T("Name(ID:%d):%s, Value:%f"), id, str, var.dblVal);
        break;
    case VT_BSTR:
        str.Format(_T("Name(ID:%d):%s, Value:%s"), id, str, var.bstrVal);
        break;
    case VT_BOOL:
        str.Format(_T("Name(ID:%d):%s, Value:%d"), id, str, var.boolVal);
        break;
    }

    AfxMessageBox(str);

    return 0L;    
}
컨트롤의 값을 변경하면 OnPropertyChanged() 함수가 여러번 호출되는 버그가 존재합니다. 

특히, CMFCPropertyGridFontProperty (폰트), CMFCPropertyGridColorProperty (색상), CMFCPropertyGridFileProperty (파일 경로) 이 3가지 클래스에서 주로 발생됩니다.


이상으로 모든 설명을 마칩니다.

개발환경

  • VS 2017, Windows 10 Pro 64bit

 소스코드

댓글

  1. 쉽게 정리해주셔서 감사합니다. 한 번 써봐야겠습니다.

    답글삭제
    답글
    1. 네 아주 쓸모가 많은 클래스입니다. ^^

      삭제
  2. 버그들을 해결하는 방법이 따로 있나요?
    아니면, 프로그램이 죽을 정도로 치명적이지 않아서 그러려니하고 사용해야 하는지요?

    답글삭제
    답글
    1. 기본적으로 버그는 제품의 결함이므로 없어야 합니다. 상업적인 목적의 코드라면 절대 있어서는 안되겠지요.

      자동차가 가다가 시동이 꺼지는 것과 같다고 생각합니다.

      버그를 줄이는 방법은 개인적으로 긴 함수나 클래스는 잘게 나누고 설계 단계부터 버그가 없는 구조로 진행하려 노력합니다.

      저 또한 많은 버그를 접하며 코드를 만들어 가지만 한땀 한땀 공들여 만들다 보면 다음 프로젝트에는 좀 더 발전된 모습을 스스로 느낍니다.

      이는 단시간에 배우는 테크닉이 아닌 장기간의 경험과 지식이 어우러진 공학의 영역이라 생각합니다.

      너무 조급하게 생각하지 마세요. 저 또한 같은 과정을 거쳐 왔습니다.

      감사합니다.

      삭제
    2. 버그를 찾고 고치는 것은 말씀대로 인고의 시간인 것 같습니다.

      CMFCPropertyGridProperty 컨트롤을 한 번 사용해 볼까하는데, 버그가 있는 컨트롤이라고 하셔서 망설여집니다. 이 컨트롤을 사용함에 있어서 내재되어 있는 버그를 고칠려면 CMFCPropertyGridProperty 원본 소스를 고쳐야 할 텐데, 이게 가능한지요?

      삭제
    3. 제가 사용하며서 본 버그는 해당 컨트롤의 값 변경시 OnPropertyChanged() 이벤트가 두세번 들어오는 버그입니다.

      그것도 폰트나, 색, 파일 부분이었고 int, double 등의 값처리에는 문제가 없었습니다.

      런타임 오류는 없으므로, 이벤트 처리를 직접 제어하면 문제는 없습니다.

      삭제
  3. 잘 정리 해주셨네요. 좋은 글 감사합니다.~

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

C++ 예제 (소켓 서버, 이미지, 파일전송)