XAMLで好きな形を描く

WPFのShapeの一つにPathというものがあります。

名前だけだとPolylineのような感じがしますが、色々なものが描画できます。

パスマークアップ

HtmlのSVGでも採用されている図形描画用の構文です。

例えば以下の図形を描くには下記のようなコードになります。

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

<Path Data="M15,15 L85,85 M85,15 L15,85 M50,0 A50,50 0 0 0 50,100 A50,50 0 0 0 50,0" Stroke="Red" StrokeThickness="5" />


Dataの中身を詳しく

M15,15 L85,85

(15, 15)へ移動して(85, 85)まで線を引く
f:id:any-programming:20170221002947p:plain
M85,15 L15,85

(85, 15)へ移動して(15, 85)まで線を引く
f:id:any-programming:20170221004052p:plain
M50,0 A50,50 0 0 0 50,100

(50, 0)へ移動して半径50の円弧を(50, 100)まで描く
f:id:any-programming:20170221004146p:plain
A50,50 0 0 0 50,0

現在の位置(50, 100)から半径50の円弧を(50, 0)まで描く
f:id:any-programming:20170221002105p:plain


マウスイベント

マウスイベントも描画されたところのみ発行されるようになっています。

その他の図形

H 横線
V 縦線
Q, T 2次ベジエ曲線
C, S 3次ベジエ曲線






F#でWPF --- 階層構造表示

今回はTreeViewを利用して階層構造のデータを表示してみます。

HierarchicalDataTemplate

基本的にはDataTemplateにItemsSourceが加わったのものです。

ItemsSourceに子供の要素を保持しているプロパティをバインドします。

DataTemplateに関しては下記記事を参照してください。
F#でWPF --- 可変個のコントロール --- 型で生成するコントロールを変更 - 何でもプログラミング

<TreeView ItemsSource="{Binding Items}" Margin="10,10,10,35">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type MyClass}" ItemsSource="{Binding Children}">
            <!-- Control -->
        </HierarchicalDataTemplate>
    </TreeView.Resources>
</TreeView>


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

フォルダダイアログで選択したフォルダ内のファイル構造を表示するアプリケーションです。

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

File、Folder

DataTemplateで型で分岐させるため、ファイル及びフォルダの情報を持つクラスを定義します。

また階層構造を作成する関数も定義します。

namespace FileTree

[<AbstractClass>]
type FileOrFolder(name : string) =
    member this.Name = name

type File(name : string) =
    inherit FileOrFolder(name)

type Folder(name : string, children : FileOrFolder list) =
    inherit FileOrFolder(name)
    member this.Children = children

[<CompilationRepresentation (CompilationRepresentationFlags.ModuleSuffix)>]
module Folder =
    open System.IO
    let rec create path =
        let name = Path.GetFileName(path)
        let files = 
            Directory.EnumerateFiles(path) 
            |> Seq.map (Path.GetFileName >> File)
            |> Seq.cast<FileOrFolder>
        let folders = 
            Directory.EnumerateDirectories(path) 
            |> Seq.map create
            |> Seq.cast<FileOrFolder>
        Folder(name, files |> Seq.append folders |> Seq.toList)


アプリケーションコード

DataTemplateの時と同様に、Resourcesの中にHierarchicalDataTemplateを定義します。

ファイルとフォルダのアイコンは、Material Design Iconsのものを利用しています。

XAML内のFolderDialogActionは下記記事を参照してください。
F#でWPF --- フォルダダイアログCommand - 何でもプログラミング

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


XAML

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
        xmlns:local="clr-namespace:FileTree;assembly=FileTree"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="200">
    <Grid>
        <TreeView ItemsSource="{Binding FileOrFolders}" Margin="10,10,10,35">
            <TreeView.Resources>
                <HierarchicalDataTemplate DataType="{x:Type local:Folder}" ItemsSource="{Binding Children}">
                    <StackPanel Orientation="Horizontal">
                        <Viewbox Width="16" Height="16">
                            <Path Data="M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z" Fill="Black" />
                        </Viewbox>
                        <TextBlock Text="{Binding Name}" Margin="5,0,0,0" />
                    </StackPanel>
                </HierarchicalDataTemplate>
                <DataTemplate DataType="{x:Type local:File}">
                    <StackPanel Orientation="Horizontal">
                        <Viewbox Width="16" Height="16">
                            <Path Data="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M11,4H6V20H11L18,20V11H11V4Z" Fill="Black" />
                        </Viewbox>
                        <TextBlock Text="{Binding Name}" Margin="5,0,0,0" />
                    </StackPanel>
                </DataTemplate>
            </TreeView.Resources>
        </TreeView>
        <Button Content="フォルダを開く" HorizontalAlignment="Right" Margin="0,0,10,10" VerticalAlignment="Bottom" Width="75">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <local:FolderDialogAction Command="{Binding OpenFolder}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </Grid>
