WinFormsのTimerで学ぶイベント処理の基本

広告

Windows Formsでは、一定時間ごとに処理を実行したい場面があります。

例えば、

  • 時計を1秒ごとに更新する
  • 一定時間ごとに画面を更新する
  • ゲームやアニメーションの処理を定期的に行う
  • 一定時間経過したらメッセージを表示する

といった処理です。

このような処理では、System.Windows.Forms.Timerを使用できます。

今回は、1秒ごとにイベントを発生させる簡単なプログラムを使って、Timerだけでなく、C#のイベント処理の仕組みについても確認していきます。

サンプルコード

using Timer = System.Windows.Forms.Timer;

namespace TimerSample
{
    public partial class Form1 : Form
    {
        private readonly Timer timer;

        public Form1()
        {
            InitializeComponent();

            timer = new Timer();

            timer.Interval = 1000;

            timer.Tick += MyTimerEvent;

            timer.Start();
        }

        private void MyTimerEvent(object? sender, EventArgs e)
        {
            MessageBox.Show("タイマーイベント発生");
        }
    }
}

実行すると、フォームが表示されたあと、1秒ごとに次のメッセージが表示されます。

タイマーイベント発生

この短いコードの中には、Windows Formsのイベント処理を理解するための重要な要素がいくつも含まれています。

using Timer = System.Windows.Forms.Timer;とは

最初の行を見てみましょう。

using Timer = System.Windows.Forms.Timer;

これは、System.Windows.Forms.Timerという型に、Timerという別名を付けています。

このような書き方をusingエイリアスといいます。

本来であれば、次のように書くこともできます。

System.Windows.Forms.Timer timer =
    new System.Windows.Forms.Timer();

しかし、毎回完全な名前を書くのは長くなります。

そこで、

using Timer = System.Windows.Forms.Timer;

と書いておけば、

Timer timer = new Timer();

と短く記述できます。

なぜTimerに別名を付けるのか

.NETには、Timerという名前を持つクラスが複数存在します。

代表的なものには次のようなものがあります。

System.Windows.Forms.Timer
System.Timers.Timer
System.Threading.Timer

そのため、複数の名前空間を使用していると、

Timer

だけでは「どのTimerなのか」が分からなくなることがあります。

そこで、

using Timer = System.Windows.Forms.Timer;

と明示することで、

このプログラムでTimerと書いた場合は、Windows Forms用のTimerを意味する

と指定しています。

Timerオブジェクトを作る

続いて次の部分です。

private readonly Timer timer;

これは、Form1クラスがTimerオブジェクトを保持するためのフィールドです。

コンストラクターでは、

timer = new Timer();

として、Timerオブジェクトを生成しています。

newは、クラスから実際に使用するオブジェクトを作成するための演算子です。

イメージとしては、

Timerクラス
    ↓ new
Timerオブジェクト

となります。

なぜローカル変数ではなくフィールドにするのか

次のように、コンストラクター内だけで宣言することもできます。

Timer timer = new Timer();

簡単なサンプルでは動作する場合がありますが、実際のアプリではフィールドとして保持する方が分かりやすくなります。

private readonly Timer timer;

としておけば、後から別のメソッドでも、

timer.Stop();

のように操作できます。

例えば、

private void StopButton_Click(object sender, EventArgs e)
{
    timer.Stop();
}

とすることもできます。

Timerをフォームが管理するオブジェクトとして考えると、フィールドにしておく方が自然です。

Intervalで時間間隔を設定する

次のコードを見てみましょう。

timer.Interval = 1000;

Intervalは、Timerイベントが発生する間隔を指定するプロパティです。

単位はミリ秒です。

1000ミリ秒 = 1秒

そのため、

timer.Interval = 1000;

は、

1秒間隔でTimerイベントを発生させる

という意味になります。

例えば500ミリ秒にしたい場合は、

timer.Interval = 500;

とします。

5秒ごとであれば、

timer.Interval = 5000;

となります。

Tickイベント

Timerで特に重要なのが、次のコードです。

timer.Tick += MyTimerEvent;

TickはTimerが持っているイベントです。

