ウィンドウタイマー

一定時間ごとにある処理をするというプログラムを作成したい場合があると思います。そのような場合、Windowsではタイマー機能が使われます。今回は、このタイマー機能について説明していきましょう。

Windowsのタイマー機能

ウィンドウタイマー(ユーザータイマー)

Windowsは、指定された時間が経過すると、ウィンドウのメッセージキューにメッセージをポストするという機能を持っています。この機能は単にタイマーと呼ばれています。ただし、これとは別にWindowsが持っている高分解能タイマー(パフォーマンスカウンタ)マルチメディアタイマーの機能と区別するため、ここではウィンドウタイマー(ウィンドウメッセージにより通知されるので)あるいはユーザータイマーuser32.dllにより提供されているので)と呼ぶことにします。

ウィンドウタイマー(ユーザータイマー)は、指定された時間ごとにメッセージキューにタイマーメッセージをポストしていく機能です。一度タイマーを設定すると、そのタイマーを破棄するまでの間、繰り返しメッセージをポストし続けます。

このタイマー機能は、ウィンドウメッセージを介したタイマーであるので、リアルタイムな処理に使用できるほど高精度というわけではありませんが、非常に便利なものです。HSPでは同様の処理をwait命令を使ったループで実現することができますが、この方法では、たとえばウィンドウの移動やサイズ変更をしている最中には処理が停止してしまいます。一方、タイマーメッセージを使用すると、ウィンドウの移動・サイズ変更中でも、HSPのメッセージ割り込み処理機能を用いて一定時間ごとの処理を行うことができるようになるのです。


アプリケーションがタイマーを使用する場合、2通りの処理方法が存在します。

1つは、ウィンドウプロシージャでWM_TIMERメッセージに対しての処理を記述する方法です。HSPでは、メッセージ割り込み機能を用いてWM_TIMERメッセージを処理することに相当します。

もう1つの方法は、コールバック関数と呼ばれる関数を用いる方法です。コールバック関数とは、Windowsから呼び出されることを目的としてアプリケーションが準備した関数のことです。こちらの方法を選択してタイマーを設定した場合、Windowsは、タイマーメッセージを送信する代わりに、このコールバック関数を呼び出すことによって、時間が経過したことをアプリケーションに通知するのです。

今回は、ウィンドウメッセージを処理する方法を用います。

タイマーを設定する

ウィンドウタイマーを設定するには、user32.dllSetTimer関数を呼び出します。

UINT_PTR SetTimer(
    HWND      hWnd,      // window handle
    UINT_PTR  nIDEvent,  // timer ID
    UINT      uElapse,   // time-out value
    TIMERPROC pTimerFunc // callback function
);

1番目のhWndパラメータにはウィンドウハンドルを指定します。タイマーがウィンドウに関連付けられ、ウィンドウメッセージがこのウィンドウに対してポストされるようになります。このパラメータに0 (NULL) を指定することによってウィンドウに関連付けられていないタイマーを設定することもできますが、今回はメッセージ処理を行うのでウィンドウハンドルを指定します。

2番目のnIDEventパラメータには、タイマーを識別するためのIDを指定します。タイマーIDは0以外の値でなければいけません。複数のタイマーを用いた場合に特定のタイマーを識別する際や、後でタイマーを破棄する際に、このタイマーIDが必要になります。このタイマーIDはウィンドウごとに固有な値であり、IDが同じでも関連付けられているウィンドウが異なる場合は、それらは異なるタイマーとして扱われます。hWndパラメータが0 (NULL) の場合にはこのパラメータは無視されます。

3番目のuElapseパラメータには、タイマーメッセージをポストする間隔をミリ秒単位で指定します。

4番目のpTimerFuncパラメータには、コールバック関数を用いた処理を行う場合に、コールバック関数へのポインタを指定します。ただし、今回はウィンドウプロシージャにおけるメッセージ処理の方法で行うので、このパラメータには0 (NULL) を指定します。

