본문 바로가기

IT/WindowsProgramming

윈도우(Windows) 애플리케이션(application)의 구조

/* 
written by kaspy (kaspyx@gmail.com)
*/ 


Windows 프로그래밍을 해본 경험이 있는 사람이라면 API 및 Win32 API를 어떤 형태로든 접해본적이 있을 것이다.

API란 Application Programming Interface라는 약어로 윈도 운영체제의 기능을 애플리케이션에서 이용하기 위한 인터페이스로, 그 실체는 C/C++ 언어로 개발한 수천개의 함수의 집합이다.

기초적인 내용이라기보다는 기본적인 내용에 가깝고, 도움이 될만한 중요한것들만 정리해 보았다.


1. Windows 애플리케이션은 이벤트 반응형이다

 Windows의 GUI 애플리케이션을 시작하면 윈도를 출력할 뿐이며, 일반적으로 아무것도 하지 않는다. 메뉴를 선택하거나 마우스 버튼을 클릭하거나 키를 누르는 등의 사용자 조작에 따라 처리해 준다. 이처럼 사용자 조작으로 이벤트가 발생하고, 그때마다 대응되는 이벤트에 처리를 하는 프로그램을 '이벤트 반응형'이라고 한다.

 이벤트 반응형 프로그램이 동작 하기 위해선 이벤트가 발생했음을 알려주는 구조가 필요하다.Windows 에서는 운영체제인 Windows 자신이 장치를 항상 감시하다가 필요에따라 애플리케이션에 통지한다.

 Windows 에서는 감지한 이벤트의 상당수는 특정 애플리케이션을 향해 통지된다. 자체 내부적으로 Windows 메시지 대기열 큐에서 발생한 메시지를 순차적으로 각각의 애플리케이션에 통지하고 각각의 애플리케이션은 메시지를 처리하는 방식을 사용하고있다. 이런 대기행렬을 시스템 큐라고한다.

2. 메시지를 사용해서 이벤트를 통지한다

 Windows 에서 각각의 애플리케이션 윈도우에 이벤트를 통지하려면 '윈도우 메시지(이하 메시지)' 라고 하는 데이터 구조체를 사용하며 그 실체는 아래와 같이 정의 되어있다.

  1. typedef struct tagMSG{
  2.         HWND hwnd;      // 윈도우 핸들
  3.         UINT message;   // 메시지 ID
  4.         WPARAM wParam;  // 메시지 전달 인자 1
  5.         LPARAM lParam;  // 메시지 전달 인자 2
  6.         DWORD time;     // 이벤트 발생 시각
  7.         POINT pt;       // 이벤트 발생시 커서위치
  8. } MSG, *PMSG;

 첫번째 hwnd 라는 멤버는 메지지의 행선지(받을 윈도우)를 식별하기 위한 정수값이다. 애플래케이션이 여러개 띄워져 있는경우 어떤 윈도우인지를 알기위한 식별자인 셈이다.

 두번째, message라는 멤버는 메시지의 종류를 나타내는 ID이다. 일반적인 메시지는 winuser.h 안에 'WM_' 으로 시작하는 심볼형태로 수백개가 정의되어있다. 발생한 이벤트의 종류를 알아내려면 이 값을 보면된다.

 세번째, 네번째 wParam, lParam은 message에 대한 추가적인 정보인데, 메시지의 종류에따라 의미가 다르다. 예를들어 마우스의 왼쪽 버튼이 눌렸을때 받는 메시지(WM_LBUTTONDOWN)에서는 마우스 위치와 Shift 키나 Alt 키 등의 상태가 담겨오고, 키보드가 눌렸을때의 메시지(WM_KEYDOWN)일때는 쿨린 키의 종류를 나타내는 값이 저장되어진다.

 그리고 나머지 2개 time과 pt는 각각 이벤트가 발생했을 때의 시각과 그 순간의 마우스 커서위치를 나타낸다.

