タイマーを用いて一定間隔で処理を繰り返す【C#】

C#のプログラムで一定間隔で処理を繰り返すにはタイマーに処理を登録させて、そのタイマを一定間隔で呼び出して処理を行います。C#にはいくつかのタイマーが用意されていますが、その中でここでは汎用的に用いられるタイマーとGUIアプリ用のタイマーの2つを紹介します。

C#におけるタイマーの種類

C#では以下のタイマーが用意されています。

System.Timers.Timerクラス汎用タイマー
System.Threading.Timerクラス汎用タイマー
Microsoft.UI.Xaml.DispatcherTimerクラスGUIアプリ用のタイマー
<注意> DispatcherTimerクラスは今後主流になると思われるWinUI 3.0のものを記載しています

一定間隔で何らかの処理をするために用いられる汎用的なタイマーはSystem.Timers.TimerクラスとSystem.Threading.Timerクラスの2つで、どちらもできることには大きな違いはありません。ただし、System.Threading.Timerクラスではイベントの登録はコンストラクタ―でしか行えなかったり、タイマーの停止には時間間隔を無限大に設定する必要があったりとややその扱い方が独特ですので、本稿ではSystem.Timers.Timerクラスの使い方を説明していきます。

それに対してDispatcherTimerクラスは特にGUIアプリ用のタイマーになります。System.Timers.Timerクラスなどの汎用タイマーはスレッドプールにキューイングされて実行されるため、そのメソッドはTimerクラスをインスタンス化したスレッドとは異なるスレッドで実行されることになります。しかしGUIアプリのコントロールはそのままではUIスレッド以外から操作できないので、このような汎用タイマーから操作することは難しくなってしまいます。そのために用意されているのがUIスレッド上で実行されるDispatcherTimerクラスのタイマーです。

なお、ここではWinUIにおけるDispatcherTimerクラスを紹介していますが、以下のようにWindowsFormやWPF、UWP (WinRT)などのそれぞれのフレームワークでも同様のものは用意されています。

汎用タイマーの使い方

ここではSystem.Timers.Timerクラスのタイマーの使い方を説明します。TimerクラスではIntervalプロパティで指定された間隔でElapsedイベントを発生させることができます。以下の例では、クラスの初期化の際にコンストラクタ―でIntervalプロパティを1000ミリ秒(=1秒)と指定し、1秒ずつカウントしていくプログラムを作成しています。

コード例
using System;
using System.Timers;

class Program
{
    Timer timer;
    int counter = 1;

    public Program()
    {
        //イベント間隔1000ミリ秒でタイマーを初期化
        timer = new Timer(1000);

        //タイマーにイベントを登録
        timer.Elapsed += OnTimedEvent;
        Console.WriteLine("カウント開始します");

        //タイマーを開始する
        timer.Start();

        //「Enter」キーでプログラムを終了
        Console.ReadLine();
    }

    //タイマーに呼び出されるメソッドの定義
    private void OnTimedEvent(Object sender, ElapsedEventArgs e)
    {
        Console.WriteLine(counter);
        counter++;
    }


    static void Main(string[] args)
    {
        new Program();
    }
}

この例ではコンストラクターの引数でIntervalプロパティを指定していますが、Intervalプロパティを直接指定することも可能です。Intervalプロパティに指定する数値はミリ秒単位となっているので、「1秒」と指定する場合は「1000」と指定します。

Intervalプロパティで設定した間隔でElapsedイベントが呼び出されるので、一定間隔で何らかの処理を行いたい場合は、Elapsedイベントに処理を登録する必要があります。Elapsedイベントの実体はElapsedEventHandlerデリゲートのインスタンスなので、そこで宣言されている引数と戻り値を持つメソッドを定義して登録しましょう。ここではOnTimedEventメソッドを定義して、15行目でElapsedイベントに登録しています。

なお、「デリゲートとイベント」に関しては次の記事もご覧ください。

Timerクラスのタイマーの開始・停止はEnabledプロパティの状態で制御されており、Enabledプロパティがtrueでタイマーが動き、falseで止まります。しかし、それでは使いづらいのでTimerクラスではStartメソッドとStopメソッドが用意されていて、それを用いて開始・停止を制御することができます。

