OxyPlotの使い方 ViewのPlotを画像保存するビヘイビア2
WPFでChartグラフを表示するためのライブラリーに OxyPlot があります。View に Plot を貼りつけて XAML で作成した Chart を保存するビヘイビアについて書いてあります。ViewModel から自動で View の Plot を保存する為の仕組みも組み込んでいます。前回のものから EventAggregator を使わないものに変更しました。
OxyPlot で Chart を表示するには、PlotModel を ViewModel に作ってコードで全部作りこむ方法と、View に Plot を貼って、XAMLで作りこむ方法の2種類があります。Chart の画像保存を行いたい場合、ViewModel に PlotModel がある場合にはコードから自由に Chart を画像保存できますが、View 側に Plot を貼った時には、Viewmodel 側にはデータしかないので View 側で Chart の保存を行わなくてはなりません。 今回は、ビヘイビアを作成して、それを使用することで、右clickからのクリップボードへのコピー、ファイルへの保存、ViewModelからも画像の自動保存を可能としました。
OxyContextMenuBehavior の仕様
OxyContextMenuBehaviorを保存したい画像の Plot に添付します。
そうすることで、Plot 上で右クリックから Copy と Save ができるようになります。
OxyContextMenuBehavior のプロパティーには次のようなものがあります。
- FileName: ViewModel とバインドして保存ファイル名を指定。空欄の時にはダイアログが開きます。
- ImageHeight: 保存画像の高さ。0の時には表示サイズ。
- ImageWidth: 保存画像の幅。0の時には表示サイズ。
- SaveChart: ViewModel とバインドして+1する時に画像が保存されます。
- Scale: 表示倍率。
プロパティの FileNameに ViewModel に作成した FileName をバインドします。プロパティのSaveChartにViewModel のChartSaveをバインドすることで、ChartSaveの変化でChartを保存する事ができるようになります。
ビヘイビアコード
今回作成した OxyContextMenuBehavior は次のようなものです。
namespace ScatterPointApp.Behaviors { [TypeConstraint(typeof(Plot))] public class OxyContextMenuBehavior : Behavior<Plot> { /// <summary> /// 要素にアタッチされた時にイベントハンドラを登録 /// </summary> protected override void OnAttached() { base.OnAttached(); menuItemCopy.Header = "Copy"; menuItemSave.Header = "Save"; menuListBox.Items.Add(menuItemCopy); menuListBox.Items.Add(menuItemSave); menuItemCopy.Click += OnCopyClick; menuItemSave.Click += OnSaveClick; this.AssociatedObject.ContextMenu = menuListBox; } private void OnSaveClick(object sender, RoutedEventArgs e) { double width = this.AssociatedObject.ActualWidth; double height = this.AssociatedObject.ActualHeight; if (ImageWidth > 0) width = ImageWidth; if (ImageHeight > 0) height = ImageHeight; if (Scale > 0) { width = width * Scale; height = height * Scale; } var dlg = new SaveFileDialog { Filter = ".png files|*.png|.pdf files|*.pdf", DefaultExt = ".png" }; if (dlg.ShowDialog().Value) FileName = dlg.FileName; var ext = Path.GetExtension(FileName).ToLower(); switch (ext) { case ".png": PngExporter.Export(this.AssociatedObject.ActualModel, FileName, (int)width, (int)height, OxyColors.White); break; case ".pdf": using (var s = File.Create(FileName)) { PdfExporter.Export(this.AssociatedObject.ActualModel, s, width, height); } break; default: break; } } /// <summary> /// 画像保存(右クリック用) /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void OnCopyClick(object sender, RoutedEventArgs e) { double width = this.AssociatedObject.ActualWidth; double height = this.AssociatedObject.ActualHeight; if (ImageWidth > 0) width = ImageWidth; if (ImageHeight > 0) height = ImageHeight; if (Scale > 0) { width = width * Scale; height = height * Scale; } var pngExporter = new PngExporter { Width = (int)width, Height = (int)height, Background = OxyColors.White }; Application.Current.Dispatcher.Invoke((Action)(() => { var bitmap = pngExporter.ExportToBitmap(this.AssociatedObject.ActualModel); Clipboard.SetImage(bitmap); })); } /// <summary> /// 要素にデタッチされた時にイベントハンドラを解除 /// </summary> protected override void OnDetaching() { base.OnDetaching(); menuItemSave.Click -= OnSaveClick; menuItemCopy.Click -= OnCopyClick; this.AssociatedObject.ContextMenu = null; } ContextMenu menuListBox = new ContextMenu(); MenuItem menuItemCopy = new MenuItem(); MenuItem menuItemSave = new MenuItem(); #region ******************************* ImageWidth public int ImageWidth { get { return (int)this.GetValue(ImageWidthProperty); } set { this.SetValue(ImageWidthProperty, value); } } public static readonly DependencyProperty ImageWidthProperty = DependencyProperty.Register("ImageWidth", typeof(int), typeof(OxyContextMenuBehavior), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ImageWidthChangeFunc, ImageWidthCoerceFunc)); static void ImageWidthChangeFunc(DependencyObject target, DependencyPropertyChangedEventArgs e) { var of = (int)e.OldValue; var nf = (int)e.NewValue; var obj = (OxyContextMenuBehavior)target; } static object ImageWidthCoerceFunc(DependencyObject target, object baseValue) { var obj = (OxyContextMenuBehavior)target; var val = (int)baseValue; if (val < 0) val = 0; return val; } #endregion #region ******************************* ImageHeight public int ImageHeight { get { return (int)this.GetValue(ImageHeightProperty); } set { this.SetValue(ImageHeightProperty, value); } } public static readonly DependencyProperty ImageHeightProperty = DependencyProperty.Register("ImageHeight", typeof(int), typeof(OxyContextMenuBehavior), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ImageHeightChangeFunc, ImageHeightCoerceFunc)); static void ImageHeightChangeFunc(DependencyObject target, DependencyPropertyChangedEventArgs e) { var of = (int)e.OldValue; var nf = (int)e.NewValue; var obj = (OxyContextMenuBehavior)target; } static object ImageHeightCoerceFunc(DependencyObject target, object baseValue) { var obj = (OxyContextMenuBehavior)target; var val = (int)baseValue; if (val < 0) val = 0; return val; } #endregion #region ******************************* Scale public double Scale { get { return (double)this.GetValue(ScaleProperty); } set { this.SetValue(ScaleProperty, value); } } public static readonly DependencyProperty ScaleProperty = DependencyProperty.Register("Scale", typeof(double), typeof(OxyContextMenuBehavior), new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ScaleChangeFunc, ScaleCoerceFunc)); static void ScaleChangeFunc(DependencyObject target, DependencyPropertyChangedEventArgs e) { var of = (double)e.OldValue; var nf = (double)e.NewValue; var obj = (OxyContextMenuBehavior)target; } static object ScaleCoerceFunc(DependencyObject target, object baseValue) { var obj = (OxyContextMenuBehavior)target; var val = (double)baseValue; if (val < 0.5) val = 0.5; if (val > 5.0) val = 5.0; return val; } #endregion #region ******************************* FileName public string FileName { get { return (string)this.GetValue(FileNameProperty); } set { this.SetValue(FileNameProperty, value); } } public static readonly DependencyProperty FileNameProperty = DependencyProperty.Register("FileName", typeof(string), typeof(OxyContextMenuBehavior), new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); #endregion #region ******************************* SaveChart public int SaveChart { get { return (int)this.GetValue(SaveChartProperty); } set { this.SetValue(SaveChartProperty, value); } } public static readonly DependencyProperty SaveChartProperty = DependencyProperty.Register("SaveChart", typeof(int), typeof(OxyContextMenuBehavior), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, SaveChartChangeFunc, SaveChartCoerceFunc)); static void SaveChartChangeFunc(DependencyObject target, DependencyPropertyChangedEventArgs e) { var of = (int)e.OldValue; var nf = (int)e.NewValue; var obj = (OxyContextMenuBehavior)target; obj.Save(); } static object SaveChartCoerceFunc(DependencyObject target, object baseValue) { var obj = (OxyContextMenuBehavior)target; var val = (int)baseValue; return val; } #endregion /// <summary> /// 画像保存 /// </summary> private void Save() { string filename = FileName; if (filename == "") { var dlg = new SaveFileDialog { Filter = ".png files|*.png", DefaultExt = ".png" }; if (dlg.ShowDialog().Value) filename = dlg.FileName; } Plot plot = this.AssociatedObject as Plot; double width = plot.ActualWidth; double height = plot.ActualHeight; if (ImageWidth > 0) width = ImageWidth; if (ImageHeight > 0) height = ImageHeight; if (Scale > 0) { width = width * Scale; height = height * Scale; } if (filename != "") { var ext = Path.GetExtension(filename).ToLower(); if (ext == ".png") { PngExporter.Export(plot.ActualModel, filename, (int)width, (int)height, OxyColors.White); } } } } }
ViewModel に次のようなコードを追加します。FileNameを空欄にするとダイアログが開くようになります。
private string fileName = ""; public string FileName { get { return fileName; } set { SetProperty(ref fileName, value); } } private int chartSave = 0; public int ChartSave { get { return chartSave; } set { SetProperty(ref chartSave, value); } } private DelegateCommand commandSave; public DelegateCommand CommandSave => commandSave ?? (commandSave = new DelegateCommand(ExecuteCommandSave)); async void ExecuteCommandSave() { await Task.Run(() => Save()); } void Save() { Application.Current.Dispatcher.Invoke( new Action(() => { FileName = "Test_.png"; ChartSave++; })); }
Chartが保存されるのは、ChartSaveの値が変化するタイミングなので、ChartSaveは毎回+1するようにしています。
注意点として、UIスレッド以外から使用すると動作が不安定となるので、その時には上記の様に Dispatcher.InvokeでUIスレッドから実行するようにします。
まとめ
Plot に添付して ContextMenu を表示する Behaivior です。
ファイル保存の Save とクリップボードへのコピーの Copy が右クリックで表示されるようになります。
ファイル保存用の FileName プロパティーと、画像サイズを指定できるように ImageHeight と ImageWidth を設定してあります。
Scale プロパティーは表示画像サイズをベースにして指定倍率で画像を扱う時に使用できます。
作成したソースコードの場所
ソースコードは次の場所に置いてあります。