본문 바로가기
컴퓨터/MFC_API

CWnd에 대하여

by adnoctum 2010. 1. 15.
원본 작성일 : 2001-04-03 오전 1:25:37
수정일 : 2010-01-15 오전 00:16

서론 MFC를 하는 사람들이 한 번은 시도해 보게 된다는 일, MSDN의 CWnd 설명 해석하기. 그만큼 MFC를 함에 있어 CWnd를 이해하는 것은 중요하다. MFC의 수많은 컨트롤들이 CWnd를 상속받기 때문에 CWnd를 제대로 이해하게 되면 컨트롤[각주:1]을 다루기 쉬워지며, 더 나아가 MFC를 이용하는 것이 매우 편해진다.

   MFC를 배우는 사람이 겪는 전형적인 문제는 책에 있는 예제를 따라해서 뭔가 하기는 했는데, 자신이 작성해 놓은 코드가 어떻게 그 일을 하게 되는지 알기 쉽지 않다는 것이다. 그 이유는, MFC는 매우 방대한 라이브러리이기 때문에 MFC의 전체적인 모습을 알지 못하고 그냥 중간에 조금 건드렸기 때문이다. 전체적인 흐름을 알지 못한 채 중간에 있는 것 조금 건드렸기 때문에 그것이 어떻게 동작하는지 알 수 없는 것이다. 바로 그 '전체적인 모습'에 해당하는 것 중 핵심이 CWnd라고 할 수 있겠다. 자, 그럼 CWnd에 대해 살펴 보자.

본론에 들어 가기에 앞서, 윈도우즈(Windows)의 '복수형'은 마이크로소프트 윈도우즈를 지칭하는 말로 운영체제를 의미하며, 윈도우(window)의 '단수형'은 말 그대로 '창', 즉 탐색기나 메모장을 띄울 때의 그 '창'을 의미한다.

본론

CWnd와 Windows의 window의 관계
   실제로 우리가 사용하는 윈도우라는 것은 윈도우즈의 윈도우, 즉 HWND로 지정할 수 있는 윈도우이다. CWnd라는 것은 속성(멤버 변수)으로 하나의 윈도우, 즉 HWND를 갖고 있고, 이 윈도우에 사용하는 많은 함수들을 모아 놓은 하나의 클래스에 불과하다. 즉, HWND라는 것은 진정한 의미에서의 윈도우이고,  CWnd라는 것은 단지 하나의 클래스(말 그대로 멤버 변수와 그에 따른 많은 멤버 함수를 제공하는 클래스)일 뿐이며, 이 클래스의 주된 멤버 변수가 윈도우즈에서 사용하는 실제의 윈도우인 HWND(m_hWnd)이고, CWnd는 이것을 제어하기 위한 많은 함수를 제공한다. 실제로 CWnd 클래스의 정의를 보면

    HWND m_hWnd;            // must be first data member

란 구문이 있다. 즉, 이 클래스의 어떤 특성이든지 이 클래스에 접근하기 위해서는 우선 그에 따른 윈도우(메모리 상에 존재하지 않는 윈도우에 어떤 동작을 수행할 수는 없다)가 존재해야 하는 것이다. 외부에서 프로그래머가 사용하기 위한 데이타 멤버는 HWND m_hWnd밖에 없다.

CWnd와 window의


(편의를 위하여 CWnd 객체를 Cls, window 객체를 Wnd라 하자)

