본문 바로가기
코스웨어/15년 스마트컨트롤러

20151116 - 홍준모 Win32API - 3일차

by 알 수 없는 사용자 2015. 11. 16.
728x90
반응형

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);






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(), 사각형 그린 뒤 무효화 이다.




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개를 찍는 것이다.





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 매크로 함수를 이용하면 글자가 깨지지 않고 출력된다.





이제 메뉴 넣을 차례이다.


위와 같이 넣어주자.







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


소스를 추가 해주고





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));
}

이와 같고,

실행 화면은



이와 같다.





커서 만들어보자.


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

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

이렇게 코드를 작성 후에,




 실행 해보면




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





다른 곳에서 작성한 내용은 2일 분 복습하고 따라가느라 이것보다  부수적인것들이 엄청나게 많은데 너무 길어서 많이 짤라서 올립니다.

728x90