본문으로 바로가기

4-1-라. TranslateMessage


키보드에서 A키를 눌렀다 뗐다고 해 보자. 이 때 발생하는 메시지는 순서대로 WM_KEYDOWN, WM_CHAR, WM_KEYUP 세가지이다. 이 중 WM_CHAR 메시지는 사용자에 의해 발생하는 메시지가 아니다. 키보드로부터 전달되는 메시지는 키를 누를 때 WM_KEYDOWN, 키를 뗄 때 WM_KEYUP 두가지뿐이다. 그럼 WM_CHAR 메시지는 어디서 발생할까? 이 메시지는 WM_KEYDOWN에 의해 추가로 발생하는 메시지이며 메시지 루프에서 인위적으로 생성된다. Key 프로젝트를 연 후 메시지 루프를 다시 보자.

while(GetMessage(&Message,0,0,0)) {
	TranslateMessage(&Message);
	DispatchMessage(&Message);
}

GetMessage는 메시지 큐에서 메시지를 꺼내온 후 이 메시지를 TranslateMessage 함수로 넘겨 준다. TranslateMessage 함수는 전달된 메시지가 WM_KEYDOWN인지와 눌려진 키가 문자키인지 검사해 보고 조건이 맞을 경우 WM_CHAR 메시지를 만들어 메시지 큐에 덧붙이는 역할을 한다. 물론 문자 입력이 아닐 경우는 아무 일도 하지 않으며 이 메시지는 DispatchMessage 함수에 의해 WndProc으로 보내진다. 만약 메시지 루프에서 TranslateMessage 함수를 빼 버리면 WM_CHAR 메시지는 절대로 WndProc으로 전달되지 않을 것이다. 과연 그런지 Key예제에서 이 함수 호출문을 주석으로 묶은 후 직접 실행해 보기 바란다.



- 큐에 ㄸㅎ 킳 만들어 진다라는 뜻이다. pDown이면 새로운 키를 또 만든다라는 뜻이다.

우리가 키를 생성하는게 아니고 TranslateMessage 가 생성해 준다라는 뜻이다.


DispatchMessage 키 다운을 하려 한다면 결론적으로 디스 패치까지 거쳐서 가야 한다라는 뜻이다.



4-2-가. Mouse

윈도우즈와 같은 GUI운영체제에서는 키보드보다 마우스가 더 많이 사용된다. 윈도우즈의 공식 입력 장치는 키보드이지만 그래픽 툴이나 DTP, CAD 등의 복잡한 프로그램에서는 마우스가 주요 입력 장치로 사용된다. 여기서 마우스라고 함은 진짜 쥐새끼처럼 생긴 마우스는 물론이고 노트북의 터치패드, 트랙볼과 출판용 타블릿 등을 모두 포함하는 명칭이다. 키보드 입력 처리를 메시지로 하는 것과 마찬가지로 마우스 입력 처리도 메시지를 받아 처리한다. 마우스 입력에 관한 메시지는 다음과 같은 종류가 있다.

버튼누름놓음더블클릭
좌측WM_LBUTTONDOWNWM_LBUTTONUPWM_LBUTTONDBLCLK
우측WM_RBUTTONDOWNWM_RBUTTONUPWM_RBUTTONDBLCLK
중앙WM_MBUTTONDOWNWM_MBUTTONUPWM_MBUTTONDBLCLK

버튼 세 개에 각각 누름, 놓음, 더블 클릭의 9가지 메시지가 있다. 이 중 중앙 버튼은 실질적으로 거의 사용되지 않으며 주로 왼쪽 버튼의 메시지가 많이 사용된다.

마우스 메시지는 lParam의 상위 워드에 마우스 버튼이 눌러진 y좌표, 하위 워드에 x좌표를 가지며 좌표값을 검출해 내기 위해 HIWORD, LOWORD 등의 매크로 함수를 사용한다. 즉 마우스 메시지가 발생한 위치의 좌표는 (LOWORD(lParam), HIWORD(lParam))이 된다.

- 우리 창 안에서의 좌표 정보. 전체적인 정보가 아니다.

-lParam 다썼으니 wParam을 쓴다면 안 눌려진 상태에서 이동하는지 눌려진 상태에서 이동하는지 알 수 있다.(밑에 나온다.)


wParam에는 마우스 버튼의 상태와 키보드 조합 키(Shift, Ctrl)의 상태가 전달된다. 조합키 상태는 다음 값들과 비트 연산을 해보면 알 수 있다.

설명
MK_CONTROLCtrl 키가 눌러져 있다.
MK_LBUTTON마우스 왼쪽 버튼이 눌러져 있다.
MK_RBUTTON마우스 오른쪽 버튼이 눌러져 있다.
MK_MBUTTON마우스 중간 버튼이 눌러져 있다.
MK_SHIFTShift 키가 눌러져 있다.

마우스 키의 누름 메시지 외에 마우스가 이동할 때마다 전달되는 WM_MOUSEMOVE 메시지가 있다. 이 메시지도 다른 마우스 메시지와 마찬가지로 lParam에 마우스 커서의 위치가 전달되며 wParam에 조합키 상태가 전달된다.

페인트 브러시와 같이 마우스 입력을 받아 작업 영역에 곡선을 그리는 예제를 만들어 보자. 마우스 버튼의 누름, 놓음, 이동 세 가지 메시지에 대한 코드만 작성해 주면 된다. 마우스 버튼이 눌러지면 그리기를 시작하고 마우스 커서의 이동에 따라 LineTo 함수로 선을 긋기만 하면 되는 아주 간단한 예제이다.

#include <windows.h>

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass="Mouse";

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance
		  ,LPSTR lpszCmdParam,int nCmdShow)
{
	HWND hWnd;
	MSG Message;
	WNDCLASS WndClass;
	g_hInst=hInstance;
	
	WndClass.cbClsExtra=0;
	WndClass.cbWndExtra=0;
	WndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
	WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);
	WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
	WndClass.hInstance=hInstance;
	WndClass.lpfnWndProc=(WNDPROC)WndProc;
	WndClass.lpszClassName=lpszClass;
	WndClass.lpszMenuName=NULL;
	WndClass.style=CS_HREDRAW | CS_VREDRAW;
	RegisterClass(&WndClass);

	hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,
		  CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
		  NULL,(HMENU)NULL,hInstance,NULL);
	ShowWindow(hWnd,nCmdShow);
	
	while(GetMessage(&Message,0,0,0)) {
		TranslateMessage(&Message);
		DispatchMessage(&Message);
	}
	return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	static int x;
	static int y;
	static BOOL bnowDraw=FALSE;
	switch(iMessage) {
	case WM_LBUTTONDOWN:
		x=LOWORD(lParam);
		y=HIWORD(lParam);
		bnowDraw=TRUE;
		return 0;
	case WM_MOUSEMOVE:
		if (bnowDraw==TRUE) {
			hdc=GetDC(hWnd);
			MoveToEx(hdc,x,y,NULL);
			x=LOWORD(lParam);
			y=HIWORD(lParam);
			LineTo(hdc,x,y);
			ReleaseDC(hWnd,hdc);
		}
		return 0;
	case WM_LBUTTONUP:
		bnowDraw=FALSE;
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

현재 마우스 위치를 저장할 변수 x,y와 현재 선을 그리고 있는 중인지를 저장할 변수 bnowDraw를 선언한다. 그리고 왼쪽 마우스 버튼이 눌러지면(WM_LBUTTONDOWN 메시지가 발생하면) 버튼이 눌러진 위치의 좌표를 x,y변수에 대입해 주고 마우스가 움직이면 시작점 (x,y)에서 마우스가 움직이고 있는 위치까지 선을 긋는다. 그리고 마우스 버튼이 놓아지면 bnowDraw를 FALSE로 변경하여 마우스가 움직여도 선이 그려지지 않도록 한다.

선을 그릴 때마다 일일이 DC 핸들을 발급받는 것 외에는 도스 프로그램과도 크게 틀리지 않으며 앨거리듬 자체가 아주 간단하다. WM_PAINT 메시지 이외의 부분에서 화면에 출력을 해야할 때는 GetDC 함수를 호출하여 DC 핸들을 발급받아야 함을 유의하자. 실행시의 모습은 다음과 같다.

마우스가 움직이는 대로 선이 그려질 것이다. 이 예제는 그림을 그리기는 하되 WM_PAINT에 관한 처리는 하지 않고 있으므로 그려진 그림은 언제든지 지워질 가능성이 있다. 즉 화면에 그림을 그리기는 해도 복구 능력이 없다. 이 문제를 해결하려면 그려지는 그림을 일일이 저장해 두어야 하는데 현재 단계에서는 그 방법을 논하기 어렵다. 화면 전체를 비트맵으로 저장하든가 아니면 연결 리스트를 사용하여 마우스의 움직임을 일일이 보관해 두어야 한다.

참고

지금까지 몇가지 메시지들을 공부해 보면서 각 메시지별로 wParam, lParam에 어떤 부가 정보가 전달되는지를 보았다. 이 정보들은 메시지별로 의미가 정해져 있고 전달해야 할 정보가 많은 어떤 메시지들은 상위, 하위 워드에 나누어 보내기도 한다. 어쨋든 둘 다 32비트값이므로 메시지가 전달할 수 있는 부가 정보는 총 64비트가 된다. 그런데 16비트 윈도우즈에서는 이와 달랐다. Win32에서는 둘 다 32비트이지만 Win16에서 wParam은 16비트였고 lParam만 32비트였었다. wParam의 w는 WORD라는 뜻이며 lParam의 l은 LONG이라는 뜻으로 인수의 길이를 나타내었다. 지금은 둘 다 32비트이므로 w, l 문자는 더 이상 길이가 아닌 단순한 이름일 뿐이다.





LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)

{

HDC hdc;

static int x;

static int y;

static BOOL bnowDraw = FALSE;

switch (iMessage) {

case WM_LBUTTONDOWN:

x = LOWORD(lParam);

y = HIWORD(lParam);

bnowDraw = TRUE;

return 0;

case WM_MOUSEMOVE:

if (bnowDraw == TRUE) {

hdc = GetDC(hWnd);

MoveToEx(hdc, x, y, NULL);

x = LOWORD(lParam);

y = HIWORD(lParam);

LineTo(hdc, x, y);

ReleaseDC(hWnd, hdc);

}

return 0;

case WM_LBUTTONUP:

bnowDraw = FALSE;

return 0;

case WM_DESTROY:

PostQuitMessage(0);

return 0;

}


return(DefWindowProc(hWnd, iMessage, wParam, lParam));

}


x = LOWORD(lParam);

y = HIWORD(lParam);


이부분은 LOWORD, HIWORD 매크로 함수다. 우리도 만들 수 있다.

가로가 65000개 까지가 윈도우의 한계다. 그 lParam, 즉 LONG 형을 4바이트가 아니고 8바이트로 사용한다면 40억을 사용할 테니까

언젠가는 이런식으로 갈아 엎을것이다.



- WM_LBUTTONDOWN 눌렀을 때, (그 지점의) 이 상태에서 움직이면 WM_MOUSEMOVE 이게 불려지게 된다.

마우스를 움직이면 그 점의 x, y를 추출하여 그 두개의 선을 그어 버린다. 우리가 마우스 움직일 때마다 선이 따라 다닌다고 느끼게 된다.

 이것을 실행하려면 if 문 안으로 들어와야 하는ㄴ데

if (bnowDraw == TRUE) {

hdc = GetDC(hWnd);

MoveToEx(hdc, x, y, NULL);

x = LOWORD(lParam);

y = HIWORD(lParam);

LineTo(hdc, x, y);

ReleaseDC(hWnd, hdc);

}

이처럼 엘버튼을 누른 상태 였을때 TRUE 가 되어 해당 명령을 실행(x, y 추출. 줄 긋기) 하게 된다.

실제로는 선을 긋고 있는 것이고 우리는 글자를 쓰고 있다라고 느끼는 것이다.

자세히 보면 현재 엘 버튼이 누르든 안 누르든 WM_MOUSEMOVE 는 무조건 호출이 되게 된다. 그리하여 Flag 변수로 bnowDraw 로 if 문을 만들어 놓은 것이다.



* 유니코드 처리

 c타입유니코드 타입 
 charTCHAR 
 char* LPSTR
 const char* LPCTSTR

120619winapi.zip

 c표준함수유니코드 지원함수 
strlen lstrlen 
 strcpy lstrcpy
 strcat lstrcat
 strcmp lstrcmp
 sprinft lsprintf

- 유니코드 문자열 표현
TCHAR *str = "string";  (X) - Ansi 타입
TCHAR *str = TEXT("string"); (O) -유니코드 타입


lsprintf 가 아닌 wsprintf 를 사용해서 소스를 완성 시키면


#include <windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"TextOut";

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance
  , LPSTR lpszCmdParam, int nCmdShow)
{
  HWND hWnd;
  MSG Message;
  WNDCLASS WndClass;
  g_hInst = hInstance;

  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = NULL;
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000)) {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }
  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  static int x;
  static int y;
  static WCHAR wcBuffer[100];

  switch (iMessage)
  {
    case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      SetTextAlign(hdc, TA_CENTER);
      TextOut(hdc, 20060, wcBuffer, lstrlen(wcBuffer));
      EndPaint(hWnd, &ps);
      return 0;

    case WM_MOUSEMOVE:
      x = LOWORD(lParam);
      y = HIWORD(lParam);
      wsprintf(wcBuffer, L"x는 %d, y는 %d이다.", x, y);  //s:메모리에 출력한다, ls:유니코드 지원함수이다.
      InvalidateRect(hWnd, NULL, TRUE);
      return 0;

    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}


이와 같은 모습이 되며 실행 화면은

이와 같이 된다. textOut은 우리가 도스창 프로그래밍을 했을 때와 마찬가지로 printf, 즉 OutPut 쪽을 담당하게 된다.

textOut의 함수 원형을 살펴보자.


BOOL TextOut(hdc, nXStart, nYStart, lpszString, cbString) 


첫 번째 인자는 윈도우 핸들러가 적용되며, HWDC 형이

HDC hdc;

PAINTSTRUCT ps;

.

.    

hdc = BeginPaint(hWnd, &ps);

이처럼 BeginPaint()로 부터 반환된 변수로 만들어진다.



자 

case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      SetTextAlign(hdc, TA_CENTER);
      TextOut(hdc, 20060, wcBuffer, lstrlen(wcBuffer));
      EndPaint(hWnd, &ps);
      return 0;


이 부분을 분석해보자면


1. BeginPaint() 로 드로잉 작업을 수행하기 위해서 CWnd를 준비한다.

윈도우즈 환경에서 화면이나 프린터로 출력을 하려면 DC를 먼저 구해야 한다. DC를 구하는 일반적인 방법은 두 가지가 있는데 GetDC와 ReleaseDC를 사용하는

방법이 있고 BeginPaint() 와 EndPaint() 를 사용하는 방법이있다. BeginPaint() 와 EndPaint() 는 짝을 이루어 사용되며 반드시 WM_PAINT 메시지 내부에서만 사용해야 한다.

2. SetTextAlign() 로 텍스트 정렬 플래그 값을 설정한다. TextOut 함수가 지정하는 좌표는

디폴트로 문자열 출력 영역의 좌상단 좌표이다. 이 함수는 문자열의 출력 영역과 출력 좌표와의 관계를 변경함으로써 문자열의 출력 위치에 영향을 준다.

함수 원형 

: UINT SetTextAlign(HDC hdc, UINT fMode);

