대화 상자나 그와 유사한 계열의 윈도우(CFormView)에서는 대화 상자 그 자체나 대화 상자에 포함된 각 컨트롤 윈도우가 출력될 필요가 있을 때, WM_CTLCOLOR이라는 메시지가 전달됩니다. 따라서 이 메시지를 중간에서 가로채어서 자신이 원하는 색상으로 변경하면 된다.

WM_CTLCOLOR 메시지는 표준 메시지 이므로 class wizard에 등록되어있기 때문에 쉽게 메시지 처리기를 등록할 수 있다. 다음의 코드는 class wizard를 이용하여 메시지를 등록한 후에 코드의 변경상태를 보여준다.



// 이 예제는 대화 상자와 CFormView 계열의 윈도우에서 동일하게 적용되며
// 여기에서는 대화 상자를 기준으로 설명한다.

/////////////////// Header File… ////////////////////////////

class CMyDialog : public CDialog
{
// 생략..

// Generated message map functions
protected:
//{{AFX_MSG(CMyDialog)
// 생략.. 기존의 다른 메시지 처리기가 존재..
afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};

/////////////////// Source File… ////////////////////////////

// 생략..

BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
//{{AFX_MSG_MAP(CMyDialog)
// 생략.. 다른 메시지 처리기에 대한 매크로가 존재…
ON_WM_CTLCOLOR ()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()

// 생략..

HBRUSH CMyDialog::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
       HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);

       // TODO: Change any attributes of the DC here

       // TODO: Return a different brush if the default is not desired
       return hbr;
}



위 예제에서 OnCtlColor 하는 메시지 처리기에 자신이 변경하고자 하는 대상을 찾아 속성을 변경하면 된다. 이 메시지 처리기는 일반적으로 사용하는 메시지 처리기보다 좀 복잡함으로 이 처리기 자체에 설명부터 먼저 하겠다.
이 함수의 반환 값은 해당 컨트롤의 배경을 채울 Brush의 색상이다. 여기서 주의 해야 할 것은 앞에서도 설명했지만 Brush 객체의 생명주기(Life Time)이다. 즉, OnCtlColor 내부에서 사용할 Brush 객체의 핸들은 전역적인 성격을 가지고 있어야 한다. 절대로 지역적인 속성을 사용하면 안 된다. 왜냐하면 지역적인 속성을 가진 객체는 OnCtlColor 함수가 종료됨과 동시에 삭제되어 버리기 때문에 이 함수가 반환한 Brush 핸들은 실제로 존재하지 않는 핸들이 되기 때문이다. 만약 지역 변수로 CBrush 객체를 선언하여 사용하고 싶다면 반드시 Stock Object을 이용하거나 C++ Wrapper 클래스(22강좌)를 이용해야 한다. Stock Object는 시스템에서 제공하기 때문에 Life Time이 전역적인 성격을 가지고 있고 C++ Wrapper 클래스는 실제로 전역적으로 선언된 Brush 핸들을 사용하고 있기 때문에 문제가 되지 않는다.

새로운 Brush 속성으로 컨트롤의 배경을 칠하고 싶다면 먼저 CBrush 객체를 해당 컨트롤을 포함하고 있는 윈도우 클래스의 데이터 멤버로 선언하고 OnInitalDialog(WM_INITALDIALOG 메시지 처리기)에서 선언된 CBrush 객체에 사용하고 싶은 형태로 Brush를 생성한 후, OnCtlColor에서 사용해야 한다.


 
 
// 이 예제는 대화 상자와 CFormView 계열의 윈도우에서 동일하게 적용되며
// 여기에서는 대화 상자를 기준으로 설명한다.
/////////////////// Header File… ////////////////////////////

class CMyDialog : public CDialog
{
private :
CBrush m_my_brush; // 대화 상자의 배경색으로 사용할 CBrush 객체
// 생략..

// Generated message map functions
protected:
//{{AFX_MSG(CMyDialog)
// 생략.. 기존의 다른 메시지 처리기가 존재..
virtual BOOL OnInitDialog();
afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};

/////////////////// Source File… ////////////////////////////

// 생략..

BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
//{{AFX_MSG_MAP(CMyDialog)
// 생략.. 다른 메시지 처리기에 대한 매크로가 존재…
ON_WM_CTLCOLOR ()
ON_WM_DESTROY()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()

BOOL TestBkColorDlg::OnInitDialog()
{
       CDialog::OnInitDialog();

       m_my_brush.CreateSolidBrush(RGB(0,0,255)); // Brush 속성을 생성한다.
       return TRUE; // return TRUE unless you set the focus to a control
       // EXCEPTION: OCX Property Pages should return FALSE
}

