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を愛用
最終更新日: 2019-11-14
作成日: 2014-05-22
iPentec all rights reserverd.