[UWP] 入門007:定期的に処理を行う(タイマーイベント)

火曜日 , 1, 3月 2016 Leave a comment

 本記事はUWP(Universal Windows Platform)の入門記事第7回です。

 今回は定期的に(もしくは特定の時間後に)処理を行うタイマーイベントについて紹介します。

 

概要

 

 本記事では以下のことが学習できます。

 

・タイマーイベントの方法

・UIスレッドについて

 

環境条件

 

 本記事は以下の環境を前提としてます。

 

・Windows 10 OS(Pro以上)

・Visual Studio 2015(+UWP SDK)

・言語はC#を用いて解説します

 

タイマーイベント(DispatcherTimer)

 

 UWPで利用できるシンプルなタイマーはDispatcherTimerです。

 DispatcherTimerは以下のように利用します。

 サンプルはGitHubのTimerSample001です。(以下の修正はMainPage.xamlまたはMainPage.xaml.csに対して行います)。

 

Step1:DispatcherTimerの生成

 

 DispatcherTimerを生成します。この後別のイベントから利用したいので、DispatcherTimerはクラスのプロパティとして用意します。

 

private DispatcherTimer _timer;

 

 ページ遷移時のイベントでタイマーを生成します。

 (OnNavigatedToイベントについては入門006:別のページに遷移するを参照ください。)

 

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            this._timer = new DispatcherTimer();
        }

 

Step2:タイマーの実行間隔を指定

 

 定期的にタイマーイベントを実行する間隔を指定します。

 

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            this._timer = new DispatcherTimer();

            // タイマーイベントの間隔を指定。
            // ここでは1秒おきに実行する
            this._timer.Interval = TimeSpan.FromSeconds(1);
        }

 

Step3:タイマーイベントの処理を記述する

 

 定期的に実行する処理を記述します。

 ここでは実行毎に数をカウントし、それをTextBlockコントロールに表示することにします。

 

(カウント用のプロパティをクラスに用意)

        // イベントの実行回数をカウントする変数
        private int _count;

 

(MainPage.xamlにTextBlockコントロールを追加)

        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="86,96,0,0" TextWrapping="Wrap" Text="0" VerticalAlignment="Top"/>

 

 プロパティとコントロールを追加後、OnNavigatedToメソッドに定期的に実行するイベントを指定します。

 

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            this._timer = new DispatcherTimer();

            // タイマーイベントの間隔を指定。
            // ここでは1秒おきに実行する
            this._timer.Interval = TimeSpan.FromSeconds(1);

            // 1秒おきに実行するイベントを指定
            this._timer.Tick += _timer_Tick;
        }

 

 定期的に実行するイベントはDispatcherTimerのTickイベントに指定します。

 _timer_Tickは以下です。

 

        private void _timer_Tick(object sender, object e)
        {
            // カウントを1加算
            this._count++;

            // TextBlockにカウントを表示
            this.textBlock.Text = this._count.ToString();
        }

 

 Tickイベントのイベントハンドラー(_timer_Tick)の第1引数のobjectはイベントの発行元であるDispatcherTimerが渡され、第2引数のobjectはnullです。

 

Step4:タイマーイベントを開始する

 

 最後にタイマーイベントを開始します。

 タイマーを開始するにはDispatcherTimerのStartメソッドを呼び出します。

 

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            this._timer = new DispatcherTimer();

            // タイマーイベントの間隔を指定。
            // ここでは1秒おきに実行する
            this._timer.Interval = TimeSpan.FromSeconds(1);

            // 1秒おきに実行するイベントを指定
            this._timer.Tick += _timer_Tick;

            // タイマーイベントを開始する
            this._timer.Start();
        }

 

 この時点で、プロジェクトをデバッグ実行するとアプリケーション起動後、1秒おきに画面の数字が1づつ増えていくことが確認できます。

 

Step5:タイマーイベントを停止する

 

  タイマーを停止するにはDispatcherTimerのStopメソッドを呼び出します。合わせて、再開もできるようにしましょう。

 まずはタイマーを停止(または再開)するためのボタンを追加します。

 

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="86,96,0,0" TextWrapping="Wrap" Text="0" VerticalAlignment="Top"/>
        <Button x:Name="button" Content="タイマーの停止" HorizontalAlignment="Left" Margin="86,180,0,0" VerticalAlignment="Top" Click="button_Click"/>

    </Grid>

 

 ButtonコントロールにはClickイベントとしてbutton_Clickイベントハンドラーが記述されています。コードビハインド側のC#にbutton_Clickイベントハンドラーを追記します。

 

        private void button_Click(object sender, RoutedEventArgs e)
        {
            if (this._timer.IsEnabled == true)
            {
                this._timer.Stop();
                this.button.Content = "タイマー再開";
            }
            else
            {
                this._timer.Start();
                this.button.Content = "タイマー停止";
            }
        }

 

 DispatcherTimerのIsEnabled プロパティで現在のタイマーが実行中か、停止中かを判定することができます。

 タイマーを停止、再開する際に、Buttonコントロールの表示も変更しています。

 デバッグ実行して動作を確認してみてください。

 

