samble头像
关注
从零搭建灌装监控系统(十七):LiveCharts 实时温度曲线封面图

从零搭建灌装监控系统(十七):LiveCharts 实时温度曲线

LiveCharts 实时温度曲线

这是「从零搭建灌装监控系统」系列第17篇。数字只能告诉你此刻的温度,曲线才能告诉你温度在往哪走。这篇用 LiveCharts 画实时温度,重点不是“怎么画出来”,而是怎么在几百毫秒一次的刷新下不卡、不崩、不无限膨胀。

温度曲线滚动窗口


曲线跑到第二天就卡住了

首页加温度曲线的时候,我的实现非常朴素:每次收到数据就往 ChartValues 里 Add 一个点。

刚开始特别顺。跑了一上午,曲线像心电图一样漂亮。第二天早上同事说软件卡了——切页面要等两秒,拖动窗口一顿一顿。

原因很直白:数据点只增不减。 200ms 一次采样,一小时就是 18000 个点,一天 43 万个点。LiveCharts 每个点都要参与布局和重绘,几万个点之后,每次刷新都在做无用功——因为屏幕上根本显示不下那么多点。

还有一个更隐蔽的问题:有时候刚启动就崩,异常信息指向图表,但代码看起来完全正常。

这两个问题的解法,就是这篇的全部内容。


为什么选 LiveCharts

WPF 上画实时曲线有几个选择,我选 LiveCharts 0.9.7 的原因:

  • 纯 WPF 原生控件,不依赖浏览器或 OpenGL 上下文
  • SeriesCollection + ChartValues 的数据模型跟 MVVM 合得来
  • 支持虚线、点样式、图例这些工业界面常用的细节
  • 包体积小,部署到现场不需要额外运行时

代价是它的老版本(0.9.x)对线程比较敏感,这点后面专门讲。


两条曲线:实际值 vs 设定值

温度只看一条线是不够的。操作员真正关心的是实际值和设定值的偏差,所以画两条:

TempLiveCharts = new SeriesCollection
{
    new LineSeries
    {
        Title = "实际温度",
        Values = new ChartValues<double>(),
        Stroke = Brushes.Cyan,
        StrokeThickness = 2,
        PointGeometry = null,
    },
    new LineSeries
    {
        Title = "设定温度",
        Values = new ChartValues<double>(),
        Stroke = Brushes.Orange,
        StrokeThickness = 2,
        PointGeometry = null,
        StrokeDashArray = new DoubleCollection { 4, 2 },
    }
};

三个刻意的选择:

实际值实线、设定值虚线。 StrokeDashArray = {4, 2} 是 4 像素实线加 2 像素空。两条线颜色不同还不够,色觉差异或者屏幕反光时颜色容易混,线型不同才能一眼分开。而且设定值本来就该是“参考线”的语义,虚线正好。

PointGeometry = null。 默认每个数据点会画一个小圆点。几百个点就是几百个图形对象,纯属浪费——曲线本身已经把趋势表达清楚了。关掉之后渲染量明显下降。

两条线用同一个 Y 轴。 实际值和设定值量纲相同,共用纵轴才能直观看出偏差。如果量纲不同(比如温度和压力),才需要拆双轴。


滚动窗口:只保留最近 40 点

解决无限膨胀的核心,是让集合长度恒定:

private void UpdateChart(DeviceStates state)
{
    if (TempLiveCharts == null || TempLiveCharts.Count == 0)
        return;

    TempLiveCharts[0].Values.Add(state.CurrentTemp);
    if (TempLiveCharts[0].Values.Count > 40)
        TempLiveCharts[0].Values.RemoveAt(0);

    if (TempLiveCharts.Count > 1)
    {
        TempLiveCharts[1].Values.Add(state.SettingTemp);
        if (TempLiveCharts[1].Values.Count > 40)
            TempLiveCharts[1].Values.RemoveAt(0);
    }
}

加一个到尾部,超长就从头部删一个。集合长度永远是 40,内存和渲染开销都恒定。

为什么是 40? 按 200ms 采样算,40 个点大约覆盖 8 秒。这个窗口要跟工艺节奏匹配:

  • 窗口太短,曲线抖动剧烈,看不出趋势
  • 窗口太长,温度变化的细节被压缩,报警前的爬升看不出来