3. 윈도우 프로시저를 통해 메시지를 처리한다

 메시지 큐에서 꺼내온 메시지는 그 행선지가 되는 윈도우에 전송되어 거기서 처리된다. 메시지의 행선지가 윈도우 이므로 행선지 윈도우 안에서 처리해야 하는 것이다. 실제 Windows에서는 윈도우에 관련된 특별 함수인 '윈도우 프로시저'에서 메시지를 처리한다

 윈도우 프로시저의 프로토타입은 아래와 같다.

  1. LRESULT CALLBACK WindowProc (
  2.         HWND hwnd,      // 윈도우 핸들
  3.         UINT uMsg,      // 메시지 ID
  4.         WPARAM wParam,  // 메시지 전달인자 1
  5.         LPARAM lParam   // 메시지 전달인자 2
  6. );

 여기서 윈도우 프로시저는 각 윈도우에 연관된 함수인데, 왜 윈도를 식별하는 핸들이 인자로 필요한 것일지 궁금해할지도 모르겠다. 이는 윈도우 프로시저에서 담당하는 윈도우가 반드시 하나라고만 단정할수 없기 때문이다. 예를들어, 애플리케이션에서 동일한 행동을 하는 윈도우 2개를 만들수도 있다. ex 한글 및 엑셀 같은 애플리케이션은 하나의 윈도 안에 자식 윈도가 여러개있다.

 이때문에 Windows에서는 각각의 윈도우와는 별도로 '윈도우 클래스'를 제공한다. 윈도우 클래스는 윈도우 프로시저를 포함해서 일부 공통된 성질을 그룹화 한것이다. 즉 윈도우 클래스는 직접 등록하는것이 일반적이고, 애플리케이션은 메지시 큐에서 메시지를 하나씩 꺼내서 윈도우 프로시저에 전송한다. 그리고 윈도우 프로시저에서는 이벤트에 대응하는 처리가 수행된다.

4. WinMain 함수에서 실행을 시작한다

 아래는 Windows 애플리케이션의 전체 소스코드로, 실행하면 회색 윈도 하나를 출력한다. 윈도 애플리케이션이라면 아래와 같은 구조를 따른다고 보면 되겠다.

  1. #include <Windows.h>
  2.  
  3. // 윈도우 클래스명
  4. #define MYWNDCLSNAME "MyWindowClass"
  5.  
  6. LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
  7.  
  8. int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
  9. {
  10.         WNDCLASS wndcls;
  11.         HWND hWnd;
  12.         MSG msg;
  13.  
  14.         // 윈도우 클래스 등록
  15.         ZeroMemory(&wndcls,sizeof(wndcls));
  16.         wndcls.lpfnWndProc = WndProc;
  17.         wndcls.hInstance = hInst;
  18.         wndcls.hIcon = LoadIcon(0,IDI_APPLICATION);
  19.         wndcls.hCursor = LoadCursor(0, IDC_ARROW);
  20.         wndcls.hbrBackground = (HBRUSH) COLOR_BACKGROUND;
  21.         wndcls.lpszClassName = MYWNDCLSNAME;
  22.         if (RegisterClass(&wndcls) == 0) return -1;
  23.  
  24.         // 메인 윈도우 생성
  25.         hWnd = CreateWindow(MYWNDCLSNAME, "My WIndow",
  26.                 WS_OVERLAPPEDWINDOW,
  27.                 CW_USEDEFAULT, CW_USEDEFAULT,
  28.                 CW_USEDEFAULT, CW_USEDEFAULT,
  29.                 0, 0, hInst, NULL);
  30.         if (hWnd == 0) return -2;
  31.  
  32.         // 윈도우 초기상태 지정
  33.         ShowWindow(hWnd, nCmdShow);
  34.         UpdateWindow(hWnd);
  35.  
  36.         // 메시지 루프
  37.         while(GetMessage(&msg, 0,0,0))
  38.         {
  39.                 DispatchMessage(&msg);
  40.         }
  41.  
  42.         // WM_QUIT 메시지의 wParam을 종료코드로 한다.
  43.         return msg.wParam;
  44. }
  45.  
  46. LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
  47. {
  48.         switch(uMsg)
  49.         {
  50.         case WM_DESTROY:
  51.                 PostQuitMessage(0);
  52.                 return 0;
  53.         }
  54.  
  55.         // 직접 처리하지 않은 메시지는 Windows에게 맡긴다.
  56.         return DefWindowProc(hWnd, uMsg, wParam, lParam);
  57.  
  58. }

 예제의 1번 라인, 앞 부분에는 Wndows.h라고 하는 헤더파일을 인클루드(include)한다. 이헤더파일은 Platform SDK에 포함된 헤더파일로, Win32 API를 C언어 함수에서 호출할때 필요한 프로토타입 선언이 들어있다. 따라서 Win32 API를 사용한 프로그램을 작성할때는 반드시 필요하다.

