タイマー

ウィンドウアプリでは、一定時間の経過後に特定の処理を実行したいということがあります。
これをC言語の機能のみで実現しようとすると結構大変なのですが、Windows APIにはタイマーの機能が用意されています。

タイマーはSetTimer関数を使用して作成します。

SetTimer関数

UINT_PTR SetTimer(
 HWND hWnd,
 UINT_PTR nIDEvent,
 UINT uElapse,
 TIMERPROC lpTimerFunc
);
ウィンドウhWndに関連付けられたタイマーを作成する。
hWndがNULLの場合、作成されたタイマーを識別するIDを返す。
hWndがNULLでない場合、0以外の整数を返す。
失敗した場合は0を返す。

hWndはウィンドウハンドルです。
この関数の呼び出し元と同じスレッドから作成したウィンドウである必要があります。
ここにはNULLを指定することもできます。
(後述)

nIDEventタイマーIDです。
タイマーは複数同時に作ることができるので、それらを識別するための番号です。
1以上の任意の整数値を指定します。
新規タイマーを作るとき、hWndNULLを指定した場合はこの引数は0を指定することが推奨されます。

uElapseはタイマーの設定時間です。
単位はミリ秒(1/1000秒)で、この時間が経過するごとに指定した処理が実行されます。
ただし設定できる最小の時間は10ミリ秒です。

lpTimerFuncはタイマー時間経過時に呼び出されるコールバック関数です。
これはNULLを指定することもできます。

SetTimer関数はミリ秒単位で時間を設定できますが、精度は低いです。
例えば1秒(1000ミリ秒)を指定しても、きっちり1秒後に処理が行われるとは限りません。
大体このくらいの時間、と思っておきましょう。

タイムアウト時の処理

SetTimer関数は、指定時間の経過後(タイムアウト時)の処理の方法が二種類あります。
なお、タイマーは破棄するまでは時間経過する度に処理が実行されます。

WM_TIMERメッセージ

ひとつはhWndにウィンドウハンドルを指定し、lpTimerFuncNULLを指定する方法です。
こうするとタイマーに設定した時間が経過したときにウィンドウプロシージャにWM_TIMERメッセージが通知されます。


#include <windows.h>
#include <strsafe.h>