</Window>

F#

open System
open System.Windows
open FileTree

type Msg = OpenFolder of string

type Model = { FileOrFolders : FileOrFolder list }

let initialModel = { FileOrFolders = [] }

let updateModel model msg =
    match msg with
    | OpenFolder x ->
        { model with FileOrFolders = [ Folder.create x ] }

[<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






F#でWPF --- リストボックスCommand --- 複数選択

下記記事にてリストボックスで利用できるBehaviorを作成しました。
F#でWPF --- リストボックスCommand - 何でもプログラミング

今回はこれを拡張して、複数選択に対応させた実装をします。

ListBoxBehavior

単選択の時とほとんど同じです。

SelectedItemを利用していたのをSelectedItemsに変更しました。

SelectedItemsにsetterはないので、AddやClearでコレクションを変更します。

open System.Collections
open System.Windows.Input

type Item(name : obj) =
    override this.ToString() = name.ToString()

type ListBoxBehavior() =
    inherit BehaviorBase<ListBox>()

    static member val ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof<IEnumerable>, typeof<ListBoxBehavior>)
    member this.ItemsSource with get()              = this.GetValue(ListBoxBehavior.ItemsSourceProperty) :?> IEnumerable
                            and  set(x:IEnumerable) = this.SetValue(ListBoxBehavior.ItemsSourceProperty, x)

    static member val SelectedIndicesProperty = DependencyProperty.Register("SelectedIndices", typeof<int list>, typeof<ListBoxBehavior>)
    member this.SelectedIndices with get()           = this.GetValue(ListBoxBehavior.SelectedIndicesProperty) :?> int list
                                and  set(x:int list) = this.SetValue(ListBoxBehavior.SelectedIndicesProperty, x)

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

    override this.OnAttached control =
        let mutable items = []
        let mutable isUpdatingFromVM = false
        let getSelectedIndices() =
            control.SelectedItems 
            |> Seq.cast<Item>
            |> Seq.choose (fun x -> List.tryFindIndex ((=) x) items)
            |> Seq.toList
        let setSelectedItems indices =
            control.SelectedItems.Clear()
            indices
            |> List.choose (fun x -> List.tryItem x items)
            |> List.iter (control.SelectedItems.Add >> ignore)
        [ DependencyProperty.changed<IEnumerable> ListBoxBehavior.ItemsSourceProperty this
          |> Observable.subscribe (fun x -> isUpdatingFromVM <- true
                                            let indices = getSelectedIndices()
                                            items <- x |> Seq.cast<obj> |> Seq.toList |> List.map Item
                                            control.ItemsSource <- items      
                                            setSelectedItems indices 
                                            isUpdatingFromVM <- false)          
          DependencyProperty.changed<int list> ListBoxBehavior.SelectedIndicesProperty this
          |> Observable.subscribe (fun x -> isUpdatingFromVM <- true
                                            setSelectedItems x
                                            isUpdatingFromVM <- false)         
          control.SelectionChanged.Subscribe(fun _ -> if (not isUpdatingFromVM) && (this.Command <> null) then
                                                        this.Command.Execute(getSelectedIndices())) ]


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

追加ボタンでリストが追加でき、削除ボタンで選択された複数のリストを削除できるアプリケーションです。
f:id:any-programming:20170218184305p:plain

アプリケーションコード

こちらも単選択の時とほとんど同じです。

SelectedIndexをintからint listに変更しました。

リストボックスで複数選択を有効にするには、SelectionModeを変更します。

Extended Ctrlクリック、Shiftクリックで複数選択
Multiple クリックで複数選択 or 解除

XAML

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
        xmlns:local="clr-namespace:Behaviors;assembly=ListBoxBehavior"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="150" Width="200">
    <Grid>
        <ListBox Margin="10,10,0,10" HorizontalAlignment="Left" Width="80" SelectionMode="Extended">
            <i:Interaction.Behaviors>
                <local:ListBoxBehavior ItemsSource="{Binding Items}" SelectedIndices="{Binding SelectedIndices}" Command="{Binding SetSelectedIndices}" />
            </i:Interaction.Behaviors>
        </ListBox>
        <Button Command="{Binding AddItem}" Content="追加" HorizontalAlignment="Left" Margin="95,10,0,0" VerticalAlignment="Top" Width="75"/>
        <Button Command="{Binding RemoveSelectedItems}" Content="削除" HorizontalAlignment="Left" Margin="95,35,0,0" VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>

