F#でWPF --- UI画像キャプチャ

今回は、表示されているWPFコントロールを画像として保存する方法を書いていきます。

F#でWPFを利用する方法は下記記事を参照してください。
F#でWPF --- ウィンドウ表示 - 何でもプログラミング

作成するアプリケーション

キャプチャボタンを押すと、Gridの内部がpngで保存されるアプリケーションを作成します。
f:id:any-programming:20170213233701p:plain

SaveImageAction

今回はTriggerActionを利用して実装していきます。

TargetにバインドされたFrameworkElementをPNG画像として出力します。

FrameworkElementからBitmapにするにはRenderTargetBitmapを利用します。

Measure、Arrange、UpdateLayoutを呼んでコントロールを希望のサイズに更新します。場合によって全部呼ぶ必要がないケースもありますが、セットにして記述しておく方が楽です。

BitmapをPNGで保存するにはPngBitmapEncoder利用します。

open System.IO
open System.Windows
open System.Windows.Media
open System.Windows.Media.Imaging
open System.Windows.Interactivity
open Microsoft.Win32
open Microsoft.FSharp.Linq.NullableOperators

type SaveImageAction() =
    inherit TriggerAction<FrameworkElement>()

    static member val TargetProperty = DependencyProperty.Register("Target", typeof<FrameworkElement>, typeof<SaveImageAction>)
    member this.Target with get()                   = this.GetValue(SaveImageAction.TargetProperty) :?> FrameworkElement
                       and  set(x:FrameworkElement) = this.SetValue(SaveImageAction.TargetProperty, x)

    override this.Invoke _ = 
        let dialog = SaveFileDialog(Filter = "PNGファイル|*.png")
        if this.Target <> null && dialog.ShowDialog() ?= true then            
            let width  = this.Target.ActualWidth
            let height = this.Target.ActualHeight
            let bmp = RenderTargetBitmap(int width, int height, 96.0, 96.0, PixelFormats.Pbgra32)
            this.Target.Measure(Size(width, height))
            this.Target.Arrange(Rect(0.0, 0.0, width, height))
            this.Target.UpdateLayout()
            bmp.Render(this.Target)

            let pngEncoder = PngBitmapEncoder()
            pngEncoder.Frames.Add(BitmapFrame.Create(bmp))
            use writer = new StreamWriter(dialog.FileName)
            pngEncoder.Save(writer.BaseStream)


アプリケーションコード

BindingでPathを設定しないと、コントロールそのものを渡すことができます。

Xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
        xmlns:local="clr-namespace:Actions;assembly=SaveWpfImage"
        Title="MainWindow" Height="100" Width="200">
    <Grid x:Name="grid" Background="CornflowerBlue">
        <Button Content="キャプチャ" HorizontalAlignment="Left" Margin="55,20,0,0" VerticalAlignment="Top" Width="75">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <local:SaveImageAction Target="{Binding ElementName=grid}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </Grid>
</Window>

F#

open System
open System.Windows

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window
    Application().Run(window) |> ignore    
    0






F#でWPF --- メッセージボックス --- System.Windows.MessageBox不使用

下記記事において、MVVMにおけるメッセージボックス表示の実装を行いました。
F#でWPF --- ファイルダイアログCommand - 何でもプログラミング

上記記事での実装は、stringの値にバインドしているというよりは、stringの変化のトリガーに引っかけて動作しているものでした。

今回は変数変更のトリガーではなく、変数の状態に基づいてメッセージボックスを表示するよう実装してみます。

作成するアプリケーション

メッセージボタンを押すとメッセージ領域が表示されるアプリケーションを作成します。

f:id:any-programming:20170213215253p:plain

構成

メインとなるコントロールの外側に新たにコントロールを用意し、メッセージ表示有効の時にメインを無効化し、メッセージボックスを表示します。

f:id:any-programming:20170213221001p:plain

アプリケーションコード

前回と比較して、IsMessageVisibleが追加されました。

type Model = 
    { Message          : string
      IsMessageVisible : bool }

let initialModel =
    { Message = ""
      IsMessageVisible = false }

type Msg = 
    | ShowMessage
    | CloseMessage

let updateModel model msg =
    match msg with
    | ShowMessage -> 
        { model with Message = "Hello"
                     IsMessageVisible = true }

    | CloseMessage ->
        { model with Message = ""
                     IsMessageVisible = false }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window
    window.DataContext <- DataContext(initialModel, updateModel, id)
    Application().Run(window) |> ignore   
    0