3. TextOut() 로 현재의 선택된 폰트를 사용하여 지정된 위치에 문자열을 출력한다. 이때 출력 좌표는 (nXStart, nYStart)이 되 이 좌표는 SetTextAlign() 가 설정한 정렬 상태에 영향을 받는다.

출력할 문자열의 색상은 SetTextColor(), SeBkColor(), SetBkMode() 의 영향을 받는다.

함수 원형

BOOL TextOut(hdc, nXStart, nYStart, lpszString, cbString) 

즉, 좌표를 설정했으니 윈도우의 x : 200, y : 60 위치에 wcBuffer 의 내용을 출력하게 된다. 이 때 wcBuffer의 내용을 집어넣는 부분은

 case WM_MOUSEMOVE:
      x = LOWORD(lParam);
      y = HIWORD(lParam);
      wsprintf(wcBuffer, L"x는 %d, y는 %d이다.", x, y);  //s:메모리에 출력한다, ls:유니코드 지원함수이다.
      InvalidateRect(hWnd, NULL, TRUE);
      return 0;

이 부분에 존재하며 메모리에 2바이트 정수형태, 즉 유니코드 형태로 메모리에 가운데 인자의 문자열을 집어넣고 있다. 위에서도 언급했듯이

마우스가 움직일 때, 라는 저 부분은 언제나 우리가 마우스를 잡고있는 동안 발동 되므로 한 번은 무조건 실행 되서 저 값이 메모리에 들어간 뒤에 

쓰레기 값이 아닌 저 값을 출력하게 될 것이다.

4. EndPaint() 로 드로잉 작업을 마친다. BeginPaint() 와 EndPaint() 는 반드시 짝을 이루어 사용되며 반드시 WM_PAINT 메시지 내부에서만 사용 되어야 한다.

WM_PAINT 함수 내에서 그리기를 종료하기 위해 사용되며 BeginPaint가 캐럿을 숨겼을 경우 캐럿을 다시 화면으로 출력해 주는 역활을 한다.

이 때 나오는 캐럿이라는 용어는 중급 API 에서 거론 되며 캐럿 쪽의 함수들이 따로 존재한다. 나중에 더 공부해 보면 이해할 듯하다.

함수 원형

: BOOL EndPaint(HWND hWnd, CONST PAINTSTRUCT *IpPaint);




 case WM_MOUSEMOVE:
      x = LOWORD(lParam);
      y = HIWORD(lParam);
      wsprintf(wcBuffer, L"x는 %d, y는 %d이다.", x, y);  //s:메모리에 출력한다, ls:유니코드 지원함수이다.
      InvalidateRect(hWnd, NULL, TRUE);
      return 0;


자 이번엔 이것을 분석 해 보자.


1. WM_MOUSEMOVE는 wParam : 키보드와 다른 마우스 버튼의 현재 상태를 나타내는 값이며 다음 플래그들의 조합값이다.

LOWORD(lPARAM), HIWORD(lParam) : 마우스의 현재 x,y 좌표이다. 이 좌표는 작업 영역의 원점을 기준으로 한 좌표이다.

대부분의 경우 좌표는 양수 값이지만 캡쳐된 특수한 상황에서는 음수일 수도 있는데 이 경우 반드시 (int)형으로 다시 한번 더 캐스팅 해 주어야 부호를

제대로 얻을 수 있다.

마우스가 움직일 때 이 메시지가 메시지 큐에 붙여진다. 마우스가 캡쳐되어 있으면 캡쳐한 윈도우로 이 메시지가 전달되며 그렇지 않을 경우 커서 아래쪽에

있는 윈도우가  이 메시지를 받는다. 마우스가 계속 작업 영역 위에서 움직이고 있으면 이 메시지는 반복적으로 계속 전달된다.

2. LOWORD, HIWORD 란. 

DWORD(4byte) 의 상위 2바이트, 하위 2바이트를 알기 위한 메크로

   

HIWORD (2byte)

LOWORD (2byte)

|--------------------- 4 byte -----------------------|

[출처] HIWORD, LOWORD|작성자 Yun

이다. 

HIWORD

   

#define HIWORD(1) ((WORD)(((DWORD)(1) >> 16) & 0Xfff))

   

   

0x12345678

0x00001234

0x0000FFFF -> 11111111

-------------------

0x00001234 byte ordering(Little)

34 12 00 00 ------> 상위 2바이트를 가리킨다.

   

 

   

LOWORD

   

#define LOWORD(1) ((WORD)(1))

[출] HIWORD, LOWORD|작성자 Yun

이와 같이 정의 되어 있다. 즉, 4바이트 long 형 변수를 상위, 하위 바이트로 나누어 각각 65536의 좌표값 x,y로 쓰고 있다라는 뜻이다. 

3. wsprintf() 는 앞에서 언급했듯이 lsprintf() 즉, 유니코드 값을 메모리에 집어 넣기 위한 printf() 이다.

저도 큰차이점을 잘 이해가 안갔는데요..

제가 느낀점을 말해보겠습니다.

먼저 wsprintf과 sprintf는 사용 포맷의 차이점이 있습니다.

wsprintf는 도스(콘솔모드)에서는 지원을 안하는 것 같구요 주로 API나 MFC를 

출력하고 싶을 때 사용합니다.

sprintf또한 char array에 똑같이 집어넣는 것이 아니냐고 말할 수 있는데..

맞습니다. 그러나 여기에서 중요한 float형의 출력을 할 수 있구요 C에서 사용할 

수 있는 함수입니다.

wsprintf -> windows.h
sprintf -> stdio.h인것이지요..



즉, wsprintf() 는 API 에서 사용하는 것이고, sprintf()는 도스 쪽에서 사용하는 것이라 보면 되겠다.

4. InvalidateRect() 는 현재의 갱신 영역에 사각형을 추가함으로써 정해진 사각형 내의 클라이언트 영역을 무효화한다.

운영체제는 윈도우의 작업 영역중 일부에 무효 영역이 있으면 WM_PAINT 메시지를 보내 다시 그리도록 한다. 

프로그램 내부에서 작업 영역을 변경한 경우 이 함수로 변경된 부분을 무효화 해 주어야 WM_PAINT 메시지가 발생하며 화면이 다시 그려진다.

배경을 지우고 그려야 할 때는 bErase를 TRUE로 설정해 주어야 이전에 출력되어 있던 내용이 삭제되고 다시 그려진다.

그리기 속도를 최대한 빠르게 하려면 lpRect에 무효화 할 최소한의 영역을 지정하여 꼭 필요한 부분만 다시 그리도록 해 주어야 한다.

InvalidateRect(hWnd, NULL, TRUE);

현재 bErase 부분을 TRUE로 설정해 주며 무효화 영역을 가장 빠르게 지우고 그리는 것을 유도하고 있다. 나머지 인자는 좀더 공부하면서 다시 보도록 하자.

함수 원형

: BOOL InvalidateRect(HWND hWnd, CONST RECT *lpRect, BOOL  bErase);




case WM_DESTROY:
      PostQuitMessage(0);
      return 0;


1. WM_DESTROY 는 윈도우가 파괴될 때 이 메시지가 전달된다. 사용자가 Alt+F4 또는 닫기 버튼을 누를 경우 WM_CLOSE 메시지가 전달되며 이 메시지를

별도로 처리하지 않으면 DefWindowProc은 DestroyWindow함수를 호출하여 윈도우를 파괴한다. 또는 프로그램 코드 내부에서 명시적으로

DestroyWindow 함수를 호출 할 때도 윈도우가 파괴되는데 이 함수 호출 결과로 WM_DESTROY 메시지가 전달된다.

 여기서 언급된 DefWindowProc 이란 함수는 윈도우 프로시저가 처리하지 않은 메시지의 디폴트 처리를 한다. WndProc은 원하는 메시지를 처리하고

자신이 처리하지 않은 메시지는 이 함수에게 전달하여 디폴트 처리를 하도록 해 주어야 한다.

 이 메시지를 받은 윈도우는 윈도우의 종료를 위한 처리를 해야 하는데 예를 들어 열어 놓을 파일을 닫고, 할당한 메모리를 해제하는 등의 정리 작업을 한다.

WM_CREATE 에서의 초기화 처리의 반대 동작이 이 메시지에 작성되는 것이 일반적이며 그 외 레지스트리에 미 보관 정보를 저장하는 등의 작업을 할 수 있다.

만약 파괴되는 윈도우가 클립보드 체인에 속해 있으면 자신을 클립보드 체인에서 제거해야 한다.

 DestroyWindow 함수는 파괴할 윈도우를 화면에서 숨긴 후 이 메시지를 보내므로 이 메시지를 받은 시점에서는 윈도우 자체가 파괴되지 않은 상태이다.

또한 DestroyWindow 함수는 자식 윈도우에게도 이 메시지를 차례대로 보내주는데 부모 윈도우가 먼저 이 메시지를 받고 자식 윈도우에게로 이 메시지가

보내진다. ㄸ라서 부모 윈도우가 이 메시지를 처리하는 동안은 모든 자식 윈도우가 아직 파괴되기 전이므로  자식 윈도우를 프로그래밍 할 수 있다.

 파괴되는 윈도우가 메인 윈도우 일 경우 PostQuitMessage 함수를 반드시 호출하여 프로세스의 메시지 루프를 종료하도록 해야 한다.

만약, 이 처리를 생략하면 윈도우만 파괴되고 메시지 루프는 계속 실행중인 상태가 되므로 프로세스가 종료되지 않는다.

2. 1.에서 언급됬듯이 파괴되는 윈도우가 메인 윈도우일 경우, 즉 우리가 컴파일 한 새로운 윈도우 화면인 경우는 PostQuitMessage() 를 호출 한다.

이 함수는 스레드 메시지 큐에 WM_QUIT 메시지를 분이고 즉시 리턴한다. WM_QUIT 메시지를 큐에 붙임으로써 시스템에게 이 스레드가 종료될 것이라는 것을

미리 알려준다. 메시지 루프는 보통 WM_QUIT 메시지를 받으면 종료하도록되어 있으므로 이 함수를 호출하면 메시지 루프가 종료된다.

 특히 이 함수를 호출하는 스레드가 주 스레드일 경우는 주 스레드의 미시지 루프가 종료됨으로써 프로세스가 종료된다.

단, 이 함수는 메시지를 큐에 붙인 후 즉시 리턴하므로 호출 즉시 프로세스가 종료되는 것은 아니다. 즉, PostQuitMessage() 호출 후 다른 처리를 계속할 수 있으며,

이미 메시지 큐에 들어와 있는 모든 메시지가 처리된 후에 WM_QUIT 메시지가 읽혀져야 종료된다.

 반면, ExitProcess() 는 즉시 프로세스를 종료하기 때문에 이 함수 아래에 작성된 코드는 실행되지 않는다. 

통상 이 함수는 메인 윈도우의 WM_DESTROY 메시지 처리 코드에 작성되어 메인 윈도우가 파괴되면 응용 프로그램을 종료하는 역활을 한다. 자식 윈도우가 WM_DESTROY에서 이메시지를 호출해서는 안된다.

그래서 DefWindowProc은 WM_DESTROY 메시지를 디폴트 처리할 때 PostQuitMessage() 를 호출하지 않도록 되어 있다.







------------------------------------------앞에 꺼 따라가기. 복습 -------------------------------------------------

WinMain에 대해서.


LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);












-------------------------------------------------------------------------------------------------------------












4-2-나. 더블클릭

마우스 버튼 누르기, 놓기, 이동하기 외에 중요한 마우스 동작으로 더블클릭(Dluble Click)이 있다. 더블클릭이란 짧은 시간안에 마우스 버튼을 두번 빠르게 누르는 동작인데 프로그램 실행, 확정적인 선택 등에 많이 사용되고 있다. 앞에서 만든 Mouse예제를 조금 더 확장하여 왼쪽 버튼을 더블클릭하면 화면을 지우도록 해 보자. 왼쪽 마우스 더블클릭 메시지인 WM_LBUTTONDBLCLK 메시지에 화면을 지우는 코드를 작성하면 될 것이다. 다음과 같이 코드를 작성했다.

	case WM_LBUTTONDBLCLK:
		InvalidateRect(hWnd, NULL, TRUE);
		return 0;

화면을 지우는 것은 아주 간단하다. InvalidateRect 함수를 호출하여 작업 영역 전체를 무효화시켜 버리면 된다. Mouse 예제의 경우 WM_PAINT 메시지를 처리하지 않고 있기 때문에 무효화 영역이 생기면 DefWindowProc이 WM_PAINT 메시지를 처리하게 되며 이 함수는 배경색으로 윈도우를 지워준다. 그래서 단순히 InvalidateRect 함수만 호출해 주면 화면이 지워지는 것이다. 그럼 과연 이 코드를 추가해 주면 화면이 지워질까? 직접 프로그램을 실행하여 왼쪽 버튼을 더블클릭해보면 화면이 전혀 지워지지 않을 것이다. 왜냐하면 우리가 만든 이 윈도우는 더블클릭에 대한 메시지를 지원하지 않기 때문이다.

마우스 왼쪽 버튼을 두번 누르면 WM_LBUTTONDOWN 메시지와 WM_LBUTTONUP 메시지가 교대로 두번 발생할 뿐이며 아무리 마우스 버튼을 잽싸게 눌러대도 더블클릭 메시지는 발생하지 않는다. 더블클릭 메시지를 받고자 하면 윈도우 클래스의 스타일에 "이 윈도우는 더블클릭 메시지를 원한다"는 의사 표시를 해 주어야 한다.

	WndClass.lpfnWndProc=(WNDPROC)WndProc;
	WndClass.lpszClassName=lpszClass;
	WndClass.lpszMenuName=NULL;
	WndClass.style=CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
	RegisterClass(&WndClass);

WndClass.style 멤버에 CS_DBLCLKS 플레그를 추가해 주면 이 윈도우는 더블클릭 메시지를 지원하게 된다. 이제 실행해 보면 더블클릭 메시지가 제대로 발생할 것이며 화면도 지워질 것이다. 윈도우 스타일에 이 플레그를 추가하고 마우스 버튼을 두번 누르면 두번째 WM_LBUTTONDOWN 메시지가 WM_LBUTTONDBLCLK 메시지로 변경된다.

보편적으로 많이 사용되는 더블클릭 메시지를 지원하지 않고 꼭 CS_DBLCLKS 플레그를 주어야만 더블클릭이 가능하도록 되어 있는 이유는 더블클릭을 검출하는데는 그만큼 실행시간의 감소가 요구되기 때문이며 어떤 프로그램은 더블클릭보다 WM_LBUTTONDOWN을 두번 받기를 원할 수도 있기 때문이다. 그래서 꼭 필요한 경우에 한해서 더블클릭을 지원하도록 되어 있다. 또한 더블클릭으로 인정할 시간간격이나 마우스 포인터의 위치 따위의 규칙을 프로그램에서 자체적으로 만들어 쓸 수 있도록 하기 위한 이유도 있다.

이와 같게끔 소스를 조금 수정하여 실습해 보면,

#include <windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"TextOut";

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance
  , LPSTR lpszCmdParam, int nCmdShow)
{
  HWND hWnd;
  MSG Message;
  WNDCLASS WndClass;
  g_hInst = hInstance;

  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = NULL;
  WndClass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000)) {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }
  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  static int x;
  static int y;
  static WCHAR wcBuffer[100];
  static BOOL bnowDraw = FALSE;

  switch (iMessage)
  {
    case WM_MOUSEMOVE:
      if (bnowDraw == TRUE) {
        hdc = GetDC(hWnd);
        MoveToEx(hdc, x, y, NULL);
        x = LOWORD(lParam);
        y = HIWORD(lParam);
        LineTo(hdc, x, y);
        ReleaseDC(hWnd, hdc);
      }
      return 0;

    case WM_LBUTTONUP:
      bnowDraw = FALSE;
      return 0;

    case WM_LBUTTONDOWN:
      x = LOWORD(lParam);
      y = HIWORD(lParam);
      bnowDraw = TRUE;
      return 0;

    case WM_LBUTTONDBLCLK:
      InvalidateRect(hWnd, NULL, TRUE);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}