위의 그림에서 보듯이 실제로 우리가 사용하는, 네모나고, 제목 표시줄이 있고 아이콘을 가지며 마우스 움직임을 받는 것은 오른쪽의 윈도우이다. 윈도우라는 하나의 객체를 나타내는 가장 기본이 되는 것, 즉 이 객체의 핸들(주소와 같은 개념)이 CWnd 의 m_hWnd에 연결되게 된다. 그래서 CWnd::GetWindowText();등의 함수를 호출하면, 이 함수는 내부적으로 우선 HWND m_hWnd가 메모리상에 윈도우의 특성을 나타내면서 존재하는 값인지를 확인한다(다른 건 하나도 없이 달랑 HWND  hWnd란 값이 주소형태로 존재하고, 나머지는 윈도우의 특성을 나타내기에 부적당한 값들이 있을 수도 있기 때문에 우선 hWnd에 의한 주소에 가서 그 주소에 있는 구조체가 윈도우의 구조체인지를 확인하는 것이다)[각주:2]. 만약 윈도우를 나타내는 것이 맞는다면 그 때 비로소 윈도우의 문자열을 반환하게 된다. CWnd의 대부분의 멤버 함수가 이런 식의 형태를 띄게 된다. 즉, CWnd 클래스의 멤버 함수는 실제로 자신의 클래스가 갖고 있는 윈도우라는 구조체의 많은 특성들을 참조/변환하기 위한 것이다.

다음 구문을 보며 CWnd 객체를 만들고 파괴시킬 때 어떤 일이 일어나는지 알아 보자.



CWnd *mp_Wnd; //이걸 멤버 변수로 선언했다.

생성자에서

mp_Wnd = NULL; //--->1

뷰에서 마우스 왼쪽 버튼이 클릭되었을 때 다음의 구문을 적어 놓았다.

mp_Wnd = new CWnd(); //--->2
mp_Wnd->Create(NULL,"", WS_OVERLAPPEDWINDOW , CRect(10,10,100,100),this,11); //--->3

mp_Wnd은 CWnd의 포인터이기 때문에 CWnd의 포인터형이 이 클래스가 생성될 때 할당이 된다. 물론 이 값은 쓰레기 값이다. 그런데 생성자가 실행될 때 널로 넣어주고 있으므로 값이 바로 널로 바뀌게 된다. 이 구문은 중요하지 않은 것 같으나 실제로는 초기화를 해주지 않아서 곤란을 겪을 수도 있다. 널로 해주지 않았다고 가정한다면, 디버깅을 할 때 전에 프로그램을 실행시켜서 mp_Wnd에 윈도우를 하나 만들어서 붙였다고 가정할 때, 그 프로그램이 끝났어도, 어떤 경우에는 컴퓨터가 mp_Wnd를 생성시킬 때 이미 전에 만들어 놓고 지우지 않은 곳, 즉 전에 윈도우를 나타내는 포인터를 갖고 있는 바로 그 곳에 다시 이 변수를 잡을 수도 있다. 이 부분에서 어떤 문제가 발생할 수 있는지는 좀 더 있으면 알 수 있다.

2번이 실행되면 mp_Wnd라는 값은 어떤 주소값을 갖게 된다. 그것은 저 위에서 Cls의 위치를 나타내는 값이 되겠다. 하지만, 아직 Wnd를 만든 것이 아니기 때문에 m_hWnd에는 널[각주:3]이 들어가게 된다(Cls와 Wnd가 뭔지는 저 위를 다시 보자!!) 이제 Wnd만 만들면 CWnd는 Wnd에 따른 많은 일을 할 수 있는 하나의 완벽한 CWnd클래스가 될 준비가 끝난 것이다.

3번을 실행하면 실제로 Wnd가 만들어져서 이 Wnd의 핸들(hWnd)이 mp_Wnd의 m_hWnd에 저장된다.

CWnd::Attach(HWND hWnd);라는 함수는 CWnd객체의 m_hWnd값에 hWnd값을 넣어준다는의미로, hWnd가 나타내는 윈도우를 CWnd의 클래스 멤버 함수로 조작하기 위한 준비단계가 되겠다.

CWnd::Detach();함수는 CWnd에 연결된 hWnd를 끊어버리는 것으로 이제 더이상 CWnd객체는 아무 Wnd 도 갖고 있지 않는 것이다. 그러므로 Detach();함수를 호출한 뒤, CWnd의 멤버 함수를 호출하면 에러가 난다. 왜냐 하면, CWnd의 멤버 함수들은 이 클래스가 갖고 있는 멤버 변수인 m_hWnd라는 실제의 윈도우를 조작하기 위한 것들인데, Detach();로 그 m_hWnd값을 없애 버리기 때문에(널로 넣었기 때문에) 더이상 조작할 수 있는 대상이 없기 때문이다.