MainWindow.xaml

一番外側をMessageBoxPresenterで囲っています。

MessageBoxPresenterは後程説明します。

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:local="clr-namespace:MessageBox;assembly=MessageBox"
        Title="MainWindow" Height="200" Width="200">
    <local:MessageBoxPresenter ClosedCommand="{Binding CloseMessage}" IsOpen="{Binding IsMessageVisible}" Text="{Binding Message}">
        <Grid>
            <Button Content="メッセージボックス" Command="{Binding ShowMessage}" HorizontalAlignment="Right" Margin="0,0,10,10" VerticalAlignment="Bottom" Width="100" />
        </Grid>
    </local:MessageBoxPresenter>
</Window>


MessageBoxPresenter

ContentProperty属性を付けることにより、子要素(MainWindow.xamlのGrid)が自動的にMainContentにセットされます。

FocusVisualStyleにnullを設定することにより、フォーカス時に点線で囲われてしまうのを防ぎます。

boolToVisibility、invertBool、executeCommandは後ほど説明します。

Xaml

<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      mc:Ignorable="d" 
      d:DesignWidth="300" d:DesignHeight="300">
    <ContentControl x:Name="contentControl" FocusVisualStyle="{x:Null}"/>
    <Grid x:Name="gridMessageBox" Margin="30" Background="DodgerBlue">
        <TextBlock x:Name="textBlock" Text="TextBlock" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap" />
        <Button x:Name="buttonClose" Content="閉じる" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0 0 10 10" Width="75" Height="23"/>
    </Grid>
</Grid>

F#

[<ContentProperty("MainContent")>]
type MessageBoxPresenter() as this =
    inherit ContentControl(FocusVisualStyle = null)
    let grid = Application.LoadComponent(Uri("MessageBox.xaml", UriKind.Relative)) :?> Grid
    let contentControl = grid.FindName("contentControl") :?> ContentControl
    let gridMessageBox = grid.FindName("gridMessageBox") :?> Grid
    let textBlock      = grid.FindName("textBlock")      :?> TextBlock
    let buttonClose    = grid.FindName("buttonClose")    :?> Button    
    do  this.Content <- grid
        textBlock.SetBinding     (TextBlock.TextProperty,           Binding("Text",        Source = this))                               |> ignore
        gridMessageBox.SetBinding(Grid.VisibilityProperty,          Binding("IsOpen",      Source = this, Converter = boolToVisibility)) |> ignore
        contentControl.SetBinding(ContentControl.ContentProperty,   Binding("MainContent", Source = this))                               |> ignore
        contentControl.SetBinding(ContentControl.IsEnabledProperty, Binding("IsOpen",      Source = this, Converter = invertBool))       |> ignore

        buttonClose.Click |> Event.add (fun _ -> executeCommand this.ClosedCommand ())

    static member val MainContentProperty = DependencyProperty.Register("MainContent", typeof<obj>, typeof<MessageBoxPresenter>)
    member this.MainContent with get()      = this.GetValue(MessageBoxPresenter.MainContentProperty)
                            and  set(x:obj) = this.SetValue(MessageBoxPresenter.MainContentProperty, x)

    static member val TextProperty = DependencyProperty.Register("Text", typeof<string>, typeof<MessageBoxPresenter>)
    member this.Text with get()         = this.GetValue(MessageBoxPresenter.TextProperty) :?> string
                     and  set(x:string) = this.SetValue(MessageBoxPresenter.TextProperty, x)

    static member val IsOpenProperty = DependencyProperty.Register("IsOpen", typeof<bool>, typeof<MessageBoxPresenter>)
    member this.IsOpen with get()       = this.GetValue(MessageBoxPresenter.IsOpenProperty) :?> bool
                       and  set(x:bool) = this.SetValue(MessageBoxPresenter.IsOpenProperty, x)

    static member val ClosedCommandProperty = DependencyProperty.Register("ClosedCommand", typeof<ICommand>, typeof<MessageBoxPresenter>)
    member this.ClosedCommand with get()           = this.GetValue(MessageBoxPresenter.ClosedCommandProperty) :?> ICommand
                              and  set(x:ICommand) = this.SetValue(MessageBoxPresenter.ClosedCommandProperty, x)


boolToVisibility、invertBool、executeCommand

Binding時に値を変換するにはIValueConverterを利用します。

TwoWayで利用することはないので、Convertのみ実装すれば問題ありません。

let createValueConverter (f : 'a -> 'b) = 
    { new IValueConverter with
        member x.Convert(value, _, _, _) =
            value :?> 'a |> f :> obj
        member x.ConvertBack(_, _, _, _) = 
            failwith "Not available" }

let boolToVisibility = createValueConverter (fun (x : bool) ->
    if x then Visibility.Visible
         else Visibility.Collapsed)

let invertBool = createValueConverter not        
    
let executeCommand (x : ICommand) parameter = 
    if x <> null then 
        x.Execute(parameter)


XAMLデザイナが動作しない

MainWindow.xamlでMessageBoxPresenterを貼り付けた際、現状デザイナが動作しなくなります。

MessageBoxPresenterでのLoadComponentでリソース位置が解決できていないようです。

解決策がわかりましたら修正いたします。

EmbeddedResourceなら動作しました。
F#でWPF --- Resource、Content、EmbeddedResource - 何でもプログラミング






F#でWPF --- 可変個のコントロール --- 型で生成するコントロールを変更

下記記事では同じ型のリストを元にコントロールを作成しました。
F#でWPF --- 可変個のコントロール - 何でもプログラミング

今回は異なる型のリスト(ベースクラスのリストで、各インスタンスの型はバラバラ)を元にコントロールを作成してみます。

作成するアプリケーション

追加ボタンを押すと、円か正方形がランダムに追加されるアプリケーションを作成します。

f:id:any-programming:20170212093152p:plain

Xaml基本フォーマット

Resourcesの中にDataTemplateを複数用意します。

DataTypeに対象の型を指定することにより関連付けされます。

<ItemsControl ItemsSource="{Binding Data}" >
    <ItemsControl.Resources>
        <DataTemplate DataType="{x:Type MyType1}">
            <!-- Control1 -->
        </DataTemplate>
        <DataTemplate DataType="{x:Type MyType2}">
            <!-- Control1 -->
        </DataTemplate>
    </ItemsControl.Resources>
</ItemsControl>


コード全体

Shapeの定義に判別共用体を使いたいのですが、DataTemplateのDataTypeで認識されません。

そのため純粋なクラスの継承で記述してあります。

判別共用体でうまくDataTypeに指定できる方法が見つかりましたらまた記事にしたいと思います。

Program.fsで利用しているDataContextは下記記事を参照してください。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

アプリケーション名はShapes.exeです。

Shapes.fs

namespace Shapes

open System.Windows

[<AbstractClass>]
type Shape(position : Point) =
    member this.Position = position

type Circle(position : Point) =
    inherit Shape(position)

type Rectangle(position : Point) =
    inherit Shape(position)

Program.fs

open System
open Shapes

type Msg = AddShape

type Model = { Shapes : Shape list }

let initialModel = { Shapes = [] }

let random = Random()

let updateModel model msg =
    let p = Windows.Point(random.Next(100) |> double, random.Next(100) |> double)
    match msg with
    | AddShape ->
        let shape = match random.Next(2) with
                    | 1 -> Circle(p)    :> Shape
                    | _ -> Rectangle(p) :> Shape
        { model with Shapes = shape :: model.Shapes }

open System.Windows

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window
    window.DataContext <- DataContext(initialModel, updateModel, id)
    Application().Run(window) |> ignore
    0

Xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Shapes;assembly=Shapes"
        Title="MainWindow" Height="180" Width="140">
    <Grid>
        <ItemsControl ItemsSource="{Binding Shapes}" Width="100" Height="100" Margin="10,10,0,0" VerticalAlignment="Top" HorizontalAlignment="Left">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.Resources>
                <DataTemplate DataType="{x:Type local:Circle}">
                    <Ellipse Width="15" Height="15" Fill="Orange" />
                </DataTemplate>
                <DataTemplate DataType="{x:Type local:Rectangle}">
                    <Rectangle Width="15" Height="15" Fill="Orange" />
                </DataTemplate>
            </ItemsControl.Resources>
            <ItemsControl.ItemContainerStyle>
                <Style>
                    <Setter Property="Canvas.Left" Value="{Binding Position.X}" />
                    <Setter Property="Canvas.Top"  Value="{Binding Position.Y}" />
                </Style>
            </ItemsControl.ItemContainerStyle>
        </ItemsControl>
        <Button Content="追加" Command="{Binding AddShape}" HorizontalAlignment="Left" Margin="10,115,0,0" VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>






