Getting a Good Grasp of F# (仮)

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

F#からWPFのGUIを扱う 《文字列データとしてのXAMLからGUI表示へ》

以前、F#からWPFのGUIを扱う その1その4 にて、C#プロジェクトを混在させてWPFを利用したプログラミングを行いました。
今回はF#のコードだけを使ってWPFを扱ってみます。XAMLファイルは用いずにXAMLコードを F#コード内で文字列データとして持つことにします。今回の開発環境には Visual Studio Community 2017 を使っています。

 

1. 新規のF#プロジェクト(コンソールアプリケーションを選択)を作ります。

2. プロジェクトのプロパティ設定で「出力の種類」を「Windows アプリケーション」に変更します。

 3. WPF に必要なアセンブリ参照を追加します。以下の4つです。

  • WindowsBase
  • PresentationCore
  • PresentationFramework
  • System.Xaml

 参照設定が完了すると以下のような構成になります。

 4. ウィンドウ上に、黒い輪郭線を持ち青く塗られた矩形(Rectangle要素)と黒い輪郭線を持ち赤く塗られた円(Ellipse要素)を配置するF#コードを以下に示します。XamlReaderXAMLコードを文字列として読み込んでWindow クラス (System.Windows) オブジェクトに変換してGUIとして表示しています。

// プロジェクトのプロパティ設定をWindowsアプリケーションとして設定すること
open System.Windows
open System
open System.Windows.Markup
 
// ダブルクォート3連続(""")で XAMLの属性値 "~" を含む文字列もエスケープ文字なしでそのまま扱える
let xamlStr = 
    """
        <Window 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"              
            Title="MainWindow" Height="350" Width="500">
            <Grid>
                <Rectangle Fill="Blue" HorizontalAlignment="Left" Height="100" Margin="130,70,0,0" Stroke="Black" VerticalAlignment="Top" Width="100"/>
                <Ellipse Fill="Red" HorizontalAlignment="Left" Height="100" Margin="260,120,0,0" Stroke="Black" VerticalAlignment="Top" Width="100"/>
            </Grid>
        </Window>
    """
 
[<EntryPoint; STAThread>] // STAThreadを指定しないとWPFアプリは実行時はエラーとなるので注意
let main _ = 
    let window = XamlReader.Parse(xamlStr) :?> Window
    window.WindowStartupLocation 
        <- WindowStartupLocation.CenterScreen // 表示位置:モニタ中央
    Application().Run(window)

 5. 実行結果:モニタ中央に以下のようなウィンドウが表示されます。デフォルト設定では XAML UI デバッグツールのボタンがウィンドウ上に付いています。

6. XAML UI デバッグツールを非表示にしたい場合は、Visual Studioのメニューバーの「ツール」→「オプション」→「デバッグ」→「全般」へと進み、項目「XAML の UI デバッグツールを有効にする」のチェックボックスOFFにします。

7. 再度実行するとXAML UI デバッグツールのボタンは消えます。

 

VS2017 で NuGet パッケージ情報が取得できない?(トラブルシューティング)

Visual Sturdio Community 2017 (この記事時点でのバージョンは15.1 (26403.3) )をインストールした後、Nuget パッケージの情報を取得できない状態になっていることに気付きました。このままでは NuGet パッケージの検索もインストールも出来ません。その理由は単に Nuget パッケージ情報の取得先となるURL設定に不備があったためなのですが、以下はその際に行った修復作業記録です。

修復作業の流れ

メニューから「ツール」→「オプション」→「NuGet パッケージマネージャー」→「パッケージソース」の画面へ移行する。

「利用可能なパッケージソース」の欄が空になっているので、右上方にあるボタンを押して項目を新規作成します。

下の方にある入力欄に「名前」として「nuget.org」を、「ソース」としてのURLを「https://api.nuget.org/v3/index.json」と入力して ボタンを押して登録します。

これでパッケージ情報取得のための環境設定が出来ました。

プロジェクトから右クリックメニューで「NuGet パッケージの管理」を選んで、

パッケージマネージャーの画面に移動します

画面右上にあるプルダウンメニューでデフォルト選択されているパッケージソースを

f:id:pongitsune:20170413230140p:plain

新規に追加したパッケージソースに変更します。(ここでは「nuget.org」を選択)

 

これで作業完了です。パッケージ情報が得られるようになりました。これでパッケージのインストールも正常に行うことが出来る状態になりました。

F#のコードをユニットテストで検証する その6 (NUnit)

NUnit を使って例外送出のテストをしてみます。以下に示すコードでは DivideByZeroException が確実にスローされることを確認するテストを実行しています。

例外送出のユニットテストの例

  1. module UnitTestingSamples2
  2. open NUnit.Framework
  3. open System
  4.  
  5. [<Test>]
  6. let ``ゼロ除算例外のテスト 1``() =
  7.     let td = new TestDelegate(fun() -> 1 / 0 |> ignore)
  8.     let result =
  9.         Assert.Throws<DivideByZeroException>(td)
  10.     TestContext.Out.WriteLine(sprintf "結果は %A" result)
  11.  
  12. [<Test>]
  13. let ``ゼロ除算例外のテスト 2``() =
  14.     let result =
  15.         Assert.Throws<DivideByZeroException>(
  16.             fun() -> 1 / 0 |> ignore
  17.         )
  18.     TestContext.Out.WriteLine(sprintf "結果は %A" result)