이와 같고, 실행 화면은


- 더블 클릭.


이처럼 바뀌게 된다.


4-3-가.타이머

키보드나 마우스는 프로그램 실행중에 사용자로부터 입력되는 메시지이다. 메시지는 이렇게 사용자의 동작으로부터 유발되는 것이 보통이지만 사용자의 동작과는 상관없이 발생하는 메시지도 있다. 대표적으로 타이머 메시지인 WM_TIMER를 들 수 있으며 이 메시지는 한번 지정해 놓기만 하면 일정한 시간간격을 두고 연속적으로 계속 발생한다. 주기적으로 같은 동작을 반복해야 한다거나 여러번 나누어 해야 할 일이 있을 때 이 메시지를 이용한다.

타이머 메시지를 이용해서 간단한 시계를 하나 만들어 보도록 하자. 시간이라는 것은 주기적으로 갱신되어야 하는 것이므로 타이머 메시지를 사용하기에 가장 적절한 예이다. 소스는 다음과 같다.

#include <windows.h>

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass="MyTimer";

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance
		  ,LPSTR lpszCmdParam,int nCmdShow)
{
	HWND hWnd;
	MSG Message;
	WNDCLASS WndClass;
	g_hInst=hInstance;
	
	WndClass.cbClsExtra=0;
	WndClass.cbWndExtra=0;
	WndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
	WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);
	WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
	WndClass.hInstance=hInstance;
	WndClass.lpfnWndProc=(WNDPROC)WndProc;
	WndClass.lpszClassName=lpszClass;
	WndClass.lpszMenuName=NULL;
	WndClass.style=CS_HREDRAW | CS_VREDRAW;
	RegisterClass(&WndClass);

	hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,
		  CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
		  NULL,(HMENU)NULL,hInstance,NULL);
	ShowWindow(hWnd,nCmdShow);
	
	while(GetMessage(&Message,0,0,0)) {
		TranslateMessage(&Message);
		DispatchMessage(&Message);
	}
	return Message.wParam;
}

#include 
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	time_t mytime;
	static HANDLE hTimer;
	static char *str;
	switch(iMessage) {
	case WM_CREATE:
		hTimer=(HANDLE)SetTimer(hWnd,1,1000,NULL);
		str="";
		return 0;
	case WM_TIMER:
		time(&mytime);
		str=ctime(&mytime);
		InvalidateRect(hWnd,NULL,TRUE);
		return 0;
	case WM_PAINT:
		hdc=BeginPaint(hWnd,&ps);
		TextOut(hdc,100,100,str,strlen(str)-1);
		EndPaint(hWnd,&ps);
		return 0;
	case WM_DESTROY:
		KillTimer(hWnd,1);
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

시간과 관련된 함수들을 사용할 것이므로 프로그램 선두에 time.h를 포함시켜 두었다. WinMain은 이때까지의 프로그램과 동일하므로 WndProc만 보도록 하자. WndProc의 선두에는 시간값을 저장할 time_t형의 변수 mytime과 이 시간값을 문자열로 변경하여 저장할 str, 그리고 타이머 핸들인 hTimer 세 개의 변수가 선언되어 있다.

WndProc에서 첫번째로 처리하는 메시지는 WM_CREATE 메시지이다. WM_CREATE 메시지는 윈도우가 처음 생성될 때 발생하는데 이 메시지에서 프로그램 시작시 꼭 한번만 초기화되어야 할 처리를 해 준다. 프로그램 실행에 필요한 메모리를 할당한다든가 전역 변수에 초기값을 대입하는 등의 초기화 처리가 보통 WM_CREATE에서 이루어진다. 이 예제에서는 WM_CREATE 메시지에서 SetTimer 함수를 사용하여 타이머를 생성시켰다. 즉 윈도우가 만들어지자 마자 타이머가 만들어진다.

UNIT SetTimer(HWND hWnd, UINT nIDEvent, UINT uElapse, TIMERPROC lpTimerFunc) 

hWnd 인수는 타이머 메시지를 받을 윈도우인데 통상 WndProc의 인수로 전달되는 hWnd를 그대로 써 주면 된다. 두번째 인수 nIDEvent는 타이머의 번호를 지정한다. 하나의 타이머만 사용할 경우 1을 주면 되며 여러개의 타이머를 사용할 경우 nIdEvent에 겹치지 않도록 번호를 부여하도록 한다. 예를 들어 세개의 타이머를 사용한다면 각각 1, 2, 3의 타이머 번호를 주면 되며 이 타이머 번호는 WM_TIMER 메시지에서 타이머를 구분하기 위한 표식으로 사용된다.

세번째 인수 uElapase는 1/1000초 단위로 타이머의 주기를 설정한다. 이 값이 1000이면 타이머 메시지가 1초에 한번씩 hWnd로 보내지게 될것이고 10000이면 10초에 한번씩 타이머 메시지가 발생할 것이다. 네번째 인수는 타이머 메시지가 발생할 때마다 호출될 함수를 지정하는데 사용하지 않을 경우 NULL로 설정하면 된다.

이 예제에서는 WM_CREATE에서 타이머 번호 1번으로 1초에 한번씩 타이머 메시지를 hWnd로 보내주도록 타이머를 설정하였다. 이제 1초에 한번씩 hWnd 윈도우에 WM_TIMER 메시지가 전달될 것이다. 또한 WM_CREATE에서는 str 문자열을 널 스트링으로 초기화하여 윈도우가 처음 나타날 때 엉뚱한 문자열이 출력되지 않도록 하였다.

WM_TIMER 메시지는 wParam으로 타이머 ID를 전달받으며 lParam으로 타이머 메시지 발생시 호출될 함수의 번지가 전달된다. 이 예제에서는 타이머가 하나밖에 없으며 타이머 메시지 처리 함수도 지정되지 않았으므로 둘 다 무시하고 사용하지 않는다. WM_TIMER 메시지가 발생하면 time 함수로 시간을 조사한 후 ctime 함수로 문자열로 바꾼후 문자열 str에 저장해 둔다. 두 함수의 원형은 다음과 같다.

time_t time( time_t *timer ); char *ctime( const time_t *timer ); 

time 함수는 현재 시간을 조사해 주는데 이 때 조사되는 값은 1970년 1월 1일 자정 이후 경과한 초이며 조사된 값은 인수로 전달된 time_t형의 변수에 대입되며 동시에 리턴값으로도 전달된다. 만약 오늘이 1998년 1월 1일 0시라면 조사되는 값은 31536000*28이 될 것이다. 이렇게 조사된 시간값은 ctime 함수에 의해 문자열로 변경된다. ctime은 time_t값으로부터 현재 날짜와 시간을 계산하여 정확하게 문자열로 변경해 주며 그 문자열을 리턴해 준다.

예제에서는 mytime이라는 time_t형의 변수에 시간값을 조사한 후 이 값을 문자열로 변경하여 str 문자열 포인터에 대입하였다. 이제 남은 일은 조사된 시간을 화면으로 출력하는 것이다. WM_TIMER에서는 시간이 갱신될 때마다 화면을 갱신시키기 위해 InvalidateRect 함수를 호출하고 있다.

그러면 WM_PAINT 메시지에서 이 문자열을 화면으로 출력해 줄 것이다. 현재 시간이 문자열로 변경되어져 있으므로 단순히 TextOut를 사용하여 화면에 이 문자열을 뿌려 주기만 하면 된다. 단 ctime 함수는 시간값을 문자열로 변경한 후 문자열 끝에 \n을 붙여주도록 되어 있는데 이 문자가 출력되지 않도록 하기 위하여 문자열 길이에서 1을 뺀 길이만큼만 출력하도록 하였다. 프로그램 실행중의 모습은 다음과 같다.

1초당 한번씩 시간이 정확하게 갱신될 것이다. 마지막으로 해 주어야 할 일은 WM_DESTROY 메시지에서 설치된 타이머를 제거해 주는 것이며 이 때는 KillTimer 함수를 사용한다.

BOOL KillTimer( HWND hWnd, UINT uIDEvent ); 

타이머는 시스템 전역 자원이므로 한번 설정해 놓으면 윈도우가 파괴되어도 파괴되지 않고 계속 남아 있게 된다. 그래서 프로그램이 종료될 때 자신이 설정해 놓은 타이머는 자기가 직접 파괴해 주어야 한다. 그렇지 않으면 프로그램만 종료되고 타이머는 남아 쓸데없이 CPU만 주기적으로 차지하게 되어 시스템 성능을 떨어뜨리게 될 것이다.

KillTimer의 첫번째 인수로 이 타이머를 소유한 윈도우 핸들을 넘겨주며 두번째 인수로 타이머 ID를 넘겨준다. 주의할 것은 타이머 ID는 SetTimer의 두번째 인수로 지정한 값을 말하는 것이지 SetTimer가 리턴한 값을 말하는 것이 아니라는 점이다. 이 예제에서는 SetTimer에서 타이머 ID를 1이라는 상수로 주었으므로 파괴할 때도 타이머 ID로 상수 1을 넘겨 주어야 한다. SetTimer의 리턴값은 타이머를 소유하는 윈도우 없이 타이머가 만들어졌을 경우, 즉 SetTimer의 첫번째 인수가 NULL일 경우에 한해 특별하게 사용하는 것이되 거의 사용되지 않는다고 보면 된다.


자 타이머, 우리는 하드웨어에서 타이머를 이미 다뤄 봤다. 밀리세컨드 단위로 세며 일정한 위치에 도달하면 어떠한 일이 벌어지도록 설정하는 것일 것이다.

여기서 새로보는 애들은 WM_CREATEWM_TIMER, SetTimer(), KillTimer(), time(), ctime() 가 되겠다. 이 때 호출되는 InvalidateRect() 함수는 무효화 영역 처리임을

다시 상기하도록 하자.


하나하나 분석해보자.

1. WM_TIMER 의 wParam은 타이머의 ID가 전달된다. 이 ID는 SetTimer() 의 두번째 인수로 지정한 값이다.

lParam은 콜백 함수가 있을 경우 콜백 함수의 번지가 전달된다.


SetTimer() 로 타이머를 설치했을 경우 지정한 시간 간격으로 이 메시지가 반복적으로 큐에 붙여진다. 주기적으로 어떤 작업을 반복해야 한다면 타이머를 설치하고

이 메시지에서 작업을 처리하도록 한다. 두개 이상의 타이머가 설치되어 있을 경우 각각의 타이머는 정해진 시간 간격으로 이 메시지를 큐에 붙이며 WM_TIMER에서는 wParam으로 어떤 타이머에 의해 이 메시지가 발생했는지 조사한다.

타이머 콜백 함수를 지정했을 경우는 이 메시지를 처리할 필요가 없으면 시스템이 콜백 함수를 주기적으로 호출해 준다.

이 메시지는 다른 메시지들에 비해 우선순위가 낮게 설정되어 있기 때문에 먼저 처리해야 할 메시지가 있을 경우 곧바로 윈도우 프로시저로 보내지지 않을 수 있다.

따라서 정확한 시간에 이 메시지가 전달되지 않는 경우도 있으므로 정확도를 요하는 작업에는 이 메시지를 사용하지 않는 것이 좋다.


여기서 언급 되었던 큐(Queue)에 대해 알아보자. 메모리 구조와 연관이 있다.

메모리의 구조 

Stack 와 Queue 들수 있다. 

Stack 는 끝이막혀 있는 빨대(파이프)으로 생각할수 있다. 

처음에 빨간 공을 넣습니다. 순서대로 빨주노초파남보 색상을 넣습니다. 

다 그럼 여기서 문제입니다. 

빨간공을빼내려고 하는데 어떻하면될까요? 

꺼꾸로 빼냐야하겠죠? 

보남파초노주빨 순서로 빼내야  빨강공을빼낼수 있죠.. 

이게 바로 Stack 입니다. (Last In First Out)


Queue (발음은 '큐')라고 합니다 

이것도 빨대로 생각하죠 여기서는 막혀 있지 않고 뚤려있습니다. 

빨간공먼저 들어가면 다음구멍으로 빨강공이 나오는 원리죠.. 

그러니들어간 순서대로 나오는거죠..(First In First Out)


여기서 메세지 큐란 OS 내부의 어떤 곳 이다.  먼저 들어간 자료가 먼저 나오는 구조 인데 줄세우기를 생각하면 이해가 편하다. 먼저 줄 선 사람이 임자라는 것이다.

메세지 큐는 우리가 윈도우를 생성하거나 키보드를 누르거나 마우스를 움직 일 때 생성되는 메시지가 쌓이는 메모리 공간이라고 할 수 있다.

여기서도 큐의 종류가 갈리게 된다.

시스템 메시지 큐 (System Message Queue) 는 사용자 입력 값들이 차곡차곡 쌓이게 되고 OS가 판단해서 활성화된 윈도우에 사용자 입력 값들을 전달하게 되는 것이다.

쓰레드 메시지 큐 (Thread Message Queue) 는 사용자가 직접 접근하는 공간으로서 GetMessage, DispatchMessage 함수 들에 의해서 접근할 수 있다.


즉, 다시말하자면 지속적으로 메시지 큐에 붙게 된다라는 뜻은, 그 시간이 지났으므로 WM_TIMER 가 해당 메시지를 수행하기 위해 줄을 서는 것이다.

2. WM_CREATE wParam : 사용 x , lParam : 윈도우 생성 정보인 CREATESTRUCT 구조체의 포인터이다. 이 구조체는 CreateWindow(Ex)함수의 인수에 대한 정보를     가진다.

CreateWindow(Ex) 함수에 의해 윈도우가 생성될 때 보내진다. 메모리에 윈도우를 생성한 후 화면에 보이기 전에 보내지며 윈도우에 관련된 초기화 작업을 할 때

사용된다. 윈도우 동작을 위한 메모리 할당, 리소스 생성, 자식 컨트롤 생성, 윈도우 속성 초기화 작업에 이 메시지가 사용된다.

CreateWindow(Ex) 함수는 이 메시지를 완전히 처리한 후에 리턴한다. 만약 이 메시지 처리중에 자식 윈도우를 생성했다면 각 자식 윈도우로도 WM_CREATE 메시지가 전달되어 개별적인 초기화를 한다. 인수로 전달되는 LPCREATESTRUCT 구조체는 보통 사용하지 않으며 무시하나 이 구조체이 lParam멤버는 CreateWindow() 의 제일 마지막 인수를 전달하며 윈도우로 사용자 정의값을 전달하고자 할 때 사용할 수 있다.

참고 : 대화상자는 이 메시지 대신 WM_INITDIALOG 메시지를 받는다.

3. SetTimer() 는 시작될 때 WM_TIMER 메시지를 보내는 시스템 타이머를 설치 한다. 타이머를 생성하고 wElapse가 지정하는 간격으로 WM_TIMER 메시지를 보낸다. 

함수 원형

UNIT SetTimer(HWND hWnd, UINT nIDEvent, UINT uElapse, TIMERPROC lpTimerFunc) 

WM_TIMER 메시지를 받는 곳은 lpTimerFunc의 설정에 따라 달라진다. lpTimerFunc가 NULL일 경우는 타이머를 설치한 윈도우의 WndProc으로 보내지며

그렇지 않을 경우는 lpTimerFunc가 지정하는 콜백함수로 보내진다. 타이머 메시지는 응용 프로그램의 메시지 큐에 저장되며 윈도우즈의 다른 프로그램에 의해 시간이 지연될 수도 있으므로 반드시 정확한 간격으로 전달된다는 보장이 없다. 시계, 간단한 애니메이션 등 일정한 주기로 호출되어져야 할 함수가 있을 때 보통

타이머 메시지를 사용한다. 또는 시스템의 속도와는 무관하게 일정한 속도를 유지해야하는 게임들에도 타이머 메시지가 사용된다.

만약 hWnd의 nIDEvent 타이머가 이미 설치되어 있다면 이 함수는 새로운 주기로 타이머를 다시 설치하며 이 경우 타이머는 리셋된다. 타이머의 주기를 바꾸고자 할 경우 같은 ID로 이 함수를 호출해준다.

4. KillTimer() 는 타이머를 해제한다. SetTimer() 에 의해 설치된 타이머를 해제하고 메시지 큐에 이미 포스팅된 WM_TIMER 메시지가 있을 경우 이 함수는 메시지를     제거하지 않는다.

즉, 메시지 큐에 남아있는 WM_TIMER 는 끝까지 실행 된다.

5. Time(), cTime() 는 찾으면 바로바로 나오므로 여기서는 그만 설명을 끊겠다.




완성된 코드 와 실행 화면이다.


#include <windows.h>
#include <time.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"TextOut";

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance
  , LPSTR lpszCmdParam, int nCmdShow)
{
  HWND hWnd;
  MSG Message;
  WNDCLASS WndClass;
  g_hInst = hInstance;

  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = NULL;
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000)) {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }
  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  SYSTEMTIME st;
  static TCHAR sTime[128];

  switch (iMessage)
  {
  case WM_CREATE:
    SetTimer(hWnd, 11000, NULL);
    return 0;
  case WM_TIMER:
    GetLocalTime(&st);
    wsprintf(sTime
      , TEXT("지금 시간은 %d:%d:%d입니다")
      , st.wHour
      , st.wMinute
      , st.wSecond);
    InvalidateRect(hWnd, NULL, TRUE);
    return 0;
  case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps);
    TextOut(hdc, 100100, sTime, lstrlen(sTime));
    EndPaint(hWnd, &ps);
    return 0;
  case WM_DESTROY:
    KillTimer(hWnd, 1);
    PostQuitMessage(0);
    return 0;
  }
  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}