F#でWPF --- 可変個のコントロール

F#でWPFプロジェクトを作成する方法は下記記事を参照してください。
F#でWPF --- ウィンドウ表示 - 何でもプログラミング

作成するアプリケーション

ボタンを押す度、ランダムな位置に円シェイプを追加するアプリケーションを作成します。

f:id:any-programming:20170211101534p:plain

コード(F#側)

コード内で利用しているDataContextは下記記事を参照してください
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

open System
open System.Windows

type Msg = AddCircle

type Model = { Circles : Point list }

let initialModel = { Circles = [] }

let random = Random()

let updateModel model msg =
    match msg with
    | AddCircle ->
        let p = Point(random.Next(100) |> double, random.Next(100) |> double)
        { model with Circles = p :: model.Circles }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window
    window.DataContext <- DataContext(initialModel, updateModel, id)
    Application().Run(window) |> ignore
    0


ItemsControl

可変個のコントロールを表示する際に、ItemsControlが利用できます。

基本的な使い方は下記のようなフォーマットになります。

<ItemsControl ItemsSource="{Binding Data}" >
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <!-- Controlを配置するPanel -->
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <!-- Control -->
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>


可変個の円

Canvas上に可変個のEllipseを配置するにはこのようなコードになります。

Ellipseは全てItemContainerで包まれているため、Canvas.Leftを設定する場合はItemContainerStyleで設定する必要があります。

<ItemsControl ItemsSource="{Binding Circles}" >
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Ellipse Width="10" Height="10" Fill="Orange" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemContainerStyle>
        <Style>
            <Setter Property="Canvas.Left" Value="{Binding X}" />
            <Setter Property="Canvas.Top"  Value="{Binding Y}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>


ItemContainerStyleを利用しない場合

EllipseをCanvasで包みます。

<ItemsControl ItemsSource="{Binding Circles}" >
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Canvas>
                <Ellipse Canvas.Left="{Binding X}" Canvas.Right="{Binding Y}" Width="10" Height="10" Fill="Orange" />
            </Canvas>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>


Xaml全体

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        Title="MainWindow" Height="180" Width="140">
    <Grid>
        <ItemsControl ItemsSource="{Binding Circles}" Width="100" Height="100" Margin="10,10,0,0" VerticalAlignment="Top" HorizontalAlignment="Left">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Ellipse Width="10" Height="10" Fill="Orange" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemContainerStyle>
                <Style>
                    <Setter Property="Canvas.Left" Value="{Binding X}" />
                    <Setter Property="Canvas.Top"  Value="{Binding Y}" />
                </Style>
            </ItemsControl.ItemContainerStyle>
        </ItemsControl>
        <Button Content="追加" Command="{Binding AddCircle}" HorizontalAlignment="Left" Margin="10,115,0,0" VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>






拡張プロパティ(F#)

F#では既存の型を拡張して、プロパティを追加することができます。

今回はいくつか使い方を紹介します。

Getterのみ

Listを拡張してみます。

ジェネリックなクラスは拡張できないので、ベースのIListを拡張します。

Countの2倍の値を返すCountDoubleを実装してみます。

open System.Collections
open System.Collections.Generic

type IList with
    member this.DoubleCount = this.Count * 2

[<EntryPoint>]
let main argv = 
    let x = List<int>([ 1; 2; 3 ])
    let s = x.DoubleCount // 6
    0


Setter対応

F#基本的にImmutableであるため、Setterを実装することは稀です。

ただ、利用する機会がある可能性があるので実装してみます。

型拡張は原理的にはstaticな関数を追加しているだけなので、fieldを追加することができません。

そこでインスタンスをキーとして状態を保持するDictionaryを用意します。

しかしDictionaryを利用するとインスタンスが参照され続けるので、弱参照で保持するConditionalWeakTableを利用します。

open System.Collections
open System.Collections.Generic
open System.Runtime.CompilerServices

type ListState = { mutable Name : string }

let initialListState = { Name = "name" }

let listStates = ConditionalWeakTable<IList, ListState>()

let getListState x = listStates.GetValue(x, ConditionalWeakTable.CreateValueCallback(fun _ -> initialListState))

type IList with
    member this.Name with get()      = (getListState this).Name
                     and  set(value) = (getListState this).Name <- value

[<EntryPoint>]
let main argv = 
    let x = List<int>([ 1; 2; 3 ])
    x.Name <- "NewList"
    printfn "%s" x.Name
    0






F#スクリプト

F#スクリプトは、わざわざソリューションやプロジェクトを作成することなく利用できるため、とても便利です。

fsxファイル作成

F# Interactiveに直接入力しても動作しますが、fsxファイルに記述したほうがインテリセンスやデバッグを利用できるためfsxを作成します。

File → New → Fileから "F# Script File" を選択します。

f:id:any-programming:20170208165714p:plain

f:id:any-programming:20170208231125p:plain

Hello World

fsxに下記コードを記述して、全体を選択 → 右クリックメニュー → Execute In Interactiveを選択します。

F# Interactiveが立ち上がり、選択したコードが実行されます。

f:id:any-programming:20170208170341p:plain

f:id:any-programming:20170208170802p:plain

デバッグ有効化

Tools → OptionsでOptionsダイアログを起動します。

F# Tools → F# Interactive → Enable script debuggingをTrueに設定します。

Debugging → General → Use Managed Compatibility Modeのチェックを外します。

設定を有効化するため、F# Interactive上で右クリックメニュー → Reset Interactive Sessionを選択して再起動をします。

f:id:any-programming:20170208231450p:plain

f:id:any-programming:20170208231508p:plain

f:id:any-programming:20170208231519p:plain

f:id:any-programming:20170208232046p:plain

デバッグ

コード選択 → 右クリックメニュ → Debug In Interactiveを選択します。

ブレークポイントやステップ実行など通常のデバッグが行えます。

f:id:any-programming:20170208231740p:plain

f:id:any-programming:20170208231905p:plain

ディレクティブ

#r dllを参照 #r "c:/lib/MyLib.dll"
#I dll検索フォルダの追加 #I "C:/lib"
#r "MyLib.dll"
#load fsファイルのロード #load "MyFSharp.fs"
#quit 宣言された行で実行を終了
#time 時間を表示 Real: 00:00:00.507, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0






サブフォルダを含む、フォルダ内のファイルの取得(F#)

下記のようなフォルダ構造があった際に、file1.txt、file2.txt、file1_1.txtのパスを取得する関数をF#で実装します。
f:id:any-programming:20170207232413p:plain

breadthFirstSearch

汎用的な幅優先探索を実装します。

開始点と、要素から子要素を取得する関数を入力とします。

Queueを用いて手続き的な実装をしました。

module Seq =
    let breadthFirstSearch (children : 'a -> 'a seq) (root : 'a) =
        seq { let queue = System.Collections.Generic.Queue<'a>([ root ])
              while 0 < queue.Count do
                  let item = queue.Dequeue()
                  children item |> Seq.iter queue.Enqueue
                  yield item }


breadthFirstSearch(Seqモジュールのみで実装)

参考例として、whileを使わずに実装してみました。

module Seq =
    let breadthFirstSearch (children : 'a -> 'a seq) (root : 'a) =
        Seq.initInfinite id
        |> Seq.scan (fun queue _ -> queue.Tail @ (children queue.Head |> Seq.toList)) [ root ]
        |> Seq.takeWhile (List.isEmpty >> not)
        |> Seq.map List.head


descendantFiles

フォルダを入力で受け取り、ファイルパスのSeqを返す関数を実装します。

open System.IO

let descendantFiles path =
    Seq.breadthFirstSearch Directory.EnumerateDirectories path
    |> Seq.collect Directory.EnumerateFiles


動作確認

FolderBrowserDialogを利用するため、System.Windows.Forms.dllを参照に追加します。

f:id:any-programming:20170207234330p:plain

[<EntryPoint>]
open System.Windows.Forms

let main argv = 
    use dialog = new FolderBrowserDialog()
    if dialog.ShowDialog() = DialogResult.OK then
        descendantFiles dialog.SelectedPath 
        |> Seq.iter (printfn "%s")

f:id:any-programming:20170207234340p:plain