Getting a Good Grasp of F# (仮)

関数型言語F#をもっと楽しみたい

WPF のバインディングソースとなるクラスを F# で作る

WPF にはデータバインディングによってオブジェクトの状態変化を別のオブジェクトへ通知する機能があります。 本記事ではバインディングソースを F# のクラスとして作り WPF コントロールと連携させます。そのために INotifyPropertyChanged インタフェースと F# のイベント通知の機能を用います。

本記事では、Window 上の TextBlock コントロールの描画位置をDispatcherTimer のタイマーイベントで定期的に更新して回転させるプログラムを作ります。

F# プロジェクトの作成

F#の新規プロジェクトとして「コンソールアプリケーション(.NET Framework」を選択します。

WPF アプリケーションとして作成するので「プロジェクトのプロパティ」の設定画面で「出力の種類」を「Windows アプリケーション」に変更します。

アセンブリ参照の追加

WPF を利用するために必要なアセンブリ参照を追加します。

  • WindowsBase
  • PresentationCore
  • PresentationFramework
  • System.Xaml

F# のソースコード(Program.fs)

  1. open System.ComponentModel
  2. open System.Runtime.CompilerServices
  3. open System
  4. open System.Windows.Threading
  5. open System.Windows.Markup
  6. open System.Windows
  7.  
  8. let xamlText =
  9.     """
  10.     <Window
  11.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  12.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  13.         Title="SimpleDataBinding"  SizeToContent="WidthAndHeight"
  14.         ResizeMode="NoResize" WindowStartupLocation="CenterScreen">
  15.  
  16.         <Canvas Width="300" Height="300">
  17.             <TextBlock Text="回転します" Foreground="Red" FontSize="30" Canvas.Left="150" Canvas.Top="150">
  18.                 <TextBlock.RenderTransform>
  19.                     <RotateTransform CenterX="0" CenterY="0" Angle="{Binding Path=Degree}" />
  20.                 </TextBlock.RenderTransform>
  21.             </TextBlock>
  22.         </Canvas>
  23.     </Window>
  24.     """
  25.  
  26. type BaseNotifyer() =
  27.     let propertyChangedEvent =
  28.             new Event<PropertyChangedEventHandler,PropertyChangedEventArgs>()
  29.  
  30.     interface INotifyPropertyChanged with
  31.         [<CLIEvent>]
  32.         override __.PropertyChanged = propertyChangedEvent.Publish
  33.  
  34.     member this.OnPropertyChanged([<CallerMemberName>]?propertyName: string) =
  35.                         let args = new PropertyChangedEventArgs(Option.defaultValue "" propertyName)
  36.                         propertyChangedEvent.Trigger(this, args)
  37.  
  38. type MyBindingSource() =
  39.     inherit BaseNotifyer() with
  40.     let mutable degree = 0.0
  41.     member this.Degree
  42.         with get(): double = degree
  43.         and set(value) =
  44.             degree <- value
  45.             this.OnPropertyChanged()
  46.  
  47. [<EntryPoint; STAThread>]
  48. let main _ =
  49.     let dpTimer = new DispatcherTimer(Interval=TimeSpan.FromMilliseconds(100.0))
  50.     let bindingSrc = MyBindingSource()
  51.  
  52.     let eventHandler(_: EventArgs) =
  53.         let step = 10.0
  54.         if bindingSrc.Degree + step >= 360.0 then
  55.             bindingSrc.Degree <- 0.0
  56.         else
  57.             bindingSrc.Degree <- bindingSrc.Degree + step
  58.  
  59.     dpTimer.Tick.Add(eventHandler)
  60.  
  61.     let window = XamlReader.Parse(xamlText) :?> Window
  62.     window.DataContext <- bindingSrc
  63.     dpTimer.Start()
  64.  
  65.     Application().Run window

 ソースコードの説明

8~24行目: Window 上に Canvas コントロールを配置し、その上に TextBlock コントロールを表示するXAMLコードとなる文字列データです。

  1. <TextBlock Text="回転します" Foreground="Red" FontSize="30" Canvas.Left="150" Canvas.Top="150">
  2.     <TextBlock.RenderTransform>
  3.         <RotateTransform CenterX="0" CenterY="0" Angle="{Binding Path=Degree}" />
  4.     </TextBlock.RenderTransform>
  5. </TextBlock>

