Pure F# WPF GUIアプリ開発に向けて

  • 53
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

F#でWPFを用いたGUIアプリ開発の場合,C#でWPFのGUIを作ってラッパーとし,F#はライブラリとしてC#側から呼び出す,というスタイルが多かったかと思われます.実際これは柔軟で易しいアプローチではありますが,それをするならばC#だけで作ってしまう,という方が多いでしょう. (私もF#とC#混ぜるぐらいならAll C#の方でいいかなと思います.)

ですが,状況は変わり, Reed Copsey氏とDaniel Mohl氏の圧倒的な力により,F#のみでWPFアプリケーションを開発することが非常に簡単になりました!本記事ではそれを紹介します.

参考文献: http://bloggemdano.blogspot.jp/2014/11/evolution-of-f-empty-wpf-template.html

何を作るか?

GUIでおなじみのテキストボックスとボタンを持ったアプリケーションを作って見ます.

無題.png

まずはプロジェクトの作成

まずは,プロジェクト(ソリューション)の作成です.New ProjectのダイアログのOnlineを選択し,「F# Empty Windows App (WPF)」を選択します.

キャプチャ.PNG

このプロジェクトは主として以下のファイルを含んでいます.

  • MainWindow.xaml
  • MainWindow.xaml.fs
  • App.xaml
  • App.fs

このうち編集する必要があるのはMainWindow.xamlとMainWindow.xaml.fsです.逆にApp.fsとApp.xamlは編集するはあまりないです.
とりあえず,プロジェクトを落とした後▶ボタンを押してStartしてやると,空ダイアログが出てきますので,これを編集していきます.

XAMLいじり

XAML上でテキストボックスとボタンを配置します.

MainWindow.xaml
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ViewModels;assembly=FsEmptyWindowsApp2"
    xmlns:fsxaml="http://github.com/fsprojects/FsXaml"
    Title="MVVM and XAML Type provider" Height="200" Width="400">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <TextBox Grid.Row="0">10</TextBox>
        <Button Grid.Row="1">+1</Button>
    </Grid>
</Window>

プレビュー画面にはこんなのが出てきます.

キャプチャ2.PNG

適当極まりないですが,デザインはこれで行きましょう.

ボタン押下から関数呼び出しを行う

まずは,ボタンを動作させてみます.(私が挑戦した時は,ここがつまづきどころでした…解説なくて辛かった)
動作のためには以下の2つを両方共行う必要があります.

  • XAML側で,ボタンのコマンドのバインディングを指定する
  • F#側で,メンバとしてバインディングされる関数を宣言する
    • バインディングした関数の実体の作成には,ライブラリ中のFactoryに入っている,CommandSync関数を使う

XAML側ですが,タグにCommandとBindingを付与します.

<Button Grid.Row="1" Command="{Binding Path=ButtonClicked}">+1</Button>

これで,ボタン側に,クリックされたときのコマンドとして,ButtonClickedという名前の何かを呼び出す,という指定を行いました.

C#+WPFの場合は,Clickedか何かからXAML上で直接C#の関数を指定することが多いと思いますが,F#+WPFでのCommandとBindingを使うのがポイントです.

次に,F#側でメンバを生成します.
MainViewModel.XAML.fsにある,MainViewModelの下に,以下のように書くことでButtonClickedに対応するメンバにアクセスできます.

 member this.ButtonClicked = ここに自作のCommand関数

Command用の関数については,MainViewModelが継承しているViewModelBaseに定義してある,CommandSync関数を使います.とりあえずMessageBoxでも出してみます.この時のMainWindow.xaml.fsはこうなります.

MainWindow.xaml.fs
namespace ViewModels

open System
open System.Windows
open FSharp.ViewModule
open FSharp.ViewModule.Validation
open FsXaml

type MainView = XAML<"MainWindow.xaml", true>

type MainViewModel() as self = 
    inherit ViewModelBase()    

    member this.ButtonClicked = self.Factory.CommandSync(fun () -> 
        MessageBox.Show("hoge") |> ignore) // MessageBox.Show には返り値があるため,ignoreしてやる必要あり

CommandSync関数はunit->unitな関数を受け取り,コマンド用の関数にラッパしてくれるものです.これでプログラムをビルドしてみてください.GUIが表示され+1ボタンを押すと,メッセージボックスが出てくるはずです.

ついでにこの時点でのxamlはこうです.

MainWindow.xaml
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ViewModels;assembly=FsEmptyWindowsApp2"
    xmlns:fsxaml="http://github.com/fsprojects/FsXaml"
    Title="MVVM and XAML Type provider" Height="200" Width="400">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <TextBox Grid.Row="0">10</TextBox>
        <Button Grid.Row="1" Command="{Binding Path=ButtonClicked}">+1</Button>
    </Grid>
</Window>

テキストボックスへのアクセス

ボタンは出来たので,次はテキストボックスに表示してあるテキストにアクセスできるようにします.こっちは結構ややこしいです(´・_・`) 次の3つを同時に行います.

  • XAMLでTextBoxのTextのバインディングを指定する
  • F#上で,TextBoxのバインディングに対応するメンバを作成する
  • F#上で,TextBoxのバインディングに対応する要素を作成する

XAMLはさっきとおなじような感じにTextBoxタグを書き換えます.

 <TextBox Grid.Row="0" Text="{Binding TextBoxText}"></TextBox>

なお,タグ内に文字があるとTextのバインディングと競合するようなので消しています.

次は,F#上でのコーディングですが,こっちはかなりの曲者です.先ほど指定したBinding名TextBoxTextのメンバを書きます.

member this.TextBoxText with get() = text.Value and set(value) = text.Value <- value

textという変数どこから出てきたという話ですが,以下のように定義します.

let text = self.Factory.Backing(<@ self.TextBoxText @>, "10")

さっきも使用したFactoryの中のBackingという関数を使用しています.<@ @>というのがなんとなく黒魔術のかほりがしますね.第二引数はデフォルトの値であり,先ほどと同じく10としました.ところで,self.TextBoxTextは何かというと,実はさっき定義したメンバです.つまりこれらは一種の循環定義なのです. そのため,どちらか一方だけではビルドすら成功しないことに注意してください.

ここまでのXAMLとF#を掲載します.

MainWindow.xaml
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ViewModels;assembly=FsEmptyWindowsApp2"
    xmlns:fsxaml="http://github.com/fsprojects/FsXaml"
    Title="MVVM and XAML Type provider" Height="200" Width="400">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <TextBox Grid.Row="0" Text="{Binding TextBoxText}"></TextBox>
        <Button Grid.Row="1" Command="{Binding Path=ButtonClicked}">+1</Button>
    </Grid>
</Window>
MainWindow.xaml.fs
namespace ViewModels

open System
open System.Windows
open FSharp.ViewModule
open FSharp.ViewModule.Validation
open FsXaml

type MainView = XAML<"MainWindow.xaml", true>

type MainViewModel() as self = 
    inherit ViewModelBase()    

    let text = self.Factory.Backing(<@ self.TextBoxText @>, "10")

    member this.TextBoxText with get() = text.Value and set(value) = text.Value <- value
    member this.ButtonClicked = self.Factory.CommandSync(fun () -> 
        MessageBox.Show("hoge") |> ignore) // MessageBox.Show には返り値があるため,ignoreしてやる必要あり

+1する機能を実装

最後に,+1する機能を実装してやりましょう.といってもここまでできていればあとは簡単で,ButtonClickedの関数を,textを取得 → 数値に変換 → +1してtextにセット,ということにしてやればいいだけです.

member this.ButtonClicked = self.Factory.CommandSync(fun () ->
  let x = text.Value.Trim() |> Int32.Parse
  text.Value <- (x + 1) |> string) 

xamlは変更してないので,F#コードだけ載せます.

MainWindow.xaml.fs
namespace ViewModels

open System
open System.Windows
open FSharp.ViewModule
open FSharp.ViewModule.Validation
open FsXaml

type MainView = XAML<"MainWindow.xaml", true>

type MainViewModel() as self = 
    inherit ViewModelBase()    

    let text = self.Factory.Backing(<@ self.TextBoxText @>, "10")

    member this.TextBoxText with get() = text.Value and set(value) = text.Value <- value
    member this.ButtonClicked = self.Factory.CommandSync(fun () ->
        let x = text.Value.Trim() |> Int32.Parse
        text.Value <- (x + 1) |> string) 

まとめ

F#でもWPFはかなり簡単にできます!WPFではC#と連携がデフォルトの時代から,WPF+F#オンリーのスタイルに変わっていくことでしょう.
Reed Copsey氏とDaniel Mohl氏に最大の賛辞を.