![[Windows API] Win32 API의 타이머를 활용해 시계 만들기 - (1)](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVJbLB%2FbtrqncVcW4f%2FZo6DLwZRsdbl96pLTRBti1%2Fimg.png)
목표
타이머에 대해 이해하고, 타이머를 활용해 다양한 작업을 하는 방법에 대해서 배워보도록 하겠습니다.
목차 클릭하면 해당 목차로 이동합니다.
개요
이전 포스팅에서 Win32 API를 활용해 입, 출력을 하는 것에 대해 배웠습니다. 텍스트, 도형 등을 출력하고, 마우스 클릭을 통해 그림을 그리는 프로그램을 작성했습니다. 코드는 순차적으로 진행되기 때문에, 이러한 동작들은 개발자가 의도한 순서에 의해 동작하는 코드들이었습니다. 이번 포스팅에서는 타이머를 통해 일정 시간마다 메세지 혹은 함수를 호출해 동작하는 것에 대해서 배워보도록 하겠습니다.
타이머 설치와 제거
Win32 API의 장점 중 하나는 백그라운드 작업을 지원한다는 것입니다. 백그라운드 작업을 구현하는 방법 중 하나는 타이머를 활용하는 것입니다. 타이머를 설치해 일정 시간마다 메세지 혹은 함수를 호출해 작업을 제어하도록 하는 것입니다. 타이머는 SetTimer()함수를 통해 설치할 수 있습니다. 구체적인 설명에 앞서 함수의 원형을 확인해보도록 하겠습니다.
UINT SetTimer(HWND hWnd, UINT nIDEvent, UINT uElapse, TIMERPROC lpTimerFunc)
각 인수의 역할은 다음과 같습니다.
- hWnd: 타이머가 발생하는 윈도우의 핸들
- nIDEvent: 해당 타이머의 ID (개발자가 부여함)
- uElapse: 타이머의 주기 (n/1000초)
- lpTimerFunc: 타이머가 발생하면 호출될 전용 함수 (NULL의 경우 WM_TIMER 발생)
당연히, 첫 번째 매개변수는 타이머가 발생하는 윈도우의 핸들이 들어갑니다. 두 번째 매개변수는 타이머의 ID 입니다. 프로그램에 타이머가 여러 개 존재할 수 있기 때문에, 구분을 위해 타이머의 ID를 설정합니다. 이는, 개발자가 함수를 작성할 때 직접 부여해야 합니다. 당연히, 여러 개의 타이머를 생성하면 철저히 관리해야 할 필요가 있습니다. 세 번째 매개변수는 타이머의 주기입니다. 몇 초마다 타이머가 발생해 함수를 호출하거나 메세지를 발생시킬지 설정해야 합니다. 단위는 1/1000초로, 1000을 넣어주면 1초에 한 번씩 타이머가 발생합니다. 마지막 매개변수는 타이머가 발생하면 호출될 전용 함수입니다. 함수를 호출할 수 있지만, NULL값을 넣게 되면 WM_TIMER 메세지를 발생시킵니다.
WM_TIMER 메세지를 발생시켜 타이머의 ID를 통해 작업을 제어할 수 있게 됩니다. WM_TIMER 메세지가 발생해 윈도우 프로시저에 전달될 때는, wParam에 타이머의 ID, lParam에 타이머 메세지 발생시 호출될 함수의 주소를 전달하게 됩니다. 따라서, WM_TIMER 메세지 내부에서 switch 문을 이용해 타이머의 ID를 구분해서 작업을 제어하는 것입니다. 구체적인 예시는 추후 프로젝트에서 확인해보도록 하겠습니다.
전체적인 사용 예시는 다음과 같습니다.
SetTimer(hWnd, 1, 1000, NULL); // 1초마다 WM_TIMER 메세지를 발생 -> ID값 1
사용이 끝난 타이머는 반드시 제거해야 합니다. 타이머를 제거할 때는 KillTimer() 함수를 사용합니다.
KillTimer(HWND, TimerID)
이렇게 KillTimer() 함수의 매개변수로 윈도우의 핸들과 타이머의 ID를 넣어, 해당 타이머를 제거해야 합니다. KillTimer() 함수는 개발자의 의도에 따라 어디든지 사용할 수 있습니다.
SetTimer() 함수와 KillTimer() 함수의 위치? - WM_CREATE, WM_DESTROY 메세지
물론 타이머의 용도에 따라 타이머를 설치하는(SetTimer를 사용하는) 위치는 자유롭게 설정해도 됩니다. 하지만, 프로그램이 시작되었을 때부터 필요한 타이머라면 어떨까요? 예를 들어, 30초의 시간제한이 있는 게임을 제작한다면? 게임이 시작되었을 때, 30초 후 발생하는 타이머를 설치해야 합니다. 이럴 때, WM_CREATE 메세지에 SetTimer 함수를 통해 타이머를 설치합니다. WM_CREATE 메세지는 윈도우가 처음 생성될 때 발생하는 메세지입니다. 해당 메세지에서 실행에 필요한 메모리 할당, 전역 변수 초기값 등 프로그램 시작 시 꼭 한 번만 초기화해야 하는 것들을 처리할 수 있습니다. 반대로, 프로그램이 종료할 때 발생하는 메세지도 있습니다. 바로, WM_DESTROY 메세지인데요. 이전 예시들에서 계속해서 봐온 메세지입니다. WM_DESTROY 메세지에서 생성된 타이머를 KillTimer() 함수를 통해 제거할 수 있습니다.
타이머를 활용해 시계 만들기
MY_TIMER 프로젝트
위에서 배운 타이머를 이용해 시계를 만들어 보도록 하겠습니다. 화면에 현재 시간을 출력하는 프로젝트입니다. 해당 프로젝트의 특징은 사용자의 동작과 상관없이 발생하는 메세지를 통해 동작한다는 것입니다.
현재 시간을 구하기 위한 SYSTEMTIME 구조체와 GetLocalTime() 함수
SYSTEMTIME 구조체는 시간 정보를 저장하고 있는 구조체입니다. 해당 구조체가 갖고 있는 멤버 변수는 다음과 같습니다.
멤버 변수 이름 | 내용 |
wYear | 현재 연도를 지정 |
wMonth | 현재 월을 지정 |
wDayOfWeek | 현재 요일을 지정 (일=0, 월=1, 화=2 ...) |
wDay | 현재 날짜를 지정 |
wHour | 현재 시간을 지정 |
wMinute | 현재 분을 지정 |
wSecond | 현재 초를 지정 |
wMilliseconds | 현재 밀리초를 지정 |
여기서 저희가 필요한 정보는 "시 : 분 : 초" 입니다. 따라서, wHour, wMinute, wSecond를 사용할 것입니다. 해당 구조체의 현재 시간 정보를 업데이트해야 합니다. 시간을 구하는 함수는 GetLocalTime() 함수와 GetSystemTime() 함수가 존재합니다. 저는 해당 구조체의 시간을 업데이트하기 위해서 GetLocalTime() 함수를 사용할 것입니다. 왜냐하면, GetSystemTime() 함수는 UTC 시간을 기준으로 업데이트하기 때문에 정확도가 떨어집니다. 반면, GetLocalTime() 함수는 컴퓨터의 시간 정보를 기준으로 업데이트하기 때문에 비교적 정확합니다. 따라서, GetLocalTime() 함수의 매개변수로 SYSTEMTIME 구조체의 주소를 넣어주면 구조체에 현재 시간 정보가 업데이트됩니다.
이제, 현재 시간을 화면에 출력하는 프로젝트의 코드를 확인해보도록 하겠습니다.
#include <stdio.h>
#include <TCHAR.h>
#include <windows.h>
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage,
WPARAM wParam, LPARAM lParam)
{
HDC hdc; // 출력을 위한 Device Context의 핸들
PAINTSTRUCT ps; // 출력을 위한 구조체
SYSTEMTIME st; // 시간 정보를 저장하기 위한 구조체
static TCHAR sTime[128] = _T(""); // 시간 정보를 출력하기 위한 문자열
static RECT rt = { 100,100,400,120 }; // 문자열을 출력할 사각 영역 정보(사각 영역 내부에 출력)
switch (iMessage) {
case WM_CREATE:
SetTimer(hWnd, 1, 1000, NULL);
SendMessage(hWnd, WM_TIMER, 1, 0);
return 0;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
TextOut(hdc, 100, 100, sTime, _tcslen(sTime));
EndPaint(hWnd, &ps);
return 0;
case WM_TIMER:
GetLocalTime(&st);
_stprintf_s(sTime, _T("지금 시간은 %d:%d:%d입니다."), st.wHour, st.wMinute, st.wSecond);
InvalidateRect(hWnd, &rt, FALSE);
return 0;
case WM_DESTROY:
KillTimer(hWnd, 1);
PostQuitMessage(0);
return 0;
}
return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}
먼저, SYSTEMTIME 구조체와 GetLocalTime() 함수를 사용하기 위해 <stdio.h>를 포함시켜야 합니다.
변수 선언은 주석에 나와 있으므로 생략하도록 하겠습니다.
메세지 처리
WM_CREATE
윈도우가 실행되면, 먼저 WM_CREATE 메세지가 실행됩니다. WM_CREATE 메세지가 발생하면 SetTimer() 함수를 통해 1번 ID를 가진 타이머를 설치합니다. SetTimer 함수의 마지막 매개변수가 NULL이기 때문에, 이 타이머는 1초에 한 번씩 WM_TIMER 메세지를 발생시킵니다.
WM_TIMER
설치된 타이머가 1개밖에 없으므로 따로 switch문을 사용하지 않고 바로 코드를 작성합니다.
1초마다 WM_TIMER 메세지가 발생할 때마다, 현재 시간을 업데이트합니다. GetLocalTime() 함수를 통해 현재 시간을 구조체에 업데이트하고, 해당 구조체의 정보를 문자열로 나타내 sTime에 저장합니다. 문자열을 저장하는 함수 _stprintf_s가 사용되었습니다. 시간이 흘렀으니, 화면에 시간을 출력해야 합니다. 시간을 업데이트 하는 것은 내부적인 동작이므로, 윈도우는 이 변화를 감지하지 못합니다. 따라서 InvalidateRect 함수를 통해 화면을 업데이트합니다.
WM_PAINT
InvalidateRect 함수로 인해 WM_PAINT 메세지가 발생했습니다. 두 번째 인자가 NULL이므로, 모든 화면이 지워졌다가 다시 그려지게 됩니다. TextOut 함수를 통해 화면에 업데이트된 시간 정보를 출력합니다.
WM_DESTORY
윈도우가 종료되면 WM_DESTORY 메세지가 발생합니다. 반대로, 개발자가 WM_DESTROY 메세지를 발생시켜 프로그램을 종료할 수도 있습니다. 혹은 사용자가 우측 상단의 X표시를 눌렀을 때도 해당 메세지가 발생합니다. 여기서 만들었던 타이머를 KillTimer 함수를 통해 삭제합니다.
결과
글을 쓰고 있는 시각인 0시 55분이 정상적으로 출력되고 있습니다. 하지만, 무언가 이상한 점이 있습니다.
- 프로그램이 시작하고 1초가 지나서야 화면에 시간이 출력된다.
- 화면이 깜빡이는 현상이 발생한다.
타이머가 설치되고 1초가 지나야 WM_TIMER 메세지가 발생하기 때문에, 프로그램이 시작하고 1초가 지나야 화면에 시간이 출력되는 문제점이 있습니다. 또한, InvalidateRect 함수 사용 시, 두 번째 매개변수를 NULL로 설정했기 때문에, 전체 화면이 지워졌다가 다시 그려집니다. 따라서, 화면이 깜빡거리거나 부자연스러운 현상이 발생합니다. 다음 포스팅에서 문제를 해결해보도록 하겠습니다.
정리
타이머를 통해 사용자의 동작과 상관없이, 백그라운드에서 동작하는 프로그램을 작성했습니다. 타이머는 SetTimer() 함수를 통해 설치할 수 있었습니다. 설치된 타이머는 반드시 KillTimer() 함수를 통해 삭제되어야 합니다. MyTimer 함수는 아직 문제점들이 존재합니다. 다음 포스팅에서 이를 해결하는 방법에 대해서 다루도록 하겠습니다. 점점 Win32 API를 이용해 할 수 있는 것들이 늘어나고 있습니다!
'개발 > Win32 API Programming' 카테고리의 다른 글
[Windows API] Win32 API의 백그라운드 작업과 콜백 함수 (0) | 2022.01.20 |
---|---|
[Windows API] Win32 API를 활용해 시계, 일회용 타이머 만들기 - (2) (0) | 2022.01.18 |
[Windows API] Win32를 활용해 마우스 입력하기 (0) | 2022.01.09 |
[Windows API] Win32 API를 활용해 키보드 입력하기 (2) | 2022.01.06 |
[Windows API] Win32 API를 활용해 긴 텍스트, 도형, 메세지박스 출력하기 (0) | 2022.01.02 |