設定された時間が経過すると、

Tickイベントが発生する

という仕組みになっています。

今回の場合、

timer.Interval = 1000;

なので、およそ1秒ごとにTickイベントが発生します。

+=は何をしているのか

ここはC#のイベントを理解するうえで非常に重要です。

timer.Tick += MyTimerEvent;

これは、

Tickイベントが発生したら、MyTimerEventメソッドを実行してください

という登録です。

イメージすると、

Timer
  │
  │ 1秒経過
  ↓
Tickイベント発生
  │
  ↓
MyTimerEvent()

となります。

+=は、イベントにイベントハンドラーを登録するために使われています。

イベントハンドラーとは

今回登録しているメソッドは次の部分です。

private void MyTimerEvent(object? sender, EventArgs e)
{
    MessageBox.Show("タイマーイベント発生");
}

このように、イベントが発生したときに呼び出されるメソッドをイベントハンドラーと呼びます。

つまり、

MyTimerEvent

はTickイベントを処理するイベントハンドラーです。

メソッドを呼び出していないことに注意

ここで重要なのは、

timer.Tick += MyTimerEvent;

と書いており、

timer.Tick += MyTimerEvent();

とは書いていないことです。

通常、メソッドを実行するときには、

MyTimerEvent();

のように()を付けます。

しかし今回はメソッドをその場で実行したいわけではありません。

C#に対して、

後でTickイベントが起きたら、このメソッドを呼び出してほしい

と伝えています。

そのため、

MyTimerEvent

というメソッドそのものを登録しています。

イベント発生時の流れ

このプログラムの流れを整理すると、次のようになります。

フォーム作成
    ↓
Timer作成
    ↓
Interval = 1000
    ↓
TickイベントにMyTimerEventを登録
    ↓
Timer.Start()
    ↓
1秒経過
    ↓
Tickイベント発生
    ↓
MyTimerEventが呼び出される
    ↓
MessageBoxを表示
    ↓
さらに1秒経過
    ↓
再びTickイベント発生

この処理がTimerを停止するまで繰り返されます。

Startメソッド

Timerを作成しただけでは、まだ時間の計測は始まりません。

次のコードによってTimerを開始しています。

timer.Start();

これ以降、Intervalで指定した時間ごとにTickイベントが発生します。

停止する場合は、

timer.Stop();

を使用します。

例えば、

private void MyTimerEvent(object? sender, EventArgs e)
{
    timer.Stop();

    MessageBox.Show("タイマーイベント発生");
}

とすると、一度イベントが発生したところでTimerを停止できます。

なぜイベント登録をStartより先に書くのか

サンプルでは、

timer.Tick += MyTimerEvent;

timer.Start();

という順番にしています。

先に、

Tickイベントが発生したら何をするのか

を設定し、その後Timerを開始しています。

つまり、

イベント処理を準備
    ↓
Timer開始

という流れです。

この順番にしておくと、コードの意味も分かりやすくなります。

senderとは何か

イベントハンドラーをもう一度見てみます。

private void MyTimerEvent(object? sender, EventArgs e)

第1引数の、

sender

には、基本的にイベントを発生させたオブジェクトが渡されます。

今回であれば、Tickイベントを発生させたTimerです。

イメージすると、

Timer
  │
  │ Tickイベント発生
  ↓
MyTimerEvent(sender, e)
             ↑
           Timer

となります。

例えば次のように確認することもできます。

private void MyTimerEvent(object? sender, EventArgs e)
{
    if (sender is Timer timer)
    {
        MessageBox.Show(timer.Interval.ToString());
    }
}

これならイベントを発生させたTimerのIntervalを取得できます。

object??は何か

今回のイベントハンドラーでは、

object? sender

となっています。

?は、この変数にnullが入る可能性があることを示しています。

つまり、

object?

は、

object型のオブジェクト、またはnull

を表します。

最近の.NETではNullable Reference Typesの仕組みにより、このように?を付けるコードをよく見かけます。

EventArgs eとは

第2引数は、

EventArgs e

です。

これはイベントに関する追加情報を受け取るための引数です。