GUIアプリ用のタイマーの使い方

ここではGUIアプリ用のタイマーとしてMicrosoft.UI.Xaml.DispatcherTimerクラスのタイマーの使い方を説明します。

GUIアプリ用のタイマーを使う意味

先ほどのTimerクラスによる汎用タイマーがあれば、わざわざGUIアプリ用に特別なタイマーを用意しなくても十分ではないかと思われるかもしれません。では、なぜGUIアプリでは敢えてDispatcherTimerクラスがTimerクラスとは別に定義されているのでしょうか?

その疑問に答えるために2つのタイマーを用いてスライダーの値を操作するアプリを作ってみましょう。次の例ではスライダーの数値を0.5秒ごとに1ずつ上げていくアプリを表しています。Timerクラスを用いた汎用タイマーでスライダーの数値を操作するボタンと、DispatcherTimerクラスのGUIアプリ用のタイマーを用いてスライダーの数値を操作するボタンを用意して、両方とも同じように実装していますが、実際の動作はどのようになるでしょうか?

コード例:XAML
<StackPanel>
    <Slider Value="{x:Bind Path=SliderValue, Mode=TwoWay}"/>
    <TextBlock Text="{x:Bind Path=SliderValue, Mode=OneWay}"/>
    <Button Content="汎用タイマー" Click="Button_Click1" Margin="1"/>
    <Button Content="GUIアプリ用タイマー" Click="Button_Click2" Margin="1"/>
</StackPanel>
コード例:C# (コードビハインド)
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private static readonly PropertyChangedEventArgs SliderPropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(SliderValue));

    Timer timer1;
    DispatcherTimer timer2;

    private int sliderValue;
    public int SliderValue
    {
        //スライダーの値を表すソース・プロパティ
        get { return sliderValue; }
        set
        {
            if (sliderValue == value) return;
            sliderValue = value;
            PropertyChanged?.Invoke(this, SliderPropertyChangedEventArgs);
        }
    }

    public MainPage()
    {
        this.InitializeComponent();
    }

    //Timerクラスからスライダーの値を操作しようとする → 実行できない(エラー)
    private void Button_Click1(object sender, RoutedEventArgs e)
    {
        timer1 = new Timer(500);
        timer1.Elapsed += (s, ev) => { SliderValue++; };
        timer1.Start();
    }

    //DispatcherTimerクラスからスライダーの値を操作する → 実行できる
    private void Button_Click2(object sender, RoutedEventArgs e)
    {
        timer2 = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
        timer2.Tick += (s, ev) => { SliderValue++; };
        timer2.Start();
    }
}

このアプリを実際に実行してみると、上側のボタンのTimerクラスで作った汎用タイマーの方は動作せずアプリが落ちてしまいますが、一方で下側のボタンのGUIアプリ用タイマー(DispatcherTimerクラス)の方は問題なく動かすことができます。

これは、TimerクラスのタイマーはUIスレッドとは違うところで実行されるのでそのままではGUIコントロールを操作できませんが、DispatcherTimerクラスのタイマーはUIスレッドを管理するDispatcher自身のタイマーなので、そのまま直接コントロールを操作することが可能であるからです。

GUIアプリ用のタイマーの基本的な使い方

DispatcherTimerクラスのタイマーの使い方の大まかな流れはTimerクラスと大きくは違いませんが、以下のようにその作法に若干の違いがあります。

  • コンストラクタ―には引数をとれない
  • イベント間隔を表すIntervalプロパティはTimeSpan型で指定する必要がある
  • タイマーイベントはElapsedイベントではなくTickイベント

先ほどの例をもう一度見てみましょう。

timer2 = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
timer2.Tick += (s, ev) => { SliderValue++; };
timer2.Start();

まず、Intervalプロパティにイベント間隔を指定してDispatcherTimerクラスを初期化しています。Timerクラスとは違ってコンストラクタにイベント間隔を指定することはできないので、ここではオブジェクト初期化子を用いて初期化を行っています。続いて、Tickイベントに行いたい処理を追加して、Startメソッドでタイマーを開始しています。


このように、Timerクラスを理解していればDispatcherTimerクラスは容易に理解できるので、2つをうまく使いこなしましょう。

スポンサーリンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)