関数が成功すると、戻り値は0以外の値になります。エラーが発生して関数が失敗した場合には、戻り値は0になります。ただし、hWndパラメータに0 (NULL) を指定することによってウィンドウに関連付けられないタイマーを設定した場合には、戻り値がタイマーIDになります。


既に存在するタイマーに対してSetTimer関数による設定をした場合、すなわち、指定されたIDのタイマーが指定されたウィンドウにすでに存在した場合には、既存の設定はキャンセルされ、同じIDでタイマーが再設定されます。

タイマーメッセージの処理

SetTimer関数によりタイマーを設定すると、そのタイマーに関連付けられたウィンドウは、指定された時間ごとにWM_TIMERメッセージを受け取ります。このメッセージコードは次のように定義されています。

#define  WM_TIMER    0x0113

このメッセージのwParamパラメータとしてタイマーIDが渡されます。複数のタイマーを使用する場合には、このタイマーIDからタイマーを識別することができます。lParamパラメータはSetTimer関数のpTimerFuncパラメータで指定される値になりますが、メッセージ処理では使われません。

タイマーの破棄

タイマーによる処理が必要なくなった場合には、KillTimer関数を呼び出します。この関数はuser32.dllによって提供されており、次のように定義されています。

BOOL KillTimer(
    HWND     hWnd,     // window handle
    UINT_PTR nIDEvent  // timer ID
);

最初のhWndパラメータにはタイマーが関連付けられているウィンドウのハンドルを、nIDEventパラメータにはタイマーIDを指定します。これらの値は、どちらもuser32.dllSetTimer関数で指定されたものと同じ値を指定しなければいけません。

タイマーを使用する場合の注意点

ウィンドウタイマーを使用する場合には注意が必要です。

1つは、ウィンドウタイマーはそれほど高精度ではないということです。というのも、タイマーで使用されているWM_TIMERメッセージは、Windowsの中で、優先度が低いメッセージとして扱われているのです。そのため、メッセージキューの中に別のメッセージが存在すると、そちらのメッセージを先に処理してしまい、結果としてタイマーメッセージの処理が遅れてしまうのです。

もう1つは、必ずしも指定された時間ごとに1回メッセージがポストされるわけではないということです。Windowsは、メッセージキューに何らかのメッセージが残っている間は、次のタイマーメッセージをポストしないようになっています。そのため、たとえば、タイマー間隔に100ミリ秒を指定したとしても、0.1秒に1回処理されるとは限りません。タイマー間隔を1秒に設定したとしても、100秒間で100回の処理が行われるとは限らないのです。

タイマーを用いた処理を行う場合には、これらのことに注意しておく必要があります。

サンプルスクリプト

タイマーを使用した時計のサンプルスクリプトです。タイマーメッセージによる処理を使用しているので、ウィンドウをマウスでドラッグしている間でも描画処理が続けられるようになっています。

#uselib "user32.dll"
#func SetTimer  "SetTimer"  int,int,int,int
#func KillTimer "KillTimer" int,int

#define WM_TIMER    0x0113

#define TIMER_ID    1    ; タイマーID

screen 0, 300, 50 : font "MS ゴシック", 50
gosub *RedrawClock

; メッセージ処理の登録
oncmd gosub *OnTimer, WM_TIMER

; プログラム終了時の処理の登録
onexit goto *OnQuit

; タイマーを設定(0.25秒間隔)
SetTimer hwnd, TIMER_ID, 250, 0
if stat == 0 {
    dialog "タイマーの設定に失敗しました。", 1, "エラー"
    end
}
stop

*OnTimer
; ====== タイマーメッセージの処理 ======
if wparam == TIMER_ID : gosub *RedrawClock
return 0

*RedrawClock
; ====== 再描画処理 ======
redraw 0
s  = strf("%02d時", gettime(4))
s += strf("%02d分", gettime(5))
s += strf("%02d秒", gettime(6))
syscolor 5 : boxf
syscolor 8 : pos 0,0 : mes s
redraw
return

*OnQuit
; ====== 終了時の処理 ======
; タイマーを破棄
KillTimer hwnd, TIMER_ID
end