DispatcherTimerクラスを利用して、WPFアプリにおいて一定間隔で処理を実行し、UIを更新する方法を解説する。
.NET Frameworkには一定時間間隔で処理を行う(メソッドを呼び出す)ためのタイマ機能として、以下の4種類のTimerクラスが用意されている。
本稿では4のWPFタイマについて、その基本的な使い方をまとめる。
特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。
なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017以降が必要である。使っているAPIは基本的に.NET Framework 3.5までのものだが、言語機能はそれ以降のものも利用している。
以降の説明で使うための画面を準備しておこう。WPFのプロジェクトを作ったら、画面に「TextBlock1」という名前を付けたTextBlockコントロールを配置する。その例を次のコードに示す。
<Grid>
<Viewbox>
<TextBlock x:Name="TextBlock1"
FontFamily="Harlow Solid Italic" FontSize="36"
TextAlignment="Center" Margin="5,0,10,0"
Text="00:00:00" FontWeight="Bold" >
<TextBlock.Foreground>
<SolidColorBrush
Color="{DynamicResource {x:Static SystemColors.HighlightColorKey}}"/>
</TextBlock.Foreground>
</TextBlock>
</Viewbox>
</Grid>
このあと説明するコードは、この画面のコードビハインドに記述していく。
WPFのUIスレッドには、UIスレッド上の処理を管理するDispatcherクラス(System.Windows.Threading名前空間)のインスタンスが結び付けられている。Dispatcherクラスは、優先順位に従ってUIスレッドに処理を割り振るだけでなく、内部的にはタイマの機能も持っている(Win32 APIのSetTimer関数を使ったタイマ)。そのタイマ機能を利用するために用意されているのが、DispatcherTimerクラス(System.Windows.Threading名前空間)である*1。
*1 このあたりの内部構造に興味のある方は、リファレンスソースを調べてみるのもよいだろう。
このDispatcherTimerクラスでは、EventHandlerデリゲート(System名前空間)を使用して、タイマにより呼び出されるメソッド(以下、タイマメソッドと記す)のデリゲートを作成し、DispatcherTimerクラスのTickイベントに登録する(VB.NETではWithEvents/Handlesキーワードによりイベントを登録することも可能)。
DispatcherTimerクラスでは、タイマメソッドは必ずUIスレッドで呼び出される。タイマメソッドの中から安心してUIにアクセスできるのだ。その代わり、タイマメソッドで時間のかかる処理を単純に実行するとUIがフリーズしてしまう(後述)。
タイマメソッドの呼び出し間隔は、IntervalプロパティにTimeSpan構造体(System名前空間)で指定する。タイマの開始/停止は、Start/Stopメソッドを呼び出して行う(IsEnabledプロパティにtrue/falseを設定してもよい)。
DispatcherTimerクラスは、インスタンスを作るときにDispatcherPriority列挙体(System.Windows.Threading名前空間)で処理の優先度を指定できる。指定しなかったときの優先度はDispatcherPriority.Backgroundである。これは、UIの処理よりも低い優先度だ。コンストラクタ引数でもっと高い優先度を指定できるが、その際にはUIの動きに影響が出るかもしれないので十分にテストしてほしい。
以下にWPFタイマを利用したサンプルコードを示す。冒頭に示したサンプル画面のコードビハインドである。MyTimerMethodメソッドがタイマにより一定間隔で実行されるメソッドだ。1秒間隔で呼び出され、現在時刻を画面に表示する。
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Threading;
namespace dotNetTips1244CS
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SetupTimer();
}
// タイマメソッド
private void MyTimerMethod(object sender, EventArgs e)
{
this.TextBlock1.Text = DateTime.Now.ToString("HH:mm:ss");
}
// タイマのインスタンス
private DispatcherTimer _timer;
// タイマを設定する
private void SetupTimer()
{
// タイマのインスタンスを生成
_timer = new DispatcherTimer(); // 優先度はDispatcherPriority.Background
// インターバルを設定
_timer.Interval = new TimeSpan(0, 0, 1);
// タイマメソッドを設定
_timer.Tick += new EventHandler(MyTimerMethod);
// タイマを開始
_timer.Start();
// 画面が閉じられるときに、タイマを停止
this.Closing += new CancelEventHandler(StopTimer);
}
// タイマを停止
private void StopTimer(object sender, CancelEventArgs e)
{
_timer.Stop();
}
}
}
Imports System.ComponentModel
Imports System.Windows.Threading
Class MainWindow
Public Sub New()
InitializeComponent()
SetupTimer()
End Sub
' タイマメソッド
Private Sub MyTimerMethod(sender As Object, e As EventArgs)
Me.TextBlock1.Text = DateTime.Now.ToString("HH:mm:ss")
End Sub
' タイマのインスタンス
Private _timer As DispatcherTimer
' タイマを設定する
Private Sub SetupTimer()
' タイマのインスタンスを生成
_timer = New DispatcherTimer() ' 優先度はDispatcherPriority.Background
' インターバルを設定
_timer.Interval = New TimeSpan(0, 0, 1)
' タイマメソッドを設定
AddHandler _timer.Tick, New EventHandler(AddressOf MyTimerMethod)
' タイマを開始
_timer.Start()
' 画面が閉じられるときに、タイマを停止
AddHandler Me.Closing, New CancelEventHandler(AddressOf StopTimer)
End Sub
' タイマを停止
Private Sub StopTimer(sender As Object, e As CancelEventArgs)
_timer.Stop()
End Sub
End Class
このコードでもちろん問題はないのだが、タイマの処理が3つのメソッドに分かれてしまっている(MyTimerMethodメソッド/SetupTimerメソッド/StopTimerメソッド)。すっきりと1つのメソッドにまとめたいときは、次に説明するようにラムダ式を使う。
.NET Framework 3.5からは、デリゲートに替えてラムダ式が使える。タイマメソッドの内容がさほど長くないときは、ラムダ式にすると簡潔に記述できる。上のコードをラムダ式を使って書き直すと、次のコードのようになる。
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Threading;
namespace dotNetTips1244CS
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SetupTimer();
}
private void SetupTimer()
{
// タイマのインスタンスを生成
var timer = new DispatcherTimer(DispatcherPriority.Normal)
{
// インターバルを設定
Interval = TimeSpan.FromSeconds(1.0),
};
// タイマメソッドを設定(ラムダ式で記述)
timer.Tick += (s, e) =>
{
this.TextBlock1.Text = DateTime.Now.ToString("HH:mm:ss");
};
// タイマを開始
timer.Start();
// 画面が閉じられるときに、タイマを停止(ラムダ式で記述)
this.Closing += (s, e) => timer.Stop();
}
}
}
Imports System.ComponentModel
Imports System.Windows.Threading
Class MainWindow
Public Sub New()
InitializeComponent()
SetupTimer()
End Sub
Private Sub SetupTimer()
' タイマのインスタンスを生成
Dim timer = New DispatcherTimer(DispatcherPriority.Normal) _
With {
.Interval = TimeSpan.FromSeconds(1.0) ' インターバルを設定
}
' タイマメソッドを設定(ラムダ式で記述)
AddHandler timer.Tick,
Sub(s, e)
Me.TextBlock1.Text = DateTime.Now.ToString("HH:mm:ss")
End Sub
' タイマを開始
timer.Start()
' 画面が閉じられるときに、タイマを停止(ラムダ式で記述)
AddHandler Me.Closing, Sub(s, e) timer.Stop()
End Sub
End Class
DispatcherTimerクラスのタイマメソッドは必ずUIスレッドで呼び出されるので、タイマメソッドで時間がかかる処理を単純に実行するとUIがフリーズしてしまう。例えば、上のコードのタイマメソッドに、(長い時間がかかる処理の代わりとして)スレッドを停止するコードを追加してみよう(次のコード)。
timer.Tick += (s, e) =>
{
// スレッドを1.5秒間だけ止める
System.Threading.Thread.Sleep(1500);
this.TextBlock1.Text = DateTime.Now.ToString("HH:mm:ss");
};
AddHandler timer.Tick,
Sub(s, e)
' スレッドを1.5秒間だけ止める
System.Threading.Thread.Sleep(1500)
Me.TextBlock1.Text = DateTime.Now.ToString("HH:mm:ss")
End Sub
DispatcherTimerクラスは、タイマメソッドから制御が返らない間は次のタイマメソッド呼び出しを行わない。そこで、上のようにインターバル(=1秒間)よりもタイマメソッドの処理時間(=1.5秒)が長くなっても、重複してタイマメソッド呼び出されることはない。
ただし、タイマメソッドから制御を返すまではUIがフリーズしてしまう。タイトルバーをマウスでドラッグして、ウィンドウをぐるぐると動かし続けてみるとよく分かるだろう。
タイマメソッドで長時間の処理を行うときは、(UIスレッド以外の)スレッドでその処理を非同期に実行するべきである。それには2つの考え方がある。
1つは、タイマメソッドの全体を別スレッドで実行して、UIにアクセスするときだけUIスレッドに移るという考え方だ。これは、サーバベースタイマのTIPSで紹介した方法である。DispatcherTimerクラスではなく、Timerクラス(System.Timers名前空間)を使う。
もう1つは、DispatcherTimerクラスを使いつつ、タイマメソッド中の時間がかかる処理だけを切り出して別スレッドで実行するという考え方だ(タイマメソッドからはすぐに制御を返すようにする)。例として、.NET Framework 4.5以降(および、Visual Studio 2012以降)で利用できるasync/awaitキーワードを使った非同期化の例を次のコードに示す。
タイマメソッドを非同期メソッドにして、長時間の処理をしつつ、制御はすぐに返すようにすると、(制御をDispatcherTimerクラスに返してしまったので)処理が終わっていなくても次のタイマメソッド呼び出しが発生してしまう。そのためタイマメソッドをリエントラント(再入可能)に作らねばならない。ここではセマフォを使い、再入時にはすぐにリターンするようにした。
// 排他制御のためのセマフォオブジェクト
System.Threading.SemaphoreSlim semaphore
= new System.Threading.SemaphoreSlim(1, 1);
// タイマメソッド
timer.Tick += async (s, e) =>
{
if (!await semaphore.WaitAsync(0))
{
// セマフォは他で使用中のため取得できなかった
return;
}
// セマフォが取得できたので、処理を実行する
try
{
// 長時間の処理を別スレッドで非同期実行
await Task.Run(() => System.Threading.Thread.Sleep(1500));
// 非同期実行が完了してから、画面を書き換え
this.TextBlock1.Text = DateTime.Now.ToString("HH:mm:ss");
}
finally
{
// 確実にセマフォを解放する(そのためのtry〜finally)
semaphore.Release();
}
};
' 排他制御のためのセマフォオブジェクト
Dim semaphore As System.Threading.SemaphoreSlim _
= New System.Threading.SemaphoreSlim(1, 1)
' タイマメソッド
AddHandler timer.Tick,
Async Sub(s, e)
If (Not Await semaphore.WaitAsync(0)) Then
' セマフォは他で使用中のため取得できなかった
Return
End If
' セマフォが取得できたので、処理を実行する
Try
' 別スレッドで長時間の処理を非同期実行
Await Task.Run(Sub() System.Threading.Thread.Sleep(1500))
' 非同期実行が完了してから、画面を書き換え
Me.TextBlock1.Text = DateTime.Now.ToString("HH:mm:ss")
Finally
' 確実にセマフォを解放する(そのためのtry〜finally)
semaphore.Release()
End Try
End Sub
カテゴリ:クラスライブラリ 処理対象:タイマ
カテゴリ:WPF 処理対象:スレッド
使用ライブラリ:DispatcherTimerクラス(System.Windows.Threading名前空間)
使用ライブラリ:EventHandlerデリゲート(System名前空間)
関連TIPS:タイマにより一定時間間隔で処理を行うには?(Windowsタイマ編)
関連TIPS:タイマにより一定時間間隔で処理を行うには?(スレッドタイマ編)
関連TIPS:タイマにより一定時間間隔で処理を行うには?(サーバベースタイマ編)
関連TIPS:非同期:awaitを含むコードをロックするには?(SemaphoreSlim編)[C#、VB]
Copyright© Digital Advantage Corp. All Rights Reserved.