DispatcherTimerの注意点

 

 DispatcherTimerの注意点として、DispatcherTimerによるタイマーイベントはUIスレッドで実行されることがあります。

 UI(User Interface)スレッドについては「画面の更新を行うスレッド」と簡単に理解しておきましょう。そのため「DispatcherTimerのタイマーイベントで重たい処理を行うと画面の更新が止まる」という問題が発生します。

 以下のように重たい処理をTickイベントで発生させてみます。

 

        private void _timer_Tick(object sender, object e)
        {
            // ここで重たい処理発生
            // ボタンの上にマウスオーバーした際にボタンがハイライトされるか確認してみましょう
            while(true)
            {
                System.Diagnostics.Debug.WriteLine("終わらない");
            }

            // カウントを1加算
            this._count++;

            // TextBlockにカウントを表示
            this.textBlock.Text = this._count.ToString();
        }

 

 すると、_timer_Tickイベントの処理が終わらないだけでなく、画面の更新(例えばButtonにマウスオーバーすると発生するはずのハイライト処理など)も行われなくなってしまいました。

 このような画面の更新の停止(フリーズ)はUWPアプリケーションでは極力避けるべきです。

 

 では、DispatcherTimerを使わない方が良いのかというと、DispatcherTimerはUIスレッドで実行されるため、UIの変更コードがシンプルになるという利点があります。

 この利点と注意点はDispatcherTimer以外のUIスレッドで実行しないタイマーイベントを知ることでより実感できると思います。

 

タイマーイベント(ThreadPoolTimer )

 

 画面の操作(UIの更新)を行わない場合はThreadPoolTimer を利用します。

 (サンプルはGitHubのTimerSample002です。)

 

 ThreadPoolTimerで画面の操作を行えないわけではありませんが、DispatcherTimerのような記述ではエラーになります。

 まずはエラーになる例を見てみましょう。DispatcherTimerのときと似たようなサンプルを以下のように用意しました。

 

( MainPage.xaml)

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="67,61,0,0" TextWrapping="Wrap" Text="TextBlock" VerticalAlignment="Top"/>
    </Grid>

 

(MainPage.xaml.cs)

    public sealed partial class MainPage : Page
    {
        // 要usingに「using Windows.System.Threading;」を追加
        private ThreadPoolTimer _timer;

        private int _count;

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

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            this._timer = ThreadPoolTimer.CreatePeriodicTimer(_timerEvent, TimeSpan.FromSeconds(1));
        }

        private void _timerEvent(ThreadPoolTimer timer)
        {
            this._count++;

            // この記述はエラーになる
            this.textBlock.Text = this._count.ToString();
        }
    }

 

 少し記述がことなりますが、処理は同様です。

 ThreadPoolTimerのCreatePeriodicTimerメソッドの第1引数に定期的に実行する処理_timerEventメソッドを指定し、第2引数に時間を指定します。

 

 しかし、このコードを実行すると以下のようなエラーになります。

001

 UIスレッド以外から画面を更新しようとしたためです。DispatcherTimerはUIスレッドで実行されるためこのようなエラーが出ませんでした。

 

 コードを以下のように書き換えるとエラーが解消されます。

        private async void _timerEvent(ThreadPoolTimer timer)
        {
            this._count++;

            // この記述はエラーになる
            //this.textBlock.Text = this._count.ToString();

            // UIスレッド以外のスレッドから画面を更新する場合はDispatcher.RunAsyncを利用する
            // 非同期処理なのでawait/asyncキーワードが必要になる
            await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                this.textBlock.Text = this._count.ToString();
            });
        }

 

 Dispatcher.RunAsync非同期メソッドを利用します。

 (Windows 8時代のストアアプリだとDispatcher.BeginInvokeで同様ことを行っていましたが、UWPではDispatcher.RunAsyncを利用します。)

 DispatcherTimerの方が簡単にUIにアクセスできることが理解できたと思います。

 

 では、ThreadPoolTimerで重い処理を実行してみます。

 

        private async void _timerEvent(ThreadPoolTimer timer)
        {
            // ここで重い処理を実行
            // Buttonコントロールを配置してマウスオーバーハイライトを確認してみると良い
            while (true)
            {
                System.Diagnostics.Debug.WriteLine("終わらない");
            }

            this._count++;

            // この記述はエラーになる
            //this.textBlock.Text = this._count.ToString();

            // UIスレッド以外のスレッドから画面を更新する場合はDispatcher.RunAsyncを利用する
            // 非同期処理なのでawait/asyncキーワードが必要になる
            await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                this.textBlock.Text = this._count.ToString();
            });
        }

 

 Buttonコントロールを配置してマウスオーバーしてみると、画面の更新は固まっていないことが確認できると思います。

 処理が止まっているので、TextBlockの値は変わりません。

 

 UWPに用意された2つのタイマーイベントについて違いが理解できたと思います。

 最後にThreadPoolTimerの停止と再開についてのサンプルを掲載しておきます。ボタンのClickイベントで処理を行いますが、ボタンイベントについてはこれまでの解説で説明不要だと思いますので、コード側の処理だけを記載します(ダウンロードできるサンプルでXAML側も確認できます)。

 

        private void button_Click(object sender, RoutedEventArgs e)
        {
            if (this._timer != null)
            {
                this._timer.Cancel();
                this._timer = null;
                this.button.Content = "タイマー再開";
            }
            else
            {
                this._timer = ThreadPoolTimer.CreatePeriodicTimer(_timerEvent, TimeSpan.FromSeconds(1));
                this.button.Content = "タイマー停止";
            }
        }

 


Please give us your valuable comment

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

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください