class CAppFrame: public CWnd {
public:
void PostNcDestroy() { delete this; }
};
class CMyApp: public CWinApp {
public:
BOOL InitInstance();
};
CMyApp g_App;
BOOL CMyApp::InitInstance()
{
m_pMainWnd=new CAppFrame;
m_pMainWnd->CreateEx(0, AfxRegisterWndClass(0), _T("Hello World"), WS_OVERLAPPEDWINDOW,
100, 100, 500, 500, NULL, NULL);
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
return TRUE;
}
위의 코드는 MFC를 써서 만들 수 있는 가장 간단한 GUI 프로그램이다. 빈 창만 달랑 뜨는 게 허전하면, message map 넣고 OnPaint에다가 Hello, world! 출력하는 코드만 추가해 주면 된다.
MFC로 창을 띄우기 위해서는 본질적인 것 딱 두 가지만 기억하면 된다.
첫째, CWnd에서 상속 받은 클래스를 만들기
둘째, CWinApp에서 상속 하나 받아서 전역 개체를 하나 반드시 만들고, InitInstance를 오버라이드하여 내 윈도우 클래스를 생성하는 코드를 짜 주기
물론 요즘은 닷넷 프레임워크 같은 더 객체 지향적이고 깔끔한 API도 나와는 있지만 저 정도만 해도 그냥 C언어 + Win32 API만 썼을 때와는 비교할 수도 없이 간단하게 내가 원하는 일을 곧장 시작할 수 있다. 윈도우 클래스 등록, 윈도우 프로시저 등 온갖 지저분한 내부 처리를 상당 부분 MFC가 알아서 해 주기 때문이다.
MFC의 핵심부이며 가장 자주 다루게 되는 부분은 역시 윈도우를 나타내는 CWnd 관련 개체이다. 응용 프로그램의 메인 윈도우부터 시작해서 대화상자와 대화상자 안의 자그마한 컨트롤까지 매우 다양한 용도로 쓰이는 HWND를 한 뿌리 클래스와 상속 클래스만으로 원활히 제어하기 위해 MFC는 굉장히 심도 있게 설계되었으며 내부적으로 자질구레한 일들을 매우 많이 해 주고 있다. 단순히 ShowWindow(hWnd, SW_SHOW)를 wnd.ShowWindow(SW_SHOW)로 바꿔 주는 것을 훨씬 넘어서는 수준이다.
더구나 메시지 맵을 통해 컨트롤 서브클래싱(기존 윈도우 컨트롤의 동작을 부분적으로 조작하는 것)까지 매끄럽게 연결시킨 것까지 보면, 솔직히 굉장히 잘 만든 프레임워크임을 인정하지 않을 수 없다.
이런 프레임워크를 만들 때 근본적으로 문제가 되는 것은 CWnd라는 C++ 오브젝트와 운영체제의 HWND 사이의 씽크를 맞춰 주는 작업이다. 둘은 원래 개념적으로 생성 주기나 scope이 서로 완전히 다른 존재이다. 하지만 MFC는 한 HWND를 가리키는 CWnd 오브젝트가 중첩되지 않도록 배려하고 있으며, 나의 C++ 코드에 의해 생성되지 않은 외부 HWND에 대해서도 임시 맵까지 만들어 가면서 CWnd를 동기화해 주고 있다. 멀티스레드 환경까지 감안하면 이는 더욱 복잡한 작업이 된다.
new 연산자로 CWnd가 생성될 때 CreateWindow를 같이 해 주고, HWND가 완전히 소멸되어 WM_NCDESTROY가 왔을 때 CWnd까지 delete로 지워 주는 것은 대부분의 경우엔 바람직한 디자인 패턴이나, 이것이 언제나 유용한 것은 아니다. 가령 modal 대화상자는 자체적으로 message loop까지 갖추고 있기 때문에 heap이 아닌 스택에다가 간단히 지역변수로 선언하는 경우가 더 유용하기 때문이다. 더구나 대화상자는 사용자가 대화상자를 닫았더라도, 이 C++ 클래스 갖고 있는 데이터 변수들을 후에 활용하는 경우가 많기 때문에 HWND 개체가 사라진 뒤라도 C++ 개체는 남아 있어 줘야 한다.
이래저래 CWnd와 HWND 사이의 관계와 생성/소멸 주기는 여러 모로 이해하기 까다롭다. CWnd 클래스 중에서 new로 생성되고 HWND의 소멸과 동시에 소멸되어야 하는 오브젝트라면, PostNcDestroy 함수를 오버라이드하여 delete this를 넣어 주어야 메모리 누수가 발생하지 않는다. 응용 프로그램의 주 프레임 윈도우라든가 modeless 대화상자가 이런 예에 속한다.
저 소스 코드에서는 CWnd를 날것으로 사용했지만, CWnd에서 파생되어 아예 전문적으로 응용 프로그램의 주 프레임 윈도우를 담당하고 있는 MFC 클래스인 CFrameWnd는 PostNcDestroy가 이미 저렇게 구현되어 있다. 그뿐만 아니라 이 클래스는 LoadFrame 함수에서 이미 윈도우 클래스의 등록까지 다 알아서 해 주기 때문에 저 소스 코드에서처럼 AfxRegisterWndClass를 번거롭게 호출할 필요도 없다. 사용자가 준 ID와 일치하는 응용 프로그램 타이틀, 아이콘, 심지어 단축키 액셀러레이터 테이블 따위를 모두 엮어서 윈도우의 기반을 자동으로 구성해 주기 때문이다.
MFC를 써서 아주 간단한 프로그램을 짜고 싶은데 비주얼 C++의 MFC 마법사가 기본으로 생성해 주는 코드는 군더더기가 너무 많다. 그래서 이럴 때 본인은 MFC 없이 일반 Win32 응용 프로그램으로 프로젝트를 시작한 후, 저 코드 템플릿을 갖다 붙이고 프로젝트 세팅을 “MFC 사용”으로 수동으로 바꾸는 방법을 쓴다.
필요한 건 MFC가 잘 캡슐화해 준 message loop, 몇몇 GDI 오브젝트 같은 것밖에 없는데 이것만 쓰기에는 MFC는 덩치가 너무 커지고 static link를 하기에도 오버헤드가 너무 큰 것도 불만이다. 차라리 known DLL만 쓰기 때문에 매우 가벼운 바이너리를 만들 수 있는 비주얼 C++ 6.0이 그리울 때도 있다. 어차피 이후 버전에서 추가된 기능은 거의 안 쓰기 때문에.
<날개셋> 타자연습은 소스를 보면 알겠지만 딱 MFC를 써서 개발되었다. 하지만 한글 입력기는 3.0부터 MFC를 쓰지 않으며, 믿거나 말거나 MFC를 어설프게 흉내 내어 본인이 내부적으로 자체 개발한 프레임워크 라이브러리를 static link하여 개발해 오고 있다. 하지만 자체 라이브러리는 오버헤드 줄이고 최대한 가볍게 만드느라, Win32 API라든가 각종 핸들을 캡슐화한 수준이 MFC 수준으로 범용적이고 체계적이고 편하지는 못하기 때문에, 여전히 MFC가 그리울 때가 있다. ^^;;
Posted by 사무엘