XAMLで好きな形を描く
WPFのShapeの一つにPathというものがあります。
名前だけだとPolylineのような感じがしますが、色々なものが描画できます。
パスマークアップ
HtmlのSVGでも採用されている図形描画用の構文です。
例えば以下の図形を描くには下記のようなコードになります。
<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)まで線を引く |
|
M85,15 L15,85 (85, 15)へ移動して(15, 85)まで線を引く |
|
M50,0 A50,50 0 0 0 50,100 (50, 0)へ移動して半径50の円弧を(50, 100)まで描く |
|
A50,50 0 0 0 50,0 現在の位置(50, 100)から半径50の円弧を(50, 0)まで描く |
マウスイベント
マウスイベントも描画されたところのみ発行されるようになっています。
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>
作成するアプリケーション
フォルダダイアログで選択したフォルダ内のファイル構造を表示するアプリケーションです。
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 - 何でもプログラミング
<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())) ]
作成するアプリケーション
追加ボタンでリストが追加でき、削除ボタンで選択された複数のリストを削除できるアプリケーションです。
アプリケーションコード
こちらも単選択の時とほとんど同じです。
SelectedIndexをintからint listに変更しました。
リストボックスで複数選択を有効にするには、SelectionModeを変更します。
Extended | Ctrlクリック、Shiftクリックで複数選択 |
Multiple | クリックで複数選択 or 解除 |
<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イベントは、ユーザーからの入力以外でも発火することがあります。
今回はこれを回避するよう実装していきます。
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#で利用しているDataContextは下記記事を参照してください。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング
<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を作成します。
作成するアプリケーション
ボタンを押してフォルダダイアログを開き、パスを選択するとテキストボックスにパスが表示されるアプリケーションを作成します。
参照の追加
今回はSystem.Windows.Forms.FolderBrowserDialogを利用するため、System.Windows.Forms.dllを参照に追加します。
System.Windows.FormsはWPFのコントロールと名前が衝突しやすいので、open System.Windows.Formsを利用しないことをお勧めします。
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の時と同じです。
<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 }
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 })
XAMLデザイナのインテリセンス
現時点において(Visual Studio 2015 Update 3)XAMLでインテリセンスがサポートされています。
例えばBinding記述時に、DesignInstanceに設定したクラスのプロパティが候補に出てきます。
しかし今回はDynamicObjectを利用しているため、このインテリセンスは利用できません。
TypeProvidersを利用すると解決できるかもしれません。
F#でWPF --- UI画像キャプチャ --- キャプチャ時VM変更
下記記事にてUI画像をキャプチャする方法を記述しました。
F#でWPF --- UI画像キャプチャ - 何でもプログラミング
今回は、キャプチャ時のみに表示を変更したい場合に対応します。
実現方法
キャプチャ実行時にViewModelが公開しているキャプチャ用のViewModelをDataContextに設定します。(保存後は元のDataContextに戻します。)
今回は関数にして、生成を遅延させています。(Viewのパラメータも渡せるので、関数の形が良いと思います。)
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に定義されています。
<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