スロットマシンを作ってみたっ!!


【経緯】:
平成28(2016)年2月の寒風吹きすさぶ冬から、梅の花がようやく蕾をつけて、ここぞと咲かんばかりに暖かくなった頃だったと思います。
職を求めてハローワークに通っていた私は、一枚のビラを見つけました。「ポリテクセンター茨城」。
プログラマーとしてのプライドをズタズタに引き裂かれたのち、心を病んで、引きこもりがちになっていた私は、今考えると他の職業に就くことを希望していたのではなく、
どうにか一開発者でありたいというある意味怨念みたいなものを抱いて、工場管理技術科なる学科のパンフレットを見つめていました。
現状打開を図るため、ポリテクセンター茨城に見学に行きました。
そこで、プログラミング学科でのカリキュラム説明も併せて行われましたものですから、そんなもん俺なら軽々できる。ましてや、講師として雇われたっていいぐらいのレベルに至っていると思って見ていました。
そこでは、生徒さんが作ったスロットマシンの紹介がありました。私は、今、湧き上がっていた感情が人に知られやしまいかと内心オドオドしていました。
それもそのはず、GUIにアクセスするために複数のスレッドを使うことはわかっていましたが、C言語チックなマルチスレッド方式ではできないないことを知っていたからです。
数か月前、かつて一世を風靡したファミリーコンピューターのソフト「エレベーターアクション」を再現しようと思いついて挫折していたのでした。
他のスレッドからGUIにアクセスることができない以上、実現不可能と思われていた?というより、方法を知らなかった私の喫緊の課題になり、奮闘が始まります。



GitHub
兎にも角にも、まずは私が作った完成品のコードをご覧ください。

0.コーディング

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace SlotMachine
{
    /// 
    /// MainWindow.xaml の相互作用ロジック
    /// 
    public partial class MainWindow : Window
    {
        Timer timer1;
        Timer timer2;
        Timer timer3;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void start_Click(object sender, RoutedEventArgs e)
        {
            timer1 = new Timer((i) =>
            {
                label1.Dispatcher.BeginInvoke(
                new Action(() =>
                {
                    var random = new Random();
                    int num = random.Next(0, 9);
                    label1.Content = num.ToString();
                }));
            }, null, 0, 100);

            timer2 = new Timer((i) =>
            {
                label2.Dispatcher.BeginInvoke(
                new Action(() =>
                {
                    var random = new Random();
                    int num = random.Next(0, 9);
                    label2.Content = num.ToString();
                }));
            }, null, 0, 10);

            timer3 = new Timer((i) =>
            {
                label3.Dispatcher.BeginInvoke(
                new Action(() =>
                {
                    var random = new Random();
                    int num = random.Next(0, 9);
                    label3.Content = num.ToString();
                }));
            }, null, 0, 50);

        }

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            timer1.Dispose();
        }

        private void button2_Click(object sender, RoutedEventArgs e)
        {
            timer2.Dispose();
        }

        private void button3_Click(object sender, RoutedEventArgs e)
        {
            timer3.Dispose();
        }
    }
}


【目次】:
【詳細】:

1.コーディング解説

上図の完成画面のGUIと合わせてみていただくとわかるのですが、構成はいたってシンプルです。
まずスロットがLabelコントロールで3個。ドラムを回転させるためのボタンが1個。各々のドラムの動きを止めるボタンが3個。
そのうち、イベントハンドラーを記述するのは、ボタン4個のみといった感じです。

その1ドラムを回転させるためのボタンの実装

Timer型の変数を3個グローバルに持っています。
それぞれのタイマー変数をラムダ式とデリゲートで記述し、実装していきます。
        private void start_Click(object sender, RoutedEventArgs e)
        {
            timer1 = new Timer((i) =>
            {
                label1.Dispatcher.BeginInvoke(
                new Action(() =>
                {
                    var random = new Random();
                    int num = random.Next(0, 9);
                    label1.Content = num.ToString();
                }));
            }, null, 0, 100);

            timer2 = new Timer((i) =>
            {
                label2.Dispatcher.BeginInvoke(
                new Action(() =>
                {
                    var random = new Random();
                    int num = random.Next(0, 9);
                    label2.Content = num.ToString();
                }));
            }, null, 0, 10);

            timer3 = new Timer((i) =>
            {
                label3.Dispatcher.BeginInvoke(
                new Action(() =>
                {
                    var random = new Random();
                    int num = random.Next(0, 9);
                    label3.Content = num.ToString();
                }));
            }, null, 0, 50);

        }

2.タイマー処理

下図は、3つあるタイマー変数のコンストラクタの実装を一つだけ切り出してみました。
Timerのコンストラクタは、4つの引数を必要とします。

Timer timer1 = Timer((1)callback, (2)state, (3)dueTime, (4)period)

(1)callback
Type: System.Threading.TimerCallback
A TimerCallback を実行するメソッドを表すデリゲートします。

(2)state
Type: System.Object
コールバック メソッドで使用される情報を格納したオブジェクトまたは nullです。

(3)dueTime
Type: System.Int32
前に遅延する時間 callback ミリ秒単位で呼び出されます。 指定 Timeout.Infinite 、タイマーが起動しないようにします。 0 を指定して、タイマーをすぐに開始します。

(4)period
Type: System.Int32
呼び出しの間の時間間隔 callback, 、(ミリ秒単位)。 指定 Timeout.Infinite 周期的なシグナル通知を無効にします。

という構成になっています。

            timer1 = new Timer((i) =>
            {
                label1.Dispatcher.BeginInvoke(
                new Action(() =>
                {
                    var random = new Random();
                    int num = random.Next(0, 9);
                    label1.Content = num.ToString();
                }));
            }, null, 0, 100);
その2ドラムを停止させるためのボタンの実装

Dispose()を使って、タイマーの動きを止めるイベントハンドラーを実装します。

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            timer1.Dispose();
        }

        private void button2_Click(object sender, RoutedEventArgs e)
        {
            timer2.Dispose();
        }

        private void button3_Click(object sender, RoutedEventArgs e)
        {
            timer3.Dispose();
        }

3.デリゲート

デリゲートは委譲と呼ばれます。「代表者」と考えるとしっくりくると解説しているサイトもあります。
つまり、直接処理機能を持っているメソッドを呼ぶのではなく、処理機能を知っている代表者にその処理を委ねるという考え方です。
C言語の関数ポインタをイメージすると良いと説明することもあるようです。


System.Threading.Timerのコンストラクタの第1引数では、TimerCallback を実行するメソッドを表すデリゲートします、と定義され、TimerCallbackを実行するデリゲートを記述することがわかります。

4.ラムダ式

ラムダ式は、デリゲート型または式ツリー型を作成するために使用できる匿名関数です、と解説されます。

Timer timer = new Timer((i) => {   }, null, 0, 100);(System.Threading.Timerのコンストラクタ)に当てはめて考えますと、第1引数の(i) => {   }のステートメントがこれに該当します。
そうです。TimerCallbackを実行するデリゲートの記述です。



5.ディスパッチャー

非同期処理の結果をUIに反映させるためには、別スレッドからUIスレッドに処理の流れを戻す必要があります。
WPFでこの役割を担うのは、「ディスパッチャ(dispatcher: 配送係)」と呼ばれるオブジェクトです。