// 생략..
HBRUSH CMyDialog::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
       HBRUSH hbr=NULL;
       // 대화 상자의 경우 새로운색상으로.......
       if(pWnd->m_hWnd==this->m_hWnd) hbr= HBRUSH(m_my_brush);
       // 나머지 들은 기본 색상을유지한다.
       else hbr =CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
       return hbr;
}

  void CMyDialog::OnDestroy()
{
      CDialog::OnDestroy();
}


위 예제서 특이한 점은 WM_INITALDIALOG 메시지 처리기의 선언부이다. 다른 메시지 처리기는 afx_msg로 시작하는 반면에 OnInitalDialog함수는 virtual 키워드로 시작된다. 즉, WM_INITALDIALOG 메시지는 대화 상자 생성시 거의 대부분 사용되기 때문에 내부 함수의 성격을 더 강하게 가지게 되었기 때문에 본래 성격은 메시지 처리를 하는 함수였지만 클래스의 확장에 따라 이름을 그대로 유지하면서 가상 함수의 형태로 변경되었다. 따라서 형태상으로는 이미 변경되었지만 본래의 의미를 유지하기 위해서 class wizard가 위치상으로 OnInitalDialog 함수를 메시지 처리기들과 같이 선언해 준다. 당연한 이야기이지만 메시지 매크로에 보면 ON_WM_INITALDiALOG 라는 매크로가 등록이 되지 않았음을 볼 수 있다.
OnCtlColor의 함수의 매개 변수들을 살펴보면 아래와 같다.

① CDC* pDC
현재 그려질 컨트롤 윈도우에 대한 display context의 포인터이다. 따라서 여기에 CDC 클래스 함수들을 이용하여 속성을 변경하면 OnCtlColor 함수의 호출 뒤 그림을 그리는 컨트롤 윈도우에게 속성이 반영된다. 하지만 여러 개의 컨트롤이 존재한다면 자신이 원하지 않는 컨트롤에도 영향을 미칠 수 있으므로 주의 해서 사용하기 바란다.
pDC가 가지고 있는 값은 C++ Wrapper 클래스 객체의 포인터이다.

② CWnd* pwnd
색상을 요청한 컨트롤 윈도우의 포인터이다. 하지만 pwnd에 들어 있는 포인터는 C++ Wrapper 클래스 객체의 포인터이므로 자신이 원하는 윈도우 객체와 바로 비교하면 안 된다. 즉, 현재 OnCtlColor를 호출한 윈도우가 대화 상자인지를 비교하려고 아래와 같이
if( pwnd == this) …; // 현재 클래스가 대화 상자이므로 this로 사용한다.

사용하면 원하는 결과를 얻을 수도 있고 그렇지 않을 수도 있다. 왜냐하면 C++ Wrapper 클래스 객체는 임시 객체이기 때문이다. 하지만 그 임시 객체가 가지고 있는 윈도우 핸들은 이미 존재하는 것에 대한 핸들이기 때문에 정확하다. 따라서 아래와 같이 비교하면 원하는 결과를 얻을 수 있다.

if( pwnd->m_hWnd == this->m_hWnd ) …;

③ UINT nCtlColor
색상을 요청한 컨트롤 윈도우의 종류에 대한 상수 값이 들어 있다. 해당 상수 값은 아래와 같다.
▶ CTLCOLOR_BTN 버튼 컨트롤 윈도우
▶ CTLCOLOR_DLG 대화상자 윈도우
▶ CTLCOLOR_EDIT 입력 컨트롤 윈도우
▶ CTLCOLOR_LISTBOX List Box 컨트롤 윈도우
▶ CTLCOLOR_MSGBOX 메시지 박스 윈도우
▶ CTLCOLOR_SCROLLBAR ScrollBar 컨트롤 윈도우
▶ CTLCOLOR_STATIC Static 컨트롤 윈도우
따라서 위 예제에서 OnCtlColor 함수를 다음과 같이 변경해도 동일한 결과를 얻을 수 있다. ( 단, 현재 사용하는 대화 상자가 정형 대화상자라고 가정한다. )


 

HBRUSH CMyDialog::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
       HBRUSH hbr=NULL; // 대화 상자의 경우 새로운 색상으로..
       if(nCtlColor==CTLCOLOR_DLG) hbr=HBRUSH(m_my_brush);
       // 나머지 들은 기본 색상을 유지한다.
       else hbr =CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
       return hbr;
}

현재 실행되고 갱신되는 대화 상자는 하나이기 때문이 위 예제와 같이 코드를 사용해도 무방하다. 위에 사용했던 방식으로 각 컨트롤을 비교하면 그룹의 개념으로 적용된다. 예를 들어 아래와 같이 사용하면,