계속 타이머 부분이 안되서 여러 함수를 사용했었다. 결국 주쌤이 해결 해 주셨다.

아까 여러가지 찾아보다가 보던 함수가 눈에 띈다. GetLocalTime()


SYSTEMTIME st;

static TCHAR sTime[128];

.

.

GetLocalTime(&st);
    wsprintf(sTime
      , TEXT("지금 시간은 %d:%d:%d입니다")
      , st.wHour
      , st.wMinute
      , st.wSecond);


자 이제 이 부분

case WM_TIMER:
    GetLocalTime(&st);
    wsprintf(sTime
      , TEXT("지금 시간은 %d:%d:%d입니다")
      , st.wHour
      , st.wMinute
      , st.wSecond);
    InvalidateRect(hWnd, NULL, TRUE);
    return 0;

분석해보자.


1. GetLocalTime() 는 로컬 시간을 조사해 주는 함수이다. 로컬 시간이랑 시스템이 유지하는 시스템 시간(UTC)에서 현재 컴퓨터가 실행되고 있는 시간대와

일광절약 설정을 계산하여 변환한 시간이다. 대한민국의 로컬 시간은 UTC 시간보다 9시간 더 빠르므로 시스템 시간에서 9시간 만큼 더 해주어야 로컬 시간이 구해진다.

일반적으로 현지 시간이라고 하면 이 함수로 구해지는 로컬 시간을 의미한다.

2. 위에서 언급된 wsprintf() 로 

TEXT() 는 매크로 함수이며

문자열 처리와 관련된 윈도우 매크로이다.
유니코드를 사용할 경우에는 컴파일할 때 TEXT("문자열")이 L"문자열"로 변환된다.
유니코드를 사용하지 않을 경우에는 "문자열"로 변환된다.
따라서 TEXT()매크로를 사용하면 유니코드를 사용 유무에 상관없이 문자열을 처리할 수 있다.


짜증나는 작업들을 내부적으로 처리해주는 매크로 함수 인듯하다.







4-3-나. SendMessage

시계 프로그램을 아주 간단한 방법으로 만들어 보았다. 이해하기에 난해한 점이 별로 없으므로 아주 쉽다고 느낄 것이다. 그런데 예제 수준으로 만들었다 치더라도 이 프로그램은 당연히 해 주어야 할 처리를 생략한 것들이 있어 몇가지 문제가 있다. 어떤 문제들이 있는지와 이런 문제들을 해결하는 방법에 대해 알아보자.

우선 첫번째 문제는 처음 실행시킨 직후에는 시간이 보이지 않다가 1초정도 경과한 후부터 시간이 보인다는 점이다. 화면 출력을 담당하는 WM_PAINT에서는 무조건 str 문자열을 화면으로 출력하기만 하고 이 문자열은 WM_TIMER에서 시간값을 조사한 후 설정해 주므로 WM_PAINT는 죄가 없다. 문제는 시간을 조사하는 WM_TIMER 메시지가 최초로 호출되는 시점이 프로그램 시작 1초후라는 점이다. 왜 그런가 하면 WM_CREATE에서 타이머를 설치할 때 타이머 주기를 1초로 주었기 때문이다. 

이 문제를 해결하려면 프로그램 시작 직후에 WM_TIMER 메시지를 강제로 발생시켜 주어야 한다. 이 때 사용되는 함수가 SendMessage이다.

LRESULT SendMessage( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam ); 
	case WM_CREATE:
		hTimer=SetTimer(hWnd,1,1000,NULL);
		str="";
		SendMessage(hWnd, WM_TIMER, 1, 0);
		return 0;

메시지는 사용자의 동작에 의해서나 시스템의 상황 변경에 따라 발생하는 것이 원칙이지만 강제로 메시지가 발생한 것처럼 만들어야 할 때는 이 함수를 사용하여 hWnd 윈도우로 Msg 메시지를 보내게 된다. 그러면 hWnd는 Msg 메시지가 발생한 것으로 인식하고 필요한 처리를 하게 될 것이다. 이 함수를 호출해야 할 시점은 프로그램이 시작된 직후인 WM_CREATE에서이다.

타이머를 설치한 직후에 SendMessage로 WM_TIMER 메시지를 보내주어 곧바로 시간을 조사한 후 조사한 시간을 화면에 출력하도록 하였다. SendMessage의 세번째, 네번째 인수는 메시지의 추가 정보인 wParam, lParam이며 물론 보내는 메시지에 따라 의미는 달라진다. WM_TIMER 메시지는 wParam으로 타이머 ID를 보내도록 되어 있으므로 SendMessage의 세번째 인수에 타이머 ID인 1을 넘겨주었다. SendMessage의 리턴값도 물론 메시지에 따라 다르다.

메시지 기반의 운영체제인 윈도우즈에서 SendMessage 함수는 아주 빈번하게 사용되는 중요한 함수이다. 이 예제에서는 WM_TIMER 메시지를 강제로 보내기 위해 사용했는데 어떤 종류의 메시지라도 누구에게나 보낼 수 있으며 SendMessage의 이런 기능은 차일드 컨트롤을 프로그래밍하는 아주 중요한 수단으로 사용된다. 중요한 함수이므로 눈여겨 봐두도록 하자. 원형이 WndProc과 같아 외우기도 아주 쉽다.

이 프로그램이 가지는 두번째 문제점은 시간이 바뀔 때마다 화면이 깜박거린다는 점이다. 컴퓨터가 굉장히 빠르거나 화면이 작다면 잘 느낄 수 없지만 윈도우를 최대화시켜 놓고 보면 시간이 바뀔때 글자들이 지워졌다가 다시 그려지는 것이 보일 것이다. 왜 그런가 하면 WM_TIMER 메시지에서 시간을 변경한 후 화면을 다시 그리기 위해 다음과 같이 함수를 호출하기 때문이다.

InvalidateRect(hWnd,NULL,TRUE); 

hWnd 윈도우를 무효화시키되 두번째 인수가 NULL이므로 화면 전체가 무효화되며 세번째 인수가 TRUE이므로 일단 화면을 지운 후 다시 그리게 된다. 화면 전체가 몽땅 다 지워졌다가 다시 출력되도록 했기 때문에 깜박거리는 것이 눈에 보이는 것이다. 그렇다고 해서 세번째 인수를 FALSE로 변경하여 지우지 않도록 한다면 이전에 출력되었던 시간위에 갱신된 시간이 덮여서 출력되므로 그렇게 해서도 안된다.

이럴 경우 무효화 영역을 최소화하여 꼭 필요한 부분만 무효화하도록 해 주어야 한다. 즉 다음과 같이 InvalidateRect 함수의 두번째 인수에 무효화시킬 영역을 전달하여 이 부분만 무효화함으로써 최대한 빨리 윈도우를 다시 그릴 수 있도록 해 주는 것이다.

long FAR PASCAL WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
.....
	static RECT rt={100,100,400,120};
	switch(iMessage) {
	case WM_TIMER:
		time(&mytime);
		str=ctime(&mytime);
		InvalidateRect(hWnd,&rt,TRUE);
		return 0;

문자열이 출력되는 영역인 (100,100) - (400,120) 사각 영역을 정의한 후 이 영역만 무효화하도록 하였다. 그러면 WM_PAINT 메시지에서는 이 영역 외부는 지우지도 않고 그리지도 않으며 전혀 신경쓰지 않게 되므로 그리는 속도가 현저히 빨라지게 된다. 그리는 속도가 빠르기 때문에 화면 깜박임도 왠만해서는 눈에 보이지 않게 될 것이다. 여기서 설정한 사각 영역은 실습의 편의를 위해 대충 눈대중으로 설정하였는데 좀 더 완벽하게 하기 위해서는 문자열이 출력될 사각 영역을 계산하여 더 좁은 영역을 무효화할 수도 있다.


몇번의 타이머가 왔을때 이렇게 반응, 저렇게 반응 이런식으로 타이머가 따로따로 동작하도록 한다. 이러한 것을 인위적으로 보내줄수 있다. (3, 4 인자에)

새로 언급되는 애들은 SendMessage(), 사각형 그린 뒤 무효화 이다.

그 전 챕터에서의 1초가 흐른 뒤에 시간이 출력되는 현상을 해결하기 위해 언급되는 함수인 SendMesssage() 를 먼저 살펴보자.


1. SendMessage() 는 메시지를 윈도우에게 보낸다. 해당 윈도우의 윈도우 프로시저를 호출하여 이 메시지가 완전히 처리되기 전에는 리턴하지 않는다. 같은 스레드에     속한 윈도우에게 메시지를 보낼 때는 마치 서브루틴을 호출하는 것과 동일하다.

함수 원형

LRESULT SendMessage( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam ); 

 여기서 언급되는 서브 루틴이란, 서브루틴이란?

인수를 받아서 처리하고 출력 하는 방법을 말할 수 있습니다.
보통 언어를 하다보면 개별적으로 움직여야 할 경우가 발생 될 것입니다.
사람도 여러가지 일을 처리 해줄 수 있는 것처럼 서부루틴도 그와 같이 여러가지 작업에 대해 처리하고 출력을 해줍니다.

보통 반환하는 값을 줄것인지 아니면 서브루틴에서 자체적으로 처리해여 결과를 보여줄 것인지는 상황에 따라 다를 것입니다.

컴퓨터 프로그래밍에서 루틴과 서브 루틴은 어떤 프로그램이 실행될 때 불려지거나 반복해서 사용되도록 만들어진 일련의 코드들을 지칭하는 용어이다. 이를 이용하면 프로그램을 더 짧으면서도 읽고 쓰기 쉽게 만들 수 있으며, 하나의 루틴이 다수의 프로그램에서 사용될 수 있어서 다른 프로그래머들이 코드를 다시 작성하지 않도록 해준다. 프로그램 로직의 주요 부분에서는 필요할 경우 공통 루틴으로 분기할 수 있으며, 해당 루틴의 작업이 완료되면 분기된 명령의 다음 명령으로 복귀한다.

즉, 정리하자면 프로그래머들을 위한 작업의 편리를 위해 함수의 분리 작업을 나타내는 듯하다.

예를 들어 메인 윈도우가 자식 윈도우인 리스트 박스에게 LB_ADDSTRING이나 LB_GETCOUNT 등의 메시지를 보내면 리스트 박느는 해당 동작을 수행하는 서브 루틴을 호출하고 이 동작이 완료될 때 까지 SendMessage는 리턴하지 않는다.

 다른 스레드에 속한 윈도우에게 메시지를 보낼 때는 스레드 스위칭이 발생하며 메시지를 받는 스레드가 메시지를 읽는 코드를 실행중이어야 한다.

만약 메시지를 받는 스레드가 메시지 처리에 오랜 시간을 소모한다면 (ex) 메시지를 받는 스레드 : WM_TIMER)  SendMessage를 호출한 스레드는 이 함수가 리턴할

때까지 블록 상태로 남아있게 된다. 리스트 박스로  문자열을 추가할 때는 LB_ADDSTRING 메시지를 보내주면 된다. 이와 같이 부모 위도우가 자식에게 명령을

내리거나 상태를 조사하는 가장 기본적인 방법은 SendMessage로 메시지를 보내는 것이다. 각 자식 별로 보낼 수 있는 메시지의 종류가 다양하다.

 SendMesssage 함수는 보낸 메시지가  완전히 처리되기 전에는 리턴하지 않는 블록 특성을 가지고 있다. 특히 이런 특성은 다른 스레드간에 메시지를 주고 받을 때

자주 목격되는데 이 문제를 해결하는 방법에 대해서는 InSendMessage 함수를 참고하기 바란다.

 WM_COPYDATA 등의 특정 메시지는 반드시 SendMessage 함수로만 보내야 하면 PostMessage를 쓸 수 없는 것도 있다.

2. 사각형 그린 뒤 무효화 는 그 부분만 무효화 시킴으로써 이런 효과를 얻을 수 있다.

WM_PAINT 메시지에서는 이 영역 외부는 지우지도 않고 그리지도 않으며 전혀 신경쓰지 않게 되므로 그리는 속도가 현저히 빨라지게 된다. 그리는 속도가 빠르기 때문에 화면 깜박임도 왠만해서는 눈에 보이지 않게 될 것이다.

분명 다시 그리기는 하지만 그 속도가 너무 빠르기에 우리 눈에는 마술처럼 인식도 하기 전에 그려지는 것이다. 그러나 우리 눈에는 마치 뒤의 물건이 그대로 존재하고 그 물건을 보기위해 가려진 위의 물건을 치우는 것과 같다.


이 쯤 하면 SendMessage의 활용은 대충 이해가 된다. 넘어가자.

자 이제, 여러개의 타이머를 사용하는 부분이 나온다.


4-3-다. 두 개의 타이머

타이머는 한꺼번에 여러개를 설치하여 사용할 수도 있다. 필요한만큼 SetTimer를 호출하되 두번째 인수인 타이머 ID를 각각 다르게 설정해 주어야 하며 타이머 간격은 물론 타이머의 용도에 따라 적절하게 설정하면 된다. 여러개의 타이머를 설치했을 경우에도 하나의 타이머를 설치했을 때처럼 전달되는 메시지는 WM_TIMER 하나뿐이다. 어떤 타이머에 의해 WM_TIMER 메시지가 발생했는지는 wParam으로 전달되는 타이머의 ID로 구분한다. 이 타이머 ID는 곧 SetTimer 함수가 타이머를 설치할 때 두번째 인수로 지정한 값이다.

