UltraDynamo (第三部分) - 实时趋势
探讨在 UltraDynamo 中实现实时趋势。
目录
注意:本文是多部分文章。下载内容可在第一部分中找到。
引言
该应用程序利用实时趋势来实时提供传感器值的历史数据趋势。实时趋势本质上是一个图表控件,它具有有限数量的数据点,会不断地在右侧添加新点,在左侧删除旧点,然后重新绘制。
我通过创建一个UserControl
来实现这一点,该控件将一个图表控件停靠在用户控件中以填充其可用区域。然后,用户控件被添加到普通 Windows 窗体中需要的地方。我为所有不同的传感器使用相同的用户控件,只根据正在趋势的数据修改内部工作。
在本节中,我们将研究 UltraDynamo
应用程序中如何创建实时趋势。
什么是实时趋势?
简而言之,实时趋势是不断变化的动态数据的图表。它显示了当前绘制的值以及一系列之前的历史数据值。
下图是加速计传感器的实时趋势的屏幕截图。它显示了 X、Y 和 Z 轴值在一个水平滚动的趋势图上绘制。最新的数据在右边,最旧的数据在左边。
使用了什么控件?
主窗口是一个标准的 Windows 窗体。窗体上放置了我创建的用户控件。我采用了用户控件的方法,以便能够有效地在应用程序的任何地方使用该用户控件,无论是作为独立的图表窗口,还是作为窗体上带有许多其他控件的小型控件。
图表本身是使用 Microsoft MSChart 控件创建的。它被放置在 usercontrol
上,并停靠以占据用户控件的整个区域。
它通常如何工作?
设计的基础非常简单。用户控件有自己的计时器,用于以固定频率轮询传感器以获取数据。每读取一个数据点,它就被添加到图表的数据点中。如果我们超过了要绘制的最大数据点数,那么我们就删除旧点以腾出新点的空间。
我最初的实时趋势包含每个传感器参数 600 个数据点。这包括 1 分钟的数据,采样率为每秒 10 个数据点,即每 100 毫秒,我获取一个传感器值。
代码详解
现在让我们详细看看代码
第一部分是典型的引用和 namespace
定义。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using UltraDynamo.Sensors;
namespace UltraDynamo.Controls
{
下面的枚举器提供了可以传递给实时趋势初始化以显示数据的各种值
public enum RealtimeTrendSource
{
Compass,
Accelerometer,
Gyrometer,
Inclinometer,
AmbientLight,
Speedometer
}
下一部分建立派生自基类 UserControl
的对象。
public partial class UITrendHorizontal : UserControl
{
现在我们定义用于内部计时器的 private
变量,并设置我们想要作为限制的数据点的数量。
private Timer updateTimer;
private int maxPoints = 600; //1 Minutes (10 points per second @100ms/point)
一个属性用于存储枚举选择的源传感器和实际源数据传感器引用
public RealtimeTrendSource sourceSensor { get; private set; }
private object sourceData;
用户控件构造函数基类会为计时器的 tick
事件添加处理程序,并将 tick
频率设置为 100 毫秒。
public UITrendHorizontal()
{
InitializeComponent();
//Setup the timer
updateTimer = new Timer();
updateTimer.Interval = 100; //100ms update interval
updateTimer.Tick += updateTimer_Tick;
}
usercontrol
构造函数用于传入我们要趋势的传感器(来自枚举列表)。这也会调用基类构造函数来建立 update timer
。
public UITrendHorizontal(RealtimeTrendSource source)
: this()
{
setSourceSensor(source);
}
当控件加载时,我们为父窗体的 closing
事件附加一个处理程序。稍后会详细介绍。
private void UITrendHorizontal_Load(object sender, EventArgs e)
{
//Detect parent form closing to terminate the timer
this.ParentForm.FormClosing += UITrendHorizontal_FormClosing;
this.Refresh();
}
当父窗体关闭时,下面的代码将执行。这将停止并处理底层计时器。如果没有这个,计时器可能会在父窗体被处理后触发,导致 null
引用异常。
void UITrendHorizontal_FormClosing(object sender, FormClosingEventArgs e)
{
updateTimer.Stop();
updateTimer.Dispose();
}
计时器滴答用于检查正在趋势哪个传感器数据,然后读取传感器值并更新图表上的相关点。
void updateTimer_Tick(object sender, EventArgs e)
{
switch (this.sourceSensor)
{
case RealtimeTrendSource.AmbientLight:
chartData.Series["Light"].Points.Add(((MyLightSensor)sourceData).LightReading);
break;
case RealtimeTrendSource.Compass:
chartData.Series["Compass"].Points.Add(((MyCompass)sourceData).Heading);
break;
case RealtimeTrendSource.Accelerometer:
chartData.Series["AccelerometerX"].Points.Add(((MyAccelerometer)sourceData).X);
chartData.Series["AccelerometerY"].Points.Add(((MyAccelerometer)sourceData).Y);
chartData.Series["AccelerometerZ"].Points.Add(((MyAccelerometer)sourceData).Z);
break;
case RealtimeTrendSource.Gyrometer:
chartData.Series["GyrometerX"].Points.Add(((MyGyrometer)sourceData).X);
chartData.Series["GyrometerY"].Points.Add(((MyGyrometer)sourceData).Y);
chartData.Series["GyrometerZ"].Points.Add(((MyGyrometer)sourceData).Z);
break;
case RealtimeTrendSource.Inclinometer:
chartData.Series["Pitch"].Points.Add(((MyInclinometer)sourceData).Pitch);
chartData.Series["Roll"].Points.Add(((MyInclinometer)sourceData).Roll);
chartData.Series["Yaw"].Points.Add(((MyInclinometer)sourceData).Yaw);
break;
case RealtimeTrendSource.Speedometer:
double speed = (((MyGeolocation)sourceData).Position.Coordinate.Speed ?? 0);
switch (Properties.Settings.Default.SpeedometerUnits)
{
case 0: // m/s
//do nothing already in m/s
break;
case 1: //kph
speed = (speed * 3600) / 1000;
break;
case 2: //mph
speed = speed * 2.23693629; //Google says:
//1 metre / second = 2.23693629 mph
break;
}
chartData.Series["Speedometer"].Points.Add(speed);
break;
}
在添加新点后,我们通过简单地删除旧点直到达到最大点限制来确保我们没有超过显示点数的限制。
//Remove excess points
foreach (System.Windows.Forms.DataVisualization.Charting.Series
series in chartData.Series)
{
while (series.Points.Count > maxPoints)
{
series.Points.RemoveAt(0);
}
}
}
此方法简单地根据初始化时传入的枚举值设置正确的源传感器。
public void setSourceSensor(RealtimeTrendSource source)
{
this.sourceSensor = source;
switch (this.sourceSensor)
{
case RealtimeTrendSource.Accelerometer:
//sourceData = new MyAccelerometer();
sourceData = MySensorManager.Instance.Accelerometer;
break;
case RealtimeTrendSource.AmbientLight:
//sourceData = new MyLightSensor();
sourceData = MySensorManager.Instance.LightSensor;
break;
case RealtimeTrendSource.Compass:
//sourceData = new MyCompass();
sourceData = MySensorManager.Instance.Compass;
break;
case RealtimeTrendSource.Gyrometer:
//sourceData = new MyGyrometer();
sourceData = MySensorManager.Instance.Gyrometer;
break;
case RealtimeTrendSource.Inclinometer:
//sourceData = new MyInclinometer();
sourceData = MySensorManager.Instance.Inclinometer;
break;
case RealtimeTrendSource.Speedometer:
//sourceData = new MyGeolocation();
sourceData = MySensorManager.Instance.GeoLocation;
break;
}
updateTimer.Stop();
BuildChartView();
}
下一个方法根据趋势的传感器设置各种图表的名称、图例等。
每个 switch case 代表一个传感器。当进入 case 时,我们会清除图表中任何现有的系列数据。添加一个新的 Series 并提供关联的图例文本。我们定义图表的最小值和最大值,并设置图表标记的刻度间隔。
当图表首次创建时,我们添加具有零值的最大数量的数据点,以便我们有一个已经正确调整大小等的图表。如果我们不这样做,图表会在捕获数据时不断增长,而这种行为在开发过程中看起来不合适,因此预加载零数据。我们还第一次启动计时器。
private void BuildChartView()
{
//Configure Series
switch (this.sourceSensor)
{
case RealtimeTrendSource.AmbientLight:
chartData.Series.Clear();
chartData.Series.Add("Light");
chartData.Series["Light"].LegendText = "Lux";
chartData.Series["Light"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyLightSensor)sourceData).Minimum;
chartData.ChartAreas[0].AxisY.Maximum = ((MyLightSensor)sourceData).Maximum;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 1000;
//Preload maxPoints with 0 data
for (int x = 0; x < maxPoints; x++)
{
chartData.Series["Light"].Points.Add(0D);
}
break;
case RealtimeTrendSource.Compass:
chartData.Series.Clear();
chartData.Series.Add("Compass");
chartData.Series["Compass"].LegendText = "Heading";
chartData.Series["Compass"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyCompass)sourceData).MinimumHeading;
chartData.ChartAreas[0].AxisY.Maximum = ((MyCompass)sourceData).MaximumHeading;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 90;
//Preload maxPoints with 0 data
for (int x = 0; x < maxPoints; x++)
{
chartData.Series["Compass"].Points.Add(0D);
}
break;
case RealtimeTrendSource.Accelerometer:
chartData.Series.Clear();
chartData.Series.Add("AccelerometerX");
chartData.Series["AccelerometerX"].LegendText = "X";
chartData.Series["AccelerometerX"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyAccelerometer)sourceData).MinimumX;
chartData.ChartAreas[0].AxisY.Maximum = ((MyAccelerometer)sourceData).MaximumX;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 1;
chartData.Series.Add("AccelerometerY");
chartData.Series["AccelerometerY"].LegendText = "Y";
chartData.Series["AccelerometerY"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyAccelerometer)sourceData).MinimumY;
chartData.ChartAreas[0].AxisY.Maximum = ((MyAccelerometer)sourceData).MaximumY;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 1;
chartData.Series.Add("AccelerometerZ");
chartData.Series["AccelerometerZ"].LegendText = "Z";
chartData.Series["AccelerometerZ"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyAccelerometer)sourceData).MinimumZ;
chartData.ChartAreas[0].AxisY.Maximum = ((MyAccelerometer)sourceData).MaximumZ;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 1;
//Preload maxPoints with 0 data
for (int x = 0; x < maxPoints; x++)
{
chartData.Series["AccelerometerX"].Points.Add(0D);
chartData.Series["AccelerometerY"].Points.Add(0D);
chartData.Series["AccelerometerZ"].Points.Add(0D);
}
break;
case RealtimeTrendSource.Gyrometer:
chartData.Series.Clear();
chartData.Series.Add("GyrometerX");
chartData.Series["GyrometerX"].LegendText = "X";
chartData.Series["GyrometerX"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyGyrometer)sourceData).MinimumX;
chartData.ChartAreas[0].AxisY.Maximum = ((MyGyrometer)sourceData).MaximumX;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 15;
chartData.Series.Add("GyrometerY");
chartData.Series["GyrometerY"].LegendText = "Y";
chartData.Series["GyrometerY"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyGyrometer)sourceData).MinimumY;
chartData.ChartAreas[0].AxisY.Maximum = ((MyGyrometer)sourceData).MaximumY;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 15;
chartData.Series.Add("GyrometerZ");
chartData.Series["GyrometerZ"].LegendText = "Z";
chartData.Series["GyrometerZ"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyGyrometer)sourceData).MinimumZ;
chartData.ChartAreas[0].AxisY.Maximum = ((MyGyrometer)sourceData).MaximumZ;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 15;
//Preload maxPoints with 0 data
for (int x = 0; x < maxPoints; x++)
{
chartData.Series["GyrometerX"].Points.Add(0D);
chartData.Series["GyrometerY"].Points.Add(0D);
chartData.Series["GyrometerZ"].Points.Add(0D);
}
break;
case RealtimeTrendSource.Inclinometer:
chartData.Series.Clear();
chartData.Series.Add("Pitch");
chartData.Series["Pitch"].LegendText = "Pitch";
chartData.Series["Pitch"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyInclinometer)sourceData).MinimumPitch;
chartData.ChartAreas[0].AxisY.Maximum = ((MyInclinometer)sourceData).MaximumPitch;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 15;
chartData.Series.Add("Roll");
chartData.Series["Roll"].LegendText = "Roll";
chartData.Series["Roll"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyInclinometer)sourceData).MinimumRoll;
chartData.ChartAreas[0].AxisY.Maximum = ((MyInclinometer)sourceData).MaximumRoll;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 15;
chartData.Series.Add("Yaw");
chartData.Series["Yaw"].LegendText = "Yaw";
chartData.Series["Yaw"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyInclinometer)sourceData).MinimumYaw;
chartData.ChartAreas[0].AxisY.Maximum = ((MyInclinometer)sourceData).MaximumYaw;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 15;
//Preload maxPoints with 0 data
for (int x = 0; x < maxPoints; x++)
{
chartData.Series["Pitch"].Points.Add(0D);
chartData.Series["Roll"].Points.Add(0D);
chartData.Series["Yaw"].Points.Add(0D);
}
break;
case RealtimeTrendSource.Speedometer:
chartData.Series.Clear();
chartData.Series.Add("Speedometer");
switch (Properties.Settings.Default.SpeedometerUnits)
{
case 0: //m/s
chartData.Series["Speedometer"].LegendText = "M/S";
break;
case 1: //kmh
chartData.Series["Speedometer"].LegendText = "KPH";
break;
case 2: //mph
chartData.Series["Speedometer"].LegendText = "MPH";
break;
}
chartData.Series["Speedometer"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = 0;
chartData.ChartAreas[0].AxisY.Maximum =
(double)Properties.Settings.Default.SpeedometerVMax;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 10;
//Preload maxPoints with 0 data
for (int x = 0; x < maxPoints; x++)
{
chartData.Series["Speedometer"].Points.Add(0D);
}
break;
}
//restart the timer
updateTimer.Start();
}
}
}
使用这种方法,根据可用的源传感器可以相对轻松地添加或修改功能,或者扩展趋势,例如增加数据点的数量或采样频率。
进入下一部分
在下一部分,我们将看看应用程序中使用的一些图形和字体例程。