8번 라인은 WinMain이라는 함수로 프로그램 실행시 최초 실행되는 함수이다. C언어를 배운사람이라면 main() 함수라고 생각하면 된다. WinMain 함수의 인자들을 설명하자면, "hInst" 라는 실행중인 애플리케이션을 식별하는 정수값(인스턴스 핸들)을 받는다. 자주 쓰이는 인자는 아니지만 애플리케이션 내부적으로 필요한 경우가 많으므로 전역변수로 저장해두는 경우가 많다.

 두번째 인자인 hPrevInst도 인스턴스 핸들이지만, Windows3.1과 16비트 Windows와 호환을 위해 남아있다. 보통 0으로 저장되어있고 사용할 일이 없을 것이다. 세번째 인자 lpCmdLine은 명령행 문자열의 포인터이다. main함수의 argv와는 달리 전달인자마다 분리될수 있는 형태가 아니기 떄문에 필요하다면 스스로 문자열을 해석해야한다. CLI 기반이라면 쓸수도 있겠지만, GUI기반 애플리케이션인만큼 이 인자도 자주 쓸일은없다.

 네번째 인자인 nCmdShow는 애플리케이션이 띄우는 메인 위도우의 최초 출력상태를 지정해준다. 이값에 따라 윈도우를 HIDE할수도 있다. 보통 디폴트값을 지정해준다.

5. 윈도우 클래스를 등록해 윈도우를 생성한다

 위에 윈도 예제 소스를 가지고 이어서 설명하도록 하겠다. 22번 라인 을 보면일단 RegisterClass API를 사용해서  윈도우 클래스를 등록한다는 볼수있을 것이다. 등록 하려는 윈도우 클래스 정보를 저장한 WNDCLASS 구조체를 준비해서 인자에 그 주소를 넘겨주면된다. WNDCLASS에 대한 구조체는 아래와 같다.

  1. typedef strcut _WNDCLASS{
  2.         UINT style;     // 윈도우 클래스 스타일
  3.         WNDPROC lpfnWndProc;    //윈도우 프로시저의 주소
  4.         int cbClsExtra;         //윈도우 클래스의 추가 데이터크기
  5.         int cbWndExtra;         //윈도우 추가 데이터 추기
  6.         HINSTANCE hInstance;    // 윈도우 프로시저를 소유한 프로그램의 인스턴스 핸들(대부분 자신)
  7.        
  8.         HICON hIcon;            // 애플리케이션의 아이콘
  9.         HCURSOR hCursor;        // 마우스 커서
  10.         HBRUSH hbrBackground;   // 윈도우 배경색
  11.         LPCTSTR lpszMenuName;   // 메뉴 리소스명
  12.         LPCTSTR lpszClassName;  // 이 윈도우의 클래스 등록명
  13. } WNDCLASS, *PWNDCLASS;