앞에서 만든 시계 예제에 하나의 타이머를 더 설치하여 5초에 한번씩 비프음을 울리도록 만들어 보았다. 소스는 다음과 같다.

#include <windows.h>

long FAR PASCAL WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	time_t mytime;
	static HANDLE hTimer, hTimer2;
	static char *str;
	static RECT rt={100,100,400,120};
	switch(iMessage) {
	case WM_CREATE:
		hTimer=(HANDLE)SetTimer(hWnd,1,1000,NULL);
		hTimer2=(HANDLE)SetTimer(hWnd,2,5000,NULL);
		str="";
		SendMessage(hWnd, WM_TIMER, 1, 0);
		return 0;
	case WM_TIMER:
		switch (wParam) {
		case 1:
			time(&mytime);
			str=ctime(&mytime);
			InvalidateRect(hWnd,&rt,TRUE);
			break;
		case 2:
			MessageBeep(MB_OK);
			break;
		}
		return 0;
	case WM_PAINT:
		hdc=BeginPaint(hWnd,&ps);
		TextOut(hdc,100,100,str,strlen(str)-1);
		EndPaint(hWnd,&ps);
		return 0;
	case WM_DESTROY:
		KillTimer(hWnd,1);
		KillTimer(hWnd,2);
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
} 

WM_CREATE에서 2번 ID로 5초 간격의 타이머를 하나 더 만들었다. 그러면 1번 타이머는 1초에 한번씩 WM_TIMER를 발생시킬 것이며 2번 타이머는 5초에 한번씩 WM_TIMER를 발생시킬 것이다. WM_TIMER 메시지 처리부분에서는 wParam값에 따라 어떤 타이머로부터 타이머 메시지가 발생했는지 보고 타이머 ID에 따른 처리를 하면 된다. wParam이 1이면 시간을 갱신한 후 화면을 다시 그리도록 하고 wParam이 2면 MessageBeep 함수를 호출하여 비프음을 낸다. WM_DESTROY에서는 설치된 타이머를 모두 해제해 주어야 한다.

세 개나 네 개 또는 그 이상의 타이머가 필요하다면 필요한만큼 타이머를 만들어 사용하고 프로그램이 종료할 때 파괴시켜주기만 하면 된다. Win32 환경에서 만들 수 있는 타이머의 개수에는 제한이 없지만 그렇다고 수십개나 만들어 쓰는 무식한 짓을 해서는 안된다. 아마 CPU가 타이머를 관리하느라 정신을 못차릴 것이다. 참고로 16비트 윈도우즈에서 타이머는 최대 16개까지 설치할 수 있다.



자 여기서 새로 보이는 애들은 MessageBeep(), 여러 개의 Timer HANDEL 이용 이다.

우선 MessageBeep() 부터 살펴보자.


1. MessageBeep() 는 함수 이름에서 유추가 되듯이 지정한 사운드를 연주한다. 이 함수는 사운드를 큐에 넣은 후 즉각 리턴하며 사운드는 비동기적으로 연주되므로

곧바로 다른 작업을 할 수 있다. 만약 지정한 사운드를 연주할 수 없으면 시스템 디폴트 비프음을 내며 시스템 디폴트 음도 낼 수 없으면 스피커로 표준 비프음을

낸다. 사용자에게 사운드로 주의를 주고자 할 때 이 함수를 사용하며 또한 디버깅 용으로 자주 사용된다.

2. 여러 개의 타이머 핸들 이용 부분은 

hTimer=(HANDLE)SetTimer(hWnd,1,1000,NULL); hTimer2=(HANDLE)SetTimer(hWnd,2,5000,NULL);

이와 같이 핸들러를 반환 받아서 사용해야 한다.

세 개나 네 개 또는 그 이상의 타이머가 필요하다면 필요한만큼 타이머를 만들어 사용하고 프로그램이 종료할 때 파괴시켜주기만 하면 된다.

만들고 나서 꼭 파괴시켜주자. 동적 할당 식으로 받는 모양이다.




자 이번엔 타이머를 이용하여 일정 시간이 지나면 타이머 함수를 호출하도록 해보자.


4-3-라. 콜백 함수

프로그램이 실행되는 동안 지속적으로 수행해야 할 작업이 있다고 해 보자. 예를 들어 로고 애니메이션이나 백그라운드 음악 연주 등을 들 수 있는데 도스에서라면 다음과 같이 코드를 작성할 것이다.

for(;;) {
	지속적인 작업
	기타 작업
}

무한 루프가 전체 프로그램 코드를 감싸고 있고 이 루프 안에서 지속적으로 해야할 작업과 그외 작업을 수행하고 있다. 도스에서는 이런식으로 프로그램을 작성하는 것이 가능하며 실제로 이렇게 한다. 그러나 윈도우즈와 같은 멀티 태스킹 환경에서는 이런 방식을 사용해서는 안된다. 왜냐하면 한 프로그램이 제어권을 독점하고 있어서는 안되며 다른 프로그램도 실행시간을 가져야 하기 때문이다. 사용자는 수시로 작업 전환을 할 수 있어야 하는데 한 프로그램이 CPU를 독차지하고 있으면 안된다. 그래서 CPU를 독점하는 이런 무한루프를 작성해서는 안되며 반드시 메시지가 전달되었을 때에 한해 필요한 작업을 하도록 해야 한다. 이럴 때 사용하는 메시지가 바로 타이머 메시지인데 잠시 후에 예제를 만들어 볼 것이다.

예제를 만들기 전에 잠깐 SetTimer의 네번째 인수에 대해 알아보자. 네번째 인수는 TIMERPROC lpTimerFunc라고 되어 있는데 이 인수는 타이머 프로시저 함수의 포인터를 가리킨다. 이 인수가 NULL로 되어 있을 경우 첫번째 인수로 지정된 hWnd로 WM_TIMER 메시지가 전달되지만 이 인수에 타이머 함수가 지정되었을 경우는 매 시간마다 이 함수가 대신 호출된다. 즉 타이머 함수가 지정되면 메시지 대신 함수를 호출해 준다. 타이머 함수는 다음과 같은 원형으로 작성되어야 한다.

VOID CALLBACK TimerProc( HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime ); 

4개의 인수를 가지는데 hWnd는 타이머를 소유한 윈도우의 핸들이며 uMsg는 WM_TIMER, idEvent는 타이머 ID, dwTime은 윈도우즈가 실행된 후의 경과시간이다. 이 함수의 인수는 잘 사용되지 않으므로 구체적으로 알 필요는 없다. 아뭏든 중요한 사실은 이런 원형을 가지는 함수를 만든 후 SetTimer 함수의 네번째 인수에 이 함수명을 적어주면 지정한 시간 간격으로 이 함수가 호출된다는 점이다.

그럼 이제 콜백 함수를 사용하여 화면의 임의 위치에 지속적으로 점을 찍는 프로그램을 만들어 보도록 하자. RandGrp 프로젝트를 만들고 다음과 같이 코드를 작성한다.

#include <windows.h>

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass="RandGrp";

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance
		  ,LPSTR lpszCmdParam,int nCmdShow)
{
	HWND hWnd;
	MSG Message;
	WNDCLASS WndClass;
	g_hInst=hInstance;
	
	WndClass.cbClsExtra=0;
	WndClass.cbWndExtra=0;
	WndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
	WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);
	WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
	WndClass.hInstance=hInstance;
	WndClass.lpfnWndProc=(WNDPROC)WndProc;
	WndClass.lpszClassName=lpszClass;
	WndClass.lpszMenuName=NULL;
	WndClass.style=CS_HREDRAW | CS_VREDRAW;
	RegisterClass(&WndClass);

	hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
		NULL,(HMENU)NULL,hInstance,NULL);
	ShowWindow(hWnd,nCmdShow);
	
	while(GetMessage(&Message,0,0,0)) {
		TranslateMessage(&Message);
		DispatchMessage(&Message);
	}
	return Message.wParam;
}

void CALLBACK TimerProc(HWND hWnd, UINT uMsg, UINT idEvent, DWORD dwTime)
{
	HDC hdc;
	int i;
	hdc=GetDC(hWnd);
	for (i=0;i<100;i++) 
		SetPixel(hdc,rand()%500, rand()%400,
		RGB(rand()%256,rand()%256,rand()%256,));
	ReleaseDC(hWnd, hdc);
}

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	switch(iMessage) {
	case WM_CREATE:
		SetTimer(hWnd, 1, 100, (TIMERPROC)TimerProc);
		return 0;
	case WM_DESTROY:
		KillTimer(hWnd, 1);
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

WndProc에서는 프로그램 시작시(WM_CREATE)에 타이머를 설치하는 일과 프로그램 종료 직전(WM_DESTROY)에 타이머를 해제하는 작업만 한다. SetTimer 함수에서 간격은 0.1초로 지정되었고 네번째 인수는 TimerProc란 함수로 지정되어 있으므로 0.1초 간격으로 TimerProc라는 함수가 호출될 것이다. TimerProc 함수는 WndProc 바로 앞에 작성되어 있으며 300개의 점을 난수로 얻은 임의 좌표에 임의의 색상으로 출력하는 일을 한다. 임의의 색상을 만들 때는 RGB 매크로 함수를 사용하는데 이 함수는 6장에서 자세히 알아볼 것이다. 결국 1초에 3000개의 점이 무작위로 출력되는 셈이다. 실행중의 모습은 다음과 같다.

이 프로그램이 어떻게 동작하는지는 쉽게 이해가 갈 것이다. 그런데 왜 이런 작업을 하는데 다음과 같이 하면 안될까?

case WM_PAINT:
	hdc=BeginPaint(hWnd, &ps);
	for (;;) {
		SetPixel(hdc,rand()%500, rand()%400,
			RGB(rand()%256,rand()%256,rand()%256,));
	}
	EndPaint(hWnd, &ps);
	return 0;

WM_PAINT에서 무한번 점을 찍으면 결과는 같지 않을까? 만약 이런 의문을 가지고 있다면 직접 코드를 작성해 넣어 보고 실행해 보면 왜 안되는지를 알 수 있을 것이다. 점이 무작위로 찍히기는 하므로 목적은 달성되겠지만 그 외 어떠한 동작도 할 수 없다. 타이틀 바를 드래그하여 위치를 옮길 수도 없으며 크기 변경도 되지 않고 시스템 메뉴를 눌러도 메뉴가 나타나지 않는다. 더구나 심각한 것은 종료 버튼을 눌러도 종료조차도 되지 않는다는 점이다. 왜냐하면 WM_PAINT 메시지 처리 구간에서 무한루프에 빠져 들었기 때문에 어떤 다른 메시지도 받을 수 없는 상태가 되었기 때문이다.

다행히 Win95는 선점형 멀티 태스킹 환경이기 때문에 다른 작업으로 전환할 수 있으며 최후의 수단으로 Ctrl-Alt-Del 키를 눌러 이 프로그램을 강제 종료시킬 수 있다. 만약 윈도우즈 3.1에서 이런 코드를 작성했다면 이는 곧 한방에 시스템을 다운시켜 버리는 코드가 된다. 그래서 이런 지속적으로 해야 할 작업은 타이머를 설치한 후 타이머 메시지를 받을 때마다 찔끔 찔끔 나누어 해야 하는 것이다.

위 예제는 콜백 함수를 사용하여 문제를 해결했는데 WM_TIMER 메시지를 사용해도 문제를 해결할 수 있다. 차이점이라면 WM_TIMER 메시지는 다른 메시지가 있을 경우 실행 순서에 밀려 늦게 호출되는 경우가 있지만 콜백 함수를 사용하면 정확한 시간에 호출된다는 점이다. 그래서 정확도를 요하는 작업은 타이머 메시지보다는 콜백 함수를 사용하는 것이 더 좋다.

그렇다면 콜백 함수(Callback Function)란 무엇인지 그 의미를 좀 더 정확하게 알아보자. 일반적으로 API 함수들은 운영체제가 제공하며 프로그램에서는 이 함수들을 호출해서 운영체제의 서비스를 받는다. 예를 들어 도스의 시스템 콜 함수를 호출하여 디스크 입출력을 받는다든가 윈도우즈의 TextOut 함수를 호출하여 문자열을 출력하도록 하는 경우가 이에 해당한다. 반면 콜백 함수는 응용 프로그램이 제공하며 운영체제가 필요할 때 호출하는 함수이다. 호출되는 방향이 거꾸로 되었기 때문에 콜백이라고 부르는 것이다. 위 예제에서 TimerProc 함수는 SetTimer에서 지정한 시간마다 운영체제에 의해 호출된다. 콜백 함수를 문장화하여 정의내린다면 "운영체제에 의해 호출되는 프로그램 내부의 함수"라고 할 수 있다.

윈도우즈에서는 이런 콜백 함수가 빈번하게 사용되고 있으므로 개념을 잘 알아두도록 하자. 타이머의 콜백 함수가 대표적이며 이 외에도 중요한 열거 함수들과 몇몇 그래픽 함수 등의 콜백 함수를 사용한다. 그보다도 가장 가까운 콜백 함수의 예는 메시지 처리 함수인 WndProc이다. 이 함수는 메시지가 발생할 때마다 윈도우즈가 호출해 주며 응용 프로그램 내부에 있지만 응용 프로그램에서 직접 이 함수를 호출하지는 않는다. 오직 운영체제만이 이 함수를 호출한다.


WM_PAINT에서 무한번 점을 찍으면 결과는 같지 않을까? 만약 이런 의문을 가지고 있다면 직접 코드를 작성해 넣어 보고 실행해 보면 왜 안되는지를 알 수 있을 것이다. 점이 무작위로 찍히기는 하므로 목적은 달성되겠지만 그 외 어떠한 동작도 할 수 없다. 타이틀 바를 드래그하여 위치를 옮길 수도 없으며 크기 변경도 되지 않고 시스템 메뉴를 눌러도 메뉴가 나타나지 않는다. 더구나 심각한 것은 종료 버튼을 눌러도 종료조차도 되지 않는다는 점이다. 왜냐하면 WM_PAINT 메시지 처리 구간에서 무한루프에 빠져 들었기 때문에 어떤 다른 메시지도 받을 수 없는 상태가 되었기 때문이다.

이와 같이 WM_PAINT로 사용하면 점 찍느라 바쁘기 때문에 다른 여러 실행이 동시에 안되며 또한 WM_TIMER 보단 콜백 함수 형태로 하는것이 낫다고 한다.

 정리하면, WM_PAINT 이런 반복적인 작업에 사용하게 되면 다른 일들을 동시에 하지 못하게 되므로 이런 반복적인 일에는 사용하지 말고,

타이머를 사용해야 하는데 WM_TIMER 보다는 콜백 함수로 함수형태로 호출하는 것이 현명하다.


완성 시킨 소스는 이러하며,


#include <windows.h>
#include <time.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"TextOut";

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance
  , LPSTR lpszCmdParam, int nCmdShow)
{
  HWND hWnd;
  MSG Message;
  WNDCLASS WndClass;
  g_hInst = hInstance;

  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = NULL;
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000)) {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }
  return Message.wParam;
}