17行目の TextBlock コントロールの RenderTransform プロパティに設定されている RotateTransform オブジェクトの Angle プロパティ(19行目)をバインディングターゲットとしています。 Angle プロパティは、バインディングソースである MyBindingSource オブジェクト内のDegreeプロパティ更新時に発生する PropertyChanged イベント通知に応じて値を更新します。

 Angle プロパティは TextBlock コントロール描画時に使われる(時計回り方向の)回転移動角度であり double  型の値です。 Canvas コントロール上の座標(150, 150)を中心として TextBlock コントロールの文字列を Angle プロパティで指定した角度だけ(見かけ上)回転移動させ描画します。


 

26~36行目:BaseNotifyer クラスは INotifyPropertyChanged インタフェースを実装してバインディングソースとなるために必要な最小限の機能を持たせたクラスです。再利用可能なクラスとして最小限の機能を定義しています。

  1. let propertyChangedEvent =
  2.         new Event<PropertyChangedEventHandler,PropertyChangedEventArgs>()

F# には event キーワードがないので C# のようなイベント構文は使えません。Event クラスを用いて PropertyChanged イベントの通知機能を作ります。

  1. interface INotifyPropertyChanged with
  2.     [<CLIEvent>]
  3.     override __.PropertyChanged = propertyChangedEvent.Publish

 PropertyChanged プロパティには CLIEvent 属性を付加します。これにより他の .NET 言語からも利用可能なイベントを定義出来ます。

  1. member this.OnPropertyChanged([<CallerMemberName>]?propertyName: string) =
  2.                     let args = new PropertyChangedEventArgs(Option.defaultValue "" propertyName)
  3.                     propertyChangedEvent.Trigger(this, args)

34行目の '?' が付いた オプショナル引数 propertyName には、付加された CallerMemberName 属性によって呼び出し側メソッド名(あるいはプロパティ名)が string option 型の値として自動的に割り当てられます。36行目Event クラスの Trigger メソッドでイベントを発生させます。


 

38~45行目:MyBindingSource クラスはバインディングソースとなるクラスで BaseNotifyer クラス(26~36行にて宣言)の派生クラスです。XAMLコード内の TextBlock コントロールに与える回転角度の値をDegree プロパティとして持っています。

  1. type MyBindingSource() =
  2.     inherit BaseNotifyer() with
  3.     let mutable degree = 0.0
  4.     member this.Degree
  5.         with get(): double = degree
  6.         and set(value) =
  7.             degree <- value
  8.             this.OnPropertyChanged()

41行目の Degree プロパティは(set メソッド経由で)値が書き変えられた直後に OnPropertyChanged メソッドを呼び出す(45行目)ことにより、バインディングターゲットに対して PropertyChanged イベントを発生させます。

45行目で基底クラス(BaseNotifyer)の OnPropertyChanged メソッドを(引数を省略して)呼び出しています。 このメソッドを呼び出す側である Degree プロパティの名前 "Degree" が自動的に OnPropertyChanged メソッドの引数として渡ります。


 

52~57行目: DispatcherTimerイベントハンドラとなる関数 eventHandler の定義です。

  1. let eventHandler(_: EventArgs) =
  2.     let step = 10.0
  3.     if bindingSrc.Degree + step >= 360.0 then
  4.         bindingSrc.Degree <- 0.0
  5.     else
  6.         bindingSrc.Degree <- bindingSrc.Degree + step
  7.  
  8. dpTimer.Tick.Add(eventHandler)

関数 eventHandler は呼び出される度に、イベントソースである MyBindingSource オブジェクトのプロパティ Degree を0.0から360.0まで10.0刻みで変化さています。59行目DispatcherTimer オブジェクトにイベントハンドラとして登録されています。このイベントハンドラは、49行目で DispatcherTimer オブジェクトの Interval プロパティを100ミリ秒に指定したので100ミリ秒間隔で呼び出され Degree プロパティの値を更新します。


 

 61行目XamlReader によりXAML文字列を Window オブジェクトへ変換します。

  1. let window = XamlReader.Parse(xamlText) :?> Window
  2. window.DataContext <- bindingSrc
  3. dpTimer.Start()

62行目XAMLコードから生成された Window オブジェクトの DataContext プロパティにバインディングソースとなる MyBindingSource オブジェクトを代入しています。これにより XAML コード内の TextBlock コントロールのプロパティと MyBindingSource オブジェクトのプロパティの間でイベント通知が可能になります。

WPFの仕様上、 DataContext プロパティ値への参照は Window オブジェクトからその子要素である TextBlock コントロールへと継承される仕組みなので、 TextBlock コントロール内のプロパティはバインディングターゲットとして PropertyChanged イベントを受け取ることができます。

63行目DispatcherTimer オブジェクトを始動させています。これ以降周期的にタイマーイベントが発生します。