timeSetEvent Windows APIを利用して高精度タイマーを実装する - C#
timeSetEvent Windows APIを用いて分解能精度の高いタイマーを実装するコードを紹介します。
概要
こちらの記事ではSetTimer Windows APIを用いてタイマーを実装しましたが、timeSetEvent Windows APIを用いると、より精度の高いタイマーを実装することができます。
プログラム
UI
下図のUIを作成します。フォームにMultiLinesプロパティをtrueに設定したテキストボックスを1つ、ボタンを2つ配置します。
コード
下記のコードを記述します。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace EventTimerDemo
{
public partial class FormMain : Form
{
[DllImport("winmm.dll", SetLastError = true)]
static extern UInt32 timeSetEvent(UInt32 msDelay, UInt32 msResolution, TimerEventHandler handler, ref UInt32 userCtx, UInt32 eventType);
[DllImport("winmm.dll", SetLastError = true)]
static extern UInt32 timeKillEvent(UInt32 timerEventId);
[DllImport("winmm.dll")]
public static extern uint timeBeginPeriod(uint uMilliseconds);
[DllImport("winmm.dll")]
public static extern uint timeEndPeriod(uint uMilliseconds);
[DllImport("kernel32.dll")]
static extern void InitializeCriticalSection(out CRITICAL_SECTION lpCriticalSection);
[DllImport("kernel32.dll")]
static extern void EnterCriticalSection(ref CRITICAL_SECTION lpCriticalSection);
[DllImport("kernel32.dll")]
static extern void LeaveCriticalSection(ref CRITICAL_SECTION lpCriticalSection);
private delegate void TimerEventHandler(UInt32 id, UInt32 msg, ref UInt32 userCtx, UInt32 rsv1, UInt32 rsv2);
[StructLayout(LayoutKind.Sequential)]
public struct CRITICAL_SECTION{
public IntPtr DebugInfo;
public long LockCount;
public long RecursionCount;
public uint OwningThread;
public uint LockSemaphore;
public int Reserved;
}
const int TIMERR_BASE = 96;
const int TIMERR_NOERROR = 0;
const int TIMERR_NOCANDO = TIMERR_BASE+1;
const int TIMERR_STRUCT = TIMERR_BASE+33;
const uint TIME_ONESHOT = 0;
const uint TIME_PERIODIC = 1;
const uint TIME_CALLBACK_FUNCTION = 0x0000;
const uint TIME_CALLBACK_EVENT_SET = 0x0010;
const uint TIME_CALLBACK_EVENT_PULSE = 0x0020;
struct callBackParam{
int value;
}
public delegate void OnTimerEventHandler();
private CRITICAL_SECTION CriticalSection;
private uint TimerID=0;
TimerEventHandler teh;
int count = 0;
public FormMain()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
teh = TimerProc;
}
private void UpdateTimer()
{
if (TimerID != 0) {
timeKillEvent(TimerID);
}
uint resolution = 100;
uint delay =1000;
if (timeBeginPeriod(resolution)== TIMERR_NOERROR){
uint userctx = 0;
TimerID = timeSetEvent(delay, resolution, teh, ref userctx, TIME_PERIODIC);
}
}
private void StopTimer(){
if (TimerID != 0) {
uint resolution = 100;
timeKillEvent(TimerID);
timeEndPeriod(resolution);
}
}
private void TimerProc(UInt32 id, UInt32 msg, ref UInt32 userCtx, UInt32 rsv1, UInt32 rsv2)
{
InitializeCriticalSection(out CriticalSection);
EnterCriticalSection(ref CriticalSection);
try {
OnTimerEventHandler onTimerEvent = TimerEvent;
this.Invoke(onTimerEvent);
}
finally {
LeaveCriticalSection(ref CriticalSection);
}
}
private void button1_Click(object sender, EventArgs e)
{
textBox1.Text += "Start\r\n";
UpdateTimer();
}
private void button2_Click(object sender, EventArgs e)
{
StopTimer();
textBox1.Text += "Stop\r\n";
}
private void TimerEvent()
{
textBox1.Text += string.Format("+ : {0:d}\r\n",count);
count++;
}
}
}
解説
タイマーの開始
タイマーの開始はUpdateTimer() メソッドで処理されています。
timeBeginPeriod(),timeSetEvent()により、タイマーの処理を開始します。
timeBeginPeriod()により、タイマーの分解能をセットします。
timeSetEvent()の引数は以下になります。
timeBeginPeriod([タイマーの分解能(ミリ秒単位)])
timeSetEvent()により、一定時間ごとにエベントを発生させる処理を開始します。
timeSetEvent()の引数は以下になります。
timeSetEvent([タイマーのディレイ(uDelay)], [タイマーの解像度], [コールバック関数(lpTimeProc)], [コールバック関数に渡すデータ], [タイマーのモード(fuEvent)]);
呼び出しが成功した場合、timeSetEvent()の戻り値でタイマーのIDが返ります。
fuEvent (タイマーのモード)の値について
タイマーのモードは以下の値のいずれかを指定します。
値 | 動作 |
TIME_ONESHOT | [タイマーのディレイ]ミリ秒経過後に 1 度イベント(コールバック関数の呼び出し)が発生します。 |
TIME_PERIODIC | [タイマーのディレイ]ミリ秒ごとにイベントが発生します。 |
通常は上記のいづれかですが、下記の値を加算して指定することもできます。
値 | 動作 |
TIME_CALLBACK_FUNCTION | タイマの期限が切れると、Windows は lpTimeProc パラメータが示す関数を呼び出します。(既定の動作) |
TIME_CALLBACK_EVENT_SET | タイマの期限が切れると、Windows は SetEvent 関数を呼び出して、lpTimeProc パラメータが示すイベントをセットします。dwUser パラメータは無視されます。 |
TIME_CALLBACK_EVENT_PULSE | タイマの期限が切れると、Windows は PulseEvent 関数を呼び出して、lpTimeProc パラメータが示すイベントをパルスします。dwUser パラメータは無視されます。 |
コールバック関数について
private void TimerProc(UInt32 id, UInt32 msg, ref UInt32 userCtx, UInt32 rsv1, UInt32 rsv2)
{
InitializeCriticalSection(out CriticalSection);
EnterCriticalSection(ref CriticalSection);
try {
OnTimerEventHandler onTimerEvent = TimerEvent;
this.Invoke(onTimerEvent);
}
finally {
LeaveCriticalSection(ref CriticalSection);
}
}
timeSetEvent()により呼び出されるコールバック関数は別スレッドで実行されるため、フォームの要素にアクセスする際はInvokeメソッドを用いてフォーム操作をするメソッドを呼び出す必要があります。
また、今回のコードでは、InitializeCriticalSection(),EnterCriticalSection(),LeaveCriticalSection()関数を用いて、タイマーの処理部分を排他制御する動作にしています。
タイマーの停止
private void StopTimer(){
if (TimerID != 0) {
uint resolution = 100;
timeKillEvent(TimerID);
timeEndPeriod(resolution);
}
}
タイマーを停止する場合は、有効なタイマーIDかを確認し、timeKillEvent()関数でタイマーを停止します。また、timeEndPeriod()関数を呼び出し、タイマーの分解能をクリアします。
実行結果
プロジェクトを実行します。下図のウィンドウが表示されます。
[Start]ボタンをクリックします。タイマー処理が始まり、1秒に1行ずつメッセージが表示されます。
[Stop]ボタンをクリックするとタイマーが停止します。
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用