using Microsoft.Win32; using ScottPlot; using ScottPlot.Plottables; using ScottPlot.WPF; using SWRIS.Core; using SWRIS.Models; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Threading; namespace SWRIS.Controls { /// /// 动态折线图用户控件(基于 ScottPlot.WPF 的高性能版本) /// public partial class RealTimeLineChart : UserControl { #region 依赖属性定义 public static readonly DependencyProperty SensorCountProperty = DependencyProperty.Register( nameof(SensorCount), typeof(int), typeof(RealTimeLineChart), new PropertyMetadata(4, OnSensorCountChanged)); public static readonly DependencyProperty RopeLengthProperty = DependencyProperty.Register( nameof(RopeLength), typeof(double), typeof(RealTimeLineChart), new PropertyMetadata(100.0, OnAxisXChanged)); public static readonly DependencyProperty SamplingStepProperty = DependencyProperty.Register( nameof(SamplingStep), typeof(double), typeof(RealTimeLineChart), new PropertyMetadata(0.1275)); #endregion #region CLR 包装属性 public int SensorCount { get => (int)GetValue(SensorCountProperty); set => SetValue(SensorCountProperty, value); } public double RopeLength { get => (double)GetValue(RopeLengthProperty); set => SetValue(RopeLengthProperty, value); } public double SamplingStep { get => (double)GetValue(SamplingStepProperty); set => SetValue(SamplingStepProperty, value); } #endregion public double? _lastPosition = null; private ConcurrentQueue _sensorDataQueues; private const double MINDAMAGE = 40; // 最小损伤值 private const double MAXDAMAGE = 120; // 最大损伤值 private int _displayPoints = 1000000; // 显示点数 private DispatcherTimer UpdatePlotTimer; private DataLogger _dataLogger; public RealTimeLineChart() { InitializeComponent(); InitializePlot(); } private void OnLoaded(object sender, RoutedEventArgs e) { InitializePlot(); } #region 初始化方法 private void InitializePlot() { WpfPlot.Menu?.Clear(); WpfPlot.Menu.Add("保存图片", pt => { OpenSaveImageDialog(pt); }); WpfPlot.Menu.Add("清除曲线", pt => { ClearPoints(); }); WpfPlot.Menu.Add("重置视图", pt => { SetLimits(); }); var plot = WpfPlot.Plot; plot.Clear(); plot.DataBackground.Color = Colors.Transparent; plot.FigureBackground.Color = Colors.Transparent; plot.Grid.MajorLineColor = new Color(59, 59, 123).WithOpacity(0.6); plot.Grid.XAxis.TickLabelStyle.ForeColor = new Color(151, 166, 212); plot.Grid.YAxis.TickLabelStyle.ForeColor = new Color(151, 166, 212); plot.Grid.XAxis.FrameLineStyle.Color = new Color(53, 53, 112); plot.Grid.XAxis.FrameLineStyle.Width = 2; plot.Grid.YAxis.FrameLineStyle.Color = new Color(53, 53, 112); plot.Grid.YAxis.FrameLineStyle.Width = 2; plot.Grid.XAxis.MajorTickStyle.Color = new Color(53, 53, 112); plot.Grid.XAxis.MajorTickStyle.Width = 1; plot.Grid.YAxis.MajorTickStyle.Color = new Color(53, 53, 112); plot.Grid.YAxis.MajorTickStyle.Width = 1; plot.Grid.YAxis.MinorTickStyle.Color = new Color(53, 53, 112); plot.Grid.XAxis.MinorTickStyle.Color = new Color(53, 53, 112); plot.Axes.Hairline(false); _sensorDataQueues = new ConcurrentQueue(); _dataLogger = plot.Add.DataLogger(); _dataLogger.LineWidth = 2; _dataLogger.Color = new Color("#f47c7c"); _dataLogger.ManageAxisLimits = false; WpfPlot.Plot.Axes.SetLimits(0, RopeLength * 1.2, MINDAMAGE, MAXDAMAGE); WpfPlot.Refresh(); UpdatePlotTimer?.Stop(); UpdatePlotTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) }; UpdatePlotTimer.Tick += (s, d) => { if (_dataLogger.HasNewData) { SetLimits(); } }; UpdatePlotTimer.Start(); } #endregion #region 数据操作方法 private void SetLimits() { if (_lastPosition == null || _lastPosition.Value < RopeLength) { WpfPlot.Plot.Axes.SetLimits(0, RopeLength * 1.2, MINDAMAGE, MAXDAMAGE); } else { WpfPlot.Plot.Axes.SetLimits(0, _lastPosition.Value + (RopeLength * 0.12), MINDAMAGE, MAXDAMAGE); } WpfPlot.Refresh(); } public void ClearPoints() { lock (_sensorDataQueues) { while (_sensorDataQueues.TryDequeue(out _)) ; } _dataLogger.Clear(); WpfPlot.Refresh(); } public void AddDataPoints(Coordinates[] chartData) { _dataLogger.Add(chartData); _lastPosition = chartData.LastOrDefault().X; WpfPlot.Refresh(); } public Coordinates[] GetChartSource() { return _dataLogger.Data.Coordinates.ToArray(); } public void AddDataPoints((double Position, LiveStreamDataModel ListStream) dataPoints, int[] inUseSensors) { if (_lastPosition == null) { _lastPosition = dataPoints.Position; return; } if (dataPoints.Position < _lastPosition) { lock (_sensorDataQueues) { while (_sensorDataQueues.TryDequeue(out _)) ; _lastPosition = dataPoints.Position; } _dataLogger.Data.Clear(); } else { for (int i = 0; i < dataPoints.ListStream.SampleCount; i++) { ushort totalValue = 0; ushort totalCount = 0; ushort[] values = dataPoints.ListStream.Data[i]; foreach (var j in inUseSensors) { totalCount++; totalValue += values[j - 1]; } _sensorDataQueues.Enqueue((ushort)(totalValue / totalCount)); } } if (dataPoints.Position > _lastPosition) { UpdateLoop(_lastPosition.Value, dataPoints.Position); _lastPosition = dataPoints.Position; } } public void OpenSaveImageDialog(Plot plot) { SaveFileDialog saveFileDialog = new SaveFileDialog { Filter = "PNG Files (*.png)|*.png|JPEG Files (*.jpg, *.jpeg)|*.jpg;*.jpeg|BMP Files (*.bmp)|*.bmp|WebP Files (*.webp)|*.webp|SVG Files (*.svg)|*.svg|All files (*.*)|*.*" }; bool? flag = saveFileDialog.ShowDialog(); if (flag.HasValue && flag == true && !string.IsNullOrEmpty(saveFileDialog.FileName)) { ImageFormat format; try { format = ImageFormats.FromFilename(saveFileDialog.FileName); } catch (ArgumentException) { MessageBox.Show("Unsupported image file format", "ERROR", MessageBoxButton.OK, MessageBoxImage.Hand); return; } try { PixelSize size = plot.RenderManager.LastRender.FigureRect.Size; plot.Save(saveFileDialog.FileName, (int)size.Width, (int)size.Height, format); } catch (Exception) { MessageBox.Show("Image save failed", "ERROR", MessageBoxButton.OK, MessageBoxImage.Hand); } } } private void UpdateLoop(double lastPosition, double position) { try { // 在后台线程准备数据 List<(double x, ushort y)> dataToAdd = new List<(double x, ushort y)>(); var step = (position - lastPosition) / Math.Max(1, _sensorDataQueues.Count); double currentX = lastPosition; while (_sensorDataQueues.TryDequeue(out ushort data)) { currentX += step; // 确保 x 值严格递增 if (dataToAdd.Count > 0 && currentX <= dataToAdd[dataToAdd.Count - 1].x) { currentX = dataToAdd[dataToAdd.Count - 1].x + Math.Abs(step) * 0.001; } dataToAdd.Add((currentX, data)); } // 在UI线程批量添加 Dispatcher.Invoke(() => { lock (_dataLogger) { // 获取最后一个点的 x 值 double lastX = _dataLogger.Data.Coordinates.Count > 0 ? _dataLogger.Data.Coordinates[_dataLogger.Data.Coordinates.Count - 1].X : double.MinValue; foreach (var point in dataToAdd) { double safeX = point.x; if (safeX <= lastX) { safeX = lastX + Math.Abs(position - lastPosition) * 0.0001; } _dataLogger.Add(safeX, point.y); lastX = safeX; } } // 清理操作 if (_dataLogger.Data.Coordinates.Count > _displayPoints) { int removeCount = (int)(_displayPoints * 0.1d); removeCount = Math.Min(removeCount, _dataLogger.Data.Coordinates.Count - 1); if (removeCount > 0) { _dataLogger.Data.Coordinates.RemoveRange(0, removeCount); } } }); } catch (Exception ex) { LogHelper.Error($"绘制曲线时发生错误:{ex.Message}", ex); } } #endregion #region 静态回调 private static void OnSensorCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is RealTimeLineChart control) control.OnSensorCountChanged((int)e.OldValue, (int)e.NewValue); } private static void OnAxisXChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is RealTimeLineChart control) control.UpdateAxisX(); } #endregion #region 坐标轴更新逻辑 private void UpdateAxisX() { if (WpfPlot?.Plot == null) return; WpfPlot.Plot.Axes.SetLimitsX(0, RopeLength * 1.2); WpfPlot.Refresh(); } #endregion #region 传感器数量变化处理 private void OnSensorCountChanged(int oldCount, int newCount) { Dispatcher.Invoke(() => { InitializePlot(); }); } #endregion } }