ただし、TimerのTickイベントでは特別な追加情報はありません。

そのため、このサンプルでは、

e

を使用していません。

それでもイベントハンドラーの決められた形式に合わせて、

object? sender, EventArgs e

と記述します。

イベントハンドラーには決められた形がある

Tickイベントに登録するメソッドは、基本的に次の形になっている必要があります。

void メソッド名(object? sender, EventArgs e)

例えば、

private void MyTimerEvent(object? sender, EventArgs e)

です。

全く違う形のメソッド、

private int MyTimerEvent()

などをそのままTickイベントに登録することはできません。

これは、イベントが呼び出せるメソッドの形が決められているためです。

この仕組みには、C#のデリゲートが関係しています。

Tickイベントの内部ではデリゲートが使われている

イベントを理解するには、デリゲートの存在を知っておくと理解しやすくなります。

非常に簡略化すると、デリゲートは、

呼び出すメソッドを記憶できる型

です。

例えば、

timer.Tick += MyTimerEvent;

と書くことで、

Tick
 └─ MyTimerEventを呼び出す

という情報が登録されます。

その後Tickイベントが発生すると、登録されているメソッドが呼び出されます。

つまり、

イベント
    ↓
デリゲート
    ↓
イベントハンドラー

という関係があります。

ボタンのClickイベントも同じ仕組み

TimerのTickイベントは特別な仕組みではありません。

Windows Formsでよく使うButtonのClickイベントも基本的には同じです。

例えば、

button1.Click += Button1_Click;

とすると、

button1
   ↓
クリック
   ↓
Clickイベント
   ↓
Button1_Click()

となります。

Timerの場合も、

timer.Tick += MyTimerEvent;

なので、

timer
   ↓
時間経過
   ↓
Tickイベント
   ↓
MyTimerEvent()

です。

発生条件が違うだけで、C#のイベントとしての仕組みは同じです。

+=で複数のメソッドを登録できる

イベントには複数のイベントハンドラーを登録することもできます。

例えば、

timer.Tick += MyTimerEvent;
timer.Tick += AnotherTimerEvent;

とします。

するとTickイベントが発生したとき、

Tick
 ├─ MyTimerEvent
 └─ AnotherTimerEvent

のように、登録されている複数のメソッドが呼び出されます。

これも+=が使われている理由の一つです。

-=でイベントを解除できる

イベントに登録したメソッドは、-=を使って解除できます。

timer.Tick -= MyTimerEvent;

すると、以降のTickイベントではMyTimerEventが呼び出されなくなります。

つまり、

+=

はイベント登録、

-=

はイベント解除

と考えることができます。

匿名関数でも書ける

イベントハンドラーは、必ず名前付きメソッドで作る必要はありません。

例えば次のようにも書けます。

timer.Tick += delegate
{
    MessageBox.Show("タイマーイベント発生");
};

これは匿名メソッドと呼ばれる書き方です。

名前のある、

MyTimerEvent

というメソッドを作らず、その場でイベント処理を書いています。

ラムダ式で書く

現在のC#では、さらに簡潔なラムダ式を使うこともできます。

timer.Tick += (sender, e) =>
{
    MessageBox.Show("タイマーイベント発生");
};

この、

=>

がラムダ演算子です。

読み方としては、

senderとeを受け取って、右側の処理を実行する

と考えると分かりやすいでしょう。

つまり、

private void MyTimerEvent(object? sender, EventArgs e)
{
    MessageBox.Show("タイマーイベント発生");
}

と、

timer.Tick += (sender, e) =>
{
    MessageBox.Show("タイマーイベント発生");
};

は、この場合ほぼ同じ役割を持っています。

名前付きメソッドとラムダ式の使い分け

処理がある程度長い場合は、

timer.Tick += MyTimerEvent;

として、別のメソッドに分けた方が読みやすくなります。

一方、

timer.Tick += (sender, e) =>
{
    label1.Text = DateTime.Now.ToString();
};

のように非常に短い処理であれば、ラムダ式も便利です。

無理にラムダ式を使えば良いというわけではありません。

コードの読みやすさを考えて使い分けます。

Windows FormsのTimerの特徴

