WPF の OxyPlot グラフを Xamarin.Forms に移植してみた
WPF で OxyPlot を用いて作ったグラフを Xamarin.Forms に移植してみました.
Xamarin.Forms で OxyPlot を試してみた記事はこちらになります.
OxyPlot for Xamarin.Forms はプレリリースパッケージです.ご注意ください.
WPF の実装
こちらが WPF で作ったグラフです.
OxyPlot for WPF を用いてグラフを描画しています.
<Page x:Class="Mobiquitous2016App.Views.Pages.ECGsPage"
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:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:oxy="http://oxyplot.org/wpf"
xmlns:vm="clr-namespace:Mobiquitous2016App.ViewModels.PageViewModels"
d:DesignHeight="1000"
d:DesignWidth="1000"
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{StaticResource MaterialDesignFont}"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<oxy:PlotView Grid.Row="0"
Grid.Column="0"
Model="{Binding PlotModelConvertLoss}" />
<oxy:PlotView Grid.Row="1"
Grid.Column="0"
Model="{Binding PlotModelAirResistance}" />
<oxy:PlotView Grid.Row="0"
Grid.Column="1"
Model="{Binding PlotModelRollingResistance}" />
<oxy:PlotView Grid.Row="1"
Grid.Column="1"
Model="{Binding PlotModelRegeneLoss}" />
</Grid>
</Page>
こちらが View になります.グラフを描画する Control が OxyPlot の PlotView になります.四辺の PlotView それぞれに OxyPlot の PlotModel をバインドしています.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Diagnostics;
using Livet;
using Livet.Commands;
using Livet.Messaging;
using Livet.Messaging.IO;
using Livet.EventListeners;
using Livet.Messaging.Windows;
using Mobiquitous2016App.Models;
using Mobiquitous2016App.Models.GraphModels;
using Mobiquitous2016App.ViewModels.WindowViewModels;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
namespace Mobiquitous2016App.ViewModels.PageViewModels
{
// ReSharper disable once InconsistentNaming
public class ECGsPageViewModel : ViewModel
{
private readonly IList _graphData;
private readonly double _maximum;
#region PlotModelConvertLoss変更通知プロパティ
private PlotModel _PlotModelConvertLoss;
public PlotModel PlotModelConvertLoss
{
get
{ return _PlotModelConvertLoss; }
set
{
if (_PlotModelConvertLoss == value)
return;
_PlotModelConvertLoss = value;
RaisePropertyChanged();
}
}
#endregion
#region PlotModelAirResistance変更通知プロパティ
private PlotModel _PlotModelAirResistance;
public PlotModel PlotModelAirResistance
{
get
{ return _PlotModelAirResistance; }
set
{
if (_PlotModelAirResistance == value)
return;
_PlotModelAirResistance = value;
RaisePropertyChanged();
}
}
#endregion
#region PlotModelRollingResistance変更通知プロパティ
private PlotModel _PlotModelRollingResistance;
public PlotModel PlotModelRollingResistance
{
get
{ return _PlotModelRollingResistance; }
set
{
if (_PlotModelRollingResistance == value)
return;
_PlotModelRollingResistance = value;
RaisePropertyChanged();
}
}
#endregion
#region PlotModelRegeneLoss変更通知プロパティ
private PlotModel _PlotModelRegeneLoss;
public PlotModel PlotModelRegeneLoss
{
get
{ return _PlotModelRegeneLoss; }
set
{
if (_PlotModelRegeneLoss == value)
return;
_PlotModelRegeneLoss = value;
RaisePropertyChanged();
}
}
#endregion
public ECGsPageViewModel(GraphWindowViewModel parentViewModel)
{
_graphData = parentViewModel.GraphDataList;
_maximum = new double[]
{
parentViewModel.GraphDataList.Max(v => v.ConvertLoss),
parentViewModel.GraphDataList.Max(v => v.AirResistance),
parentViewModel.GraphDataList.Max(v => v.RollingResistance),
parentViewModel.GraphDataList.Max(v => v.RegeneLoss)
}.Max();
Initialize();
}
public void Initialize()
{
PlotModelConvertLoss = CreatePlotModel("ConvertLoss");
PlotModelAirResistance = CreatePlotModel("AirResistance");
PlotModelRollingResistance = CreatePlotModel("RollingResistance");
PlotModelRegeneLoss = CreatePlotModel("RegeneLoss");
}
private PlotModel CreatePlotModel(string propertyName)
{
string title = null;
switch (propertyName)
{
case "ConvertLoss":
title = "Convert loss";
break;
case "AirResistance":
title = "Air resistance";
break;
case "RollingResistance":
title = "Rolling resistance";
break;
case "RegeneLoss":
title = "Regene loss";
break;
}
var model = new PlotModel
{
Subtitle = title,
PlotMargins = new OxyThickness(double.NaN, double.NaN, 80, double.NaN)
};
var colorAxis = new LinearColorAxis
{
HighColor = OxyColors.Gray,
LowColor = OxyColors.Black,
Position = AxisPosition.Right,
MajorStep = 0.02,
Minimum = 0,
Maximum = _maximum,
Unit = "kWh",
AxisTitleDistance = 0
};
model.Axes.Add(colorAxis);
var xAxis = new LinearAxis
{
Title = "Transit time",
Unit = "s",
Position = AxisPosition.Bottom
};
model.Axes.Add(xAxis);
var yAxis = new LinearAxis
{
Title = "Lost energy",
Unit = "kWh"
};
model.Axes.Add(yAxis);
var scatterSeries = new ScatterSeries();
foreach (var datum in _graphData)
{
scatterSeries.Points.Add(new ScatterPoint(datum.TransitTime, datum.LostEnergy)
{
Value = (float)typeof(GraphDatum).GetProperty(propertyName).GetValue(datum)
});
}
model.Series.Add(scatterSeries);
return model;
}
}
}
WPF では MVVM アーキテクチャとして Livet を用いています.
scatterSeries.Points.Add(new ScatterPoint(datum.TransitTime, datum.LostEnergy)
{
Value = (float)typeof(GraphDatum).GetProperty(propertyName).GetValue(datum)
});
ScatterSeries に ScatterPoint を追加する部分では、メソッドの引数として与えた propertyName で値を取り出しています.ScatterPoint の Value プロパティにバインドができれば ViewModel がもっと簡素になるのですが、バインドできなかったためこのようにしています.
以上が WPF の実装になります.
Xamarin.Forms の実装
こちらが Xamarin.Forms で作ったグラフです.OxyPlot for Xamarin.Forms を用いてグラフを描画しています.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
xmlns:forms="clr-namespace:OxyPlot.Xamarin.Forms;assembly=OxyPlot.Xamarin.Forms"
prism:ViewModelLocator.AutowireViewModel="True"
x:Class="TOD2017MobileApp.Views.ECGsDemoPage">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<forms:PlotView Grid.Row="0"
Grid.Column="0"
Model="{Binding PlotModelConvertLoss.Value}" />
<forms:PlotView Grid.Row="1"
Grid.Column="0"
Model="{Binding PlotModelAirResistance.Value}" />
<forms:PlotView Grid.Row="0"
Grid.Column="1"
Model="{Binding PlotModelRollingResistance.Value}" />
<forms:PlotView Grid.Row="1"
Grid.Column="1"
Model="{Binding PlotModelRegeneLoss.Value}" />
</Grid>
</ContentPage>
こちらが View になります.WPF との違いは Page が ContentPage になっているところ、OxyPlot の名前空間、PlotModel のバインディングが ReactiveProperty になっているところです.WPF とほとんど変わらないのが見て取れると思います.
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
using Plugin.Geolocator.Abstractions;
using Prism.Navigation;
using Reactive.Bindings;
using TOD2017MobileApp.Models;
namespace TOD2017MobileApp.ViewModels
{
public class ECGsDemoPageViewModel : BindableBase
{
private readonly ECGModel _ecgModel;
private readonly double _maximum;
public ReactiveProperty PlotModelConvertLoss { get; set; }
public ReactiveProperty PlotModelAirResistance { get; set; }
public ReactiveProperty PlotModelRollingResistance { get; set; }
public ReactiveProperty PlotModelRegeneLoss { get; set; }
public ECGsDemoPageViewModel()
{
PlotModelConvertLoss = new ReactiveProperty();
PlotModelAirResistance = new ReactiveProperty();
PlotModelRollingResistance = new ReactiveProperty();
PlotModelRegeneLoss = new ReactiveProperty();
AtentionText = new ReactiveProperty();
_ecgModel = ECGModel.GetECGModel(new SemanticLink{ SemanticLinkId = 196 });
_maximum = new double[]
{
_ecgModel.GraphData.Max(v => v.ConvertLoss),
_ecgModel.GraphData.Max(v => v.AirResistance),
_ecgModel.GraphData.Max(v => v.RollingResistance),
_ecgModel.GraphData.Max(v => v.RegeneLoss)
}.Max();
PlotModelConvertLoss.Value = CreatePlotModel("ConvertLoss");
PlotModelAirResistance.Value = CreatePlotModel("AirResistance");
PlotModelRollingResistance.Value = CreatePlotModel("RollingResistance");
PlotModelRegeneLoss.Value = CreatePlotModel("RegeneLoss");
}
private PlotModel CreatePlotModel(string propertyName)
{
string title = null;
switch (propertyName)
{
case "ConvertLoss":
title = "Convert loss";
break;
case "AirResistance":
title = "Air resistance";
break;
case "RollingResistance":
title = "Rolling resistance";
break;
case "RegeneLoss":
title = "Regene loss";
break;
}
var model = new PlotModel
{
Subtitle = title,
PlotMargins = new OxyThickness(double.NaN, double.NaN, 80, double.NaN)
};
var colorAxis = new LinearColorAxis
{
HighColor = OxyColors.Gray,
LowColor = OxyColors.Black,
Position = AxisPosition.Right,
MajorStep = 0.02,
Minimum = 0,
Maximum = _maximum,
Unit = "kWh",
AxisTitleDistance = 0
};
model.Axes.Add(colorAxis);
var xAxis = new LinearAxis
{
Title = "Transit time",
Unit = "s",
Position = AxisPosition.Bottom
};
model.Axes.Add(xAxis);
var yAxis = new LinearAxis
{
Title = "Lost energy",
Unit = "kWh"
};
model.Axes.Add(yAxis);
var scatterSeries = new ScatterSeries();
foreach (var datum in _ecgModel.GraphData)
{
scatterSeries.Points.Add(new ScatterPoint(datum.TransitTime, datum.LostEnergy)
{
Value = (float)typeof(GraphDatum).GetRuntimeProperty(propertyName).GetValue(datum)
});
}
model.Series.Add(scatterSeries);
return model;
}
}
}
こちらが Xamarin.Forms の ViewModel です.MVVM アーキテクチャとして Prism.Forms を用いています.
scatterSeries.Points.Add(new ScatterPoint(datum.TransitTime, datum.LostEnergy)
{
Value = (float)typeof(GraphDatum).GetRuntimeProperty(propertyName).GetValue(datum)
});
ScatterSeries に ScatterPoint を追加する部分では、Type.GetProperty から Type.GetRuntimeProperty に変更しています.
以上が Xamarin.Forms の実装になります.
WPF と Xamarin.Forms の比較
WPF と Xamarin.Forms で View、ViewModel のコード変更量を視覚的に比較してみます.コード変更箇所がオレンジのラインです.
View の変更点としては OxyPlot の名前空間 と バインディングの値です.
WPF の Page と Xamarin.Forms の ContentPage で違いはありますが、テンプレートから作成すれば変更する必要がないので除外しています.
コード変更量としてはごくごく少ない量であることが見て取れます.
ViewModel の変更点としては、ReactiveProperty の使用と、Type.GetProperty から Type.GetRuntimeProperty への変更です.
こちらもごくごく少ないコード変更量であることが分かります.
私の実装の場合、WPF は Livet を使用、Xamarin.Forms は Prism.Forms + ReactiveProperty を使用するように、MVVM アーキテクチャの利用に違いがありましたが、WPF も Prism を用いる実装にすればさらに移植が楽になると思われます.
まとめ
OxyPlot のように、WPF と Xamarin.Forms で共通に利用できるライブラリがあれば、たとえグラフのような複雑な View であっても、WPF から Xamarin.Forms に簡単に移植が行えます.
XAML を共通化できること、バインディングにより ViewModel を共通化できることが非常に効率的です.
既存の WPF アプリケーションからモバイルアプリへの移植を考える場合、共通に使えるライブラリがあれば比較的簡単に移植ができる例の紹介でした.
以上です.
ディスカッション
コメント一覧
まだ、コメントがありません