■ タイマー ■ ■ System.Windows.Forms.Timer Windows フォームの Timer は、一定の間隔でイベントを発生させるコンポーネントで有る。此のコン ポーネントは、Windows フォーム環境で使用する。サーバー環境に適したタイマが必要な場合は、後 述のSystem.Timers.Timer を使用する。 イベントの発生する間隔は、ミリ秒単位で、Interval プロパティで設定しする。此のコンポーネントを 有効(Enabled プロパティを True に設定、又は、Start メソッドを実行)にすると、Tick イベントが 一定の間隔で発生する。 System.Windows.Forms.Timer を使用して、簡単なデジタル時計を作成するコード例を、下記に示す。 猶、フォーム上には、下図の様に、時刻を表示するラベルlblClock、アラーと時間を設定するテキスト ボックスtxtTimeSet、一定間隔で自動的に処理を行う為のタイマーtmrClock が配置されて居る物とす る。 Visual Basic Public Class clock
' フォームが読み込まれた時の処理
Private Sub clock_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles MyBase.Load ' タイマーの間隔設定と始動 tmrClock.Interval = 100 tmrClock.Enabled = True End Sub ' フォームが閉じられ様と仕た時の処理
Private Sub clock_FormClosing(ByVal sender As Object, _
ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing ' タイマーの停止
tmrClock.Enabled = False End Sub
' タイマーが一定間隔で自動的に行う処理
Private Sub tmrClock_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles tmrClock.Tick
ス
Dim S As String = DateTime.UtcNow.AddHours(9).ToString("HH:mm:ss") lblClock.Text = S If S = txtTimeSet.Text Then My.Computer.Audio.Play("ファンファーレ.wav", AudioPlayMode.WaitToComplete) End If End Sub End Class C# using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace clock {
public partial class clock : Form { public clock() { InitializeComponent(); } // フォームが読み込まれた時の処理
private void clock_Load(object sender, EventArgs e) { tmrClock.Interval = 100; tmrClock.Enabled = true; } // フォームが閉じられ様と仕た時の処理
private void clock_FormClosing(object sender, FormClosingEventArgs e) {
tmrClock.Enabled = false; }
// タイマーが一定間隔で自動的に行う処理
private void tmrClock_Tick(object sender, EventArgs e) { string s = DateTime.UtcNow.AddHours(9).ToString("HH:mm:ss"); lblClock.Text = s; if (s == txtTimeSet.Text) { System.Media.SoundPlayer player = new System.Media.SoundPlayer("ほなはじめよか.wav"); player.Play(); } } } } 上記のプログラムは、タイマー(System.Windows.Forms.Timer)をフォームに配置せず、其のインス タンスをコードで生成する事も出来る。下記に、其のコード例を示す。
Visual Basic Public Class clock
Private tmrClock As System.Windows.Forms.Timer
' フォームが読み込まれた時の処理
Private Sub clock_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles MyBase.Load
' タイマー(System.Windows.Forms.Timer)のインスタンス生成とイベントハンドラの追加 tmrClock = New System.Windows.Forms.Timer()
AddHandler tmrClock.Tick, AddressOf tmrClock_Tick ' タイマーの間隔設定と始動 tmrClock.Interval = 100 tmrClock.Enabled = True End Sub ' フォームが閉じられ様と仕た時の処理
Private Sub clock_FormClosing(ByVal sender As Object, _
ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing ' タイマーの停止
tmrClock.Enabled = False
' イベントハンドラの削除とタイマーのインスタンス破棄 RemoveHandler tmrClock.Tick, AddressOf tmrClock_Tick tmrClock.Dispose()
End Sub
' タイマーが一定間隔で自動的に行う処理
Private Sub tmrClock_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Dim S As String = DateTime.UtcNow.AddHours(9).ToString("HH:mm:ss")
lblClock.Text = S If S = txtTimeSet.Text Then My.Computer.Audio.Play("ファンファーレ.wav", AudioPlayMode.Background) End If End Sub End Class C# using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace clock {
public partial class clock : Form {
private System.Windows.Forms.Timer tmrClock; public clock() { InitializeComponent(); }
// フォームが読み込まれた時の処理
private void clock_Load(object sender, EventArgs e) {
// タイマーのインスタンス生成とイベントハンドラの追加 tmrClock = new System.Windows.Forms.Timer();
tmrClock.Tick += new System.EventHandler(this.tmrClock_Tick); // タイマーの間隔設定と始動 tmrClock.Interval = 100; tmrClock.Enabled = true; } // フォームが閉じられ様と仕た時の処理
private void clock_FormClosing(object sender, FormClosingEventArgs e) {
// タイマーの停止
tmrClock.Enabled = false;
// イベントハンドラの削除とタイマーのインスタンス破棄
tmrClock.Tick -= new System.EventHandler(this.tmrClock_Tick); tmrClock.Dispose();
}
// タイマーが一定間隔で自動的に行う処理
private void tmrClock_Tick(object sender, EventArgs e) { string s = DateTime.UtcNow.AddHours(9).ToString("HH:mm:ss"); lblClock.Text = s; if (s == txtTimeSet.Text) { System.Media.SoundPlayer player = new System.Media.SoundPlayer("ほなはじめよか.wav"); player.Play(); } } } } ■ System.Timers.Timer
System.Timers.Timer コンポーネントは、Interval プロパティの値を基に Elapsed イベントを発生さ せるコンポーネントで有る。
前述のインスタンスをコードで生成するプログラムを、タイマー(System.Timers.Timer)に置き換え たコード例を、下記に示す。
Visual Basic Public Class clock
Private Tm As System.Timers.Timer
' フォームが読み込まれた時の処理
Private Sub clock_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles MyBase.Load
Tm = New System.Timers.Timer(100)
AddHandler Tm.Elapsed, New System.Timers.ElapsedEventHandler( _ AddressOf tmrClock_Tick) ' タイマーの間隔設定と始動 Tm.Interval = 100 Tm.Start() End Sub ' フォームが閉じられ様と仕た時の処理
Private Sub clock_FormClosing(ByVal sender As Object, _
ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing ' タイマーの停止
Tm.Stop()
' イベントハンドラの削除とタイマーのインスタンス破棄 RemoveHandler Tm.Tick, AddressOf tmrClock_Tick Tm.Dispose()
End Sub
' タイマーが一定間隔で自動的に行う処理
Private Sub tmrClock_Tick(ByVal sender As System.Object, _ ByVal e As System.Timers.ElapsedEventArgs)
Dim S As String = DateTime.UtcNow.AddHours(9).ToString("HH:mm:ss") lblClock.Text = S If S = txtTimeSet.Text Then My.Computer.Audio.Play("ファンファーレ.wav", AudioPlayMode.WaitToComplete) End If End Sub End Class C# 略 上記のコードを実行した場合、InvalidOperationException 例外が発生し、『有効でないスレッド間の操 作:コントロールが作成されたスレッド以外のスレッドからコントロール xxxx がアクセスされまし た。』と謂うメッセージが表示される。此れは、スレッドセーフでないユーザーインターフェイス(UI) 要素に、作成元のスレッド以外からアクセスすると謂う危険性の有る操作に対する警告で有る。 此の場合、System.Timers.Timer は、フォームのスレッドとは異なるスレッドで動作して居り、フォー ムのスレッドに属するラベルにアクセスした事が原因で有る。 ※ タイマーとは、其の性格上、フォームとは別スレッドで動作する物で有る。因みに、上記のラベル は、Me.Controls("lblClock") の様に仕て取得出来るが、タイマーは、Me.Controls("tmrClock") の 様に仕て取得する事は出来ない。此の事からも、タイマーがフォームに属するコンポーネントでは ない事が解る。 即ち、System.Windows.Forms.Timer も、実際には、フォームとは別のスレッドで動作して居るの だが、使用頻度が高い為、恰もフォームと同じスレッドで動作して居る様に扱える様に設計されて 居るので有る(此の事が、実態を解り難く仕て居る様に思える)。 併し、Windows アプリケーションに於いては、一定間隔で行われる処理を、フォーム上の何等かのコ ントロールに表示(又は、描画)する必要が有る事が多い。其の様な場合には、デリゲート(委譲)を 使用する事に成る。
上記のプログラムを、デリゲートを使用して書き直したコード例を、下記に示す。 Visual Basic
Public Class clock
Private Tm As System.Timers.Timer
Private Delegate Sub TimerDelegate()
' フォームが読み込まれた時の処理
Private Sub clock_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles MyBase.Load
' System.Timers.Timer のインスタンス生成 Tm = New System.Timers.Timer(100)
' イベントハンドラの追加
AddHandler Tm.Elapsed, New System.Timers.ElapsedEventHandler( _ AddressOf tmrClock_Tick) ' タイマーの始動 Tm.Start() End Sub ' フォームが閉じられ様と仕た時の処理
Private Sub clock_FormClosing(ByVal sender As Object, _
ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing ' タイマーの停止 Tm.Stop() ' System.Timers.Timer のインスタンス破棄 Tm.Dispose() Application.DoEvents() End Sub ' タイマーが一定間隔で自動的に行う処理
Private Sub tmrClock_Tick(ByVal sender As System.Object, _ ByVal e As System.Timers.ElapsedEventArgs)
Invoke(New TimerDelegate(AddressOf DispTime)) End Sub
' タイマーからデリゲート(委譲)される処理 Private Sub DispTime()
Dim S As String = DateTime.UtcNow.AddHours(9).ToString("HH:mm:ss") lblClock.Text = S If S = txtTimeSet.Text Then My.Computer.Audio.Play("ファンファーレ.wav", AudioPlayMode.WaitToComplete) End If End Sub End Class C# using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms;
namespace clock {
public partial class clock : Form {
private System.Timers.Timer tm;
private delegate void TimerDelegate(); public clock() { InitializeComponent(); } // フォームが読み込まれた時の処理
private void clock_Load(object sender, EventArgs e) {
// タイマーのインスタンス生成 tm = new System.Timers.Timer(100);
// イベントハンドラの追加
tm.Elapsed += new System.Timers.ElapsedEventHandler(this.tmrClock_Tick); // タイマーの始動 tm.Start(); } // フォームが閉じられ様と仕た時の処理
private void clock_FormClosing(object sender, FormClosingEventArgs e) { // タイマーの停止 tm.Stop(); // System.Timers.Timer のインスタンス破棄 tm.Dispose(); Application.DoEvents(); } // タイマーが一定間隔で自動的に行う処理
private void tmrClock_Tick(object sender, EventArgs e) {
Invoke(new TimerDelegate(DispTime)); }
// タイマーからデリゲート(委譲)される処理 private void DispTime()
{
string s = DateTime.UtcNow.AddHours(9).ToString("HH:mm:ss"); lblClock.Text = s;
if (s == txtTimeSet.Text) {
System.Media.SoundPlayer player = new
System.Media.SoundPlayer("ほなはじめよか.wav"); player.Play(); } } } }
※ デリゲート(委譲)とは、簡単に謂えば、其れを取り扱う権限の無い者が、其れを取り扱う権限の 有る者に、処理を依頼する事で有る。其の際、呼出(Call)ではなく、呪文で呼出(Invoke)を行 う事に成る。デリゲートの詳細に付いては、稿を改めて解説する。 ■ Win32API – timeSetEvent 関数 Visual Basic 6.0 の頃には、スムースタイマーと謂う事で、ゲーム等に於いて、良く此の関数が使用さ れた。timeSetEvent 関数は、指定されたタイマイベントを開始する関数で有る。マルチメディアタイ マは独自のスレッドで動作する。 Visual Basic 6.0
Public Declare Function timeSetEvent Lib "WINMM" ( _ ByVal uDelay&, _ ByVal uResolution&, _ ByVal lpTimeProc&, _ ByVal dwUser&, _ ByVal uFlags& _ ) As Long Visual Basic.NET/2005/2008/2010
<DllImport("winmm.dll ")> Shared Function timeSetEvent( _ ByVal uDelay As UInteger, _
ByVal uResolution As UInteger, _
ByVal lpTimeProc As TimerProcDelegate, _ ByVal dwUser As UInteger, _
ByVal uFlags As UInteger) As IntPtr End Function
C#
[DllImport("winmm.dll", SetLastError = true)] static extern IntPtr timeSetEvent(
UInt32 uDelay, UInt32 uResolution, TimerProcDelegate lpTimeProc, UInt32 dwUser, UInt32 uFlags); C/C+ MMRESULT timeSetEvent( UINT uDelay, UINT uResolution, LPTIMECALLBACK lpTimeProc, DWORD dwUser, UINT uFlags );
猶、Visual Basic.NET 以降、C#で使用して居る TimerProcDelegate は、デリゲートで、下記の様に定 義されて居る。
Visual Basic
Public Delegate Sub TimerProcDelegate( _ ByVal uID As Integer, _
ByVal uMsg As Integer, _ ByVal dwUser As Integer, _ ByVal dw1 As Integer, _ ByVal dw2 As Integer)
C#
public delegate void TimerProcDelegate( int uID, int uMsg, int dwUser, int dw1, int dw2); 引数は、下記の通りで有る。 引数 解説 uDelay イベント遅延をミリ秒で指定(此の値が、タイマでサポートされるイベント遅延の最 小値から最大値迄の範囲にない場合、関数はエラーを返す) uResolution タイマイベントの分解能をミリ秒で指定(値が小さい程、分解能が増加し、分解能が 0 の場合は、最も可能な精度で周期イベントが発生するが、システムのオーバーヘッ ドを減らすには、アプリケーションに適した最大値を使う事が推奨される) lpTimeProc コ ー ル バ ッ ク 関 数 の ア ド レ ス を 指 定 ( uFlags 引 数 で TIME_CALLBACK_EVENT_SET フラグか TIME_CALLBACK_EVENT_PULSE フラグを指定する場合、此の引数は、イベントオブジェクトのハンドルと解釈される) dwUser ユーザーが提供するコールバックデータを指定 uFlags タイマイベントのタイプを指定(下表の値の孰れかを指定する) 値 意味 TIME_ONESHOT イベントは、uDelay ミリ秒経過後に 1 度発生 TIME_PERIODIC イベントは、uDelay ミリ秒毎に発生 uFlags パラメータには、下表の値の孰れかが入る場合も有る。 値 意味
TIME_CALLBACK_FUNCTION タイマの期限が切れると、Windows は lpTimeProc 引数が示 す関数を呼び出す(既定値)
TIME_CALLBACK_EVENT_SET タイマの期限が切れると、Windows は SetEvent 関数を呼び 出して、lpTimeProc 引数が示すイベントをセットする (dwUser 引数は無視される)
TIME_CALLBACK_EVENT_PULSE タイマの期限が切れると、Windows は PulseEvent 関数を 呼び出して、lpTimeProc 引数が示すイベントをパルスする (dwUser 引数は無視される) 戻り値には、関数が成功すると、タイマイベントの識別子が返り、失敗すると、エラーが返る。関数が 失敗してタイマイベントが作成されなかった場合、NULL を返す(此の識別子はコールバック関数に渡 される)。 猶、周期タイマイベントに対して、timeSetEvent 関数を呼び出す度に、其れに対応する timeKillEvent 関数を呼び出す必要が有る。 timeSetEvent 関数で取得したハンドルを破棄する timeKillEvent 関数の構文を、下記に示す。 Visual Basic 6.0
Public Declare Function timeKillEvent Lib "WINMM" (ByVal uID&) As Long Visual Basic
<DllImport("winmm.dll")> Shared Function timeKillEvent( _ ByVal uID As IntPtr) As Integer
C#
[DllImport("winmm.dll", SetLastError=true)] static extern UInt32 timeKillEvent(
UInt32 uID ); Windows XP の timeSetEvent 関数に付いて MSDN の timeSetEvent の記述では、「タイマでサポートされるイベント遅延の最小値から最大値迄の 範囲にない場合、関数はエラーを返す。」と有る。範囲は、タイマーデバイスに依存すると謂う訳だが、 実際には、ソフトウェア、即ち、API レベルで 1000 秒が最大と謂う制限が有る。 併し、Windows XP では、エラーを返さないにも拘らず(実際にタイマーが設定される)、設定したイ ベ ン ト 遅 延 よ り も 遥 か に 短 い 時 間 で 発 生 す る 事 が 有 る 。 具 体 的 に は 、timeSetEvent の引数で TIME_PERIODIC を指定した場合、イベント遅延時間は 429,496 ミリ秒が最大で、其れより 1 ミリ秒 めも大きく成ると、オーバーフローに依り、イベント遅延時間が1 ミリ秒に戻る。 此れは、引数で受け取った uDelay(ミリ秒単位)を内部で 100ns(ナノ秒)単位の値と仕て計算する 際に32bit の整数で扱うのが原因で有る。
429,496 (msec) = 4,294,960,000 (100nsec) = 0xFFFFE380
上記に1 ミリ秒を加算すると、下記の様に成る。
429,497 (msec) = 4,294,970,000 (100nsec) = 0x100000A90
此の為、繰り上がってオーバーフローするので、実際には0xA90(100ns)と成る。
解り易い概数で謂うと、約7 分 9 秒が最大で、7 分 10 秒はオーバーフローして、即座にイベントが発
生する。
Windows Vista の timeSetEvent 関数に付いて
Windows Vista では実装が変わり、途中の計算で正しく 64bit で扱う。上限は物凄い時間に成る筈だが、
実際には、前述の様に1000 秒に制限される。詰まり、約 16 分 40 秒が上限と成る。此れ以上の時間を 指定しようとすると、タイマーは設定されない。 其の他のOS に付いて纏めた物を、下記に示す。 OS オーバーフローの発生 1000 秒の制限 Windows XP 有 有 Windows Server 2003 有 有 Windows Vista 無 有 Windows Server 2008 無 有 Windows XP に付いては、イベントのコールバック関数内で回数をカウントして、5 分のイベントを 3 回数えて15 分のイベント遅延とする等の対策が必要と成る。 timeSetEvent 関数を使用して、前述のデジタル時計を作成するコード例を、下記に示す。 Visual Basic Imports System.Runtime.InteropServices
Public Class clock
Public Delegate Sub TimerProcDelegate( _ ByVal uID As Integer, _
ByVal uMsg As Integer, _ ByVal dwUser As Integer, _ ByVal dw1 As Integer, _ ByVal dw2 As Integer)
<DllImport("winmm.dll ")> Shared Function timeSetEvent( _ ByVal uDelay As UInteger, _
ByVal uResolution As UInteger, _
ByVal lpFunction As TimerProcDelegate, _ ByVal dwUser As UInteger, _
ByVal uFlags As UInteger) As IntPtr End Function
<DllImport("winmm.dll")> Shared Function timeKillEvent( _ ByVal uID As IntPtr) As Integer
End Function
Private Proc As TimerProcDelegate Private TimerID As IntPtr
Delegate Sub TimerDelegate()
' フォームが読み込まれた時の処理
Private Sub clock_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles MyBase.Load
' タイマーの始動
Proc = AddressOf tmrClock_Tick
TimerID = timeSetEvent(20, 10, Proc, 0, 1) End Sub
' フォームが閉じられ様と仕た時の処理
Private Sub clock_FormClosing(ByVal sender As Object, _
ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing ' タイマーの停止 timeKillEvent(TimerID) Application.DoEvents() End Sub ' タイマーが一定間隔で自動的に行う処理
Private Sub tmrClock_Tick(ByVal uID As Integer, ByVal uMsg As Integer, _ ByVal dwUser As Integer, ByVal dw1 As Integer, ByVal dw2 As Integer) Invoke(New TimerDelegate(AddressOf DispTime))
End Sub
' タイマーからデリゲート(委譲)される処理 Private Sub DispTime()
Dim S As String = DateTime.UtcNow.AddHours(9).ToString("HH:mm:ss") lblClock.Text = S If S = txtTimeSet.Text Then My.Computer.Audio.Play("ファンファーレ.wav", AudioPlayMode.Background) End If End Sub End Class
C# using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Runtime.InteropServices; namespace clock {
public partial class clock : Form {
public delegate void TimerProcDelegate( int uID, int uMsg, int dwUser, int dw1, int dw2);
[DllImport("winmm.dll", SetLastError = true)] static extern IntPtr timeSetEvent(
UInt32 uDelay, UInt32 uResolution, TimerProcDelegate lpTimeProc, UInt32 dwUser, UInt32 uFlags);
[DllImport("winmm.dll", SetLastError = true)] static extern UInt32 timeKillEvent(
IntPtr uID ); TimerProcDelegate proc; IntPtr timerid;
private delegate void TimerDelegate(); public clock() { InitializeComponent(); } // フォームが読み込まれた時の処理
private void clock_Load(object sender, EventArgs e) {
// タイマーの始動 proc = tmrClock_Tick;
timerid = timeSetEvent(20, 10, proc, 0, 1); }
// フォームが閉じられ様と仕た時の処理
private void clock_FormClosing(object sender, FormClosingEventArgs e) {
// タイマーの停止 timeKillEvent(timerid); Application.DoEvents(); } // タイマーが一定間隔で自動的に行う処理 private void tmrClock_Tick(
int uID, int uMsg, int dwUser, int dw1, int dw2) { Invoke(new TimerDelegate(DispTime)); } // タイマーからデリゲート(委譲)される処理 private void DispTime()
{ string s = DateTime.UtcNow.AddHours(9).ToString("HH:mm:ss"); lblClock.Text = s; if (s == txtTimeSet.Text) { System.Media.SoundPlayer player = new System.Media.SoundPlayer("ほなはじめよか.wav"); player.Play(); } } } } 矢張り、System.Timers.Timer コンポーネントと同様に、他のスレッドのコントロールを操作すると謂 う事で、デリゲートを使用する必要が有る。更に、AddressOf TimerProc(C#では TimerProc 自身が アドレスなのでAddressOf は不要)で生成された TimerProcDelegate デリゲートの参照は、何処にも 保持されて居ないので、ガベージコレクションが動作すると、此のデリゲートは回収されて了う為、参 照を保存する変数が必要と成る。