System.Windows.Forms.Timerには重要な特徴があります。

Tickイベントは基本的にWindows FormsのUIスレッド上で実行されます

そのため、

private void MyTimerEvent(object? sender, EventArgs e)
{
    label1.Text = DateTime.Now.ToString();
}

のように、そのままLabelなどのコントロールを操作できます。

これはWindows Formsでは非常に便利です。

時計を表示してみる

Timerの代表的な使い方として、現在時刻をLabelに表示してみましょう。

using Timer = System.Windows.Forms.Timer;

namespace TimerSample
{
    public partial class Form1 : Form
    {
        private readonly Timer timer;

        public Form1()
        {
            InitializeComponent();

            timer = new Timer();

            timer.Interval = 1000;

            timer.Tick += MyTimerEvent;

            timer.Start();
        }

        private void MyTimerEvent(object? sender, EventArgs e)
        {
            label1.Text = DateTime.Now.ToString("HH:mm:ss");
        }
    }
}

これで1秒ごとに、

10:15:31
10:15:32
10:15:33

のようにLabelの表示が更新されます。

Timerの処理に時間をかけすぎない

System.Windows.Forms.TimerのTickイベントはUIスレッド上で実行されます。

そのため、Tickイベントの中で時間のかかる処理をすると、

画面が反応しない

状態になることがあります。

例えば、

private void MyTimerEvent(object? sender, EventArgs e)
{
    Thread.Sleep(10000);
}

のような処理を入れるのは適切ではありません。

10秒間UIスレッドを止めるため、その間フォームの操作ができなくなります。

Windows FormsのTimerは、

UIを定期的に少し更新する

ような処理に向いています。

Intervalは厳密な時間保証ではない

もう一つ注意したいのが、

timer.Interval = 1000;

としたからといって、必ず厳密に1000ミリ秒ごとに処理されるとは限らない点です。

UIスレッドが別の処理を行っている場合などには、Tickイベントの実行が遅れることがあります。

そのため、Windows Forms Timerは、

厳密な時間計測

よりも、

一定間隔で画面を更新する

用途に適しています。

Timerを破棄する

Timerも不要になったら破棄することができます。

例えばフォームが閉じられるときに、

timer.Stop();
timer.Dispose();

とすることができます。

ただし、Visual StudioのデザイナーからTimerをフォームに配置した場合は、通常はフォームのcomponentsによって管理されます。

コードから自分でTimerを生成するときには、

誰がそのオブジェクトを管理するのか

という視点も大切です。

最初のコードから学べること

今回のコードは非常に短いものですが、C#の重要な仕組みが多数含まれています。

timer.Tick += MyTimerEvent;

という1行だけでも、

Timer
イベント
イベントハンドラー
デリゲート
メソッド参照
+=演算子
sender
EventArgs

といったC#の重要な概念につながっています。

さらに、

timer.Tick += (sender, e) =>
{
};

と書けば、

匿名関数
ラムダ式
デリゲート
イベント

へと学習を広げることができます。

まとめ

Windows FormsのTimerは、単に「一定時間ごとに処理をするためのクラス」として覚えるだけではもったいない題材です。

今回の中心となるコードは、

timer.Tick += MyTimerEvent;

です。

これは、

Tickイベントが発生したとき
        ↓
MyTimerEventを実行する

という登録を行っています。

そして、

private void MyTimerEvent(object? sender, EventArgs e)

がイベントを受け取るイベントハンドラーです。

この仕組みはTimerだけのものではありません。

button1.Click += Button1_Click;

も、

textBox1.TextChanged += TextBox1_TextChanged;

も、

timer.Tick += MyTimerEvent;

も、基本的には同じC#のイベントの仕組みです。

Windows Formsを学ぶときは、

ボタンをダブルクリックしたらイベント処理を書く

という操作だけを覚えるのではなく、

イベントが発生する
        ↓
登録されているイベントハンドラーが呼ばれる

というC#の仕組みとして理解しておくと、その後のコードがかなり読みやすくなります。

訪問数 4 回, 今日の訪問数 7回

広告

C#

Posted by hidepon