F#

open System
open System.Windows

type Msg =
    | AddItem
    | RemoveSelectedItems
    | SetSelectedIndices of int list

type Model = 
    { Items           : string list
      SelectedIndices : int list }

let initialModel = 
    { Items = []
      SelectedIndices = [] }

let updateModel model msg =
    match msg with
    | AddItem ->        
        { model with Items = "item" :: model.Items }

    | RemoveSelectedItems ->
        { model with Items = model.Items 
                             |> List.indexed 
                             |> List.filter (fun (i, _) -> List.contains i model.SelectedIndices |> not)
                             |> List.map snd
                     SelectedIndices = [] }

    | SetSelectedIndices x ->
        { model with SelectedIndices = x }

[<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






F#でWPF --- リストボックスCommand

今回はListBoxに、選択されたindexを送るCommandを実装します。

実装はBehaviorを利用して行います。

Behaviorの詳しい内容は下記記事を参照してください。
F#でWPF --- チェックボックスCommand --- Behavior利用 - 何でもプログラミング

SelectionChanged

ListBoxにあるSelectionChangedイベントは、ユーザーからの入力以外でも発火することがあります。

今回はこれを回避するよう実装していきます。
f:id:any-programming:20170218171652p:plain

SelectedItem、SelectedIndex、SelectedValue

ListBoxには現在の選択状態を示すのに、いくつかのプロパティが存在します。

その中で、SelectedItemをベースに作成してすれば不可解な挙動をしなくなると思います。

ListBoxBehavior

Item毎にインスタンスを変えるため(SelectedItemで判別するため)、独自のItemクラスでラップします。

ViewModelからの変更でSelectionChangedを発行しないよう、isUpdatingFromVMをフラグとして利用しています。

BehaviorBaseは下記記事を参照してください。
F#でWPF --- チェックボックスCommand --- Behavior利用 - 何でもプログラミング

DependencyProperty.changedは下記記事を参照してください。
F#でWPF --- テキストボックスCommand --- 1文字変更毎に発行 - 何でもプログラミング

open System.Collections
open System.Windows.Input

type Item(name : obj) =
    override this.ToString() = name.ToString()

type ListBoxBehavior() =
    inherit BehaviorBase<ListBox>()

    static member val ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof<IEnumerable>, typeof<ListBoxBehavior>)
    member this.ItemsSource with get()              = this.GetValue(ListBoxBehavior.ItemsSourceProperty) :?> IEnumerable
                            and  set(x:IEnumerable) = this.SetValue(ListBoxBehavior.ItemsSourceProperty, x)

    static member val SelectedIndexProperty = DependencyProperty.Register("SelectedIndex", typeof<int>, typeof<ListBoxBehavior>)
    member this.SelectedIndex with get()      = this.GetValue(ListBoxBehavior.SelectedIndexProperty) :?> int
                              and  set(x:int) = this.SetValue(ListBoxBehavior.SelectedIndexProperty, x)

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

    override this.OnAttached control =
        let mutable items = []
        let mutable isUpdatingFromVM = false
        let setSelectedItem index = 
            if 0 <= index && index < items.Length then
                control.SelectedItem <- items.[index]
            else
                control.SelectedItem <- null    
        [ DependencyProperty.changed<IEnumerable> ListBoxBehavior.ItemsSourceProperty this
          |> Observable.subscribe (fun x -> isUpdatingFromVM <- true
                                            let index = control.SelectedIndex
                                            items <- x |> Seq.cast<obj> |> Seq.toList |> List.map Item
                                            control.ItemsSource <- items           
                                            setSelectedItem index
                                            isUpdatingFromVM <- false)          
          DependencyProperty.changed<int> ListBoxBehavior.SelectedIndexProperty this
          |> Observable.subscribe (fun x -> isUpdatingFromVM <- true
                                            setSelectedItem x
                                            isUpdatingFromVM <- false)         
          control.SelectionChanged.Subscribe(fun _ -> if (not isUpdatingFromVM) && (this.Command <> null) then
                                                        this.Command.Execute(control.SelectedIndex)) ]


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

