← 全部文章 WinForm

用 LiveCharts 画实时时序图

2023 年 7 月 18 日 · 2 分钟读完

目录
  1. 效果
  2. 基本实现
  3. 数据点和坐标轴
  4. 添加曲线
  5. 实时更新
  6. 改源码:悬停时线越来越粗
  7. 延伸

项目里要用图表显示信号的实时变化(E84 交接信号,每路只有高低两种状态),找了 LiveCharts 这个好看的开源图表库,并按需求改了点源码。我改的是旧版(0.9.x)的源码,不确定作者后来有没有修。

效果

一个随时间滚动、实时更新的时序图:

基本实现

引用 LiveCharts.dll、LiveCharts.WinForms.dll、LiveCharts.Wpf.dll(NuGet 装 LiveCharts.WinForms 即可),往窗体里拖一个 CartesianChart。

数据点和坐标轴

每个点记录时间和值:

public class MeasureModel
{
    public DateTime DateTime { get; set; } // X 轴
    public double Value { get; set; }      // Y 轴
}

告诉 LiveCharts 怎么把 MeasureModel 映射到坐标,再定义两个轴的显示格式:

var mapper = Mappers.Xy<MeasureModel>()
    .X(model => model.DateTime.Ticks) // 用 Ticks 作为 X
    .Y(model => model.Value);
Charting.For<MeasureModel>(mapper);

chart_E84.AxisY.Add(new Axis
{
    // Y 轴的数值其实是枚举,显示成枚举名
    LabelFormatter = value => Enum.IsDefined(typeof(E84Signal), (int)value)
        ? ((E84Signal)(int)value).ToString()
        : ""
});

chart_E84.AxisX.Add(new Axis
{
    DisableAnimations = true,
    LabelFormatter = value => new DateTime((long)value).ToString("mm:ss"),
});

// 设置 X 轴最小值,否则从 0 开始,范围太大,波形看起来不动
chart_E84.AxisX[0].MinValue = DateTime.Now.Ticks;

添加曲线

信号只有高低两种状态,用步进线 StepLineSeries。图里有很多条,这里只写一条,其他照抄:

public StepLineSeries A_CS_0;
public ChartValues<MeasureModel> A_CS_0_Values { get; } = new ChartValues<MeasureModel>();

private void FrmE84Chart_Load(object sender, EventArgs e)
{
    chart_E84.Series.Add(A_CS_0 = new StepLineSeries
    {
        Values = A_CS_0_Values,
        BaseStrokeThickness = 3, // 我在源码里加的属性,原版用 StrokeThickness
        PointGeometry = null     // 不显示数据点的空心圆
    });
}

实时更新

开一个后台线程,定时往数据里追加点:

var thread = new Thread(UpdateCharts) { IsBackground = true };
thread.Start();

private void UpdateCharts()
{
    while (true)
    {
        int v = (int)ReadSignal(); // 读当前信号
        var values = A_CS_0_Values;

        // 值没变时,删掉最后一个点再加一个新的,相当于把线「拉长」到现在
        // 只改最后一个点的时间,LiveCharts 认为数据没变,不会重绘
        if (values.Count != 0 && values.Last().Value == v)
            values.RemoveAt(values.Count - 1);

        values.Add(new MeasureModel { DateTime = DateTime.Now, Value = v });

        Thread.Sleep(interval);
    }
}

原文这段代码在 else 分支里多加了一次点,值变化时会连续加两个相同的点。这里整理成了先删后加的统一逻辑。

改源码:悬停时线越来越粗

鼠标悬停在线上时,LiveCharts 会把线加粗提示,源码的做法是 StrokeThickness + 1,移开时再 - 1。刷新太快时,加和减对不上,线越悬停越粗,移开也回不去。

在 Series.cs 里加一个基准粗细,悬停时基于它计算,而不是在当前值上加减:

static double baseStrokeThickness = 1;
public double BaseStrokeThickness
{
    get => baseStrokeThickness;
    set => baseStrokeThickness = value;
}

注意这里写成了 static,所有曲线共用同一个基准值。各条线粗细不同时,去掉 static 更合适。

不想要气泡提示和悬停加粗的话,也可以直接设置 chart.DataTooltip = null; 和 chart.Hoverable = false;,不用改源码。

延伸

  • 一定要限制点数:只加不删,几个小时后几万个点,图表会越来越卡。超过窗口范围的旧点要删掉,同时让 X 轴的 MinValue、MaxValue 跟着时间往前移:
while (values.Count > 0 && values[0].DateTime < DateTime.Now.AddSeconds(-60))
    values.RemoveAt(0);
chart_E84.AxisX[0].MinValue = DateTime.Now.AddSeconds(-60).Ticks;
chart_E84.AxisX[0].MaxValue = DateTime.Now.Ticks;
  • 跨线程:LiveCharts 0.9 的 ChartValues 在后台线程里修改,内部会自己调度到界面线程,但同时还要改坐标轴等界面属性时,最好统一 BeginInvoke 到界面线程里做。
  • 新版本:LiveCharts 的作者后来重写了 LiveCharts2,基于 SkiaSharp,性能好得多,支持 WinForm、WPF、Avalonia、MAUI。
  • 高频数据换 ScottPlot:采样率在几百 Hz 以上、要显示几十万个点时,LiveCharts 这类带动画的库会吃力。ScottPlot 专门为大量数据设计,DataStreamer 适合示波器式的滚动显示。

# WinForm # LiveCharts # 图表

原文首发于 blog.csdn.net