void CALLBACK TimerProc(HWND hWnd, UINT uMsg, UINT idEvent, DWORD dwTime)
{
  HDC hdc;
  int i;
  hdc = GetDC(hWnd);
  for (i = 0; i<100; i++)
    SetPixel(hdc, rand() % 500, rand() % 400,
    RGB(rand() % 256, rand() % 256, rand() % 256, ));
  ReleaseDC(hWnd, hdc);
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  switch (iMessage)
  {
    case WM_CREATE:
      SetTimer(hWnd, 1100, (TIMERPROC)TimerProc);
      return 0;

    case WM_DESTROY:
      KillTimer(hWnd, 1);
      KillTimer(hWnd, 2);  // 추가 
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}


실행화면은 이러하다.



SetTimer(hWnd, 1100, (TIMERPROC)TimerProc);

밀리세컨드 단위 이므로 100이면 0.1 초만에 

for (i = 0; i<100; i++)
    SetPixel(hdc, rand() % 500, rand() % 400,
    RGB(rand() % 256, rand() % 256, rand() % 256, ));


100개의 점을 찍는 것이므로 1초면 1,000개, 10초면 10,000개를 찍는 것이다.



void CALLBACK TimerProc(HWND hWnd, UINT uMsg, UINT idEvent, DWORD dwTime)
{
  HDC hdc;
  int i;
  hdc = GetDC(hWnd);
  for (i = 0; i<100; i++)
    SetPixel(hdc, rand() % 500, rand() % 400,
    RGB(rand() % 256, rand() % 256, rand() % 256, ));
  ReleaseDC(hWnd, hdc);
}

이 부분을 분석해보자. 새로 보이는 함수가 존재한다.

1. SetPixel() 은 지정된 컬러의 근사값으로 지정된 좌표에 점을 찍는다.

점을 찍는 함수로써 모든 장치가 이 함수를 지원 하지 않으므로 GetDeviceCaps함수로 지원 여부를 조사해 본 후 이 함수를 호출해야 한다.

함수 원형

: COLORREF SetPixel(HDC hdc, int x, int y, COLORREF crColor);

2. rand 함수를 사용하여 500 이하, 400 이하의 좌표를 랜덤으로 생성하여 4번째 인자에 RGB 값 또한 랜덤으로 설정하여 그 반환된 색으로 

'랜덤한 위치의 랜덤한 색깔을 출력하라' 가 된다.



다음 강좌는 간단하게 설명으로 넘어가도록 하고 


지금까지 실습해 본 바대로 키보드, 마우스, 타이머 등의 입력 처리의 중심에는 메시지가 있다. 이런 명시적인 입력 외에 윈도우에 관한 기본적인 관리도 메시지로 한다.

4-4-가. 생성및 파괴

윈도우와 관련된 메시지 중 가장 간단한 메시지는 윈도우가 생성될 때 보내지는 WM_CREATE와 윈도우가 파괴될 때 보내지는 WM_DESTROY 두가지가 있다. WM_CREATE 메시지는 윈도우가 생성될 때 보내지므로 각종 초기화를 하기에 적합한 장소이며 반대로 WM_DESTROY 메시지는 종료처리를 하기에 적합하다. 이 두 메시지는 앞에서 몇번 사용해 보았으며 직감적으로 이해될만큼 상당히 쉬운 편이다.

이 메시지 대신 사용할 수 있는 방법으로는 WinMain에서 직접 초기화와 종료 처리를 해 주는 방법이 있다. WM_CREATE 메시지는 CreateWindow 함수에 의해 메인 윈도우가 생성된 직후에 보내지므로 CreateWindow 함수 호출문 다음에 초기화 코드를 작성해도 효과는 같다. WM_DESTROY 메시지는 메인 윈도우가 파괴되기 직전에 보내지는데 메인 윈도우 파괴후에는 메시지 루프가 끝나게 되므로 메시지 루프 다음에 종료 처리 코드를 작성할 수도 있다. 앞에서 만들었던 MyTimer 예제를 이런 식으로 변경해 보면 다음과 같아진다.

HANDLE hTimer, hTimer2;
char *str;

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance
		  ,LPSTR lpszCmdParam,int nCmdShow)
{

	================  중간 생략 ===================
	hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,
		  CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
		  NULL,(HMENU)NULL,hInstance,NULL);
	ShowWindow(hWnd,nCmdShow);
	
	hTimer=(HANDLE)SetTimer(hWnd,1,1000,NULL);
	hTimer2=(HANDLE)SetTimer(hWnd,2,5000,NULL);
	str="";
	SendMessage(hWnd, WM_TIMER, 1, 0);

	while(GetMessage(&Message,0,0,0)) {
		TranslateMessage(&Message);
		DispatchMessage(&Message);
	}

	KillTimer(hWnd,1);
	KillTimer(hWnd,2);

	return Message.wParam;
}

CreateWindow 호출문 다음에 타이머를 설정하는 초기화 처리를 하였고 메시지 루프 다음에 타이머를 파괴하는 종료 처리를 하였으며 관련된 전역 변수들을 WinMain 이전에 선언하였다. WM_CREATE와 WM_DESTROY에 있던 코드를 그대로 WinMain으로 옮겼는데 실행시켜 보면 완전히 동일하게 동작할 것이다.

그런데 어쨋든 결과가 같기는 하지만 WinMain에서 하는 초기화와 종료 처리는 WM_CREATE, WM_DESTROY 메시지가 발생했을 때 하는 것과는 엄밀하게 따지면 다르다. 두 메시지는 특정한 윈도우에 관련된 초기/종료 처리를 하는데 사용하는 것이 좋고 WinMain에서는 프로그램 전역적인 초기/종료 처리를 하는 것이 좋다. 여기서는 메인 윈도우 하나만 사용되므로 어디에서 하나 결과가 같지만 여러개의 윈도우가 사용된다면 그 의미가 조금 달라진다. 필자가 잘 쓰지도 않는 이런 코드를 보여주는 이유는 WM_CREATE와 WM_DESTORY 메시지의 용도에 대한 설명을 하기 위해서이다.




그 다음 강좌 작업 영역을 보자.


4-4-나. 작업 영역

윈도우는 작업 영역(Client Area)과 비작업 영역(Non Client Area) 두 부분으로 구성되어 있다. 작업 영역이란 쉽게 말해서 윈도우 중앙의 흰 부분을 말하며 비작업 영역이란 그 외의 영역을 말한다. 비작업 영역에 속하는 부분은 일단 타이틀 바와 경계선이 있고 메뉴가 있을 경우 메뉴도 비작업 영역에 속한다.

작업/비작업 영역의 구분은 윈도우를 이해하는데 중요한 비중을 차지하는데 왜냐하면 프로그래머에게 프로그래밍 대상이 되는 것은 작업 영역에 한정되기 때문이다. 일반적으로 비작업 영역은 프로그래밍 대상이 아니며 운영체제가 알아서 관리해 주도록 되어 있다. 타이틀 바나 경계선을 프로그래머가 직접 그려 주어야 할 필요가 없다는 얘기다. 또한 모든 출력을 기준은 작업 영역인데 좌표 (10,10)을 지정하면 이는 작업 영역의 좌상단을 기준으로 (10,10)을 의미하는 것이지 윈도우를 기준으로 (10,10)을 의미하는 것이 아니다.

그래서 원하는 위치에 정확하게 출력하려면 윈도우가 차지하고 있는 영역의 좌표를 조사해야 하는 것이 아니라 작업 영역의 좌표를 조사해야 한다. 이때는 다음 함수를 사용한다.

BOOL GetClientRect( HWND hWnd, LPRECT lpRect); 

이름 그대로 Client가 차지하고 있는 Rect를 Get하는 함수이다. 첫번재 인수로 대상 윈도우 핸들을 주고 두번째 인수로 리턴값을 돌려받기 위한 RECT 구조체의 포인터를 넘겨주면 된다. 이 함수 호출 후에 RECT 구조체에는 작업 영역의 좌표가 들어가는데 left, top은 항상 0이며 right, bottom에 우하단의 좌표가 대입된다.

그럼 이 함수를 사용하여 문자열을 작업 영역의 정중앙에 출력하는 예제를 만들어 보자. 작업 영역의 중앙 좌표를 알려면 작업 영역의 크기를 구한후 우하단 좌표를 2로 나누어 구하면 된다. 소스는 다음과 같다.

#include <windows.h>

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass="Client";

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance
		  ,LPSTR lpszCmdParam,int nCmdShow)
{
	HWND hWnd;
	MSG Message;
	WNDCLASS WndClass;
	g_hInst=hInstance;
	
	WndClass.cbClsExtra=0;
	WndClass.cbWndExtra=0;
	WndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
	WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);
	WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
	WndClass.hInstance=hInstance;
	WndClass.lpfnWndProc=(WNDPROC)WndProc;
	WndClass.lpszClassName=lpszClass;
	WndClass.lpszMenuName=NULL;
	WndClass.style=CS_HREDRAW | CS_VREDRAW;
	RegisterClass(&WndClass);

	hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
		NULL,(HMENU)NULL,hInstance,NULL);
	ShowWindow(hWnd,nCmdShow);
	
	while(GetMessage(&Message,0,0,0)) {
		TranslateMessage(&Message);
		DispatchMessage(&Message);
	}
	return Message.wParam;
} 

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	static RECT rt;
	switch(iMessage) {
	case WM_CREATE:
		GetClientRect(hWnd, &rt);
		return 0;
	case WM_PAINT:
		hdc=BeginPaint(hWnd, &ps);
		SetTextAlign(hdc,TA_CENTER);
		TextOut(hdc,rt.right/2, rt.bottom/2, "Center String",13);
		EndPaint(hWnd, &ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

WM_CREATE에서 GetClientRect 함수로 작업 영역의 좌표를 구해 rt에 대입해 주었다. 그리고 WM_PAINT에서는 작업 영역의 중앙 좌표를 구해 문자열을 출력하되 정확하게 중앙이 되도록 하기 위해 문자열을 중앙 정렬하였다. 실행중의 결과는 다음과 같다.


완성된 코드는 이러하며

#include <windows.h>
#include <time.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"Client";

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance
  , LPSTR lpszCmdParam, int nCmdShow)
{
  HWND hWnd;
  MSG Message;
  WNDCLASS WndClass;
  g_hInst = hInstance;

  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = NULL;
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000)) {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }
  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  static RECT rt;

  switch (iMessage) 
  {
    case WM_CREATE:
      GetClientRect(hWnd, &rt);
      return 0;
    case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      SetTextAlign(hdc, TA_CENTER);
      TextOut(hdc, rt.right / 2, rt.bottom / 2, TEXT("Center String Hong"), 18);
      EndPaint(hWnd, &ps);
      return 0;
    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}


실행 결과, 




이렇게 출력하게 된다. 이때 편리한 TEXT 매크로 함수를 이용하면 글자가 깨지지 않고 출력된다.


  case WM_CREATE:
      GetClientRect(hWnd, &rt);
      return 0;


이 부분 분석해보자. 


1. WM_CREATE 는 앞에서 언급했듯이 윈도우 프로그래밍 작성 후 우리가 윈도우를 생성했을 시 실행 되는 이른 바 선 처리 부분이다.

2. GetClientRect() 는 CWnd 클라이언트 영역의 크기를 얻는다.

함수 원형

:BOOL GetClientRect( HWND hWnd, LPRECT lpRect); 

윈도우의 작업영역 크기를 계산해 준다. 크기만 계산해 주기 때문에 좌상단(left, top)값은 항상 0, 0이며 우 하단 좌표(right, bottom)가 곧 윈도우의 크기를 나타낸다. 작업영역이란 윈도우의 타이틀바, 스크롤 바, 경계선, 메뉴 들을 제외한 영역이며 윈도우가 그리기를 하는 대상 영역이다.

작업/비작업 영역의 구분은 윈도우를 이해하는데 중요한 비중을 차지하는데 왜냐하면 프로그래머에게 프로그래밍 대상이 되는 것은 작업 영역에 한정되기 때문이다. 일반적으로 비작업 영역은 프로그래밍 대상이 아니며 운영체제가 알아서 관리해 주도록 되어 있다. 타이틀 바나 경계선을 프로그래머가 직접 그려 주어야 할 필요가 없다는 얘기다. 또한 모든 출력을 기준은 작업 영역인데 좌표 (10,10)을 지정하면 이는 작업 영역의 좌상단을 기준으로 (10,10)을 의미하는 것이지 윈도우를 기준으로 (10,10)을 의미하는 것이 아니다.

그래서 원하는 위치에 정확하게 출력하려면 윈도우가 차지하고 있는 영역의 좌표를 조사해야 하는 것이 아니라 작업 영역의 좌표를 조사해야 한다. 이때는 다음 함수를 사용한다.

이 부분에 해당한다.



 case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      SetTextAlign(hdc, TA_CENTER);
      TextOut(hdc, rt.right / 2, rt.bottom / 2, TEXT("Center String Hong"), 18);
      EndPaint(hWnd, &ps);
      return 0;


이 부분을 앞에서도 했지만 다시 해독해보자면


1. BeginPaint() 로 드로잉 작업을 수행하기 위해서 CWnd를 준비한다. 우리는 어떠한 것을 출력할 것이기에 윈도우즈 절차인 DC를 구해야 하므로 GetDC(),     ReleaseDc() 가 아닌 BeginPaint(), EndPaint() 를 이용하기로 한다.

2. SetTextAlign() 로 텍스트 정렬 플래그 값을 설정한다. TextOut() 가 지정하는 좌표는 디폴트로 문자열 출력 영역의 좌상단 좌표이다. 이 함수는 문자열의 출력     영역과 출력 좌표와의 관계를 변경함으로써 문자열의 출력 위치에 영향을 준다.

이 때 두번째 인자의 저 매크로는 

설명

TA_TOP

지정한 좌표가 상당 좌표

TA_BOTTOM

지정한 좌표가 하단 좌표

TA_CENTER

지정한 좌표가 수평 중앙 좌표

TA_LEFT

지정한 좌표가 수평 왼쪽 좌표

TA_RIGHT

지정한 좌표가 수평 오른쪽 좌표

TA_UPDATECP

지정한 좌표대신 CP사용하며 출력후 CP변경

TA_NOUPDATECP

CP를 사용하지 않고 지정한 좌표를 사용하며 CP 변경안함

이와 같다. 중앙에 출력하도록 설정해준다.

그 다음은 앞에서도 자세히 언급이 되었으니 말을 줄이겠다.

그래도 중요한것은 

 case WM_DESTROY:
      PostQuitMessage(0);
      return 0;

이 부분에서 파괴되는 윈도우가 메인 윈도우 일 경우 PostQuitMessage 함수를 반드시 호출하여 프로세스의 메시지 루프를 종료하도록 해야 한다.



4-4-다. WM_SIZE

앞에서 만든 예제는 과연 작업 영역 중앙에 문자열을 출력하기는 하였다. 윈도우가 만들어질 때 작업 영역 좌표를 구해 놓고 그릴 때 이 좌표를 기준으로 중앙 좌표를 계산하기 때문이다. 그런데 일단 출력된 윈도우의 크기를 변경하면 작업 영역의 크기가 달라져 더 이상 문자열은 작업 영역의 중앙에 있지 않게 된다.

문자열을 계속 작업 영역 중앙에 두고 싶으면 윈도우의 크기가 변경될 때마다 다시 출력해 주어야 하는데 이때 사용되는 메시지가 WM_SIZE 메시지이다. 이 메시지는 윈도우의 크기가 변경되었을 때 보내진다. 이때 lParam의 하위 워드에는 변경된 후의 윈도우 폭이, 상위 워드에서는 높이가 전달되며 wParam에는 이 메시지가 발생한 이유를 나타내는 플레그가 전달된다.

플레그
SIZE_MAXHIDE다른 윈도우가 최대화되어 이 윈도우가 가려졌다.
SIZE_MAXIMIZED최대화되었다.
SIZE_MAXSHOW다른 윈도우가 원래 크기로 복구되어 이 윈도우가 드러났다.
SIZE_MINIMIZED최소화되었다.
SIZE_RESTORED크기가 변경되었다.

