이번 글에서는, 커스텀 화면을 만들어야 하는 경우 어떻게 해야하는지 알아보겠습니다. 장문의 글이 될 것 같네요.
우선 개발로 들어가기 전에 몇가지 알아 보겠습니다.
1. Windows 화면 구조
PC를 켜면, Windows OS가 구동 되어 바탕화면이 보일 것이고, 바탕화면의 프로그램을 실행 시키면 프로그램이 모니터에 표시됩니다. 이때 모니터에 표시되는 바탕화면, 프로그램, 프로그램의 버튼, 리스트 컨트롤 등등 모두 핸들을 가지고 있으며, 화면 출력을 위한 핸들인 DC(Device Context)도 가지고 있습니다.
Static, Button 모드 각각의 핸들을 가지고 있습니다.
DC는 Windows GDI에서 화면 출력을 위해 필요한 모든 정보를 가지고 있는 데이터 구조체입니다.
Win32, MFC상의 화면에 출력되는 모든 컨트롤(Button, Static, ComboBox, List)들은 모두 이 DC를 통해 그려진 녀석들이며, 개발자는 커스텀 화면을 만들기 위해서 이 DC를 이용하여 화면 출력을 해야합니다.
2. 응용 프로그램 화면 표시 기본
윈도우는 2차원 좌표에서 그림을 그립니다. 보통 2차원 좌표라고 하면 가운데가 (0, 0)인 2차원 좌표계를 생각합니다.
하지만 윈도우 좌표계는 아래와 같이 좌측 상단이 (0, 0)이며 X는 우측으로 갈수록 값 증가, Y는 아래로 갈 수록 값이 증가합니다.
3. MFC hierarchy - CWnd
화면 출력을 위해 DC가 필요한지 알겠는데, 어떻게 얻어야 하는지, 또 DC를 이용하여 어떻게 그려야 하는지 머리속으로 바로 떠오르지 않으실 겁니다.
결론부터 말하자면 CWnd 클래스를 상속 받은 클래스를 만들면 쉽게 만들 수 있습니다.
이 CWnd 클래스는 MFC 수많은 컨트롤들의 부모가 되는 클래스이여서 CWnd를 상속받아 개발한다는 것은 자신만의 컨트롤을 만든다는 의미이기도 합니다.
Windows에서는 프로그램이 화면에 그림을 그릴 수 있게 WM_PAINT 메시지를 프로그램에 전달 하는데, CWnd의 OnPaint 함수에서 – 이름 그대로 – WM_PAINT 함수가 발생 했을 때 DC에 그리는 처리를 할 수 있습니다.
구글에 “MFC hierarchy“라고 검색하셔서 아래와 같은 이미지를 한번 쓱 훑어보는걸 추천드립니다.
4. 커스텀 화면 생성
자 이제 기본적인건 끝났고, 개발을 시작해보겠습니다. 정확히는 CWnd를 상속받은 클래스를 만들어만 볼 것이며 글 하단에 제가 만든 클래스를 첨부해 드릴 예정입니다.
우선 프로젝트 하나 생성해주신 뒤, 클래스 마법사를 실행 시켜 CCustomWatchWnd 클래스를 하나 생성 해줍니다.
중요한 것은 MFC 클래스를 추가할 때 기본 클래스를 CWnd로 하셔야하는 겁니다. 아래 이미지를 참고해주세요
CCustomWatchWnd 생성
CCustomWatchWnd 생성
CCustomWatchWnd 생성
정상적으로 생성이 되면 아래와 같이 .h와 .cpp가 생성 됩니다.
/////////////////////////////////////////
// CCustomWatchWnd.h
#pragma once
// CCustomWatchWnd
class CCustomWatchWnd : public CWnd
{
DECLARE_DYNAMIC(CCustomWatchWnd)
public:
CCustomWatchWnd();
virtual ~CCustomWatchWnd();
protected:
DECLARE_MESSAGE_MAP()
};
/////////////////////////////////////////
// CustomWatchWnd.cpp: 구현 파일
//
#include "pch.h"
#include "MFCApplication1.h"
#include "CustomWatchWnd.h"
// CCustomWatchWnd
IMPLEMENT_DYNAMIC(CCustomWatchWnd, CWnd)
CCustomWatchWnd::CCustomWatchWnd()
{
}
CCustomWatchWnd::~CCustomWatchWnd()
{
}
BEGIN_MESSAGE_MAP(CCustomWatchWnd, CWnd)
END_MESSAGE_MAP()
// CCustomWatchWnd 메시지 처리기
WM_PAINT 메시지를 처리하는 OnPaint 함수를 추가해보겠습니다.
클래스 마법사를 실행 하신 뒤, CCustomWatchWnd 클래스를 선택하고 메시지 탭에서 WM_PAINT를 클릭하여 처리기 추가 버튼을 누릅니다. 그 이후 적용 버튼을 눌러줍니다.
CCustomWatchWnd 클래스에 OnPaint라는 함수가 자동 생성 됩니다.
위에서 설명한 것과 같이 이 함수에서는 그리는 작업을 해주면 됩니다.
// .h
afx_msg void OnPaint(); // 그리는 코드가 들어가야 할 곳
// .cpp
void CCustomWatchWnd::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: 여기에 메시지 처리기 코드를 추가합니다.
// 그리기 메시지에 대해서는 CWnd::OnPaint()을(를) 호출하지 마십시오.
// Something Draw ...
}
CCustomWathWnd를 이제 대화상자의 컨트롤에 연결 해보겠습니다.
아래 그림과 같이 Static Control을 만들어 준 뒤, IDC_STATIC_WATCH를 아이디를 설정해 줍니다.
Static Control을 우클릭 하여 변수 추가를 눌러줍니다. 그 다음 아래와 같이 입력해줍니다.
이 상태로 실행 하면 아무런 화면이 나타나지 않으니 CCustomWatchWnd::OnPaint()에 아래와 같이 코딩을 합니다.
void CCustomWatchWnd::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: 여기에 메시지 처리기 코드를 추가합니다.
// 그리기 메시지에 대해서는 CWnd::OnPaint()을(를) 호출하지 마십시오.
// 화면 크기 구하기
CRect rectWnd;
GetClientRect(rectWnd);
CBitmap bmp;
bmp.CreateCompatibleBitmap(&dc, rectWnd.Width(), rectWnd.Height());
CDC dcTemp;
dcTemp.CreateCompatibleDC(&dc);
CBitmap* pBmpOld = (CBitmap*)dcTemp.SelectObject(&bmp);
CBrush br;
br.CreateSolidBrush(RGB(0, 0, 0));
CBrush* brOld = (CBrush*)dcTemp.SelectObject(&br);
dcTemp.Rectangle(rectWnd);
dcTemp.SelectObject(brOld);
br.DeleteObject();
// 화면에 표시하기
dc.BitBlt(0, 0, rectWnd.Width(), rectWnd.Height(), &dcTemp, 0, 0, SRCCOPY);
dcTemp.SelectObject(pBmpOld);
bmp.DeleteObject();
dcTemp.DeleteDC();
}
이 상태를 실행하면 아래와 같이 표시됩니다.
이렇게 CCustomWatchWnd::OnPaint 내부의 DC에 원하는 그림을 그리면 사용자가 원하는 화면을 표시할 수 있습니다.
만약 개발자가 동그라미와, 선, 그리고 텍스트를 그릴수 있다면 시계도 만들수 있지 않을까요?
위 파일을 다운받으시면 CCustomWatchWnd.h와 cpp가 있습니다.
CCustomWatchWnd 클래스는 아래 그림과 같이 시침, 분침, 초침을 표시하고, 하단에 현재 시간을 텍스트로 표시합니다. 위 클래스를 위와같은 방식으로 스태틱 컨트롤에 붙이면 아래와 같은 화면이 나타납니다.
5. Invalidate
OnPaint는 호출되는 함수가 아니므로 계속 화면을 갱신해야 하는 경우 강제로 WM_PAINT를 호출하게 해야합니다.
이 경우 Invalidate라는 함수를 사용하여 커스텀 화면을 갱신 할 수 있습니다.
아래와 같이 CCustomWatchWnd에 DisplayWatch라는 함수를 만들어 외부에서 호출하면 화면이 갱신됩니다.
void CCustomWatchWnd::DisplayWatch()
{
// TODO: 여기에 구현 코드 추가.
if (this->GetSafeHwnd() != NULL)
Invalidate(TRUE);
}
전체 코드는 아래 첨부 파일 확인 바랍니다.