본문 바로가기

[ C/ C++ 프로그래밍 ]/[ MFC ]

[ MFC ] 본격적인 윈도우 프로그램

ㅇ 참고 및 출처  :


ㅇ 본격적인 윈도우 프로그램

윈도우 운영체제는 관리하는 모든 객체(object)의 정보를 담고 있는 구조체(structure)를 리스트(list)로 유지한다. 객체들은 가시적일 수도 있고, 그렇지 않을 수도 있는데
객체의 예로 프로세스, 윈도우, 디바이스 컨텍스트(device context)를 들 수 있으며, 이중에서 윈도우는 가시적이며, 프로세스와 디바이스 컨텍스트는 가시적이지 않다.

프로세스: 실행중인 프로그램
디바이스 컨텍스트 : 윈도우의 표면에 그리기 작업을 수행할 때 필요한 정보를 담고 있는 구조체, 모든 그리기 함수는 다비아스 컨텍스트를 필요로 한다.

핸들 : 윈도우 운영체제가 유지하는 객체를 표현하는 구조체와 관련된것, 각 프로세스를 구분하기 위해 각각의 구조체에 다른 프로세스의 구조체와 구분되는 유일한 ID를 할당하는데
          이 ID를 핸들 (handle)아리고 한다. 여기서는 프로세스 핸들 (process handle)이 된다. 핸들은 의미있는 값일 수도 있고, 단지 구초제가 할당된 메모리의 시작 주소일수도 있다.




우의 그림은 운영체제가 메모리에 유지하는 몇가지 구조체를 나타낸다. 운영체제는 객체들의 정보를 구조체의 리스트로 유지한다. 각각의 구조체를 구분하는 유일한 값을 핸들이라고
한다.

우리가 고려할  첫 번째 핸들은 인스턴스 핸들(instance handle)이다.  인스턴스 핸들은 윈도우에서 현재 실행중인 프로세스 핸들을 가리키는 말로, 이 값은 WinMain()의
첫번째 파라미터로 전달된다.

단계 1. WinMain()의 작성



위의 프로그램이 실행되면 운영체제는 프로세스 인스턴스 블록(process instance block)을 만들고, WinMain()을 호출한다.
그리고 WinMain()의 첫번째 파라미터(HINSTANCE hInstance )로 인스턴스 핸들을 전달한다.
두번째 파라미터(HINSTANCE hPrevInstance)는 이전 프로세스에 대한 인스턴스 핸들이다.

윈도우 3.1시절까지는 같은 프로그램이 두번 실행되더라도 내용이 같은 코드 블록(code block)이 별도로 유지되었다. 윈도우95 시절부터
이러한 비효율적인 구조는 없어졌으나 호환성을 위해 두번쨰 파라미터는 남아 있다. 그러나 사용되지는 않는다.
세 번째로 파리미터는 명령행 인자(commnad line argument)이다.



 네번째 파리미터는 윈도우의 초기 상태를 나타내는 값으로 후에 윈도우를 표시하기 위해
호출할 ShowWindow()의 두번째 파라미터와 역할이 같다.

ㅇ 2단계 윈도우 클래스의 등록

윈도우가 생성하는 모든 윈도우들은 윈도우의 동작과 모양을 정의하는 윈도우 클래스(window class)로 부터 만들어진다.
그러므로 응용 프로그램에서 윈도우를 만들기 위해서는 윈도우를 API함수에 이미 존재하는 윈도우 클래스 구조체의 핸들을 파라미터로 전달해야 한다.
그렇게 하려면 우선 윈도우 클래스 구조체를 만들어서 운영체제에 등록해야 한다.

※ 윈도우 클래스를 C++에서 사용하는 클래스와 혼동하지 말자. 윈도우 클래스에 사용되는 클래스는 분류(class)를 나타내는 일반적 단어이다.


모든  프로그램에서 윈도우 클래스를 등록할 필요는 없다. 윈도우 운영체제가 이미 등록해 놓은 윈도우 클래스만을 사용한다면 윈도우 클래스를 등록하는 과정은 필요없다.
하지만 대부분의 경우 특정한 메시지를 처리하는 자신만의 윈도우 프로시저를 가지는 윈도우 클래스를 사용하므로 윈도우를 만든느 함수를 호출하기 전에 윈도우 클래스
구조체를 만들고 이를 운영체제에 등록하는 일을 먼저 해야 한다.


