SetTimerとKillTimerでタイマーを設定・削除する
Windows APIのタイマーは、一定時間が経過したときに処理を実行するための基本的な仕組みです。ウィンドウアプリケーションでは、SetTimerでタイマーを登録し、通知されたWM_TIMERを処理します。不要になったタイマーはKillTimerで解除します。
この機能は、画面の定期更新、入力状態の監視、一定時間後の処理、簡単なアニメーションなどに利用できます。ただし、タイマーは高精度な計測機能ではなく、メッセージキューやスレッドの状態によって通知が遅れる点に注意が必要です。
タイマーを安全に扱うには、識別子の管理、ウィンドウハンドルの対応、メッセージループ、終了時の後始末を一貫させることが重要です。ここでは、基本的な呼び出し方からコールバック方式、削除時の注意点まで整理します。
SetTimerの基本的な使い方
ウィンドウに関連付けてタイマーを作成する場合は、次のように呼び出します。
UINT_PTR timerId = SetTimer(hWnd, 1001, 1000, NULL);
第1引数は対象のウィンドウハンドル、第2引数はアプリケーション側で管理するタイマーID、第3引数は間隔をミリ秒で指定する値です。第4引数にNULLを渡すと、タイマー通知はウィンドウメッセージとして処理します。上の例では、おおむね1秒ごとにWM_TIMERが発生します。
戻り値は作成されたタイマーの識別子です。SetTimerが失敗すると0を返すため、戻り値を保存して成否を確認します。すでに同じウィンドウとIDの組み合わせが存在する場合は、新しいタイマーが追加されるのではなく、既存タイマーの間隔が更新されます。
WM_TIMERを受け取って処理する
メッセージ方式では、ウィンドウプロシージャのWM_TIMERで処理を分岐します。wParamにはタイマーIDが格納されるため、複数のタイマーを使う場合もIDによって処理を分けられます。
case WM_TIMER:
if (wParam == 1001) {
UpdateStatus();
}
return 0;
タイマー処理は、ウィンドウのメッセージループが動作していることを前提にしています。重い処理を実行すると次の通知が遅れたり、複数回分がまとめて扱われたりします。画面更新の合図として使い、時間の正確な測定や長時間の計算は別の仕組みに分離するのが適切です。
レジストリ操作のような処理をタイマーから呼び出す場合も、処理時間とエラー処理を意識します。たとえば設定値の読み取りには、レジストリ読み取りで紹介されるRegOpenKeyExやRegQueryValueExなどのAPIが使われます。失敗時に画面を固まらせない設計が必要です。
タイマーIDとウィンドウハンドルの管理
KillTimerでタイマーを削除するときは、作成時に対応するウィンドウハンドルとIDを渡します。
KillTimer(hWnd, 1001);
複数のウィンドウや複数のタイマーを扱う場合、IDだけをグローバルに使い回すと誤削除につながります。タイマーIDをウィンドウクラスや管理オブジェクトごとに定義し、作成した場所と破棄する場所が明確になるようにします。
タイマーを登録する前に確認したい点は次のとおりです。
SetTimerの戻り値が0でないか確認する- タイマーIDと対象ウィンドウを対応付ける
- 同じIDの再登録が更新動作になることを把握する
- ウィンドウ破棄前に関連タイマーを停止する
WM_TIMER内で長時間処理を実行しない
ウィンドウが破棄されると、ウィンドウに関連付けたタイマーも通常はシステムによって削除されます。それでも、明示的にKillTimerを呼ぶコードを終了処理へ置くと、所有関係が読みやすくなり、登録方法を変更した場合の事故も防ぎやすくなります。
コールバック関数を使う方法
SetTimerの第4引数にTIMERPROC型の関数を渡すと、WM_TIMERをウィンドウプロシージャで分岐する代わりに、タイマーコールバックが呼び出されます。
void CALLBACK TimerProc(HWND hwnd, UINT message,
UINT_PTR idEvent, DWORD time)
{
if (idEvent == 2001) {
RefreshDisplay();
}
}
UINT_PTR id = SetTimer(NULL, 2001, 500, TimerProc);
hWndにNULLを指定するタイマーでは、指定したIDがそのまま使われるとは限らず、戻り値として返されたIDを保存して管理します。この方式では、タイマーと特定のウィンドウを直接結び付けずに処理できますが、コールバック内でウィンドウやオブジェクトの寿命を確認しなければなりません。
| 作成方法 | 通知の受け取り方 | 削除時の指定 | 向いている用途 |
|---|---|---|---|
| ウィンドウ関連付け | WM_TIMER |
ウィンドウハンドルとID | 画面更新、UI制御 |
NULLハンドル |
メッセージまたはコールバック | NULLと戻り値のID |
特定ウィンドウに依存しない処理 |
| コールバック指定 | TIMERPROC |
作成時の対応ID | 小規模な定期処理 |
| 高精度タイマー以外 | 通知時刻に揺らぎあり | APIごとの規則 | 低頻度の監視や更新 |
タイマー通知はスレッド単位で扱われます。コールバック方式でも、関連するスレッドのメッセージ処理が止まっていれば期待どおりに呼び出されません。スレッドをまたいでタイマーを削除する設計では、所有スレッドと同期方法を明確にします。
KillTimerで確実に停止する
タイマーを止める処理は、ダイアログの終了、ウィンドウの破棄、モード変更など、タイマーが不要になる時点で実行します。
case WM_DESTROY:
KillTimer(hwnd, 1001);
PostQuitMessage(0);
return 0;
SetTimer(NULL, id, ...)で作ったタイマーは、削除時にも第1引数へNULLを渡し、SetTimerの戻り値を使います。作成時に指定したIDと削除時のIDが一致しないと、対象のタイマーは停止しません。
解除処理で確認する項目は次のように整理できます。
- 作成時の
hWndと削除時のhWndを一致させる NULLハンドルのタイマーは戻り値のIDを保存する- 二重削除を避けるため、管理変数を無効値へ戻す
- コールバック中に所有オブジェクトを破棄しない
- ウィンドウ終了処理とタイマー停止処理の順序を決める
KillTimerの後に、すでにキューへ入っていた通知が処理される可能性も考慮します。WM_TIMERを受け取った時点で対象オブジェクトが有効か、停止済みのフラグが立っていないかを確認すると、安全性が高まります。
周期処理を設計するときの注意点
SetTimerの間隔は、指定値どおりの正確な周期を保証するものではありません。システム負荷、他のメッセージ、スレッドの処理時間によって通知時刻は変動します。時計や経過時間を測る場合は、通知回数を時間の代わりにせず、GetTickCount64やQueryPerformanceCounterなどで実際の経過時間を計測します。
マウスカーソルの制御など、ユーザー入力と組み合わせる処理では、タイマーの停止条件を細かく定義します。たとえばマウス移動範囲の制限で扱うClipCursorのようなAPIを使う場合、画面切り替えやアプリ終了時に制限を解除する処理も必要です。
画面描画を定期実行する用途では、タイマーのたびに全画面を再描画するより、更新が必要な領域だけを無効化する方が効率的です。互換デバイスコンテキストやオフスクリーン描画を使う場合は、互換DCの作成に関連するリソースの確保と解放も、タイマーのライフサイクルに合わせて管理します。
実装を確認する手順
まず、タイマーを登録する場所、通知を処理する場所、削除する場所をコード上で分けて確認します。IDを定数化し、SetTimerの戻り値をログへ出力しておくと、登録漏れや意図しない再登録を発見しやすくなります。
次に、処理中にウィンドウを閉じるケース、タイマーを再設定するケース、負荷が高い状態で通知が遅れるケースを検証します。タイマーは簡潔な周期通知として使い、正確なスケジューリングや重い非同期処理には、専用スレッドやスレッドプールなど別の手段を選びます。
最初の実装では、WM_CREATEで1つのタイマーを登録し、WM_TIMERでIDを判定し、WM_DESTROYでKillTimerを呼ぶ最小構成を作成します。その後、SetTimerの戻り値とKillTimerの引数を同じ管理変数で扱う形へ整理してください。