WPF のバインディングソースとなるクラスを F# で作る
WPF にはデータバインディングによってオブジェクトの状態変化を別のオブジェクトへ通知する機能があります。 本記事ではバインディングソースを F# のクラスとして作り WPF コントロールと連携させます。そのために INotifyPropertyChanged インタフェースと F# のイベント通知の機能を用います。
本記事では、Window 上の TextBlock コントロールの描画位置をDispatcherTimer のタイマーイベントで定期的に更新して回転させるプログラムを作ります。
F# プロジェクトの作成
F#の新規プロジェクトとして「コンソールアプリケーション(.NET Framework)」を選択します。
WPF アプリケーションとして作成するので「プロジェクトのプロパティ」の設定画面で「出力の種類」を「Windows アプリケーション」に変更します。
アセンブリ参照の追加
- WindowsBase
- PresentationCore
- PresentationFramework
- System.Xaml
F# のソースコード(Program.fs)
- open System.ComponentModel
- open System.Runtime.CompilerServices
- open System
- open System.Windows.Threading
- open System.Windows.Markup
- open System.Windows
- let xamlText =
- """
- <Window
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- Title="SimpleDataBinding" SizeToContent="WidthAndHeight"
- ResizeMode="NoResize" WindowStartupLocation="CenterScreen">
- <Canvas Width="300" Height="300">
- <TextBlock Text="回転します" Foreground="Red" FontSize="30" Canvas.Left="150" Canvas.Top="150">
- <TextBlock.RenderTransform>
- <RotateTransform CenterX="0" CenterY="0" Angle="{Binding Path=Degree}" />
- </TextBlock.RenderTransform>
- </TextBlock>
- </Canvas>
- </Window>
- """
- type BaseNotifyer() =
- let propertyChangedEvent =
- new Event<PropertyChangedEventHandler,PropertyChangedEventArgs>()
- interface INotifyPropertyChanged with
- [<CLIEvent>]
- override __.PropertyChanged = propertyChangedEvent.Publish
- member this.OnPropertyChanged([<CallerMemberName>]?propertyName: string) =
- let args = new PropertyChangedEventArgs(Option.defaultValue "" propertyName)
- propertyChangedEvent.Trigger(this, args)
- type MyBindingSource() =
- inherit BaseNotifyer() with
- let mutable degree = 0.0
- member this.Degree
- with get(): double = degree
- and set(value) =
- degree <- value
- this.OnPropertyChanged()
- [<EntryPoint; STAThread>]
- let main _ =
- let dpTimer = new DispatcherTimer(Interval=TimeSpan.FromMilliseconds(100.0))
- let bindingSrc = MyBindingSource()
- let eventHandler(_: EventArgs) =
- let step = 10.0
- if bindingSrc.Degree + step >= 360.0 then
- bindingSrc.Degree <- 0.0
- else
- bindingSrc.Degree <- bindingSrc.Degree + step
- dpTimer.Tick.Add(eventHandler)
- let window = XamlReader.Parse(xamlText) :?> Window
- window.DataContext <- bindingSrc
- dpTimer.Start()
- Application().Run window
ソースコードの説明
8~24行目: Window 上に Canvas コントロールを配置し、その上に TextBlock コントロールを表示するXAMLコードとなる文字列データです。
17行目の TextBlock コントロールの RenderTransform プロパティに設定されている RotateTransform オブジェクトの Angle プロパティ(19行目)をバインディングターゲットとしています。 Angle プロパティは、バインディングソースである MyBindingSource オブジェクト内のDegreeプロパティ更新時に発生する PropertyChanged イベント通知に応じて値を更新します。
Angle プロパティは TextBlock コントロール描画時に使われる(時計回り方向の)回転移動角度であり double
型の値です。 Canvas コントロール上の座標(150, 150)を中心として TextBlock コントロールの文字列を Angle プロパティで指定した角度だけ(見かけ上)回転移動させ描画します。
26~36行目:BaseNotifyer クラスは INotifyPropertyChanged インタフェースを実装してバインディングソースとなるために必要な最小限の機能を持たせたクラスです。再利用可能なクラスとして最小限の機能を定義しています。
- let propertyChangedEvent =
- new Event<PropertyChangedEventHandler,PropertyChangedEventArgs>()
F# には event
キーワードがないので C# のようなイベント構文は使えません。Event クラスを用いて PropertyChanged イベントの通知機能を作ります。
- interface INotifyPropertyChanged with
- [<CLIEvent>]
- override __.PropertyChanged = propertyChangedEvent.Publish
PropertyChanged プロパティには CLIEvent 属性を付加します。これにより他の .NET 言語からも利用可能なイベントを定義出来ます。
- member this.OnPropertyChanged([<CallerMemberName>]?propertyName: string) =
- let args = new PropertyChangedEventArgs(Option.defaultValue "" propertyName)
- propertyChangedEvent.Trigger(this, args)
34行目の '?
' が付いた オプショナル引数 propertyName
には、付加された CallerMemberName 属性によって呼び出し側メソッド名(あるいはプロパティ名)が string option
型の値として自動的に割り当てられます。36行目、Event クラスの Trigger メソッドでイベントを発生させます。
38~45行目:MyBindingSource クラスはバインディングソースとなるクラスで BaseNotifyer クラス(26~36行にて宣言)の派生クラスです。XAMLコード内の TextBlock コントロールに与える回転角度の値をDegree プロパティとして持っています。
41行目の Degree プロパティは(set メソッド経由で)値が書き変えられた直後に OnPropertyChanged メソッドを呼び出す(45行目)ことにより、バインディングターゲットに対して PropertyChanged イベントを発生させます。
45行目で基底クラス(BaseNotifyer)の OnPropertyChanged メソッドを(引数を省略して)呼び出しています。 このメソッドを呼び出す側である Degree プロパティの名前 "Degree"
が自動的に OnPropertyChanged メソッドの引数として渡ります。
52~57行目: DispatcherTimer のイベントハンドラとなる関数 eventHandler の定義です。
- let eventHandler(_: EventArgs) =
- let step = 10.0
- if bindingSrc.Degree + step >= 360.0 then
- bindingSrc.Degree <- 0.0
- else
- bindingSrc.Degree <- bindingSrc.Degree + step
- 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 オブジェクトへ変換します。
- let window = XamlReader.Parse(xamlText) :?> Window
- window.DataContext <- bindingSrc
- dpTimer.Start()
62行目で XAMLコードから生成された Window オブジェクトの DataContext プロパティにバインディングソースとなる MyBindingSource オブジェクトを代入しています。これにより XAML コード内の TextBlock コントロールのプロパティと MyBindingSource オブジェクトのプロパティの間でイベント通知が可能になります。
WPFの仕様上、 DataContext プロパティ値への参照は Window オブジェクトからその子要素である TextBlock コントロールへと継承される仕組みなので、 TextBlock コントロール内のプロパティはバインディングターゲットとして PropertyChanged イベントを受け取ることができます。
63行目で DispatcherTimer オブジェクトを始動させています。これ以降周期的にタイマーイベントが発生します。