if(nCtlColor==CTLCOLOR_EDIT) hbr=HBRUSH(m_my_brush);

현 대화상자에 포함된 모든 Edit 컨트롤에게 적용된다. 이것을 좀더 구체적으로 제어하기 위해선 pWnd를 이용해야 한다. 예를 들어 Edit 컨트롤이 두개가 있고 각각의 ID가 IDC_EDIT1, IDC_EDIT2이면 아래와 같이 코딩하면 된다.

if(pWnd->m_hWnd==GetDlgItem(IDC_EDIT1)->m_hWnd) hbr=HBRUSH(m_my_brush1);
else if(pWnd->m_hWnd==GetDlgItem(IDC_EDIT2)->m_hWnd) hbr=HBRUSH(m_my_brush2);

이렇게 하면 두 개의 Edit 컨트롤이 같은 대화 상자에 존재하더라도 다른 배경색을 가진다. 물론 m_my_brush1과 m_my_brush2는 미리 선언되어 있어야 한다.

여기 하나 이상한 것이 있다. 왜 배경을 채우기 위한 색상을 요구하는 메시지 처리기에
"CDC 클래스 포인터가 전달되는가??"
Edit 컨트롤 윈도우의 배경색을 바꾸고 실행해 보면 알겠지만, 배경색이 변경된 Edit 컨트롤에 글자를 한번 써보기 바란다. 그러면 글자의 기본 배경색이 흰색이므로 배경색과 어울리지 않게 되어 영~! 보기가 좋지 않다. 이러한 경우를 해결하려면 Edit 컨트롤에 출력되는 문자열의 배경색을 컨트롤 윈도우의 배경색과 일치 시켜야 한다. 따라서 이때 이 CDC *pDC 매개변수를 사용한다. Edit 컨트롤 윈도우의 배경색이 회색(192,192,192)이면 아래와 같이 하면 될 것이다.

pDC->SetBkColor(RGB(192,192,192));

이 것을 보면 알 수 있듯이, 다양한 응용이 가능하다. 예를 들어 입력 컨트롤에 출력되는 문자열의 색상을 변경하고 싶다면 아래와 같이

pDC->SetTextColor(RGB(0,0,255));

사용하면 문자열의 색상이 파랑 색으로 출력될 것이다. 이러한 방식으로 SetBkMode나 그 밖에 CDC 클래스가 제공하는 많은 함수를 적절하게 이용할 수 있다.

위에서 사용한 방법대로 나머지 컨트롤(List Box, Static, …)에게도 적용하면 원하는 배경색으로 각 컨트롤 윈도우의 색상을 변경할 수 있다. 또한 여기에 사용된 방법은 CFormView에도 동일하게 적용된다.

대화 상자나 CFormView의 경우 여러 개의 컨트롤이 포함되어 있다. 이때 OnCtlColor 메시지 처리기가 호출되는 순서는 대화상자나 CFormView 윈도우 그 자체를 위해서 제일 먼저 OnCtlColor 메시지가 처리될 것이고 그 다음부터는 대화 상자의 자원 파일에서 설정한 Tab Order가 높은 순서부터 처리가 된다. 예를 들어 컨트롤이 3개이고 Tab Order가 1, 2, 3이라면 OnCtlColor는 3, 2, 1 순으로 호출됨을 알기 바란다.

[ 주의 ]
nCtlColor 매개변수를 이용해서 Single-Line Edit 컨트롤의 배경색을 변경하기 위해서는 nCtlColor의 값이 CTLCOLOR_EDIT인 경우만 처리하면 안 되고 CTLCOLOR_MSGBOX인 경우를 같이 처리해야 한다. 즉, Single_Line Edit 컨트롤의 배경색을 변경하려면 CTLCOLOR_EDIT와 CTLCOLOR_MSGBOX에 동일한 Brush를 설정해야 한다. 그리고 출력되는 문자열의 배경색과 Edit 컨트롤의 배경색을 일치시키기 위해서 CTLCOLOR_EDIT처리기에 SetBkColor 함수를 호출하는 것을 잊지 말아야 한다.

OnCtlColor 처리기에서는 drop-down ComboBox의 list box에 대해서는 처리하지 않는다. 왜냐하면 drop-down list box는 대화상자의 자식 윈도우가 아니라 drop-down ComboBox의 자식 윈도우이기 때문이다. 따라서 이 문제를 해결하려면 CComboBox에서 클래스를 계승 받아 그 클래스에 OnCtlColor 메시지 처리기를 등록하여 배경색을 처리해야 한다.

Posted by Solitude mind444