Getting a Good Grasp of F# (仮)

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

F#からWPFのGUIを扱う その3

WPFImage コントロールに、動的に生成したビットマップ画像を表示させるコードを書いてみます。

生成する画像については「Windows Presentation Foundation 4.5 Cookbook」Chapter 9 : Graphics and Animation / Manipulating a bitmap programmatically にあるマンデルプロ集合のビットマップ画像表示サンプルを一部お手本にします。

WPFGUI 部分はC#のプロジェクトで作成し、それをF#のプロジェクトから操作出来るように「F#からWPFのGUIを扱う その1」と同様の手順で Visual Studio 2013 のソリューションを作成します。

F#からWPFのGUIを扱う その1」に合わせて、 WPF を F# で操作するためのソリューションを作る作業手順は以下の図のような流れで行っています。

image/svg+xml C#のプロジェクト WPF アプリケーションとして新規作成 F#のプロジェクト コンソール アプリケーションとして新規作成 クラスライブラリに変更 App.xaml を削除App.config を削除 Windows アプリケーションに変更 スタートアッププロジェクトに設定 C#のプロジェクトをアセンブリ参照に追加 以下をアセンブリ参照に追加 ・WindowsBase・PresentationCore・PresentationFramework・System.Xaml WPF のコントロールを操作するコードを書く XAML デザイナーにてコントロール(UI要素)を配置 XAML ファイル内のコントロールに必要なプロパティを追加 ・x:Name="識別子となる文字列"・x:FieldModifier="public"

今回も同様の手順で C#のプロジェクト名は WpfTest としています。

新規作成されたC#のプロジェクトのXAMLファイルを以下のように修正します。

600 × 600 ピクセルのサイズの Image コントロールを1つ配置しNameプロパティを「 TestImage1 」とします。F#のコードからアクセスできるようにプロパティ「 x:FieldModifier="public" 」も追加します。

Window のサイズはクライアント領域の Image コントロールのサイズにフィットさせる設定「 SizeToContent="WidthAndHeight" 」とします。

  1. <Window x:Class="WpfTest.MainWindow"
  2.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.         Title="Mandelbrot Set" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize">
  5.     <Grid>
  6.         <Image x:FieldModifier="public" Width="600" Height="600" x:Name="TestImage1" />
  7.     </Grid>
  8. </Window>

 

F#のプロジェクト WinAppTest において、今回は Bitmap クラス (System.Drawing) オブジェクトへ描画処理を行うのでさらにSystem.Drawing へのアセンブリ参照を追加します。最終的にF#のプロジェクトは以下のような参照設定となります。

ファイル Program.fs に書くコードは以下の通り。

  1. open System.Numerics
  2.  
  3. let mandelbrotColor(c: Complex): int =
  4.     let colorInit = 256.0
  5.     let rec calcColor (z: Complex) (col: double) : int =
  6.         if (z.Real + z.Imaginary < 4.0) && (col > 0.0) then
  7.             calcColor (z * z + c) (col - 1.0)
  8.         else
  9.             int col
  10.  
  11.     calcColor Complex.Zero colorInit
  12.  
  13. let createMandelbrotBitmap(): System.Drawing.Bitmap =
  14.     let drawingBmp = new System.Drawing.Bitmap(600, 600,
  15.                                                 System.Drawing.Imaging.PixelFormat.Format24bppRgb)
  16.     let from = Complex(-1.5, -1.0)
  17.     let dest = Complex(1.0, 1.0)
  18.     let deltax = (dest.Real - from.Real) / (double drawingBmp.Width)
  19.     let deltay = (dest.Imaginary - from.Imaginary) / (double drawingBmp.Height)
  20.     
  21.     for y = 0 to (drawingBmp.Height - 1) do
  22.         for x = 0 to (drawingBmp.Width - 1) do
  23.             let col = mandelbrotColor(from + Complex(double(x) * deltax, double(y) * deltay))
  24.             drawingBmp.SetPixel(x, y, System.Drawing.Color.FromArgb(col, col, col))
  25.     
  26.     do
  27.         use g = System.Drawing.Graphics.FromImage(drawingBmp)
  28.         use font = new System.Drawing.Font("Meiryo UI", 25.0f, System.Drawing.GraphicsUnit.Pixel)  
  29.         use brush = new System.Drawing.SolidBrush(System.Drawing.Color.Red)
  30.         g.DrawString("マンデルブロ集合", font, brush, 230.0f, 200.0f)
  31.     
  32.     drawingBmp
  33.  
  34. [<System.Runtime.InteropServices.DllImport("gdi32")>]
  35. extern bool DeleteObject(nativeint hObject)  
  36.  
  37. [<System.Windows.Data.ValueConversion(typeof<System.Drawing.Bitmap>, typeof<System.Windows.Media.ImageSource>)>]
  38. type MyBitmapConverter() =
  39.     interface System.Windows.Data.IValueConverter with
  40.         override x.Convert(value: obj, targetType: System.Type, parameter: obj, culture: System.Globalization.CultureInfo) =
  41.             let drawingBmp = value :?> System.Drawing.Bitmap
  42.             assert(not <| obj.ReferenceEquals(drawingBmp, null))
  43.             let hBitmap: nativeint = drawingBmp.GetHbitmap() // GDIビットマップオブジェクトへのハンドルを取得
  44.             let sourceRect = System.Windows.Int32Rect.Empty
  45.             let sizeOptions = System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions()
  46.             let bmpSource = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hBitmap, System.IntPtr.Zero, sourceRect, sizeOptions)
  47.             bmpSource.Freeze()
  48.             DeleteObject(hBitmap) |> ignore // GDIビットマップオブジェクトの後始末
  49.             bmpSource :> obj
  50.  
  51.         override x.ConvertBack(value: obj, targetType: System.Type, parameter: obj, culture: System.Globalization.CultureInfo) =
  52.             failwith("not implemented")
  53.  
  54. [<EntryPoint; System.STAThread>]
  55. let main _ =
  56.     let app = new System.Windows.Application()
  57.     let win = new WpfTest.MainWindow()
  58.  
  59.     use bitmap: System.Drawing.Bitmap = createMandelbrotBitmap()
  60.     let binding = new System.Windows.Data.Binding(
  61.                                             Source = bitmap,
  62.                                             Converter = new MyBitmapConverter())
  63.     win.TestImage1.SetBinding(System.Windows.Controls.Image.SourceProperty, binding) |> ignore
  64.  
  65.     app.Run win

