F#からWPFのGUIを扱う その2
WPFボタンの簡単なClickイベント処理を試みます。前回作成したソリューションにコードを追加していきます。
C#のプロジェクト WpfTest にて MainWindow.xaml にボタンを追加します。XAMLデザイナーの画面でマウス操作でボタンを配置してみます。
ツールボックスから「ボタン」の項目をドラッグ&ドロップすればボタンの追加がXAMLコードに反映されます。配置する位置や大きさは任意で構いません。
その後でXAMLコードに直接テキストでボタンのプロパティを追加していきます。
ここでは以下の2つのプロパティの記述を追加しています。
- x:Name="Button1"
- x:FieldModifier="public"
変更後の MainWindow.xaml は以下のようになります。
<Window x:Class="WpfTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <Button x:Name="Button1" x:FieldModifier="public" Content="Button" HorizontalAlignment="Left" Margin="63,56,0,0" VerticalAlignment="Top" Width="75"/> </Grid> </Window>
プロジェクト WpfTest をいったんビルドします。
次に F# のプロジェクト WinAppTest のファイル Program.fs にてコードを追加していきます。
- let buttonEventHandler (eventArgs: System.Windows.RoutedEventArgs) =
- let button = eventArgs.Source :?> System.Windows.Controls.Button
- System.Windows.MessageBox.Show(sprintf "%sからのイベントです" button.Name, "イベントハンドラのテスト")
- |> ignore
- [<EntryPoint; System.STAThread>]
- let main _ =
- let win = new WpfTest.MainWindow()
- win.Button1.Click
- |> Observable.add buttonEventHandler
- let app = new System.Windows.Application()
- app.Run win
1~4行目の関数 buttonEventHandler はボタンクリック時のイベントハンドラに使う関数です。シグネチャは System.Windows.RoutedEventArgs -> unit です。
2行目で System.Windows.Controls.Button型にキャストしてイベントソースの Button オブジェクトのプロパティにアクセスできるようにしています。
3行目でメッセージボックスを表示させています。Button オブジェクトの Name プロパティ(XAMLコードで「 x:Name="Button1" 」と記述した部分)の情報を表示させます。
9~10行目はボタンの Click イベントに関数 buttonEventHandler をイベントハンドラとして登録しています。Buttonオブジェクトへのアクセスですが、予めXAMLコードで「 x:Name="Button1" 」と明示的に名前(Name プロパティ)を指定しておいたので Button1 という名前でアクセス出来ます。
WPFのボタンの Click イベントには
ビルド、実行するとボタンの付いたウィンドウが表示されます。
ボタンをクリックするとメッセージボックスが表示されます。
F#からWPFのGUIを扱う その1
WPF(Windows Presentation Framework)のウィンドウをF#のコードを使って表示させてみます。XAMLの編集作業の利便性を考えてC#のプロジェクトと連携させることにします。以下の説明で使用している開発環境はVisual Studio 2013 です。
まず準備として新規のソリューションを用意します。C#で「WPFアプリケーション」の項目を選んでプロジェクトを新規作成します。プロジェクト名は何でもいいのですが、ここでは WpfTest としています。
以下の画面のようなC#のプロジェクトが出来上がります。C#やXAMLに関してはデフォルトで生成されるファイルの内容のままにしておきます。特に修正・追加するコードはありません。
メニューバーから、このプロジェクトのプロパティの設定画面を開きます。
アプリケーションの設定画面で出力の種類を「Windowsアプリケーション」から「クラスライブラリ」に変更します。
この段階では以下のようにエラーが出ます。
App.config および App.xaml を削除します。エラーは消えます。
次に、同じソリューション内で F#のプロジェクトを追加で新規作成します。名前は WinAppTest としていったんコンソールアプリケーションの項目を選びます。
以下のようなF#のプロジェクトが追加されます。
プロジェクト WinAppTest をアプリケーションのエントリーポイントとするので「スタートアッププロジェクトに設定」します。
プロジェクト WinAppTest に必要な参照の追加を行います。
先ほど作成したプロジェクト WpfTest を参照マネージャから追加します。
さらに必要な以下のアセンブリ群を追加していきます。
- WindowsBase
- PresentationCore
- PresentationFramework
- System.Xaml
F#のプロジェクト WinAppTest は最終的に以下の画面のような参照設定となります。
ここでプロジェクト WinAppTest のプロパティ設定画面を開きます。
アプリケーションの出力の種類を「コンソールアプリケーション」から「Windowsアプリケーション」に変更します。
F#のプロジェクト WinAppTest のソースファイル Program.fs にウィンドウを表示するためにアプリケーションのエントリーポイントのコードを書いていきます。
単にウィンドウを1つ開くだけのシンプルなコードです。このときC#のプロジェクトには手を加える必要はありません。
- [<EntryPoint; System.STAThread>]
- let main _ =
- let win = new WpfTest.MainWindow()
- let app = new System.Windows.Application()
- app.Run win
3行目でC#のプロジェクト WpfTest 内にある MainWindow クラスを使ってウィンドウオブジェクトを生成しています。Windows フォームアプリケーションとは異なり、ウィンドウは System.Windows.Window クラスの派生クラスです。このコードで使っている MainWindow クラスは、C#のプロジェクト WpfTest を新規作成したときに自動生成されていたクラスです。
4行目で System.Windows.Application クラスを使ってアプリケーションオブジェクトを生成しています。Windows フォームアプリケーションで使うApplication クラスとは名前空間が異なるまったく別のクラスです。クラス名が同じで紛らわしい上に同名の Run メソッドも持っているので注意。
5行目で Run メソッドにウィンドウオブジェクトを渡してWPFアプリケーションを起動させます。
ソリューションをビルド、 実行すると以下のようにWPFのウィンドウが表示されます。
F#で集合を扱う その2
名前空間:Microsoft.FSharp.CollectionsのSetモジュールについて。
Set が生成する集合オブジェクトはイミュータブル(immutable)つまり変更不可な値です。
以下のコードでAdd メソッドを呼び出していますが、1行目で生成した集合オブジェクトの内容を書き替えているわけではありません。
- let s1 = set [1; 2]
- s1.Add 10
- s1.Add 20
2~3行目は Add メソッド呼び出し毎に s1に要素を加えた新規の集合オブジェクトが生成されています。代入(束縛)先の変数が存在しないので2行目と3行目の計算結果はそれぞれ捨てられます。
F# Interactive で上記コードを実行して変数 s1 の値を調べてみます。
> s1;;
val it : Set<int> = set [1; 2]
s1の値はAdd の呼び出しを実行しても変化していないことが分かります。
もしs1に新たに要素を追加した集合オブジェクトが欲しければ、オブジェクトを新たに生成して別の変数(下記のコードでは s2)に代入(束縛)する必要があります。
- let s1 = set [1; 2]
- let s2 = s1.Add(10).Add(20)
2行目で s1 の内容にさらに要素(10、20)を加えた集合オブジェクトを生成して変数 s2 に代入(束縛)しています。
ここで、変数 s1 と変数 s2 の内容を調べてみます。
> s1;;
val it : Set<int> = set [1; 2]
> s2;;
val it : Set<int> = set [1; 2; 10; 20]
s1は要素は依然2つのままで初期状態から変化していませんが、新たに生成された集合オブジェクト s2には4つの要素が存在しています。
次に同様に集合を扱う機能を持つ HashSet の動作を調べてみます。名前空間:System.Collections.Generic にこのクラスはあります。このクラスによって生成されるオブジェクトはミュータブル(mutable)つまり変更可能です。
- open System.Collections.Generic
- let hs1 = HashSet([1; 2])
- hs1.Add 10
- hs1.Add 20
2行目で生成された hs1 の値に3~4行目の HashSet.Add で新たな要素を加えていって次々と更新していきます。
> hs1;;
val it : HashSet<int> = seq [1; 2; 10; 20]
HashSet ではAddメソッドによって内容が書き変えが可能です。Set の同名のメソッドとは動作が異なり、呼び出しによって新たな集合オブジェクトが生成されることはありません。
F#で集合を扱う その1
F#で数学における「集合」を扱うために、名前空間:Microsoft.FSharp.Collections には Set というモジュールが用意されています。
整数0から5までの集合を作るには、
let is = Set.ofList [0..5]
このコードは、整数0から5のリスト [0; 1; 2; 3; 4; 5] に Set.ofList という関数を適用して集合 is を生成しています。
F# Interactiveで実行してみると
> let is = Set.ofList [0..5];;
val is : Set<int> = set [0; 1; 2; 3; 4; 5]
Set.ofList は set で代用できるので以下のコードも同じ意味。
let is = set [0..5]
> let is = set [0..5];;
val is : Set<int> = set [0; 1; 2; 3; 4; 5]
コンソール出力ではリストとの区別は分かりにくいのですが 、F#の集合(Set)は内部的に平衡二分木によって要素を保持しています。要素の重複は許可されません。順序付けのために各要素を比較する必要があるため、この集合における要素は前提条件として、型制約があり IComparable インターフェイスを実装している必要があります。普通のプリミティブ型などは特に問題なく要素として使えます。
例えば、順序を無視して要素を与えても同じ集合を生成できます。
> let is = set [1; 0; 2; 4; 3; 5];;
val is : Set<int> = set [0; 1; 2; 3; 4; 5]
リストどうしでは要素の並び順が違えば同値とはみなしませんが、
> [0; 1; 2; 3; 4; 5] = [1; 0; 2; 4; 3; 5];;
val it : bool = false
集合どうしでは要素の与え方に関わらず同値とみなします。
> set [0; 1; 2; 3; 4; 5] = set [1; 0; 2; 4; 3; 5];;
val it : bool = true
注意:上の例では、単純にイコール記号(=)を使ってリストどうしや集合どうしの同値判定をしていますが、この場合は内部的に System.Object クラスの Equals メソッドのオーバーライドが行われていて、データ構造に配慮した適切な同値判定が行われています。
集合を定義する方法は複数あり、パイプライン演算子 |> を用いて要素を1つずつ集合に追加していくこともできます。
> let s2 =
set []
|> Set.add 1
|> Set.add 0
|> Set.add 2
|> Set.add 4
|> Set.add 3
|> Set.add 5 ;;
val s2 : Set<int> = set [0; 1; 2; 3; 4; 5]
集合において要素の重複は許可されないので、下記コードのように同じ値を何度追加しても2回目以降は要素を追加することはできません。
> let s3 =
set []
|> Set.add 1
|> Set.add 1
|> Set.add 1 ;;
val s3 : Set<int> = set [1]
F#のコードをユニットテストで検証する その4(xUnit)
前回の xUnit.net を利用したユニットテストの続きです。前回とは違うやり方で、ひとつのテスト関数に複数のテストデータを渡す方法を試します。
サンプルコード その1
- module Sample3
- open Xunit
- let SampleData01 : seq<obj array> =
- seq [
- [| ""; 0 |]
- [| "ab"; 2 |]
- [| "ABC"; 3 |]
- [| "ABCD"; 4 |]
- [| "12345"; 5 |]
- ]
- [<Theory>]
- [<MemberData("SampleData01")>]
- let test01(str: string, expected: int) : unit =
- Assert.Equal(str.Length, expected)
4~11行目で5個のテストデータを定義しています。
4行目の変数 SampleData01 で定義した5つのテストデータ(6~10行目)がテスト実行時に、15行目のテスト関数 test01 に渡されます。
15行目のテスト関数 test01 には Theoryアトリビュートが付いています。さらにMemberDataアトリビュートを使ってテストデータ群が格納された変数 SampleData01 の識別子名を文字列として指定して関連付けています。
4行目の変数の型注釈は以下のように書いても同じ意味です。コンパイルも通ります。
- let SampleData01 : System.Collections.Generic.IEnumerable<System.Object[]> =
Visual Studio のテストエクスプローラーを実行してみます。
次に、個々のテストデータの型をコンパイル時にチェックできるスタイルに書き直してみます。
サンプルコード その2
- module Sample4
- open Xunit
- let SampleData02 = new TheoryData<string, int>()
- SampleData02.Add ("", 0)
- SampleData02.Add ("ab", 2)
- SampleData02.Add ("ABC", 3)
- SampleData02.Add ("ABCD", 4)
- SampleData02.Add ("12345", 5)
- [<Theory>]
- [<MemberData("SampleData02")>]
- let test02(str: string, expected: int) : unit =
- Assert.Equal(str.Length, expected)
4~9行目で5個のテストデータを定義しています。変数 SampleData02 にテストデータ群が格納されます 。5~9行目にかけて TheoryData クラスのAddメソッドを使ってテストデータを1つずつ追加しています。
4行目の TheoryData<string, int> において、test02 に渡すデータの型が string 型とint 型のペアであることを明示的に指定し、コンパイル時の型チェックが行われるようにしてあります。
テストを実行すると「サンプルコード その1」と同様の結果が得られます。