일반적으로 이 플레그는 잘 사용되지 않는다. WM_SIZE 메시지를 사용하여 윈도우 크기가 변경될 때마다 문자열 위치를 수정하도록 WndProc을 다음과 같이 수정해 보자.

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	static RECT rt;
	switch(iMessage) {
	case WM_PAINT:
		hdc=BeginPaint(hWnd, &ps);
		SetTextAlign(hdc,TA_CENTER);
		TextOut(hdc,rt.right/2, rt.bottom/2, "Center String",13);
		EndPaint(hWnd, &ps);
		return 0;
	case WM_SIZE:
		GetClientRect(hWnd, &rt);
		InvalidateRect(hWnd, NULL, TRUE);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

WM_CREATE 대신 WM_SIZE에서 윈도우 크기가 변경될 때마다 작업 영역의 크기를 다시 계산하고 화면을 다시 그리므로 문자열은 윈도우의 크기에 상관없이 항상 같은 위치에 있게 된다. WM_SIZE에서 GetClientRect 함수를 호출하여 작업 영역의 크기를 다시 조사하는데 lParam으로도 작업 영역 크기가 전달되므로 다음과 같이 쓸 수도 있다.

case WM_SIZE:
	rt.right=LOWORD(lParam);
	rt.bottom=HIWORD(lParam);
	InvalidateRect(hWnd, NULL, TRUE);
	return 0;

WM_SIZE 메시지는 윈도우 크기에 상관없이 일정한 레이아웃을 유지하기 위해 빈번하게 사용된다.




4-4-라. WM_MOVE

WM_SIZE와 유사한 메시지로 WM_MOVE 메시지가 있다. 이 메시지는 윈도우의 위치가 변경될 때마다 보내지는데 lParam의 하위 워드에 윈도우의 새 X좌표, 상위 워드에 윈도우의 새 Y 좌표가 전달된다. 윈도우가 이동될 때마다 특별한 일을 해야 할 필요가 있다면 이 메시지를 사용하면 될 것이나 현실적으로 이 메시지는 잘 사용되지 않는다.

참고

본문에서 윈도우(Window)라는 말과 윈도우즈(Windows)라는 말을 섞어서 사용하고 있는데 이 두 단어는 엄격하게 의미가 다르다. 윈도우는 화면에 나타난 사각영역을 이르는 말이고 윈도우즈는 운영체제 자체를 이르는 말이다. 전자는 일반명사이고 후자는 고유명사인데 이 둘을 잘 구분하지 못하면 무척 헷갈리게 된다.

창이 이동되면 호출된다.



메뉴는 윈도우즈용 프로그램이 제공하는 가장 표준적인 사용자 인터페이스이며 프로그램의 전체 기능을 총괄적으로 제공해주는 중요한 기능을 가지고 있다. 메뉴의 구조와 기능, 그리고 내부적인 운용 방법은 사실 간단한 것은 아니지만 프로그래밍하기에는 비교적 쉬운 편에 속한다.

5-2-가. 리소스 작성

여기서는 메뉴를 가진 프로그램을 작성하면서 리소스를 만드는 절차와 만들어진 리소스를 프로그램에서 사용하는 방법에 대해서 알아본다. 비주얼 C++을 사용하여 다음 절차를 따라 메뉴 리소스를 사용하는 Menu.dsw를 만들어 보도록 하자.

1File/New 명령을 사용하여 Menu.dsw 프로젝트를 먼저 만든다. 그리고 새 텍스트 파일을 만든 후 ApiStart.txt 파일을 복사해 붙이고 lpszClass 문자열을 "Menu"로 변경한다. 이 파일을 Menu.cpp로 저장하고 프로젝트에 포함시키면 프로젝트가 만들어졌다. 아직 메뉴는 붙이지 않았다.

2이제 메뉴 리소스를 만들어 붙인다. 아직 이 프로그램에는 사용하는 리소스가 하나도 없으므로 새로운 리소스 스크립트 파일을 만들어야 한다. Insert/Resource 메뉴 항목을 선택하면 다음과 같은 대화상자를 보여줄 것이다.

어떤 종류의 리소스를 만들 것인가를 물어오는 대화상자이다. 이 대화상자에서 Menu를 더블클릭하여 선택해 보자. 그러면 Script1이라는 새로운 리소스 스크립트(RC파일)를 만들어 주며 RC파일 안에 메뉴 리소스를 생성해 준다. 그리고 메뉴를 편집할 수 있는 메뉴 편집기를 열어준다. 다음과 같은 두개의 윈도우가 동시에 열릴 것이다.

 

왼쪽이 RC파일의 모양을 보여주는 스크립트 윈도우이다. RC파일은 텍스트 파일이지만 비주얼 C++은 RC파일의 내용을 텍스트로 보여주지 않고 등록된 리소스 종류별로 계층적으로 보여주며 편집 또한 텍스트를 직접 편집하는 것이 아니라 비주얼 편집 방식을 사용한다. 리소스 스크립트 윈도우를 보면 IDR_MENU1이라는 메뉴가 생성되어 있을 것이다. 메뉴 편집기의 작업영역에는 빈칸만 하나 있다. 이 빈칸에 메뉴 이름을 써 넣으면 메뉴 항목이 만들어진다. &File이라고 입력하면 File이라는 메뉴 항목이 생길 것이며 속성 편집 윈도우가 열린다.

속성 편집 윈도우를 사용하여 개별적인 메뉴 항목에 대해 속성을 설정한다. File메뉴를 만들고 난 후의 메뉴 편집기는 다음과 같다.

File메뉴가 만들어졌고 오른쪽과 아래쪽으로 빈칸이 두 개 생겼다. 이 빈칸에 새로운 메뉴 항목을 만들어 나가되 아래쪽으로 메뉴 항목을 만드려면 아래쪽의 빈칸을 사용하고 오른쪽으로 메뉴 항목을 만들어 나가려면 오른쪽 빈칸을 사용한다. 이번에는 아래쪽에 Menu1이라는 메뉴 항목을 만들어 보자. 아래쪽의 빈칸을 더블 클릭하면 속성 편집 윈도우가 열린다. 이 윈도우에 다음과 같이 ID와 Caption을 입력한다.

Caption은 사용자에게 보여질 메뉴의 이름이며 메뉴 리스트에 그대로 나타난다. 문자열일 뿐이므로 기호나 숫자, 한글까지 사용할 수 있다. ID는 프로그램에서 이 메뉴 항목을 지칭하는 이름이다. 변수나 함수의 이름과 같은 자격을 가지는 명칭(Identifier)이므로 명칭을 만드는 규칙에 맞게 작성해야 한다. 즉 첫문자를 숫자로 쓸 수 없고 한글을 써서도 안되며 메뉴 항목끼리 이름이 중복되어서도 안된다. 관습적으로 메뉴 항목의 이름은 "ID_상위메뉴_캡션"의 형식을 따라 만들면 된다. Caption에 Menu1, ID에 ID_FILE_MENU1이라고 설정하고 나머지 속성은 그대로 두도록 하자.

계속해서 Menu2, Exit 메뉴 항목을 만들고 각각 ID를 ID_FILE_MENU2, ID_FILE_EXIT로 준다. 여기까지 메뉴를 만들면 메뉴 편집기는 다음과 같아질 것이다.

이제 메뉴 리소스가 다 만들어졌다. 메뉴의 편집 방식이 비주얼 방식이기 때문에 아주 쉽게 작성할 수 있도록 되어 있다.

3이렇게 만들어진 리소스 스크립트를 일단 디스크에 저장한다. 현재 이름이 Script1으로 되어 있는데 File/Save 항목을 선택하고 파일 이름을 Menu로 주면 자동으로 확장자 RC가 붙어 Menu.rc라는 이름으로 디스크에 저장된다. 리소스의 이름도 가급적이면 프로젝트와 같은 이름을 주는 것이 좋다. 다음은 RC파일을 프로젝트에 포함시켜 프로젝트에서 메뉴 리소스를 사용할 수 있게 해 주어야 한다. 리소스를 작성하기는 했지만 아직 프로젝트와 논리적인 연관은 없으므로 이대로는 프로젝트에서 리소스를 사용할 수 없다. Project/Add to Project/Files 메뉴 항목을 선택하여 Menu.rc 파일을 프로젝트에 포함시킨다. 워크 스페이스의 리소스 뷰에 다음과 같이 현재까지 작성된 리소스가 나타날 것이다.

이후부터 리소스를 편집하려면 리소스 뷰에서 원하는 리소스를 더블클릭하면 된다. IDR_MENU1을 더블클릭하면 메뉴 편집기가 다시 열린다. 또한 이 리소스 뷰의 팝업 메뉴를 통해 새로운 리소스를 작성하거나 다른 프로젝트에 있는 리소스를 임포트하는 등의 전반적인 리소스 관리를 할 수 있다.

4만들어진 리소스를 프로젝트에서 사용하도록 코드를 수정한다. Menu.cpp를 열어서 다음과 같이 수정 또는 추가해 준다.

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass="Menu";

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance
		  ,LPSTR lpszCmdParam,int nCmdShow)
{
	HWND hWnd;
	MSG Message;
	WNDCLASS WndClass;
	g_hInst=hInstance;
	
	WndClass.cbClsExtra=0;
	WndClass.cbWndExtra=0;
	WndClass.hbrBackground=GetStockObject(WHITE_BRUSH);
	WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);
	WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
	WndClass.hInstance=hInstance;
	WndClass.lpfnWndProc=(WNDPROC)WndProc;
	WndClass.lpszClassName=lpszClass;
	WndClass.lpszMenuName=MAKEINTRESOURCE(IDR_MENU1);
	WndClass.style=CS_HREDRAW | CS_VREDRAW;
	RegisterClass(&WndClass);

우선 resource.h라는 헤더 파일을 먼저 포함시켜 주어야 하는데 이 파일에는 다음과 같이 우리가 만든 메뉴 항목의 ID가 정의되어 있다. 이 파일은 리소스 편집기가 리소스 편집시에 작성해 주며 리소스를 편집하면 자동으로 같이 편집되므로 우리가 직접 편집할 필요는 없다.

//{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by Menu.rc
//
#define IDR_MENU1                       101
#define ID_FILE_MENU1                   40001
#define ID_FILE_MENU2                   40002
#define ID_FILE_EXIT                    40003

메뉴 항목의 ID는 정수로 정의되어야 하되 사람이 일일이 정수값을 기억하기가 곤란하므로 매크로 상수를 사용하여 ID를 정의하고 메뉴 편집기는 사용자가 입력한 ID를 정수 매크로로 정의하여 resource.h에 작성해 준다. 그래서 우리는 메뉴의 ID를 문자열 형태의 매크로 상수로 정의해 주고 resource.h만 인클루드해 주면 된다.

다음으로 WndClass의 lpszMenuName 멤버에 우리가 작성한 메뉴 이름인 IDR_MENU1을 대입해 준다. 이때 숫자로 정의된 메뉴 이름을 문자열로 바꾸어 주기 위해 MAKEINTRESOURCE 매크로를 사용하였다. 윈도우에서 사용할 메뉴에 관한 정보를 가지는 lpszMenuName은 문자열 변수이되 우리가 작성한 메뉴의 ID는 정수로 되어 있기 때문이다.

여기까지 작성하고 다시 컴파일해 보면 리소스로 정의한 메뉴가 타이틀 바 아래쪽에 나타날 것이다. 그러나 아직 메뉴에 대한 코드가 작성되지 않았기 때문에 메뉴만 나타날 뿐 메뉴를 선택해도 아무런 반응이 없다. 이제 메뉴가 선택되었을 때의 프로그램 동작을 정의해 준다. Menu.cpp를 열어서 WndProc를 다음과 같이 수정한다.

long FAR PASCAL WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	switch(iMessage) {
	case WM_COMMAND:
		switch(LOWORD(wParam)) {
		case ID_FILE_MENU1:
			MessageBox(hWnd,"첫번째 메뉴를 선택했습니다.","Menu Demo",MB_OK);
			break;
		case ID_FILE_MENU2:
			MessageBox(hWnd,"두번째 메뉴를 선택했습니다.","Menu Demo",MB_OK);
			break;
		case ID_FILE_EXIT:
			PostQuitMessage(0);
			break;
		}
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	default:
		return(DefWindowProc(hWnd,iMessage,wParam,lParam));
	}
}

이 코드를 보면 감이 잡히겠지만 메뉴 항목을 선택할 경우 WM_COMMAND라는 메시지가 전달된다. 코드를 작성한 후 컴파일, 실행시켜보자. 타이틀 바 바로 아래에 메뉴가 나타나며 메뉴를 선택할 경우 메시지 박스가 화면 중앙에 나타날 것이다.

어디까지나 예제이므로 메뉴 항목 선택시 메시지 박스만 열어 보였는데 실제 프로그램을 작성할 때는 선택한 메뉴 항목에 맞는 코드를 작성해 주면 될 것이다.

처음으로 리소스를 사용하는 예제를 작성해 봤는데 리소스를 만들고 프로젝트에 포함시키는 과정을 다시 한번 정리해 보자. 좀 복잡해 보일지도 모르겠지만 익숙해지면 별것도 아니다.

일단 이 과정을 거쳐 리소스 스크립트가 프로젝트에 포함되면 프로젝트 워크 스페이스에 리소스 뷰가 나타난다. 리소스 뷰는 프로젝트에 포함된 리소스를 종류별로 보여주며 관리하는 역할을 한다. 이후부터 리소스를 열거나 삭제하거나 새로 추가하는 작업은 리소스 뷰의 팝업 메뉴를 사용하면 된다.

팝업 메뉴를 보면 Open, Insert 등의 메뉴가 있는데 영어뜻 그대로이므로 Open이 열기이고 Insert가 새 리소스 삽입이라는 설명은 하지 않아도 될 것 같다.


이제 메뉴 넣을 차례이다.


위와 같이 넣어주자.







이것을 그대로 적용시켜보자.


소스를 추가 해주고





long FAR PASCAL WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	switch(iMessage) {
	case WM_COMMAND:
		switch(LOWORD(wParam)) {
		case ID_FILE_MENU1:
			MessageBox(hWnd,"첫번째 메뉴를 선택했습니다.","Menu Demo",MB_OK);
			break;
		case ID_FILE_MENU2:
			MessageBox(hWnd,"두번째 메뉴를 선택했습니다.","Menu Demo",MB_OK);
			break;
		case ID_FILE_EXIT:
			PostQuitMessage(0);
			break;
		}
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	default:
		return(DefWindowProc(hWnd,iMessage,wParam,lParam));
	}
}


이 부분의 소스를 보자면,

사용자가 원하는 메뉴를 클릭하면.

WM_COMMAND에 메세지 큐가 들어온다라는 것을 알 수 있고 wParam에 어느 메뉴가 실행됬는지 고유 번호가 저장된다 라는 걸 확인 할 수 있다.

그러므로 이와 같이 switch-case 문을 통해 해당 고유번호가 실행 되었다면, 을 기술 해주면 된다.


자동으로 발급 받은 고유 번호를 이용하여 이와 같이 코딩 하면 해당 메뉴 switch-case 문 밑에 정의 된 대로 실행 된다.