追加、削除ボタンでリストを管理するアプリケーションです。
f:id:any-programming:20170218173250p:plain

アプリケーションコード

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

XAML

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
        xmlns:local="clr-namespace:Behaviors;assembly=ListBoxBehavior"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="150" Width="200">
    <Grid>
        <ListBox Margin="10,10,0,10" HorizontalAlignment="Left" Width="80">
            <i:Interaction.Behaviors>
                <local:ListBoxBehavior ItemsSource="{Binding Items}" SelectedIndex="{Binding SelectedIndex}" Command="{Binding SetSelectedIndex}" />
            </i:Interaction.Behaviors>
        </ListBox>
        <Button Command="{Binding AddItem}" Content="追加" HorizontalAlignment="Left" Margin="95,10,0,0" VerticalAlignment="Top" Width="75"/>
        <Button Command="{Binding RemoveSelectedItem}" Content="削除" HorizontalAlignment="Left" Margin="95,35,0,0" VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>

F#

open System
open System.Windows

type Msg =
    | AddItem
    | RemoveSelectedItem
    | SetSelectedIndex of int

type Model = 
    { Items         : string list
      SelectedIndex : int }

let initialModel = 
    { Items = []
      SelectedIndex = -1 }

let updateModel model msg =
    match msg with
    | AddItem ->        
        { model with Items = "item" :: model.Items }

    | RemoveSelectedItem ->
        match model.SelectedIndex with
        | -1 -> model
        | x  -> let items = (model.Items |> List.take x) @ (model.Items |> List.skip (x + 1))     
                { model with Items = items
                             SelectedIndex = min x (items.Length - 1) }

    | SetSelectedIndex x ->
        { model with SelectedIndex = x }

[<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






F#でWPF --- フォルダダイアログCommand

下記記事にてOpenFileDialogに対応したActionを作成しました。
F#でWPF --- ファイルダイアログCommand - 何でもプログラミング

今回はフォルダを選択するダイアログに対応したActionを作成します。

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

ボタンを押してフォルダダイアログを開き、パスを選択するとテキストボックスにパスが表示されるアプリケーションを作成します。

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

参照の追加

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

System.Windows.FormsはWPFのコントロールと名前が衝突しやすいので、open System.Windows.Formsを利用しないことをお勧めします。

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

FolderDialogAction

ダイアログを開いたときに指定のフォルダが選択されているようにするには、SelectedPathにパスを設定しておきます。

namespace Actions

open System.Windows
open System.Windows.Input
open System.Windows.Interactivity

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

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

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

    override this.Invoke parameter = 
        use dialog = new System.Windows.Forms.FolderBrowserDialog(SelectedPath = this.InitialPath)
        if dialog.ShowDialog() = System.Windows.Forms.DialogResult.OK && this.Command <> null then
            this.Command.Execute dialog.SelectedPath


アプリケーションコード

F#側はファイルダイアログCommandの時と同じです。

Xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
        xmlns:local="clr-namespace:Actions;assembly=FolderDialogAction"
        Title="MainWindow" Height="80" Width="300">
    <Grid>
        <Button Content="フォルダダイアログ" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="100">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <local:FolderDialogAction Command="{Binding SetPath}" InitialPath="C:\src" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
        <TextBlock Text="{Binding Path}" HorizontalAlignment="Left" Margin="115,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>
    </Grid>
</Window>






F#でWPF --- XAMLデザイナでサンプルデータ利用

VisualStudioに備え付けのXAMLエディタでデザインをする際、実行時に決定される可変個のコントロールを扱うのは大変です。

そこで今回は、デザイン時にサンプルのViewModelを利用する方法を書いていきます。

DesignInstance

下記のようにXAMLを記述することにより、デザインビュー上のDataContextにインスタンスを設定できます。

ここではnamespace SamplesにあるSampleVMクラスを参照しています。

尚、DesignInstanceに指定する型は、引数なしコンストラクタを公開している必要があります。

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:local="clr-namespace:Samples;assembly=DesignTimeSample"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance local:SampleVM, IsDesignTimeCreatable=True}">
</Window>


レコードからサンプルViewModelの自動生成

F#のレコードには引数なしコンストラクタがありません。

そこでレコードと同じプロパティを持ち、引数なしコンストラクタを持つクラスを作ります。

今回はDynamicObjectを用いて自動的にクラスを作成するようにします。

open System.Dynamic
open System.Runtime.InteropServices

