Chapter 04

カウンター

eagle
eagle
2021.12.26に更新

目標

次のように「+1」ボタンと「-1」ボタンがあり、カウントを増減させることができるようなシンプルなカウンターアプリケーションを作成します。


カウンターアプリ

コントロールの配置

れでは前回の続きから行います。
まずはDesignerプロジェクトのMainForm.csをフォームデザイナで開き、ボタンをもう1つ追加しましょう。
ツールボックスから新規追加することもできますし、既存のボタンをコピー&ペーストで複製することもできます。新規追加した際はModifierプロパティをPublicに設定することを忘れないでください。

ラベルやボタンのTextプロパティやSizeプロパティなどを適宜変更し、カウンターアプリの画面を作成します。お好みで文字色や背景色を変更しても良いでしょう。
文字色はForeColorプロパティ、背景色はBackColorプロパティで変えることができます。

最終的には次のように3つのコントロールがフォームデザイナに配置されるはずです。


フォームデザイナ

コントロールの命名

既定ではコントロールは自動的にbutton1, button2など、機械的に命名されます。
まだコントロールの数が少ないので大丈夫ですが、数が増えてくるとMainプロジェクトでF#コードを書く際に
どのコントロールがどの名前か分かりにくいため厄介です。
そこで、コントロールに命名しておくことにしましょう。そのためにはNameプロパティを変更します。

今回はカウント数を表示するラベルをlabelCountとし、カウントを増やすためのボタンをbuttonIncrement
カウントを減らすためのボタンをbuttonDecrementとします。
フォームデザイナで各コントロールのNameプロパティを変更しましょう。


Nameプロパティの変更

Nameプロパティを変更すると一時的に次のようなエラーが出ますので、まずはこれを解消しましょう。


エラー

コードの修正

MainプロジェクトのMainForm.fsを開くと、先ほどbutton1label1の名前を変更したためにエラーが発生しています。

MainForm.fs
module MainForm

open App.Designer

type View() as this =
    inherit MainForm()

    let subscription =
        this.button1.Click
        |> Observable.subscribe (fun e ->
            this.label1.Text <- "Hello, from source code!")

    override this.Dispose(disposing) =
        if disposing then subscription.Dispose()

        base.Dispose disposing

代わりに、先ほどNameプロパティに指定したlabelCountbuttonIncrementを使用します。

MainForm.fs
  module MainForm
  
  open App.Designer
  
  type View() as this =
      inherit MainForm()
  
      let subscription =
-         this.button1.Click
+         this.buttonIncrement.Click
          |> Observable.subscribe (fun e ->
-             this.label1.Text <- "Hello, from source code!")
+             this.labelCount.Text <- "Hello, from source code!")
  
      override this.Dispose(disposing) =
          if disposing then subscription.Dispose()
  
          base.Dispose disposing

これで一時的にエラーが解消されるはずです。
これによりアプリケーションを実行できるようになります。


起動画面

ボタンを押したときの処理はまだ期待通りではありませんが、画面の見た目はカウンターアプリにふさわしくなりました。

1足す処理

それでは、+1ボタンをクリックしたときにカウントを1増加させてみましょう。
buttonIncrementコントロールがクリックされたときに、labelCountのテキストを変更してみます。

MainForm.fs
  module MainForm
  
  open App.Designer
  
  type View() as this =
      inherit MainForm()
  
      let subscription =
          this.buttonIncrement.Click
          |> Observable.subscribe (fun e ->
-             this.labelCount.Text <- "Hello, from source code!")
+             this.labelCount.Text <- "Count: 1")
  
      override this.Dispose(disposing) =
          if disposing then subscription.Dispose()
  
          base.Dispose disposing

これにより、1回のクリックまでならばカウントすることができるようになりました。
ただし、2回目以降のクリックには対応していません。


1回カウントアプリ

カウント数の取得

現在のカウント数を取得してみましょう。
今のところラベルの最初の文字はCount: で固定なので、最初の7文字分を飛ばして8文字目以降の部分文字列を取得すればカウントが取得できます。

これを行うためにはString.Substringメソッドを使用します。
文字列を数値に変換するには組み込みのintを使います。

新しい関数が出てきたらF#インタラクティブを利用して試しに使ってみると理解が深まります。

> "Count: 42".Substring 7;;
val it: string = "42"

> int "42";;
val it: int = 42

これらを組み合わせると現在のカウントが取得できます。

let count = this.labelCount.Text.Substring 7 |> int

次に、このカウントを基にしてその次の数をlabelCountコントロールのTextプロパティにセットしましょう。
数値から文字列を作るときはF# 6で追加された文字列補間の構文が便利です。

先ほど取得したcount1足した値を表示すれば良いでしょう。

this.labelCount.Text <- $"Count: %d{count + 1}")

最終的に次のようなコードになります。

module MainForm

open App.Designer

type View() as this =
    inherit MainForm()

    let subscription =
        this.buttonIncrement.Click
        |> Observable.subscribe (fun e ->
            let count = this.labelCount.Text.Substring 7 |> int
            this.labelCount.Text <- $"Count: %d{count + 1}")

    override this.Dispose(disposing) =
        if disposing then subscription.Dispose()

        base.Dispose disposing

それでは実行してみましょう。


カウンター(+1)

「-1」ボタンの実装

+1ボタンを実装できたので、-1ボタンも同様に実装してみましょう。
2つ目のボタンのクリックイベントを新たに購読する必要があります。

let subscription2 =
    this.buttonDecrement.Click
    |> Observable.subscribe (fun e ->
        let count = this.labelCount.Text.Substring 7 |> int
        this.labelCount.Text <- $"Count: %d{count - 1}")

購読をしたらその解除も忘れずに行っておきましょう。
今回もフォームを使い終わったタイミングで購読解除を行うため、Disposeメソッド内に記述します。

subscription2.Dispose()

最終的に次のようなコードになります。

MainForm.fs
module MainForm

open App.Designer

type View() as this =
    inherit MainForm()

    let subscription =
        this.buttonIncrement.Click
        |> Observable.subscribe (fun e ->
            let count = this.labelCount.Text.Substring 7 |> int
            this.labelCount.Text <- $"Count: %d{count + 1}")

    let subscription2 =
        this.buttonDecrement.Click
        |> Observable.subscribe (fun e ->
            let count = this.labelCount.Text.Substring 7 |> int
            this.labelCount.Text <- $"Count: %d{count - 1}")

    override this.Dispose(disposing) =
        if disposing then
            subscription.Dispose()
            subscription2.Dispose()

        base.Dispose disposing


カウンターアプリの実行

期待通り動作していますね!

現在の実装の問題点

このままでも期待通りに動作していますが、保守性の観点から幾つか問題が残っています。

その中でも最も重大な問題は、表示するテキストをCount: XXXから別の形式(例えば現在のカウントは XXX です。)に変更するときに、カウントの取得ロジックが変わることです。

そもそも、ラベルのテキスト(文字列)を解析してカウント(数値)を取り出し、その数値から文字列を再度生成しているので、これは2度手間です。
最初からカウントを数値として記憶しておけば、文字列を数値に解析するという面倒な処理を行わずに済みます。

次回は、このようにアプリケーションの状態を的確に表すデータを抽出し、コードのリファクタリングを行います。