この例では(動作が全く同じ)2つの例外送出のテスト関数が書いてあります。ゼロ除算をわざと行って例外送出を行っています。

5~10行目:``ゼロ除算例外のテスト 1`` では TestDelegate クラスとラムダ式を使って Assert.Throws へ引数として渡すオブジェクトを作っています。

12~18行目:``ゼロ除算例外のテスト 2`` では TestDelegate クラスを経由せず、Assert.Throws の引数として直接ラムダ式を記述していますが同じ動作が期待できます。

Assert.Throws メソッドには型パラメータとして(送出される予定の型である) DivideByZeroException クラスを与えます。

テストエクスプローラーによる実行結果

f:id:pongitsune:20161026195436p:plain

``ゼロ除算例外のテスト 1``  で Assert.Throws メソッドの返り値をテスト結果として出力するために10行目の TestContext.Out.WriteLine メソッドを使っています。

テストエクスプローラー内で「``ゼロ除算例外のテスト 1`` 」の項目を選んで「出力」の部分をクリックすると以下のような TestContext.Out.WriteLine メソッドによる出力を見ることが出来ます。

f:id:pongitsune:20161026200304p:plain

F#のコードをユニットテストで検証する その5 (NUnit)

 Visual Studio 2013 から NUnit を使ってF# コードのユニットテストを行ってみます。(F# のプロジェクトはコンソールアプリケーションのプロジェクトとして作成)

プロジェクトへ必要な Nuget パッケージをインストールします。(記事作成時の NUnit のバージョンは3.5.0)

 Visual Studio 2013 のテストエクスプローラーでユニットテストができるように NUnit 3 Test Adapter for Visual Studio(記事作成時のバージョンは3.5.0)を(F#のプロジェクトが所属する)ソリューションへインストールします。

テストコードの例

  1. module UnitTestingSamples
  2. open NUnit.Framework
  3.  
  4. [<Test>]
  5. let simpleTest() =
  6.     Assert.AreEqual(2.0**10.0, 1024.0)
  7.  
  8. [<TestCase("",      0)>]
  9. [<TestCase("ab",    2)>]
  10. [<TestCase("ABC",   3)>]
  11. [<TestCase("ABCD",  4)>]
  12. [<TestCase("12345", 5)>]
  13. let parameterizedTest(str: string, expected: int) :unit =
  14.     Assert.AreEqual(str.Length, expected)

2行目: NUnit名前空間 NUnit.Framework をインポートします。

4~6行目:Assert.AreEqual を使って値の等値性検査を行います。関数 simpleTest では2の10乗が1024に等しいかどうかを検査しています。

8~14行目:Assert.AreEqual を使って値の等値性検査を行います。関数 parameterizedTest には2つの引数があり、TestCase で指定したテストデータの組み合わせが渡されます。この例では TestCase が5つ置かれているので、関数 parameterizedTest を使ったテストが5回実行されます。

テストエクスプローラーから実行した結果

f:id:pongitsune:20161025145455p:plain

 

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

前回(F#からWPFのGUIを扱う その3)の別解として、今度は Win32 API を直接利用せずにインターフェイス IValueConverter の実装を別の方法で試みます。

MyBitmapConverter クラスを再実装します。よって、前回のF#のコードの34~52行目を以下のコードで置き換えます。

  1. [<System.Windows.Data.ValueConversion(typeof<System.Drawing.Bitmap>, typeof<System.Windows.Media.ImageSource>)>]
  2. type MyBitmapConverter() =
  3.     interface System.Windows.Data.IValueConverter with
  4.         override x.Convert(value: obj, targetType: System.Type, parameter: obj, culture: System.Globalization.CultureInfo) =
  5.             let drawingBmp = value :?> System.Drawing.Bitmap
  6.             assert(not <| obj.ReferenceEquals(drawingBmp, null))
  7.  
  8.             // ビットマップからピクセルデータ取り出し    
  9.             let  bmpData: System.Drawing.Imaging.BitmapData =
  10.                     drawingBmp.LockBits(
  11.                         System.Drawing.Rectangle(System.Drawing.Point(0, 0), drawingBmp.Size),
  12.                         System.Drawing.Imaging.ImageLockMode.ReadOnly,
  13.                         drawingBmp.PixelFormat)
  14.  
  15.             let scanLineWidth = abs(bmpData.Stride) // Strideプロパティが負の場合にも対処
  16.             let buffSize = scanLineWidth * drawingBmp.Height
  17.             use graphics = System.Drawing.Graphics.FromImage(drawingBmp)
  18.             // BGRの順にデータが並んでいるので注意
  19.             let bmpSrc = System.Windows.Media.Imaging.BitmapSource.Create(
  20.                                             drawingBmp.Width,
  21.                                             drawingBmp.Height,
  22.                                             double graphics.DpiX,
  23.                                             double graphics.DpiY,
  24.                                             System.Windows.Media.PixelFormats.Bgr24,
  25.                                             null,
  26.                                             bmpData.Scan0,
  27.                                             buffSize,
  28.                                             scanLineWidth)
  29.             bmpSrc.Freeze()
  30.             drawingBmp.UnlockBits(bmpData)
  31.             bmpSrc :> obj
  32.  
  33.         override x.ConvertBack(value: obj, targetType: System.Type, parameter: obj, culture: System.Globalization.CultureInfo) =
  34.             failwith("not implemented")

内部実装は変えましたが 実行結果は前回と同じ外観です。