CWnd::DestroyWindow();라는 함수는, CWnd::m_hWnd가 나타내는 실제의 윈도우를 파괴한다. 즉, CWnd 자체를 파괴하는 것이 아니고, CWnd가 멤버 변수로 갖고 있는 윈도우라는 실체를 파괴하는 것일 뿐이다.

CWnd::GetSafeHwnd();라는 함수는 CWnd의 객체가 갖고 있는 Wnd라는 것, 즉 CWnd의 유일한, 실제의 윈도우의 핸들을 반환해준다. 만약 CWnd라는, 클래스라는 객체만 생성되었고, 아직 실제의 윈도우가 만들어져서 CWnd에 연결된 것(Attach된 것, 즉 CWnd의 m_hWnd에 실제 윈도우의 hWnd가 들어간 것)이 아니라면 널을 반환한다.

윈도우를 생성시킬 때 보통 다음 세 구문을 사용할 수 있다.

if(!mp_Wnd){ ~~~}; // or                  ----->1
if(!mp_Wnd->GetSafeHwnd()); // or         ----->2
if(!::IsWindow(mp_Wnd->GetSafeHwnd()); // ----->3

1번은 CWnd객체가 생성되었는지를 확인한다. 즉 실제의 윈도우인 Wnd라는 것의 생성 여부는 확인하지 않는다. 이 구문이 실행되려면 mp_Wnd가 널일 경우이므로 이 구문은 비교적 안정하다고 할 수 있다. 하지만, 만약 mp_Wnd를 널로 초기화하지 않았다거나 예전에 이것에 관계된 윈도우를 파괴시킬 때

mp_Wnd->DestroyWindow();
delete mp_Wnd;

로 했다면, 프로그래머는 이 구문이 실행될 것을 기대하지만, 실제로는 실행되지 않는다. 왜냐 하면, mp_Wnd->DestroyWindow();구문은 mp_Wnd->m_hWnd가 가리키는 실제의 윈도우 객체를 파괴하는 것이므로 mp_Wnd를 널로 만들지 않고, delete mp_Wnd;구문도 결코 mp_Wnd라는 값을 널로 만들지는 않기 때문이다. 따라서 윈도우를 생성할 때 1번과 같은 구문으로 생성하고 정확한 결과를 기대할려면 윈도우를 파괴할 때

mp_Wnd->DestroyWindow();
delete mp_Wnd;
mp_Wnd = NULL; //--->요 구문을 꼭 써 넣어야 한다.

2번과 같이 생성을 하는 경우는 mp_Wnd->m_hWnd의 값이 널이 아닐 경우에 실행된다. mp_Wnd->m_hWnd에 널이 들어가는 경우는 mp_Wnd->DestroyWindow();나 mp_Wnd->Detach();를 실행한 후이다. 이 구문은 윈도우를 파괴할 때 mp_Wnd->DestroyWindow();로만으로도 생성/파괴를 반복할 수 있다. 하지만 여기서 문제점은  delete mp_Wnd;라는 구문을 해주지 않았기 때문에 mp_Wnd가 가리키는 메모리의 위치가 계속 바뀌면서 새로 생성되어서 결국 메모리 누수를 가져온다는 것이다. 따라서 2번과 같이 윈도우를 생성하고 윈도우의 생성/파괴를 반복하기 위해서는 윈도우를 다음과 같이 파괴해야 한다.

mp_Wnd->DestroyWindow();
delete mp_Wnd;
mp_Wnd = NULL; --->요 구문을 꼭 써 넣어야 한다.

너무 위에 있으니 다시 한 번 적어 본다.

if(!::IsWindow(mp_Wnd->GetSafeHwnd()) ----->3

3번과 같이 생성하는 경우. 가장 안전하고 가장 올바른 경우라 할 수 있다. 우선 CWnd::GetSafeHwnd();는 CWnd와 연결된 hWnd가 있으면 그것을 반환해주고, 없으면 널을 반환해준다. 또한 IsWindow();라는 함수는 hWnd가 가리키는 것이 실제의 윈도우인지, 그러니까 이게 값만 덜렁 갖고 있고, 실제로 그 주소로 가보니까 윈도우의 모양새를 한 녀석이 아닌, 얼토당토 않은 놈은 아닌지, 이 모든 걸 확인해 준다. 따라서 이 함수의 의미는 mp_Wnd가 가리키는 것이 정말 윈도우가 되는가를 가장 정확하게 확인해 주는 구문이라 할 수 있다. 물론 이 때는 mp_Wnd->DestroyWindow();함수로만 지워도 되기는 하나, 좀 더 명확성을 위해서 1,2번처럼 파괴시켜 주는 것이 좋다.

자, 그럼 1번과 같이 생성시키는 루틴을 집어 넣고, 만약 mp_Wnd를 초기화해주지 않는다면 어떤 일이 일어날 수도 있는지 살펴 보자. 만약 지금 프로그램을 디버깅하는 단계라서 수시로 중단했다 다시 실행시키고 있는 중이라면 초기화를 해주지 않는 것은 상당한 문제가 될 수 있다. 만약 mp_Wnd를 생성했다가 이에 연결된 hWnd를 파괴하는 과정 없이 프로그램을 끝냈다고 가정하자. 그러면 물론 mp_Wnd라는 것은 메모리에서 해제가 되었을지라도 그에 연결된 hWnd는 완전히 해제되지 않은 상태이다. 그 상태에서 다시 프로그램을 구동시키면 mp_Wnd에 처음에 자동으로 쓰레기값이 들어가는 데(멤버 변수이기 때문에)그 값이 공교롭게도 전에 있던 위치 그대로가 될 수 있다. 그러면 그 위치의 m_hWnd가 가리키는 것은 여전히 윈도우의 형태를 하고 있을 수 있는 것이다. 따라서 1번과 같이 하는 것은 바람직하다고 할 수 없다.


정리하면 윈도우를 생성하고 파괴하는 가장 안정적이고 올바른 루틴은 다음과 같다.

--생성에서는

if(!::IsWindow(mp_Wnd->GetSafeHwnd())){
    ~~~여기서 생성시킨다.
}

--파괴에서는

if(::IsWindow(mp_Wnd->GetSafeHwnd())){
    mp_Wnd->DestroyWindow(); //mp_Wnd에 붙어 있는 hWnd의 실제 윈도우 파괴
delete mp_Wnd; // mp_Wnd를 메모리 상에서 해제
mp_Wnd = NULL;// 다시 초기화
}

결론 이로써 CWnd가 사실은 API로 작성할 때의 window라는 것을 알았고, 우리가 보는 윈도우 바로 그 창에 여러 가지 동작을 할 수 있는 멤버 함수를 제공하고 있는 객체라는 것을 알게 되었다. 윈도우를 생성할 때 부모 윈도우를 설정하게 되어 있는데, 뷰의 윈도우를 부모로 지정해 주면 버튼 컨트롤을 뷰의 중간에 떵그러니 만들 수도 있는 것이다.

  1. Document-VIew구조라 해도 CView나 CFrameWnd도 결국 CWnd를 상속받기 때문에 dialog-based project가 아니어도 CWnd를 제대로 이해하는 것은 중요하다 [본문으로]
  2. debug 모드에서 [본문으로]
  3. 실제로는 INVALID_HANDLE_VALUE로 된 값이 들어 가거나, 디버깅 모드에서 보면 0xfdfdfdfd와 같은 값이 들어 있다. 널은 아니다. [본문으로]

'컴퓨터 > MFC_API' 카테고리의 다른 글

CTreeCtrl 에 디렉토리 표시하기  (0) 2010.02.14
시스템 폴더의 실제 경로, 문자열 얻어 오기  (0) 2010.02.14
전체 화면으로 만들기  (0) 2009.12.27
팝업 윈도우 만들기  (0) 2009.12.27
task bar 감추기  (0) 2009.12.27