자 이대로 WinProc에 코딩하면 완성된 코드 모습은,

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"Menu";

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance
  , LPSTR lpszCmdParam, int nCmdShow)
{
  HWND hWnd;
  MSG Message;
  WNDCLASS WndClass;
  g_hInst = hInstance;

  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1);
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000)) {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }
  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  switch (iMessage) 
  {
    case WM_COMMAND:
      switch (LOWORD(wParam))  
      {
        case ID_FILE_MENU1:
          MessageBox(hWnd, TEXT("첫번째 메뉴를 선택했습니다."), TEXT("Menu1 Demo"), MB_OK);
          break;
        case ID_FILE_MENU2:
          MessageBox(hWnd, TEXT("두번째 메뉴를 선택했습니다."), TEXT("Menu2 Demo"), MB_OK);
          break;
        case ID_FILE_EXIT:
          PostQuitMessage(0);
          break;
      }
    return 0;

    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}

이와 같고,

실행 화면은



이와 같다.



여기서 나는 복습을 하고 가도록 하자. 


WndProc() 는 윈도우로 전달되는 메시지를 처리하는 메시지 처리 함수 이며 보통 윈도우 프로시저(Window Procedure)라고 부른다. 사용자 정의 함수 이므로

이름은 정해져 있지 않으나 보통 WndProc 또는 WindowProc 이라는 이름을 많이 사용한다. 운영체제는 사용자의 조작과 시스템 내부의 변화가 있을 때,

메시지 큐에 메시지를 넣으며 WinMain의 메시지 루프는 메시지 큐에서 메시지를 꺼내 윈도우 프로시저로 전달 해 준다.

윈도우 프로시저는 자신에게 전달된 메시지의 의미를 분석하여 응용 프로그램 고유의 처리를 한다. 보통 하나의 프로그램이 복수개의 메시지를 처리하므로 윈도우

프로시저는 일반적으로 메시지별로 고유한 처리를 할 수 있는 switch문으로 구성 된다. 그래서 기본형은 switch-case 문으로 WndProc() 의 내부가 메인 윈도우가

종료되었을 시, 까지만 정의 되어 있는 것이다.


또한, WinMain() 는 모든 윈도우 프로그래밍에 대한 Entry Point를 담당하며 컴파일 할 때 없어지는 함수 이다.

함수의 call 방식을 define 하고 컴파일러에게 알려준다.

인자 값들에 대해서는 1, 2 인자는 Win32 로 업 그레이드 되며 안쓰지만 남아있는 인자이고,

LPSTR lpszCmdParam,int nCmdShow)

lpszCmdParam   dos C프로그래밍에서의 argv와 같은 기능을 담당한다.

nCmdShow   은 창을 화면에 보이게 할 것인가를 결정한다.

이제 내부 정의 부분을 살펴보면

HWND hWnd;

: 윈도우 핸들러 이다. 기본적으로 무조건 필요하다.

WPARAM

: unsigned int 형(4byte)

LPARAM

: long 형(4byte), intrk 16bit이던 시절에 32bit로 쓰다가 32비트로 넘어와서 int가 4byte가 되어서도 그대로 유지된다.



5-2-나. WM_COMMAND

프로그램 실행중에 사용자가 메뉴 항목을 선택하면 WM_COMMAND 메시지가 발생한다. 이때 어떤 메뉴 항목이 선택되었는가는 wParam의 하위 워드로 전달되므로 LOWORD(wParam)을 읽어 판단할 수 있다. 그래서 Menu.cpp의 WndProc에서는 WM_COMMAND 메시지를 받을 경우 switch문으로 다시 LOWORD(wParam)의 값에 따라 분기를 하여 각 메뉴 항목에 따른 처리를 수행한다. DI_FILE_MENU1메뉴 항목(Menu1)이 선택되었을 경우 메시지 박스를 열어 메뉴 항목이 선택되었음을 알려주고 ID_FILE_EXIT메뉴 항목(Exit)이 선택되었을 경우 프로그램을 종료하도록 하였다. 메뉴 항목이 더 늘어난다면 WM_COMMAND의 switch문안에 case만 계속 늘려주면 된다.

WM_COMMAND 메시지는 메뉴 항목을 선택할 때 뿐만 아니라 액셀러레이터를 누를 때도 발생하며 또한 버튼, 에디트 박스 등의 컨트롤이 부모 윈도우로 통지 메시지를 보낼 때도 발생한다. 이 메시지의 추가 정보는 다음과 같은 구조를 가진다.

설명
lParam통지 메시지를 발생시킨 컨트롤의 윈도우 핸들
LOWORD(wParam)메뉴나 액셀러레이터, 컨트롤의 ID
HIWORD(wParam)컨트롤이 보내주는 통지 메시지, 메뉴가 선택된 경우는 0이 되며 액셀러레이터가 선택된 경우는 1이 된다.

WM_COMMAND 메시지가 워낙 여러가지 명령을 받아들이는 메시지이다보니 추가 정보도 복잡하다. 여기서 우리가 사용한 값은 메뉴ID가 전달되는 LOWORD(wParam)뿐이며 통지 메시지니, 컨트롤이니 하는 말들은 지금 당장은 몰라도 상관없다. 아뭏든 여기서 기억할 것은 메뉴 선택시 WM_COMMAND 메시지가 전달되며 LOWORD(wParam)을 읽어 어떤 메뉴가 선택되었는지를 판단할 수 있다는 것이다.





5-2-다.메뉴 편집기의 사용법

메뉴 리소스는 리소스 파일(RC)에 텍스트 형태로 작성하는 것이 원칙이며 실제로 과거의 프로그래머들은 텍스트 편집기로 메뉴를 만들었었다. 그런데 편리한 툴이 많이 나와 지금은 텍스트로 메뉴 리소스를 직접 편집하는 사람은 거의 없으며 그럴 필요도 없다. 비주얼 C++도 편리한 메뉴 편집기를 제공하여 비주얼 메뉴 편집을 지원하므로 편리하게 메뉴 리소스를 작성할 수 있도록 해준다. 메뉴 편집기는 사용자가 편집한 메뉴를 텍스트 형태로 리소스 파일에 기록해 준다. 여기서는 메뉴 자체에 대해서는 잠시 접어두고 메뉴 편집기 사용법을 알아보자.

새 메뉴의 작성

새로운 메뉴를 작성할 때는 Insert/Resource/Menu 항목을 선택하면 된다. 아무 것도 없는 빈 메뉴를 준비해 줄 것이다.

좌상단에 빈칸이 하나 준비되어 있는데 이 빈칸을 출발점으로 하여 메뉴 항목을 오른쪽으로 또는 아래로 만들어 나가면 된다.


빈칸에 새로운 메뉴를 추가하려면 빈칸을 더블 클릭하여 속성 편집기를 열고 캡션과 기타 속성들을 입력하면 된다. 좌상단의 빈칸을 더블클릭하여 File이라는 캡션을 주면 다음과 같이 된다.

오른쪽과 아래쪽에 각각 빈칸이 나타나는데 이 빈칸들에 또 메뉴 항목을 만들면 된다. 빈칸에 메뉴를 만들면 또 그 옆(또는 아래)에 빈칸이 나타날 것이다. 이런 식으로 다음과 같이 메뉴를 만들어 보자.

메뉴 항목의 수정

기존에 이미 만들여져 있는 메뉴의 이름이나 속성을 변경하려면 기존 메뉴 항목을 더블 클릭하여 속성 편집기를 불러낸 후 원하는 속성을 다시 입력해 주면 된다. 예를 들어 File/Open의 캡션을 Read로 변경하려면 이 메뉴 항목을 더블 클릭한 후 Caption을 Read로 변경해 주기만 하면 된다. 다른 속성들도 마찬가지다.

메뉴 항목의 삽입

기존에 작성되어 있는 메뉴 리스트 가운데 새로운 메뉴 항목을 끼워 넣는 것이다 삽입할 위치에 커서럴 위치시킨 후 키보드에서 Ins키를 누르면 삽입할 위치에 빈칸이 새로 생성되며 이 빈칸에 원하는 항목을 입력해 주면 된다. 예를 들어 Save와 Print 항목 사이에 New 항목을 삽입하려면 Print위치에서 Ins키를 누른 후 New를 입력해 주면 된다.

메뉴 항목의 삭제

삭제하고자 하는 메뉴 항목에 커서를 위치시킨 후 키보드의 Del키를 눌러주면 된다. 너무 너무 간단하다. 단 메뉴 리스트의 메뉴 항목은 그냥 삭제가 되지만 하위 메뉴를 거느린 팝업 메뉴를 삭제할 때는 바로 삭제하지 않고 다음과 같이 일차 경고를 한다.

팝업 메뉴를 삭제할 때는 그 아래에 딸린 메뉴 항목들도 한꺼번에 삭제되므로 주의하라는 경고이다. 혹시 실수로 애써 만들어 놓은 메뉴를 망칠까봐 한번 더 확인을 하는 것이다. 만약 실수로 지워서는 안될 메뉴를 지웠다면 Edit/Undo를 선택해 원래대로 취소할 수 있다.

메뉴의 이동

이동이란 메뉴끼리 자리를 바꾸는 것이다. 이동하고자 하는 메뉴를 마우스로 드래그하여 원하는 위치에 떨어뜨리기만 하면 된다. 드래그중에 삽입될 위치를 화살표로 보여주므로 쉽게 위치를 선택할 수 있을 것이다. 이 기능을 잘 이용하면 메뉴 바에 있는 메뉴를 하위 메뉴로 만들 수도 있다. 예를 들어 Edit메뉴를 File메뉴의 Save와 Print사이에 끼워 넣으면 다음과 같이 Edit메뉴 전체가 File메뉴의 하위 메뉴가 된다.

복사 및 붙여넣기

메뉴의 복사, 붙여넣기는 워드 프로세서에서 클립보드를 사용하여 문서의 일부분을 복사하는 것과 개념적으로 동일하다. 똑같은 메뉴를 만든다거나 위치를 바꾸고자 할 때 이 기능을 사용한다. 또한 다른 프로젝트에 작성되어 있는 메뉴 항목(또는 메뉴 리스트)를 복사해 올 때도 이 기능을 사용한다. File/Open 명령으로 다른 프로젝트의 RC파일을 읽어온 후 복사, 붙이기를 하면 된다. 복사나 붙이기를 하려는 메뉴 항목 위에서 오른쪽 마우스 버튼을 눌러 팝업 메뉴를 사용한다.

팝업 메뉴를 보면 Cut, Copy, Paste 등의 항목들이 있으므로 이 항목들을 사용한다. 여러개의 메뉴 항목을 선택할 때는 Ctrl키나 Shift키를 사용하여 선택하면 된다.

이상으로 메뉴 리소스를 작성하여 프로젝트에서 메뉴를 사용하는 간단한 예제를 만들어 보았다. 메뉴에 대한 더 상세한 내용은 12장에서 자세하게 다룰 예정이므로 여기서는 메뉴 그자체보다는 리소스의 개념을 익히는 데 치중하도록 하자.







5-3 아이콘, 커서

윈도우즈용 프로그램은 자기 자신을 나타내기 위한 예쁜 그림인 아이콘을 가지며 또한 작업 영역에 마우스가 위치할 경우 자기만의 마우스 커서를 가진다. 아이콘이나 커서도 리소스의 일종이며 각각의 편집기를 사용하여 제작할 수 있다. 앞에서 만들었던 menu 예제를 수정하여 아이콘과 커서를 만들어 보기로 하자. Insert/Resource 메뉴 항목을 선택하여 이번에는 Icon을 선택하여 새로운 아이콘을 만든다. 다음과 같이 아이콘을 편집할 수 있는 아이콘 편집기를 열어줄 것이다.

아이콘 편집기를 사용하는 방법은 페인트 브러시를 사용하는 방법과 비슷하며 마우스로 원하는 툴을 선택하여 그리기만 하면 되므로 아주 쉽다. 어디까지나 연습이므로 대충 만들어 보자. 필자는  와 같은 모양의 아주 유아틱한 아이콘을 디자인해 보았다.

다음은 커서를 만든다. 아이콘을 만드는 방법과 마찬가지로 Insert/Resource 메뉴 항목을 선택하여 Cursor를 선택한다. 커서를 편집할 수 있는 커서 편집기가 열린다. 사용하는 방법은 아이콘 편집기와 비슷하되 사용할 수 있는 색상이 제한되어 있다는 점만 다르다. 다음과 같이 커서를 만들어 보자.

대충 커서를 만든 후 커서 편집기를 닫고 이번에는 만들어 놓은 커서와 아이콘을 사용하도록 코드를 수정한다. Menu.cpp를 열어 다음 두 줄만 수정해 주면 된다.

	WndClass.cbClsExtra=0;
	WndClass.cbWndExtra=0;
	WndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
	WndClass.hCursor=LoadCursor(hInstance,MAKEINTRESOURCE(IDC_CURSOR1));
	WndClass.hIcon=LoadIcon(hInstance,MAKEINTRESOURCE(IDI_ICON1));
	WndClass.hInstance=hInstance;
	WndClass.lpfnWndProc=(WNDPROC)WndProc;
	WndClass.lpszClassName=lpszClass;

프로그램에서 사용할 커서와 아이콘은 윈도우 클래스에 등록하므로 WinMain의 윈도우 클래스 정의 부분만 수정해 주면 된다. 프로그램의 작업 영역에 마우스가 위치하면 우리가 만들었던 마우스 커서가 나타날 것이며 윈도우의 타이틀 바 왼쪽에 우리가 만든 아이콘이 나타난다. 또한 윈도우를 최소화시키면 타스크 바에서도 이 아이콘 모양을 볼 수 있을 것이다.

참고로 리소스 파일(RC)은 텍스트 파일이므로 에디트로 직접 만들 수도 있으며 어떤 모양을 가지는지 직접 확인할 수도 있다. 비트맵이나 커서를 리소스 에디터로 별도의 파일로 만들어 둔 후 메뉴나 단축 키, 문자열 등의 리소스들과 함께 텍스트 파일에 포함시키는 방식으로 리소스를 작성하는 것도 가능하다. MENU.RC 파일은 다음과 같은 모양을 가진다.

//Microsoft Developer Studio generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// Korean resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_KOR)
#ifdef _WIN32
LANGUAGE LANG_KOREAN, SUBLANG_DEFAULT
#pragma code_page(949)
#endif //_WIN32

/////////////////////////////////////////////////////////////////////////////
//
// Menu
//

IDR_MENU1 MENU DISCARDABLE 
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "Menu1",                       ID_FILE_MENU1
        MENUITEM "Menu2",                       ID_FILE_MENU2
        MENUITEM "Exit",                        ID_FILE_EXIT
    END
END


#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE 
BEGIN
    "resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#include ""afxres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
//
// Icon
//

// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_ICON1               ICON    DISCARDABLE     "Menu.ico"

/////////////////////////////////////////////////////////////////////////////
//
// Cursor
//

IDC_CURSOR1             CURSOR  DISCARDABLE     "Menu.cur"
#endif    // Korean resources
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//


/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED

주석문이나 조건부 컴파일 지정문이 좀 많아서 그렇지 리소스 스크립트의 문법 자체는 별로 복잡하지 않다. 하긴 리소스 편집기가 워낙 편리하게 제공되므로 스크립트 파일을 직접 편집해야할 일은 거의 없을 것이다. RC파일은 어떤 개발툴을 사용했는가에 따라 모양이 조금 다르게 나타난다.




커서 만들어보자.


이와 같이 똑같이 리소스에 add 로 커서를 임의로 만들어주고,

그리하면 헤더파일에 자동으로 추가가 된다.

이렇게 코드를 작성 후에,




 실행 해보면


이처럼 커서의 모양이 달라졌음을 확인 할 수 있다.


지금 이러한 것들은 별로 중요치 않은데 나중에 비트맵 조절할 때가 제일 중요하다고 한다.

꼭 예습, 복습이 필요할 것 같다.