F#でGUIを扱うのはなかなか大変なのですが、できないことはありません…と思っていましたが、WPFアプリならばプロジェクトテンプレートがあるよ、ってのを最近知りました。
Visual Studio 2013 で「オンラインテープレートの検索」で「F# WPF」で検索してみます。すると、いつか F# で Windows アプリを作るテンプレートがあるんですね。
が…、WPF アプリと、WPF+MVVM アプリのプロジェクトをダウンロードしたものの、いまいちよく分からなくて途方に暮れます。WPF アプリのほうは中身が空っぽ(Emptyなのでそりゃそうなんですが)で先がわからなく、WPF+MVVMは、グリッドへの DataBind の例なんですが、ふつうの DataBind がどうするのかわかりません。
そんなわけで、上をベースにして簡単なサンプルを作りました。
moonmile/SampleFSharpUI
https://github.com/moonmile/SampleFSharpUI
これも簡単すぎて実務には足りないんですが、取っ掛かりぐらいにはなるかと。WPF + F# の組み合わせはふつうに XAML デザイナを使ってアプリを作れるので、C# と同じように作れます。ただし、コードビハインドの仕組みがないので、ボタンのクリックイベントは自前で用意する必要がありますね。以下、ざっと説明を。
■SampleFSharpUI.WPF
先の F# empty windows app をベースにして XAML にテキストボックスを並べます。テンプレートの XAML では Grid タグをつけ忘れているので、それを追加するところから。
MainWindow.xaml.fs のようなコードビハインドはなくて、App.fs にある
type MainWindow = XAML<"MainWindow.xaml">
が、その役割を果たします。TypeProvider.XAML でコーディング時に MainWindow.xaml を解析して、クラス(type)に割り付けます。こうしておくと、
let window = MainWindow() ... let x = Convert.ToInt32( window.text1.Text )
な感じで x:Name=”text1” で指定した名前を、window.text1 のようにプロパティとして参照できるようになります。TypeProvider 自体がよくわからないのですが、なんかファイルか文字列を読み込んで型にして返してくれるものみたい。
ボタンのクリックイベントは
window.button1.Click.Add( fun _ -> let x = Convert.ToInt32( window.text1.Text ) let y = Convert.ToInt32( window.text2.Text ) let ans = x + y window.text3.Text <- ans.ToString() )
と書いていますが、window.button1.Click |> Event.add を使って
window.button1.Click |> Event.add( fun _ -> let x = Convert.ToInt32( window.text1.Text ) let y = Convert.ToInt32( window.text2.Text ) let ans = x + y window.text3.Text <- ans.ToString() )
のように書くこともできます。まあ、普通にメソッドにしたほうがいい気も。
アプリの起動自体は
[<STAThread>] (new Application()).Run(loadWindow()) |> ignore
となって、最初の loadWindow を呼び出すわけですが、この書き方だとすべてグローバル変数扱いになってしまうので、実務的には適度にクラスかモジュール分けが必要ですよね。
■SampleFSharpUI.MVVM
MVVM では、INotifyPropertyChanged を継承した ViewModelBase クラスを作っておいて、DataModel クラスを作ってます。MVVM の ViewModel にあたるクラスなんですが、Model クラスがないのは気持ち悪いのでこんな名前にしています。
type DataModel() = inherit ViewModelBase() let mutable _X : int = 10 let mutable _Y : int = 20 let mutable _ANS : int = 30 member this.X with get() = _X and set(value) = _X <- value base.OnPropertyChanged "X" member this.Y with get() = _Y and set(value) = _Y <- value base.OnPropertyChanged "Y" member this.ANS with get() = _ANS and set(value) = _ANS <- value base.OnPropertyChanged "ANS"
プロパティの get/set の並べ方はこんな感じで。クラスの最初に mutable を置かなければいけないのと(これでよかったっけ?)、OnPropertyChanged メソッドに「メソッド名」を渡さないといけないのがいまいちですね。CallerMemberName 属性を使って、コンパイル時にチェックしたいところです。
XAML へのバインドは C# と同じで Text=”{Binding X, Mode=TwoWay}” のように書けます。この方法で C# とビューが共有できます。
type MainWindow = XAML<"MainWindow.xaml"> let _model = new DataModel() let loadWindow() = let window = MainWindow() // Your awesome code goes here and you have strongly typed access to the XAML via "window" // 初期値 _model.X <- 0 _model.Y <- 0 _model.ANS <- 0 window.button1.Click.Add( fun _ -> // データバインドで設定 _model.ANS <- _model.X + _model.Y ) window.Root.DataContext <- _model window.Root
あとは、_model オブジェクトを作って DataContext にバインドすれば OK です。XAML のデザイン時に Binding でインテリセンスを効かせたいときは、C# のときと同じようにデザイン時のバインドを使えば OK です。
d:DataContext="{d:DesignInstance {x:Type local:DataModel}, IsDesignTimeCreatable=True}"
実行結果はこんな感じ
■Window Store App ではどうするのか?
タイププロバイダで XAML が読めるんだから、ストアアプリの XAML も読めるはず…なんですが、F# でストアアプリを作ろうとすると XAML デザイナがうまく認識しません。さらに言うと、そのままでは、参照設定がうまく WinRT にほうにできません。どうやらコンパイル時の *.targets をうまく設定しないといけないんですよね。現時点ではうまくできてません。
XAML 用のデザイナは C:/Program Files (x86)/MSBuild/Microsoft/WindowsXaml/v12.0/ あたりを見ているので、ここに FSharp 版をつくらないとダメな模様
あとプロジェクトテンプレートのGUIDが BC8A1FFA-BEE3-4634-8014-F334798102B3 で、C:/Program Files (x86)/Microsoft Visual Studio 12.0/Common7/IDE/CommonExtensions/Microsoft/WindowsXamlFlavor/Microsoft.VisualStudio.Windows.UI.Xaml.Project.pkgdef になるので、これを参考にしながら F# 版を作らないとダメかも。
ピンバック: F# Weekly #17, 2014 | Sergey Tihon's Blog