8 秒对灌装加热这种慢过程偏短,实际项目可以放宽到 100-200 点。文章里用 40 是为了讲清机制,具体数值要按工艺周期调。原则是:窗口时长应该能覆盖一次完整的状态变化。

流程如下:

是

否

轮询收到数据

Dispatcher 切回 UI 线程

实际值集合 Add

点数超过 40?

移除头部一个点

保持

设定值同样处理

图表自动重绘


必须在 UI 线程创建

这是那个“刚启动就崩”的根因。

SeriesCollection、LineSeries、ChartValues 都是 WPF 的 DependencyObject 体系里的对象,有线程亲和性。如果在非 UI 线程创建,之后 UI 线程去渲染它就会抛异常——而且异常点往往在渲染阶段,堆栈指向 LiveCharts 内部,看起来跟你的代码无关。

所以创建过程要用 Dispatcher 包起来:

public DashBoardViewModel()
{
    PlcService.DataReceived += OnDataReceived;

    Application.Current.Dispatcher.BeginInvoke(() =>
    {
        TempLiveCharts = new SeriesCollection
        {
            // ... 两条 LineSeries
        };
    });
}

用 BeginInvoke 而不是 Invoke:构造过程不需要等图表建完,异步投递到 UI 队列就行,不会阻塞当前线程。

同样的道理,后续每次改 Values 也要在 UI 线程。DataReceived 是从轮询线程发出的,所以更新前必须切线程:

private void OnDataReceived(object? sender, DeviceStates state)
{
    Application.Current.Dispatcher.Invoke(() =>
    {
        CurrentTemp = state.CurrentTemp;
        SettingTemp = state.SettingTemp;
        UpdateChart(state);
    });
}

Invoke(同步)在这里是合适的,因为更新图表和更新卡片数值要在一个批次里完成,避免界面出现“数字已经是新值、曲线还是旧点”的错位。


不要每帧重建集合

我见过一种写法:每次收到数据就 TempLiveCharts = new SeriesCollection {...},把整个集合换掉。

看起来“数据总是最新的”,实际上每次都在重建图形对象、重新绑定、重新布局。数据一快,GC 压力和渲染压力一起上来。

正确做法是建一次,之后只改 Values。ChartValues 实现了集合变更通知,增删元素会自动触发增量重绘,这才是 LiveCharts 的设计意图。


断线时把曲线也清掉

第 16 篇讲过断线要清零,曲线同样适用:

private void OnConnectionChanged(object? sender, bool connected)
{
    if (!connected)
    {
        Application.Current.Dispatcher.Invoke(() =>
        {
            CurrentTemp = 0;
            SettingTemp = 0;
            TempLiveCharts[0].Values.Clear();
            TempLiveCharts[1].Values.Clear();
        });
    }
}

保留断线前的曲线会让操作员误判——曲线停在 25.5℃,看起来像“温度稳定”,其实是没数据了。清空之后图表是空的,配合报警提示,语义才正确。


踩坑记录

后台线程直接改 ChartValues

崩在渲染阶段,堆栈在 LiveCharts 内部,看不出跟自己的代码有关。所有图表对象操作都要切 UI 线程。

数据点只增不减

跑几小时就卡,跑一天基本不能看。用滚动窗口把集合长度固定住。

每次刷新重建 SeriesCollection

数据越快越卡,而且图表会闪。建一次,之后只改 Values。

实际值和设定值不做区分

两条同色实线叠在一起,操作员分不清哪条是哪条。颜色 + 线型双区分。


本篇小结

问题做法
集合无限膨胀滚动窗口,超长移除头部
渲染开销大PointGeometry = null,只画线不画点
两条线分不清实际值实线、设定值虚线
后台线程崩溃创建和更新都在 UI 线程
频繁重建集合建一次,之后只改 Values
断线误判清空曲线,不保留旧数据

图表让趋势可见了,但界面上还有一堆重复的视觉元素:每个页面都要一个红绿圆点、一个状态标签、一个液位指示。复制粘贴到最后,改一次配色要翻十几个文件。下一篇把这些做成可复用的自定义控件。


下期预告

第18篇:自定义控件:三色灯与状态指示

下一篇把三色灯和状态指示器做成独立的 UserControl,用依赖属性和值转换器驱动,顺便讲一个 WPF 里非常容易踩的坑:本地值为什么会让触发器失效。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/samble/article/details/166645295

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--