3行目:mandelbrotColor 関数は複素平面上の指定座標における描画色(輝度、256階調)を計算する関数です。

13行目:createMandelbrotBitmap 関数は GDI+ ビットマップ(System.Drawing.Bitmap)を生成します。ビットマップのサイズは 600 × 600 ピクセルです。ビットマップ上すべてのピクセルの色を計算してその値を書き込みます。また、「マンデルブロ集合」という文字列を描き加えます。
ビットマップは、1ピクセルあたり 24 bit、つまりR・G・Bそれぞれに 8 bit を割り当てるカラー画像フォーマットです。ただし、24行目で各ピクセル中のR・G・Bそれぞれに対して等しい輝度の値を与えているのでここで描画されるマンデルプロ集合は見かけ上はモノクロ画像に見えます。

34~35行目Win32 APIDeleteObject 関数を直接呼び出すための宣言です。MyBitmapConverter クラスのメソッド Convert 内で DeleteObject 関数を呼び出すため必要になります。

38行目WPFImage コントロールへのデータバインディングに今回必要な型変換クラス MyBitmapConverter の定義です。System.Drawing.Bitmap オブジェクトを Image コントロールの Source プロパティ(ImageSource クラス)に適合するように型変換を行うクラスです。IValueConverter インターフェイス を実装しています。37行目ValueConversion アトリビュートによって変換前の型と変換後の型を予め指定する必要があります。

55行目以降がエントリーポイントとなる main 関数です。
57行目C#のプロジェクトの名前空間WpfTest 」にある WPFウィンドウとなる MainWindow クラスのオブジェクトを生成しています。

59行目System.Drawing.Bitmap オブジェクトの生成。ただしそのままでは Image コントロールの Source プロパティとしては使えない型なので、
60~62行目Binding オブジェクトを生成する際に、System.Drawing.Bitmap 型を System.Windows.Media.Imaging.BitmapSource 型に変換する MyBitmapConverter クラスオブジェクトを Converter プロパティとして渡します。

63行目SetBinding メソッドを使って Image コントロール(今回の例では「TestImage1」という名前)の Source プロパティに Binding オブジェクトを付与します。

参考資料:「Windows Presentation Foundation 4.5 Cookbook」Chapter 9 : Graphics and Animation / Manipulating a bitmap programmatically

実行結果

以下のようなウィンドウが表示されます。

 

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 にてコードを追加していきます。

  1. let buttonEventHandler (eventArgs: System.Windows.RoutedEventArgs) =
  2.     let button = eventArgs.Source :?> System.Windows.Controls.Button
  3.     System.Windows.MessageBox.Show(sprintf "%sからのイベントです" button.Name, "イベントハンドラのテスト")
  4.     |> ignore   
  5.  
  6. [<EntryPoint; System.STAThread>]
  7. let main _ =
  8.     let win = new WpfTest.MainWindow()
  9.     win.Button1.Click
  10.     |> Observable.add buttonEventHandler
  11.      
  12.     let app = new System.Windows.Application()
  13.     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 イベントには Observable.add メソッドを使ってイベントハンドラの登録が出来ます。

ビルド、実行するとボタンの付いたウィンドウが表示されます。

ボタンをクリックするとメッセージボックスが表示されます。

F#からWPFのGUIを扱う その1

WPFWindows 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#のプロジェクトには手を加える必要はありません。

  1. [<EntryPoint; System.STAThread>]
  2. let main _ =
  3.     let win = new WpfTest.MainWindow()
  4.     let app = new System.Windows.Application()
  5.     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行目で生成した集合オブジェクトの内容を書き替えているわけではありません。

  1. let s1 = set [1; 2]
  2. s1.Add  10
  3. s1.Add  20

2~3行目は Add メソッド呼び出し毎に s1に要素を加えた新規の集合オブジェクトが生成されています。代入(束縛)先の変数が存在しないので2行目と3行目の計算結果はそれぞれ捨てられます。

 F# Interactive で上記コードを実行して変数 s1 の値を調べてみます。

> s1;;
val it : Set<int> = set [1; 2]

 s1の値はAdd の呼び出しを実行しても変化していないことが分かります。

 

もしs1に新たに要素を追加した集合オブジェクトが欲しければ、オブジェクトを新たに生成して別の変数(下記のコードでは s2)に代入(束縛)する必要があります。

  1. let s1 = set [1; 2]
  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)つまり変更可能です。

  1. open System.Collections.Generic
  2. let hs1 = HashSet([1; 2])
  3. hs1.Add 10
  4. 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]