25번 라인은 메인 윈도우 생성이다. CreateWindow API를 사용하는데, 각 인자는 아래와 같다.

  1. HWND CreateWindow(              // 리턴값: 생성된 윈도우 핸들
  2.         LPCTSTR lpClassName,    // 윈도우 클래스명
  3.         LPCTSTR lpWindowName,   // 윈도우 이름
  4.         DWORD dwStyle,          // 윈도우 스타일
  5.         int x,                  // 출력할 윈도우의 좌측 상단 x좌표
  6.         int y,                  // 출력할 윈도우의 좌측 상단 y좌표
  7.         int nWidth,             // 수평방향 크기
  8.         int nHeight,            // 수직방향 크기
  9.         HWND hWndParent,        // 부모 윈도우 핸들
  10.         HMENU hMenu,            // 메뉴 핸들
  11.         HINSTANCE hInstance,    // 윈도우를 생성하는 프로그램의 인스턴스 핸들
  12.         LPVOID lpParam          // WM_CREATE 메시지에 넘길 전달 인자
  13. );

이함수는 윈도우 생성에 성공하면 윈도우 핸들을 반환해준다. 4~7 번째 인자에는 CW_UESDEFAULT 라는 상수값이 지정되어있는데, 이는 윈도우 출력 크기를 Windows에게 맡긴다라는 의미이다. 또한 부모 윈도우와 메뉴 핸들은 0을 넘기면된다.(부모 윈도가 없으므로, 메뉴도 마찬가지)

 33번,34번 라인은 윈도우를 출력해주는 함수로 nCmdShow 라는 값을 넘겨서 윈도우가 보일수 있게 출력해주고 UpdateWindow라는 함수가 윈도우의 변화가 있을경우에 실제 그려주는 API이다.

6. 메시지 루프로 메시지를 하나씩 꺼낸다

 37번 라인 이어서 윈도우를 출력했으면 중요한 메시지 루프를 볼 차례이다. while 문안에서 호출되는 GetMessage는 메시지 큐에서 메시지를 꺼내는 API이다. 첫번째 인자인 MSG 구조체는 수신한 메시지를 저장하는 주소이다. 두번쨰 이후의 인자에는 특정 종류의 메시지만 꺼낼때의 메시지 범위를 지정한다. 보통 0을 지정한다.

 GetMessage의 반환값 형태는 보통 BOOL이며, 꺼낸 메시지가 애플리케이션의 종류를 의미하는 WM_QUIT 인 경우에만 FALSE를 반환한다. 따라서 예제 소스는 WM_QUIT 메시지(종료버튼을 눌렀을때 발생)를 받을때까지 루프가 계손돈다. 39번 라인은 DispatchMessage라는 API 하나만 호출되는데 이는 윈도우 프로시저를 MSG 구조체의 hwnd 멤버를 참조해서 윈도우 프로시저를 호출하도록 해주는 API이다. 메시지 처리를 WndProc에서 하게끔 자동으로 지정해주는 함수라고 보면된다.

7. 메시지의 대부분은 DefWindowProc 함수에 처리를 맡긴다.

 이어서 46번 라인 윈도우 프로시저 함수를 보겠다. 설명했듯이 윈도우 프로시저는 메시지를 처리해주는 함수이다. 16번 라인에서 윈도우를 등록하기전에 프로시저 함수를 지정해준것을 확인할수있다. 단 메시지 처리 방식은 메시지 종류별로 case로 처리하면 되는것이고, 나머지 함수들에 대해서는 DefWindowProc에 이벤트를 넘겨서 처리해주면된다.

 예제는 WM_DESTROY 라는 메시지만 처리하고 나머지는 DdefWindowProc을 호출하여 처리를 맡긴다.


* 참고자료 및 서적

API로 배우는 Windows 구조와 원리

* 다음 링크 참조

http://kaspyx.tistory.com/entry/윈도우Window와-메시지Window-Message