封装支持Binding
正好项目有个需求,需要使用图表库。在了解一番后,选择使用ScottPlot库,据说性能很不错,就是不支持Binding。想着在该基础上进行封装一下,对此进行简单记录一下。希望有了解这块的小伙伴,指导一下!只是使用,如果需要相关的库案例及API,可以看一下官网教程:ScottPlot
原理:
-
创建一个自定义控件,继承Control
-
前台Xaml文件中自定义样式,在样式中包一个WPFScottPlot控件,并且给该控件一个命名
-
在后台VS文件中通过OnApplyTemplate()方法,找到前台Xaml中的WPFScottplot控件,给该控件进行一系列的需求配置
-
定义相关依赖属性,如果依赖属性是用来配置折线图的话,可以在OnRender()方法中进行初始化。并且,在该依赖属性中使用new FrameworkPropertyMetadata()参数进行初始化,该方法中设置属性FrameworkPropertyMetadataOptions.AffectsRender,这样每次Xaml中改变该依赖属性的时候,都会调用后台cs文件中的OnRender()方法。从而立马改变初始化的设置样式。
-
-
ViewModel中的double属性值,在Xaml中进行Binding
-
后台cs文件中,创建一个double类型的依赖属性,用于接收ViewModel中的属性值
-
在该依赖属性中,设置回调函数,ViewModel中属性值发生改变的时候,触发此回调函数
-
回调函数中接收此次改变的值,将此值存储在一个集合中
-
开辟一个DispatcherTimer线程,进行数据的渲染(设置好渲染的间隔时间)
-
(如果ViewModel中的数据变化周期时间,符合自己的预期时间,可以直接在依赖属性中的回调方法中进行渲染)
-
相关代码:
using Octopus.Shared.Extension; using ScottPlot; using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.IO; using Octopus.Shared.Components.Fields; using System.Windows.Threading; using System.Collections.Generic; using System.Linq; using System.Diagnostics; namespace Octopus.UI.Controls { /// <summary> /// 基于ScottPlot图表库封装自己的图表库(支持MVVM-binding) /// </summary> public class OctPlot : Control { // xaml中的图表对象 private WpfPlot _plot; // 存放实时时间/Y轴值的集合 private OctRealTimePlotList<double> _realTimeList; // 存放实时时间/Y轴值的集合 private OctRealTimePlotList<double> _realTimeList2; // 将数据渲染至UI画面 private DispatcherTimer _renderTimer; // 折线1的颜色 private System.Drawing.Color _lineColor1; // 折线2的颜色 private System.Drawing.Color _lineColor2; /// <summary> /// ctor /// </summary> static OctPlot() { DefaultStyleKeyProperty.OverrideMetadata(typeof(OctPlot), new FrameworkPropertyMetadata(typeof(OctPlot))); } public CornerRadius BorderHostCornerRadius { get { return (CornerRadius)GetValue(BorderHostCornerRadiusProperty); } set { SetValue(BorderHostCornerRadiusProperty, value); } } public string PlotTitle { get { return (string)GetValue(PlotTitleProperty); } set { SetValue(PlotTitleProperty, value); } } public string XAxisLogo { get { return (string)GetValue(XAxisLogoProperty); } set { SetValue(XAxisLogoProperty, value); } } public string YAxisLogo { get { return (string)GetValue(YAxisLogoProperty); } set { SetValue(YAxisLogoProperty, value); } } public Brush AxisBackground { get { return (Brush)GetValue(AxisBackgroundProperty); } set { SetValue(AxisBackgroundProperty, value); } } public Brush DataDailBackground { get { return (Brush)GetValue(DataDailBackgroundProperty); } set { SetValue(DataDailBackgroundProperty, value); } } public Brush GridLineBackground { get { return (Brush)GetValue(GridLineBackgroundProperty); } set { SetValue(GridLineBackgroundProperty, value); } } public Brush AxisTicksBackground { get { return (Brush)GetValue(AxisTicksBackgroundProperty); } set { SetValue(AxisTicksBackgroundProperty, value); } } public Brush AxisTitleBackground { get { return (Brush)GetValue(AxisTitleBackgroundProperty); } set { SetValue(AxisTitleBackgroundProperty, value); } } public Brush PlotTitleBackground { get { return (Brush)GetValue(PlotTitleBackgroundProperty); } set { SetValue(PlotTitleBackgroundProperty, value); } } public double YValue { get { return (double)GetValue(YValueProperty); } set { SetValue(YValueProperty, value); } } public double YAxisMaxLimit { get { return (double)GetValue(YAxisMaxLimitProperty); } set { SetValue(YAxisMaxLimitProperty, value); } } public double YAxisMinLimit { get { return (double)GetValue(YAxisMinLimitProperty); } set { SetValue(YAxisMinLimitProperty, value); } } public double XAxisMaxLimit { get { return (double)GetValue(XAxisMaxLimitProperty); } set { SetValue(XAxisMaxLimitProperty, value); } } public double XAxisMinLimit { get { return (double)GetValue(XAxisMinLimitProperty); } set { SetValue(XAxisMinLimitProperty, value); } } public bool IsSetYAxisLimit { get { return (bool)GetValue(IsSetYAxisLimitProperty); } set { SetValue(IsSetYAxisLimitProperty, value); } } public bool IsSetXAxisLimit { get { return (bool)GetValue(IsSetXAxisLimitProperty); } set { SetValue(IsSetXAxisLimitProperty, value); } } public int DataLineCount { get { return (int)GetValue(DataLineCountProperty); } set { SetValue(DataLineCountProperty, value); } } public int DataPointCount { get { return (int)GetValue(DataPointCountProperty); } set { SetValue(DataPointCountProperty, value); } } public double YValueLine2 { get { return (double)GetValue(YValueLine2Property); } set { SetValue(YValueLine2Property, value); } } public Brush YLineColor1 { get { return (Brush)GetValue(YLineColor1Property); } set { SetValue(YLineColor1Property, value); } } public Brush YLineColor2 { get { return (Brush)GetValue(YLineColor2Property); } set { SetValue(YLineColor2Property, value); } } // 折线2的颜色 public static readonly DependencyProperty YLineColor2Property = DependencyProperty.Register("YLineColor2", typeof(Brush), typeof(OctPlot), new PropertyMetadata(Brushes.LightBlue)); // 折线1的颜色 public static readonly DependencyProperty YLineColor1Property = DependencyProperty.Register("YLineColor1", typeof(Brush), typeof(OctPlot), new PropertyMetadata(Brushes.Black)); // 第二条折线的Y值 public static readonly DependencyProperty YValueLine2Property = DependencyProperty.Register("YValueLine2", typeof(double), typeof(OctPlot), new PropertyMetadata(0d, OnYValueLine2Changed)); // 图表中数据点数上限,超过此点数就保存成图片 public static readonly DependencyProperty DataPointCountProperty = DependencyProperty.Register("DataPointCount", typeof(int), typeof(OctPlot), new PropertyMetadata(3600)); // 渲染的数据线的数量 public static readonly DependencyProperty DataLineCountProperty = DependencyProperty.Register("DataLineCount", typeof(int), typeof(OctPlot), new FrameworkPropertyMetadata(1 , FrameworkPropertyMetadataOptions.AffectsRender)); // X轴是否设置限制并且不能随意拖动(限制的值,为坐标轴的最大最小值) public static readonly DependencyProperty IsSetXAxisLimitProperty = DependencyProperty.Register("IsSetXAxisLimit", typeof(bool), typeof(OctPlot), new FrameworkPropertyMetadata(default(bool) , FrameworkPropertyMetadataOptions.AffectsRender)); // Y轴是否设置限制并且不能随意拖动(限制的值,为坐标轴的最大最小值) public static readonly DependencyProperty IsSetYAxisLimitProperty = DependencyProperty.Register("IsSetYAxisLimit", typeof(bool), typeof(OctPlot), new FrameworkPropertyMetadata(default(bool) , FrameworkPropertyMetadataOptions.AffectsRender)); // 设置X轴显示的最小值 public static readonly DependencyProperty XAxisMinLimitProperty = DependencyProperty.Register("XAxisMinLimit", typeof(double), typeof(OctPlot), new FrameworkPropertyMetadata(0d , FrameworkPropertyMetadataOptions.AffectsRender)); // 设置X轴显示的最大值 public static readonly DependencyProperty XAxisMaxLimitProperty = DependencyProperty.Register("XAxisMaxLimit", typeof(double), typeof(OctPlot), new FrameworkPropertyMetadata(100d , FrameworkPropertyMetadataOptions.AffectsRender)); // 设置Y轴显示的最小值 public static readonly DependencyProperty YAxisMinLimitProperty = DependencyProperty.Register("YAxisMinLimit", typeof(double), typeof(OctPlot), new FrameworkPropertyMetadata(0d , FrameworkPropertyMetadataOptions.AffectsRender)); // 设置Y轴显示的最大值 public static readonly DependencyProperty YAxisMaxLimitProperty = DependencyProperty.Register("YAxisMaxLimit", typeof(double), typeof(OctPlot), new FrameworkPropertyMetadata(100d , FrameworkPropertyMetadataOptions.AffectsRender)); // 图表Y轴的数据(单个数值) public static readonly DependencyProperty YValueProperty = DependencyProperty.Register("YValue", typeof(double), typeof(OctPlot), new PropertyMetadata(default(double), OnYValueChanged)); // 图表的标题前景色 public static readonly DependencyProperty PlotTitleBackgroundProperty = DependencyProperty.Register("PlotTitleBackground", typeof(Brush), typeof(OctPlot), new FrameworkPropertyMetadata(Brushes.Transparent , FrameworkPropertyMetadataOptions.AffectsRender)); // 坐标轴标题的前景色 public static readonly DependencyProperty AxisTitleBackgroundProperty = DependencyProperty.Register("AxisTitleBackground", typeof(Brush), typeof(OctPlot), new FrameworkPropertyMetadata(Brushes.Transparent , FrameworkPropertyMetadataOptions.AffectsRender)); // 坐标轴刻度的前景色 public static readonly DependencyProperty AxisTicksBackgroundProperty = DependencyProperty.Register("AxisTicksBackground", typeof(Brush), typeof(OctPlot), new FrameworkPropertyMetadata(Brushes.Transparent , FrameworkPropertyMetadataOptions.AffectsRender)); // 表盘的网格线的前景色 public static readonly DependencyProperty GridLineBackgroundProperty = DependencyProperty.Register("GridLineBackground", typeof(Brush), typeof(OctPlot), new FrameworkPropertyMetadata(Brushes.Transparent , FrameworkPropertyMetadataOptions.AffectsRender)); // 折线表盘的背景色 public static readonly DependencyProperty DataDailBackgroundProperty = DependencyProperty.Register("DataDailBackground", typeof(Brush), typeof(OctPlot), new FrameworkPropertyMetadata(Brushes.LightBlue , FrameworkPropertyMetadataOptions.AffectsRender)); // 坐标轴前景色 public static readonly DependencyProperty AxisBackgroundProperty = DependencyProperty.Register("AxisBackground", typeof(Brush), typeof(OctPlot), new FrameworkPropertyMetadata(Brushes.Aqua , FrameworkPropertyMetadataOptions.AffectsRender)); // Plot的纵坐标轴标识 public static readonly DependencyProperty YAxisLogoProperty = DependencyProperty.Register("YAxisLogo", typeof(string), typeof(OctPlot), new FrameworkPropertyMetadata(string.Empty , FrameworkPropertyMetadataOptions.AffectsRender)); // Plot的横坐标轴标识 public static readonly DependencyProperty XAxisLogoProperty = DependencyProperty.Register("XAxisLogo", typeof(string), typeof(OctPlot), new FrameworkPropertyMetadata(string.Empty , FrameworkPropertyMetadataOptions.AffectsRender)); // Plot的标题 public static readonly DependencyProperty PlotTitleProperty = DependencyProperty.Register("PlotTitle", typeof(string), typeof(OctPlot), new FrameworkPropertyMetadata(string.Empty , FrameworkPropertyMetadataOptions.AffectsRender)); // 外层Border的圆角 public static readonly DependencyProperty BorderHostCornerRadiusProperty = DependencyProperty.Register("BorderHostCornerRadius", typeof(CornerRadius), typeof(OctPlot), new PropertyMetadata(new CornerRadius(0d))); /// <summary> /// 第二条折线的值 /// </summary> /// <param name="d"></param> /// <param name="e"></param> /// <exception cref="NotImplementedException"></exception> private static void OnYValueLine2Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is OctPlot plot) { _tempList2.Add(Math.Round(plot.YValueLine2, 3)); if (_tempList2.Count >= 10) { // 抽稀算法,得到平均值 var res = GetThinningData(_tempList2); // 向集合中添加Y轴的数据 plot._realTimeList2.GetYs().Add(res); // 向集合中添加X轴的数据 plot._realTimeList2.GetXs().Add(DateTime.Now.ToOADate()); _tempList2.Clear(); } } } // 存放实际读到的Y值1 private static List<double> _tempList1; // 存放实际读到的Y值2 private static List<double> _tempList2; /// <summary> /// 当Y轴的数据变化的回调 /// </summary> /// <param name="d"></param> /// <param name="e"></param> private static void OnYValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is OctPlot plot) { Stopwatch stopwatch = Stopwatch.StartNew(); _tempList1.Add(Math.Round(plot.YValue, 3)); if (_tempList1.Count >= 9) { // 抽稀算法,得到平均值 var res = GetThinningData(_tempList1); // 向集合中添加Y轴的数据 plot._realTimeList.GetYs().Add(res); // 向集合中添加X轴的数据 plot._realTimeList.GetXs().Add(DateTime.Now.ToOADate()); _tempList1.Clear(); } stopwatch.Stop(); Console.WriteLine($"时间:--{stopwatch.Elapsed}"); } } /// <summary> /// 当子类使用模板的时候进行一系列的逻辑(相关数据的初始化) /// </summary> public override void OnApplyTemplate() { // 获取Xaml中的图表对象 _plot = this.GetTemplateChild("RealTimePlot") as WpfPlot; _plot.Plot.Width = 600; _plot.Plot.Height = 600; // 实时数据集合(没有使用官方的数组类型,使用集合的方法进行实现) _realTimeList = new OctRealTimePlotList<double>(); _realTimeList2 = new OctRealTimePlotList<double>(); _tempList1 = new List<double>(); _tempList2 = new List<double>(); _lineColor1 = MediaBrushToDrawingColor(YLineColor1); ValidateColorData(_lineColor1); _lineColor2 = MediaBrushToDrawingColor(YLineColor2); ValidateColorData(_lineColor2); _renderTimer = new DispatcherTimer(); _renderTimer.Interval = TimeSpan.FromSeconds(1); _renderTimer.Tick -= RenderTimer_Tick; _renderTimer.Tick += RenderTimer_Tick; _renderTimer.Start(); base.OnApplyTemplate(); } /// <summary> /// 抽稀算法 /// </summary> /// <param name="datas"></param> /// <returns></returns> private static double GetThinningData(List<double> datas) { return datas.Sum() / datas.Count; } /// <summary> /// 将数据渲染至UI画面 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> /// <exception cref="NotImplementedException"></exception> private void RenderTimer_Tick(object sender, EventArgs e) { // 至少有一个数据 if (_realTimeList.Count > 0 && _realTimeList2.Count > 0) { // 折线盘清盘 _plot.Plot.Clear(); // 添加折线1的数据 _plot.Plot.AddScatter(_realTimeList.GetXs().ToArray() , _realTimeList.GetYs().ToArray() , label: "小车电流" , color: _lineColor1); // 添加折线2的数据 _plot.Plot.AddScatter(_realTimeList2.GetXs().ToArray() , _realTimeList2.GetYs().ToArray() , label: "喷头电流" , color: _lineColor2); // 折线标识说明 _plot.Plot.Legend(); _plot.Refresh(); // 图表中得数据大于DataPointCount值,将此数据保存成图片,保存在路径中 if (_realTimeList.Count > DataPointCount) { // 图表得宽度 int plotWidth = (int)Math.Ceiling(_plot.Plot.Width); // 图表得高度 int plotHeight = (int)Math.Ceiling(_plot.Plot.Height); var bmp = new System.Drawing.Bitmap(plotWidth, plotHeight); _plot.Plot.Render(bmp); try { string path = Fields._logDataDirectory + "\\" + DateTime.Now.ToString("D") + "\\" + "DataImages" + "\\" + DateTime.Now.ToString("D") + "\\"; string imageName = +DateTime.Now.Minute + "h.png"; if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } bmp.Save(path + imageName); } catch (Exception) { throw; } // Path = "Fields._logDataDirectory + "\\" + DateTime.Now.ToString("D") + "\\" + DataImage" //var str = plot._plot.Plot.SaveFig(Fields._logDataDirectory + "\\" + DateTime.Now.ToString("D") + "\\" + "DataImages" // , width: plotWidth // , height: plotHeight // , lowQuality: false // , scale: 1); // 在折线图中数据点的数量达到设定值后,将值清空,绘制新值 _realTimeList.GetXs().Clear(); _realTimeList.GetYs().Clear(); _realTimeList2.GetXs().Clear(); _realTimeList2.GetYs().Clear(); } } } // OnApplyTemplate()方法先执行,然后执行OnRender()方法 /// <summary> /// UI渲染 /// </summary> /// <param name="drawingContext"></param> protected override void OnRender(System.Windows.Media.DrawingContext drawingContext) { if (_plot != null) { _plot.Plot.Title(PlotTitle); _plot.Plot.XLabel(XAxisLogo); _plot.Plot.YLabel(YAxisLogo); _plot.Plot.XAxis.DateTimeFormat(true); // 判断X轴限制值是否相等 ValidateAxisLimit(XAxisMinLimit, XAxisMaxLimit); if (XAxisMinLimit > XAxisMaxLimit) { _plot.Plot.SetAxisLimitsX(XAxisMaxLimit, XAxisMinLimit); SetXAxisLimit(_plot, XAxisMaxLimit, XAxisMinLimit); } else { _plot.Plot.SetAxisLimitsX(XAxisMinLimit, XAxisMaxLimit); SetXAxisLimit(_plot, XAxisMinLimit, XAxisMaxLimit); } // 判断Y轴限制值是否相等 ValidateAxisLimit(YAxisMinLimit, YAxisMaxLimit); if (YAxisMinLimit > YAxisMaxLimit) { _plot.Plot.SetAxisLimitsY(YAxisMaxLimit, YAxisMinLimit); SetYAxisLimit(_plot, YAxisMaxLimit, YAxisMinLimit); } else { _plot.Plot.SetAxisLimitsY(YAxisMinLimit, YAxisMaxLimit); SetYAxisLimit(_plot, YAxisMinLimit, YAxisMaxLimit); } #region 设置坐标轴部分/折线盘背景颜色 var axisBackground = MediaBrushToDrawingColor(AxisBackground); ValidateColorData(axisBackground); var dataDailBackground = MediaBrushToDrawingColor(DataDailBackground); ValidateColorData(dataDailBackground); var gridLineBackground = MediaBrushToDrawingColor(GridLineBackground); ValidateColorData(gridLineBackground); var axisTicksBackground = MediaBrushToDrawingColor(AxisTicksBackground); ValidateColorData(axisTicksBackground); var axisTitleBackground = MediaBrushToDrawingColor(AxisTitleBackground); ValidateColorData(axisTicksBackground); var plotTitleBackground = MediaBrushToDrawingColor(PlotTitleBackground); ValidateColorData(plotTitleBackground); _plot.Plot.Style(figureBackground: axisBackground , dataBackground: dataDailBackground , grid: gridLineBackground , tick: axisTicksBackground , axisLabel: axisTitleBackground , titleLabel: plotTitleBackground , dataBackgroundImage: null , figureBackgroundImage: null); #endregion _plot.Refresh(); } base.OnRender(drawingContext); } /// <summary> /// 将System.Windows.Media.Brush类型转换为System.Drawing.Color类型 /// </summary> /// <param name="brush"></param> /// <returns></returns> private System.Drawing.Color MediaBrushToDrawingColor(Brush brush) { if (brush is SolidColorBrush solidBrush) { System.Windows.Media.Color mc = solidBrush.Color; return System.Drawing.Color.FromArgb(mc.A, mc.R, mc.G, mc.B); } else return System.Drawing.Color.Empty; } /// <summary> /// 转换结果校正 /// </summary> /// <param name="color"></param> /// <exception cref="InvalidCastException"></exception> private void ValidateColorData(System.Drawing.Color color) { if (color == System.Drawing.Color.Empty) throw new InvalidCastException("颜色转换失败"); } private void ValidateAxisLimit(double minLimit, double maxLimit) { if (minLimit == maxLimit) { throw new ArgumentOutOfRangeException("限制范围错误"); } } /// <summary> /// 是否设置Y轴防滚动限制 /// </summary> /// <param name="plot"></param> /// <param name="min"></param> /// <param name="max"></param> private void SetYAxisLimit(WpfPlot plot, double min, double max) { if (IsSetYAxisLimit) { plot.Plot.YAxis.SetBoundary(min, max); } } /// <summary> /// 是否设置X轴防滚动限制 /// </summary> /// <param name="plot"></param> /// <param name="min"></param> /// <param name="max"></param> private void SetXAxisLimit(WpfPlot plot, double min, double max) { if (IsSetXAxisLimit) { plot.Plot.XAxis.SetBoundary(min, max); } } } }
用于盛放渲染数据的集合:
/// <summary> /// 应用于实时折现数据表的集合 /// </summary> public class OctRealTimePlotList<T> : ScatterPlotList<T> { /// <summary> /// 返回X轴点的集合 /// </summary> /// <returns></returns> public List<T> GetXs() { // 将父类中的集合Xs返回 return Xs; } /// <summary> /// 返回Y轴点的集合 /// </summary> /// <returns></returns> public List<T> GetYs() { // 将父类中的集合Ys返回 return Ys; } }
问题点:
-
软件关闭的时候,存储渲染数据的集合有没有释放
-
多条渲染折线,横坐标(时间轴),保证统一,待解决
-