ㅇ 단계3 윈도우의 생성

 윈도우 클래스가 등록되었으므로 이제 윈ㄷ우 클래스를 기반으로 윈도우를 만들 수 있다. 윈도우 운영체제는 윈도우 클래스를 기반으로 만들어진 각각의 윈도우에 대해서
윈도우 구조체를  유지하며, 윈도우 구조체에 대한 핸들을 윈도우 핸들(window handle)이라고 한다.

윈도우를 만드는 함수는 CreateWindow()로, 이 함수는 새로 만들어진 윈도우의 핸들을 리턴한다.
윈도우와 관련된 API함수를 호출할 때는 항상 이 윈도우 핸들을 파리미터로 전달해야 한다.


ㅇ 4단계 윈도우 를 화면에 나타내기

ShowWindow()를 호출한 이후 프로그램이 즉시 종료된다.
ShowWindow 뒤에 무한 루프를 걸어 줄수도 있지만 그렇게 하면 어떤 작업도 할 수가 없다. ==> 무한 대기
윈도우에 필요한 작업을 하면서 필요한 경우 종료하도록 만들기 위해서 메시지 루프(message loop)를 사용해야한다.

ㅇ 5단계 메시지 루프의 작성

WinMain()의 마지막에는 생성된 윈도우에 전달될 메시지를 처리하는 루프가 존대한다. 이 루프는 응용 프로그램 메시지 큐(applicatino message queue)에서 메시지를
가져와서 윈도우에 메시지를 불러 달라는 요청(dispatch)을 하는 비교적 짧은 문장들로 구성된다.
메시지 루프를 종료하는 것은 프로그램을 종료하는 것을 의미한다.

while  ( GetMessage (&msg, NULL, 0, 0 ) )
{
   DispatchMessage(&msg);
}


ㅇ 6단계 윈도우 프로시저의 구현

일반적으로 윈도우 프로시저의 이름은 WndProc()이다.  윈도우 프로시저는 윈도우에 으해 호출되는 함수이므로 호출 관례로 _stdcall 보다는 CALLBACK으로 적는다.
(WinMain() 처럼 WINAPI를 적어도 무방하지만 CALLBACK으로 적은 이유는 윈도우 프로시저가 윈도우에 의해 호출되는 함수임을 명확히 알 수 있게 하기 위해서 이다.)



ㅇ 자세히 살펴보기

- 큐에 저장되는 메지와 저장되지 않는 메시지
   CreateWIndow()를 호출 했을 때 내부에서 윈도우 프로시저를 호출했다. 그러므로 윈도우 프로시저가 항상 메시지 루프에서만 호출된다는 가정을 해서는 안된다.
즉 메시지 큐에 저장되지 않는 메시지도 있다는 것이다. 이러한 대표적인 예로 CreateWindow()가 발생시키는 WM_CREATE 메시지가 있다.
WM_CREATE는 메시지큐에 넣어서 처리하는 것은 불가능하다. CreateWIndow() 호출에서 직접 윈도우 프로시저를 호출하지 않고, 단지 메시지 쿠에 WM_CREATE 메시지를
넣기만 한다면, WM_CREATE는 윈도우가 생성된 시점이 아닌 이상한 시점에 처리가 될것이기 떄문이다.

- BeginPaint() vs GetDC()

마우스 버튼을 누르면 클라이언트영역의 좌측 상단인(0,0)에서 (100,100)으로 두께가 1픽셀인 검은색 실선을 그리는 작업



GDI 함수의 기본 좌표계는 (윈도우는 매핑 모드(mapping mode)라고 한다.) 클라이언트의 영역의 좌측 상단이 ( 0, 0 ) 이고 아래로 증가하는 y축을 가진다.
MoveToEx()는 GDI의 CP(current point)를 옮긴다. LineTo()는  CP에서 전달된 파라미터의 위치까지 선을 그린다.

WM_PAINT가 아닌 메시지에서 DC를 얻는 API함수는 GetDC()이다. GetDC()가 할당한 DC구조체를 해제하는 함수는 ReleaseDC()다.
이 곳에서 DC를 얻기 위해 BeginPaint()를 사용하면 안된다. 그렇다면 WM_PAINT 메시지를 처리할 떄 GetDC()를 사용하용 안된다.