[<AbstractClass>]
type DesignTimeViewModel(instance : obj) =
    inherit DynamicObject()
    let properties = 
        instance.GetType().GetProperties()
        |> Array.map (fun x -> x.Name, x)
        |> dict
    override this.TryGetMember(binder : GetMemberBinder, [<Out>] result : obj byref) = 
        result <- properties.Item(binder.Name).GetValue(instance)
        true


DesignInstanceなし

DesignInstanceなしの場合、Bindingの中身が実行時にならないとわからないため、デザイナには何も表示されません。

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        Title="MainWindow" Height="130" Width="130">
    <Grid>
        <TextBlock Text="{Binding Text}" HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
        <Polyline Margin="0,30,0,0" Points="{Binding Points}" Stroke="Red" StrokeThickness="10" />
    </Grid>
</Window>
type ViewModel = 
    { Text   : string
      Points : PointCollection }

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

DesignInstance利用

デザイナにSampleVMの内容が反映されていることが確認できます。

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:local="clr-namespace:Samples;assembly=DesignTimeViewModel"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance local:SampleVM, IsDesignTimeCreatable=True}"        
        Title="MainWindow" Height="130" Width="130">
    <Grid>
        <TextBlock Text="{Binding Text}" HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
        <Polyline Margin="0,30,0,0" Points="{Binding Points}" Stroke="Red" StrokeThickness="10" />
    </Grid>
</Window>
namespace Samples

type SampleVM() =
    inherit DesignTimeViewModel(
        { Text = "Hello"
          Points = [ Point(10.0, 40.0)
                     Point(60.0, 10.0) 
                     Point(40.0, 50.0)
                     Point(100.0, 10.0) ] 
                   |> PointCollection })

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

XAMLデザイナのインテリセンス

現時点において(Visual Studio 2015 Update 3)XAMLでインテリセンスがサポートされています。

例えばBinding記述時に、DesignInstanceに設定したクラスのプロパティが候補に出てきます。

しかし今回はDynamicObjectを利用しているため、このインテリセンスは利用できません。

TypeProvidersを利用すると解決できるかもしれません。





F#でWPF --- UI画像キャプチャ --- キャプチャ時VM変更

下記記事にてUI画像をキャプチャする方法を記述しました。
F#でWPF --- UI画像キャプチャ - 何でもプログラミング

今回は、キャプチャ時のみに表示を変更したい場合に対応します。

実現方法

キャプチャ実行時にViewModelが公開しているキャプチャ用のViewModelをDataContextに設定します。(保存後は元のDataContextに戻します。)

今回は関数にして、生成を遅延させています。(Viewのパラメータも渡せるので、関数の形が良いと思います。)
f:id:any-programming:20170215001118p:plain

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

キャプチャボタンを押すとGridをPNGで保存するアプリケーションです。

保存されたPNGのテキストは"Hello"から"World"に変わっています。
f:id:any-programming:20170215000318p:plain

SaveImageAction

前回と比べ、VMを生成する関数のプロパティを追加し、キャプチャ時にDataContextを入れ替えています。

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)

    static member val VMForCapturingProperty = DependencyProperty.Register("VMForCapturing", typeof<unit -> obj>, typeof<SaveImageAction>)
    member this.VMForCapturing with get()              = this.GetValue(SaveImageAction.VMForCapturingProperty) :?> unit -> obj
                               and  set(x:unit -> obj) = this.SetValue(SaveImageAction.VMForCapturingProperty, x)

    override this.Invoke _ = 
        let dialog = SaveFileDialog(Filter = "PNGファイル|*.png")
        if this.Target <> null && dialog.ShowDialog() ?= true then            
            let oldVM = this.Target.DataContext
            this.Target.DataContext <- this.VMForCapturing()

            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)

            this.Target.DataContext <- oldVM

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


アプリケーションコード

アプリケーション名はSaveWpfImage.exeで、SaveImageActionはnamespace Actionsに定義されています。

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="100,35,0,0" VerticalAlignment="Top" Width="75">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <local:SaveImageAction Target="{Binding ElementName=grid}" VMForCapturing="{Binding VMForCapturing}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
        <TextBlock Text="{Binding Text}" Foreground="White" HorizontalAlignment="Left" Margin="35,15,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>
    </Grid>
</Window>

F#

open System
open System.Windows

type ViewModel =
    { Text : string }
    member this.VMForCapturing = fun () -> { Text = "World" } :> obj

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