Where The Streets Have No Name

CWnd와 Windows의 window의 관계 본문

Developement/C, C++, C#

CWnd와 Windows의 window의 관계

highheat 2008. 5. 25. 14:39
출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNo=20&no=1892

실제로 우리가 사용하는 윈도우라는 것은 윈도우즈의 윈도우, 즉 HWND로 지정할 수 있
는 윈도우이다. CWnd라는 것은 하나의 윈도우, 즉 HWND를 갖고 있고,이 윈도우에 사용
하는 많은 함수들을 모아 놓은 하나의 클래스에 불과하다. 즉, HWND라는 것은 진정한 
의미에서의 윈도우이고,  CWnd라는 것은 단지 하나의 클래스(말 그대로 멤버 변수와 
그에 따른 많은 멤버 함수를 제공하는 클래스)일 뿐이며, 이 클래스의 주된 멤버 변수
가 윈도우즈에서 사용하는 실제의 윈도우인 HWND(m_hWnd)이고, 이것을 제어하기 위한 
많은 함수를 제공한다. 실제로 CWnd 클래스의 정의를 보면 
    HWND m_hWnd;            // must be first data member
란 구문이 있다. 즉, 이 클래스의 어떤 특성이든지 이 클래스에 접근하기 위해서는 우
선 그에 따른 윈도우(운영 체제의 실제 윈도우)가 존재해야 하는 것이다. 외부에서 프
로그래머가 사용하기 위한 데이타 멤버는 HWND m_hWnd밖에 없다. 


        CWnd                                         Window
         |                                              |
    HWND m_hWnd;  ----이게 이걸루 연결된다----> HWND hWnd;//핸들 
                                                WNDCLASS WndClass; //모양
 그외의 멤버 함수들                             HMENU hMenu;//메뉴
                                                   ...등등등...

   <CWnd 객체의 모양새>                    <실제 윈도우 구조체의 모양새>
<편의를 위해 Cls라고 하자>                   <편의를 위해 Wnd라고 하자>

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

다음 구문을 보며 실제로 어떤 일이 일어나는 지 확인해보자.
CWnd *mpWnd; //이걸 멤버 변수로 선언했다.

생성자에서 
mp_Wnd = NULL; --->1

뷰에서 마우스 왼쪽 버튼이 클릭되었을 때 다음의 구문을 적어 놓았다.
mp_Wnd = new CWnd(); --->2
mp_Wnd->Create(NULL,"", WS_OVERLAPPEDWINDOW , CRect(10,10,100,100),this,11);--->3


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

2번이 실행되면 mp_Wnd라는 값은 어떤 주소값을 갖게 된다. 그것은 저 위에서 Cls의 
위치를 나타내는 값이 되겠다. 하지만, 아직 Wnd를 만든 것이 아니기 때문에 m_hWnd에
는 널이 들어가게 된다(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;// 다시 초기화
}