//ウィンドウの生成等は省略

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static const WCHAR *format = L"%d秒";
	
	static WCHAR buf[BUFFERSIZE];
	static int timeCount;

	HDC hdc;
	PAINTSTRUCT ps;
	static RECT rt;

	switch (message)
	{
	case WM_CREATE: //ウィンドウの作成
		StringCchPrintf(buf, BUFFERSIZE, format, 0);
		//1秒のタイマーを作成(IDは適当)
		SetTimer(hWnd, 123, 1000, NULL);
		break;

	case WM_TIMER: //タイムアウト
		StringCchPrintf(buf, BUFFERSIZE, format, ++timeCount);
		InvalidateRect(hWnd, NULL, FALSE);
		break;

	case WM_SIZE: //ウィンドウサイズ変更
		GetClientRect(hWnd, &rt);
		break;

	case WM_PAINT: //ウィンドウ描画
		hdc = BeginPaint(hWnd, &ps);

		DrawText(hdc, buf, -1, &rt, DT_WORDBREAK);

		EndPaint(hWnd, &ps);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

このコードはプログラム実行後の経過時間を秒単位で表示します。
SetTimer関数によりWM_TIMERメッセージが1秒(1000ミリ秒)ごとに送られてくるので、それを利用して画面を更新しています。
タイマーのサンプル1

WM_TIMERメッセージ通知時のWPARAMはタイマーIDです。
タイマーが複数ある場合はこの値を見て処理を振り分けます。
LPARAMは第四引数に指定したコールバック関数へのポインタです。

コールバック関数

もうひとつはコールバック関数を使用する方法です。
SetTimer関数の第四引数lpTimerFuncにコールバック関数を指定すると、タイムアウト時にこの関数が実行されます。
(ウィンドウプロシージャにWM_TIMERメッセージは通知されません)

コールバック関数に指定できる関数の引数と戻り値は決まっており、以下のようになっています。
(関数名や引数名は自由です)

VOID CALLBACK TimerProc(
 HWND hWnd,
 UINT message,
 UINT idTimer,
 DWORD dwTime
)
SetTimer関数で使用するコールバック関数。

ウィンドウプロシージャと同じく、CALLBACKキーワードが必要です。
これはシステムが実行する関数という意味です。

hWndはSetTimer関数で指定した値が送られてきます。
messageはウィンドウメッセージですが、これは常にWM_TIMERです。
idTimerはタイマーのIDが格納されています。
dwTimeはWindows起動後の経過時間が格納されています。


#include <windows.h>
#include <strsafe.h>

//関数プロトタイプ宣言
ATOM MyRegisterClass(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
VOID CALLBACK TimerProc(HWND, UINT, UINT, DWORD);

//ウィンドウの生成等は省略

#define BUFFERSIZE 32

WCHAR buf[BUFFERSIZE];

//タイマーコールバック関数
VOID CALLBACK TimerProc(HWND hWnd, UINT message, UINT idTimer, DWORD dwTime)
{
	static int timeCount;

	StringCchPrintf(buf, BUFFERSIZE, L"%d秒", ++timeCount);
	InvalidateRect(hWnd, NULL, FALSE);
}

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	static RECT rt;

	switch (message)
	{
	case WM_CREATE: //ウィンドウの作成
		StringCchPrintf(buf, BUFFERSIZE, L"0秒");
		//1秒のタイマーを作成(IDは適当)
		SetTimer(hWnd, 123, 1000, TimerProc);
		break;

	case WM_SIZE: //ウィンドウサイズ変更
		GetClientRect(hWnd, &rt);
		break;

	case WM_PAINT: //ウィンドウ描画
		hdc = BeginPaint(hWnd, &ps);

		DrawText(hdc, buf, -1, &rt, DT_WORDBREAK);

		EndPaint(hWnd, &ps);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

このコードの実行結果はWM_TIMERメッセージの時と同じです。
新しい関数を定義するので関数プロトタイプ宣言を追加します。
関数間でデータを共有するためにグローバル変数を使用しています。

ウィンドウへの関連付け

SetTimer関数の第一引数はウィンドウハンドルの指定です。
ここで指定されたウィンドウのウィンドウプロシージャに対してWM_TIMERメッセージが通知されます。
ただし指定できるのはSetTimer関数の呼び出し元と同じスレッドで作成されたウィンドウです。

タイマーIDはウィンドウごとに固有のものになります。
つまりウィンドウが異なるならタイマーIDが重複することがあります。
(それぞれ別のタイマーとして動作します)

ウィンドウハンドルにはNULLを指定することもできます。
このタイマーはどのウィンドウとも関連付けられていないタイマーとなります。
この場合はWM_TIMERメッセージでの処理ができないので、コールバック関数を利用することになります。

戻り値

ウィンドウハンドルがNULLか非NULLかによって、戻り値の意味が異なります。

ウィンドウハンドルがNULLの場合、戻り値はそのタイマーのタイマーIDになります。
第二引数に指定するタイマーID(nIDEvent)とは異なる値が返ってくる可能性があるので、タイマーIDを管理するために必ず戻り値を取得する必要があります。
(新規のタイマーを作成する場合、第二引数のタイマーIDは無視されます)

ウィンドウハンドルが非NULLの場合、戻り値は0以外の整数値です。
タイマーIDは第二引数に指定したものが使用されます。
戻り値は関数の成否の判定以外には使用できません。

関数の実行に失敗した場合はどちらの場合も0が返ります。

タイマーIDはタイマーの管理に重要なので、両者を混同しないように気を付ける必要があります。

タイマーの上書き

作成したタイマーは、同じタイマーIDを持つ新しいタイマーを定義することで動作を置き換えることができます。
ただし、ウィンドウが異なる場合はタイマーIDが同じでも別のタイマーになりますので注意してください。

KillTimer関数

必要なくなったタイマーはKillTimer関数で破棄できます。

BOOL KillTimer(
 HWND hWnd,
 UINT_PTR uIDEvent
);
ウィンドウhWndに関連付けられた、タイマーID uIDEventで識別されるタイマーを破棄する。
成功した場合は0以外を、失敗した場合は0を返す。

hWndはタイマーが関連付けられているウィンドウハンドルです。
SetTimer関数でウィンドウハンドルにNULLを指定して作成したタイマーは、KillTimer関数でもNULLを指定します。
uIDEventはタイマーIDです。

タイマーは破棄しない限り繰り返し動作し続けるので、不要になったら破棄するようにしましょう。

サンプルコード

タイマーを利用して、時計とストップウォッチの機能を実装したサンプルコードです。


#include <windows.h>
#include <strsafe.h>

//関数プロトタイプ宣言
ATOM MyRegisterClass(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HRESULT GetNowDateString(WCHAR*, size_t);

//ウィンドウの生成等は省略

//現在の日時を表す文字列を生成し、
//length文字分のバッファを持つ文字列配列bufに格納する
HRESULT GetNowDateString(WCHAR* buf, size_t length)
{
	static const WCHAR *format =
		L"%04d年 %02d月 %02d日 %s曜日\n"
		L"%02d時 %02d分 %02d秒";
	static const WCHAR *dayOfWeek[] =
	{ L"日", L"月", L"火", L"水", L"木", L"金", L"土" };

	SYSTEMTIME st;
	GetLocalTime(&st);

	return StringCchPrintf(buf, length, format,
		st.wYear, st.wMonth, st.wDay, dayOfWeek[st.wDayOfWeek],
		st.wHour, st.wMinute, st.wSecond);
}

#define BUFFERSIZE 64

#define TIMERID_CLOCK 1
#define TIMERID_STOPWATCH 2

//ストップウォッチの更新頻度
#define STOPWATCH_TIME (1000 / 30)

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;

	//時計, ストップウォッチの描画領域
	static RECT rtClock, rtStopWatch;

	static WCHAR bufClock[BUFFERSIZE];
	static WCHAR bufStopWatch[BUFFERSIZE];
	static const WCHAR *txtStopWatch =
		L"StopWatch\n"
		L"%02u:%02u:%02u.%03u";

	//計測開始時間, 前回までの計測時間, 現在の計測時間
	static ULONGLONG stopWatchStart, stopWatchLatest, stopWatchNow;
	//計測中か否か
	static BOOL enableStopWatch = FALSE;

	switch (message)
	{
	case WM_CREATE: //ウィンドウの作成
		GetNowDateString(bufClock, BUFFERSIZE);
		StringCchPrintf(bufStopWatch, BUFFERSIZE, txtStopWatch,
			0, 0, 0, 0);

		hdc = GetDC(hWnd);

		//時計とストップウォッチの表示領域を計算しておく
		DrawText(hdc, bufClock, -1, &rtClock, DT_CALCRECT);
		DrawText(hdc, bufStopWatch, -1, &rtStopWatch, DT_CALCRECT);
		rtStopWatch.bottom += rtClock.bottom + 10;
		rtStopWatch.top += rtClock.bottom + 10;

		ReleaseDC(hWnd, hdc);

		//時計用タイマー
		SetTimer(hWnd, TIMERID_CLOCK, 1000, NULL);
		break;

	case WM_LBUTTONDOWN: //マウス左ダウン
		if (enableStopWatch) {
			enableStopWatch = FALSE;
			KillTimer(hWnd, TIMERID_STOPWATCH);

			//最後の計測時間を保存しておく
			stopWatchLatest += GetTickCount64() - stopWatchStart;
		}
		else {
			enableStopWatch = TRUE;
			stopWatchStart = GetTickCount64();
			SetTimer(hWnd, TIMERID_STOPWATCH, STOPWATCH_TIME, NULL);
		}
		break;

	case WM_RBUTTONUP: //マウス右アップ
		if (!enableStopWatch) {
			//ストップウォッチをクリア
			stopWatchLatest = stopWatchNow = 0;
			StringCchPrintf(bufStopWatch, BUFFERSIZE, txtStopWatch,
				0, 0, 0, 0);
			InvalidateRect(hWnd, &rtStopWatch, FALSE);
		}
		break;

	case WM_TIMER: //タイマー
		switch (wParam)
		{
		case TIMERID_CLOCK: //時計
			GetNowDateString(bufClock, BUFFERSIZE);
			InvalidateRect(hWnd, &rtClock, FALSE);
			break;
		case TIMERID_STOPWATCH: //ストップウォッチ
			stopWatchNow = stopWatchLatest + GetTickCount64() - stopWatchStart;
			StringCchPrintf(bufStopWatch, BUFFERSIZE, txtStopWatch,
				(DWORD)(stopWatchNow / 1000 / 60 / 60 % 100),	//時間
				(DWORD)(stopWatchNow / 1000 / 60 % 60),			//分
				(DWORD)(stopWatchNow / 1000 % 60),				//秒
				(DWORD)(stopWatchNow % 1000));					//ミリ秒
			InvalidateRect(hWnd, &rtStopWatch, FALSE);
			break;
		}
		break;

	case WM_PAINT: //ウィンドウ描画
		hdc = BeginPaint(hWnd, &ps);

		DrawText(hdc, bufClock, -1, &rtClock, 0);
		DrawText(hdc, bufStopWatch, -1, &rtStopWatch, 0);

		EndPaint(hWnd, &ps);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

このコードの実行結果です。
時計とストップウォッチのサンプル

時計はタイマーで1秒ごとに更新され、何もしなくても動作し続けます。

画面を左クリックするとストップウォッチ機能で時間を計測開始します。
再度クリックで停止します。
ストップ中に右クリックすることで表示をクリアします。

計測した時間はミリ秒単位です。
これを「hh:mm:ss.fff」というよく見る時間の形式に変換するのですが、それぞれの単位への変換方法は上記コードを参照してください。
剰余(割り算のあまり)で目的の値未満になるように数を切り捨てるのがポイントです。

GetTickCount64関数ULONGLONG型を返しますが、これをそのままStringCchPrintf関数の書式指定文字列に指定することはできないので、DWORD型にキャストしています。