그이유는??


 메시지의 삭제 : GetMessage()는 메시지 큐에서 메시지를 제거한다. 제거된 메시지는 GetMessage()의 첫 번째 파라미터에 전달되어 넘어온다.

GetMessage()는 대부분의 경우 메시지 큐에서 메시지를 가져오고 큐에서 메시지를 지운다. 하지만 메시지가  WM_PAINT인 경우 GetMessage()는 메시지를 큐에서 지우지
않는다. 큐에서 WM_PAINT 메시지를 지우는 일은 그리기가 시작되는 시점,즉 BeginPaint()에서 한다
이것은 WM_PAINT 메시지를 처리할 때, DC를 얻기 위해 반드시 BeginPaint()를 호출애햐 하는 이유다. 비록 그리기 작업이 필요 없더라도, 아래의 코드는 WM_PAINT 메시지에서
반드시 호출해야 한다.

 case WM_PAINT:  
    hdc = BeginPaint(hWnd, &ps);
    EndPaint( hWnd, &ps );
  return 0;

윈도우 프로시저의 WM_PAINT에서 BeginPaint()를 호출하면 메시지 큐의 WM_PAINT 메시지는 삭제된다.

만약 BeginPaint()가 아닌 GetDC()를 사용하면, DC는 제대로 얻어지지만, GetDC()는 메시지큐에서 WM_PAINT 메시지를 지우지 않는다. 따라서 이 경우, 메시지 루프는 계속해서
메시지 큐에 남아 있는 WM_PAINT 메시지를 꺼내올 것이고, 윈도우 프로시저는 계속해서 WM_PAINT 메시지를 처리할 것이므로 클라이언트 영역을 무한히 계속 그리게 될것이다.




WM_PAINT 메시지의 특별한 동작 : WM_PAINT 메시지가 메시지 큐에 있을 때, GetMessage()는 메시지를 제거하지 않는다.
WM_PAINT 메시지를 제거하는 곳은 BeginPaint() 호출에서다. 그러므로 WM_PAINT 메시지를 처리하는 곳에서는 반드시 BeginPaint()를 호출해야 한다.
그렇지 않으면 윈도우 프로시저는 제거 되지 않는 WM_PAINT 메시지를 계속 받게 된다. 또한 WM_PAINT 메시지가 아닌 곳에서 DC를 얻기 위해 BeginPaint()를
사용하는 것은 메시지 큐에서 WM_PAINT를 지울 수 없으므로 타당한 DC를 얻지 못하게 된다.


이것은 MFC(Microsoft Foundation Class)에서 BeginPaint()를 사용하는 DC와 GetDC()를 사용하는 DC 클래스가 다르게 구현되어야 하는 이유다.


- GetMessage() vs PeekMessge()

 메시지 루프에서 사용한 GetMessage()는 처리해야 할 메시지가 없는 경우, 블록 상태가 된다. 하지만 큐에 메시지가 없는 경우에 즉시 리턴하고 계속해서 다른 일을 해야 하는
경우가 생긴다. 이 때는  PeekMessage()를 사용한다. PeekMessage()는 메시지를 꺼내 올수도 , 보기만 할수도 있으면 두 경우 모두 즉시 리턴된다.

블럭상태란 운영체제가 처리할 일이 없어서 이벤트를 기다리는 상태를 말한다. 블록 상태의 CPU 점유율 0%가 된다.
한 프로그램에서 처리할 메시지가 없는 경우 블록 상태가 되는 것은 멀티태스킹 환경에서 다른 프로그램을 위해 필요한 일이다.
 
밑으 소스를 추가해서 실행시켜보자 (GetMessage 대신에)
while(1)
 {
  if ( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
  {
   if ( msg.message == WM_QUIT) break;
   DispatchMessage( &msg);
  }
  static int i = 0;
  char buffer[8];
  wsprintf( buffer, "%d\n", ++i);
  OutputDebugString( buffer );
 }

프로그램은 Sleep()등을 써서 명시적으로 프로그램을 블록 상태로 만들지 않는 한, CPU를 점유한 상태에서 블록 상태가 되지 않는다.
마지막 줄의 OutputDebugString()은 디버거의 창에 문자열을 출력하는 API함수로 비주얼 C++의 디버거 창에 문자열을 출력한다.

이 메시지 루프를 잘 기억해두기 바란다. 왜냐하면 MFC의 메시지 루프도 이와 비슷한 구조로 되어 있기 때문이다.