using OfficeOpenXml; using Sunny.UI; using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; using System.Windows.Threading; namespace PLCDataMonitor { public partial class CurvePage : UserControl { private Size _lastCanvasSize = Size.Empty; private System.Collections.Concurrent.ConcurrentQueue> _sharedDataQueue; private DispatcherTimer _updateTimer; private int _currentTimeRange = 60; public Action UpdateFrictionData; // 绘图参数 private double _canvasWidth = 0; private double _canvasHeight = 0; private double _axisMargin = 80; // 坐标轴边距 private double _axisStartX = 0; private double _axisEndX = 0; private double _axisStartY = 0; private double _axisEndY = 0; private double _axisWidth = 0; private double _axisHeight = 0; // 缓存上一次的绘图范围,避免重复绘制 private double _lastTotalSeconds = 0; private double _lastMinValue = 0; private double _lastMaxValue = 0; public CurvePage(System.Collections.Concurrent.ConcurrentQueue> sharedDataQueue) { InitializeComponent(); _sharedDataQueue = sharedDataQueue ?? new System.Collections.Concurrent.ConcurrentQueue>(); UpdateFrictionData = AddFrictionData; } private void UserControl_Loaded(object sender, RoutedEventArgs e) { // 初始计算画布大小 UpdateCanvasSize(); _updateTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) }; _updateTimer.Tick += UpdateTimer_Tick; _updateTimer.Start(); // 监听尺寸变化 this.SizeChanged += UserControl_SizeChanged; } private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e) { UpdateCanvasSize(); } private void UpdateCanvasSize() { // 等待布局完成 Dispatcher.BeginInvoke(new Action(() => { double newWidth = CurveCanvas.ActualWidth; double newHeight = CurveCanvas.ActualHeight; // 只有当画布大小实际变化时才更新 if (Math.Abs(newWidth - _lastCanvasSize.Width) > 1 || Math.Abs(newHeight - _lastCanvasSize.Height) > 1) { _canvasWidth = newWidth; _canvasHeight = newHeight; if (_canvasWidth > 0 && _canvasHeight > 0) { // 关键修改:根据画布大小动态计算坐标轴边距 // 边距应该与画布大小成比例,而不是固定值 _axisMargin = Math.Min(_canvasWidth, _canvasHeight) * 0.08; // 8%的边距 _axisMargin = Math.Max(_axisMargin, 40); // 最小40像素 _axisMargin = Math.Min(_axisMargin, 100); // 最大100像素 _axisStartX = _axisMargin; _axisEndX = _canvasWidth - _axisMargin; _axisStartY = _axisMargin; _axisEndY = _canvasHeight - _axisMargin; _axisWidth = _axisEndX - _axisStartX; _axisHeight = _axisEndY - _axisStartY; _lastCanvasSize = new Size(_canvasWidth, _canvasHeight); // 标记需要重绘坐标轴 _lastTotalSeconds = 0; _lastMinValue = 0; _lastMaxValue = 0; } } }), DispatcherPriority.Render); } private void UserControl_Unloaded(object sender, RoutedEventArgs e) { if (_updateTimer != null) { _updateTimer.Stop(); _updateTimer.Tick -= UpdateTimer_Tick; _updateTimer = null; } } public void AddFrictionData(double f1, double f2) { // 数据已通过共享队列添加 } private void UpdateTimer_Tick(object sender, EventArgs e) { // 关键修改:每次都要检查画布大小 if (_canvasWidth <= 0 || _canvasHeight <= 0 || Math.Abs(CurveCanvas.ActualWidth - _canvasWidth) > 1 || Math.Abs(CurveCanvas.ActualHeight - _canvasHeight) > 1) { UpdateCanvasSize(); } if (_canvasWidth <= 0 || _canvasHeight <= 0) return; if (_sharedDataQueue == null || _sharedDataQueue.Count == 0) { Friction1Polyline?.Points.Clear(); Friction2Polyline?.Points.Clear(); ClearAllDynamicElements(); return; } // 获取数据 var allData = _sharedDataQueue.ToList(); if (allData.Count < 2) return; // 计算时间范围 DateTime firstTime = allData.First().Item1; DateTime lastTime = allData.Last().Item1; double totalSeconds = Math.Max(1, (lastTime - firstTime).TotalSeconds); // 清理超出时间范围的数据 if (totalSeconds > _currentTimeRange) { var maxDataCount = (int)(_currentTimeRange * 10); // 使用 TryDequeue 安全地移除多余项 while (_sharedDataQueue.Count > maxDataCount) { _sharedDataQueue.TryDequeue(out _); } allData = _sharedDataQueue.ToList(); if (allData.Count > 0) { firstTime = allData.First().Item1; lastTime = allData.Last().Item1; totalSeconds = Math.Max(1, (lastTime - firstTime).TotalSeconds); } } // 计算Y轴范围 double minValue = double.MaxValue; double maxValue = double.MinValue; foreach (var data in allData) { minValue = Math.Min(minValue, Math.Min(data.Item2, data.Item3)); maxValue = Math.Max(maxValue, Math.Max(data.Item2, data.Item3)); } // 确保包含0点 minValue = Math.Min(minValue, 0); maxValue = Math.Max(maxValue, 0); // 设置合理的范围 if (maxValue - minValue < 10) { minValue = -10; maxValue = 10; } else { double margin = (maxValue - minValue) * 0.1; minValue -= margin; maxValue += margin; } double yRange = maxValue - minValue; // 检查是否需要重绘坐标轴(只有当范围变化较大时) bool shouldRedrawAxis = ShouldRedrawAxis(totalSeconds, minValue, maxValue); if (shouldRedrawAxis) { ClearAllDynamicElements(); DrawAxes(); DrawYAxisGridAndLabels(minValue, maxValue); DrawXAxisTimeTicks(totalSeconds); _lastTotalSeconds = totalSeconds; _lastMinValue = minValue; _lastMaxValue = maxValue; } // 总是绘制曲线(数据点会变化) DrawCurves(allData, firstTime, totalSeconds, minValue, maxValue); } private void DrawYAxisGridAndLabels(double minValue, double maxValue) { if (CurveCanvas == null) return; double yRange = maxValue - minValue; if (yRange <= 0) return; // 计算合适的刻度间隔 double tickInterval = CalculateNiceInterval(yRange); double startValue = Math.Ceiling(minValue / tickInterval) * tickInterval; // 使用HashSet记录已绘制的刻度值,避免重复 HashSet drawnValues = new HashSet(); // 计算标签字体大小,根据画布大小动态调整 double fontSize = Math.Max(10, Math.Min(16, _canvasHeight * 0.02)); // 绘制网格线和标签 for (double value = startValue; value <= maxValue; value += tickInterval) { // 避免浮点数精度问题导致重复 double roundedValue = Math.Round(value, 3); if (drawnValues.Contains(roundedValue)) continue; drawnValues.Add(roundedValue); // 计算Y坐标 double y = _axisEndY - (_axisHeight * (value - minValue) / yRange); y = Math.Clamp(y, _axisStartY, _axisEndY); // 绘制水平网格线 var gridLine = new Line { X1 = _axisStartX, Y1 = y, X2 = _axisEndX, Y2 = y, Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#EEEEEE")), StrokeThickness = 1, Tag = "DynamicElement" }; CurveCanvas.Children.Add(gridLine); // 绘制Y轴刻度标签 - 关键修改:动态计算标签位置 var label = new TextBlock { Text = value.ToString("F0"), FontSize = fontSize, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#7F8C8D")), Tag = "DynamicElement" }; // 测量文本宽度以正确放置 label.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); double labelWidth = label.DesiredSize.Width; Canvas.SetLeft(label, _axisStartX - labelWidth - 10); // 根据文本宽度调整位置 Canvas.SetTop(label, y - label.DesiredSize.Height / 2); CurveCanvas.Children.Add(label); } // 绘制0点特殊标记 if (minValue <= 0 && maxValue >= 0) { double zeroY = _axisEndY - (_axisHeight * (0 - minValue) / yRange); zeroY = Math.Clamp(zeroY, _axisStartY, _axisEndY); // 0点特殊标记线 var zeroLine = new Line { X1 = _axisStartX - 8, Y1 = zeroY, X2 = _axisStartX, Y2 = zeroY, Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#7F8C8D")), StrokeThickness = 2, Tag = "DynamicElement" }; CurveCanvas.Children.Add(zeroLine); var zeroText = new TextBlock { Text = "0", FontSize = fontSize, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#7F8C8D")), FontWeight = FontWeights.Bold, Tag = "DynamicElement" }; zeroText.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); double zeroTextWidth = zeroText.DesiredSize.Width; Canvas.SetLeft(zeroText, _axisStartX - zeroTextWidth - 15); Canvas.SetTop(zeroText, zeroY - zeroText.DesiredSize.Height / 2); CurveCanvas.Children.Add(zeroText); } } private bool ShouldRedrawAxis(double totalSeconds, double minValue, double maxValue) { // 如果这是第一次绘制 if (_lastTotalSeconds == 0 && _lastMinValue == 0 && _lastMaxValue == 0) return true; // 检查时间范围变化是否超过10% double timeDiff = Math.Abs(totalSeconds - _lastTotalSeconds); if (timeDiff > _lastTotalSeconds * 0.1) return true; // 检查Y轴范围变化是否超过10% double yRangeDiff = Math.Abs((maxValue - minValue) - (_lastMaxValue - _lastMinValue)); if (yRangeDiff > (_lastMaxValue - _lastMinValue) * 0.1) return true; return false; } // 修改 DrawAxes 方法,动态调整标题位置 private void DrawAxes() { if (CurveCanvas == null) return; // 计算字体大小,根据画布大小动态调整 double axisFontSize = Math.Max(12, Math.Min(18, _canvasHeight * 0.025)); // 绘制X轴 var xAxis = new Line { X1 = _axisStartX, Y1 = _axisEndY, X2 = _axisEndX, Y2 = _axisEndY, Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2C3E50")), StrokeThickness = Math.Max(1, _canvasHeight * 0.003), // 根据画布大小调整粗细 Tag = "DynamicElement" }; CurveCanvas.Children.Add(xAxis); // 绘制Y轴 var yAxis = new Line { X1 = _axisStartX, Y1 = _axisStartY, X2 = _axisStartX, Y2 = _axisEndY, Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2C3E50")), StrokeThickness = Math.Max(1, _canvasHeight * 0.003), // 根据画布大小调整粗细 Tag = "DynamicElement" }; CurveCanvas.Children.Add(yAxis); // X轴标题 var xAxisTitle = new TextBlock { Text = "时间(秒)", FontSize = axisFontSize, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#7F8C8D")), FontWeight = FontWeights.Bold, Tag = "DynamicElement" }; xAxisTitle.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); double xTitleWidth = xAxisTitle.DesiredSize.Width; Canvas.SetLeft(xAxisTitle, (_axisStartX + _axisEndX) / 2 - xTitleWidth / 2); Canvas.SetTop(xAxisTitle, _axisEndY + 25); CurveCanvas.Children.Add(xAxisTitle); // Y轴标题 var yAxisTitle = new TextBlock { Text = "摩擦力 (N)", FontSize = axisFontSize, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#7F8C8D")), FontWeight = FontWeights.Bold, Tag = "DynamicElement" }; yAxisTitle.RenderTransform = new RotateTransform(-90); yAxisTitle.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); double yTitleHeight = yAxisTitle.DesiredSize.Height; Canvas.SetLeft(yAxisTitle, 15); Canvas.SetTop(yAxisTitle, (_axisStartY + _axisEndY) / 2 - yTitleHeight / 2); CurveCanvas.Children.Add(yAxisTitle); } private void DrawCurves(List> allData, DateTime firstTime, double totalSeconds, double minValue, double maxValue) { if (allData.Count == 0) return; double yRange = maxValue - minValue; if (yRange <= 0) return; // 创建新的点集合 PointCollection points1 = new PointCollection(); PointCollection points2 = new PointCollection(); foreach (var data in allData) { // 计算X坐标(时间,单位:秒) - 使用实际时间差 double timeDiff = (data.Item1 - firstTime).TotalSeconds; // 确保timeDiff不超过totalSeconds timeDiff = Math.Min(timeDiff, totalSeconds); double x = _axisStartX + (_axisWidth * timeDiff / totalSeconds); x = Math.Clamp(x, _axisStartX, _axisEndX); // 计算Y坐标(摩擦力) double y1 = _axisEndY - (_axisHeight * (data.Item2 - minValue) / yRange); double y2 = _axisEndY - (_axisHeight * (data.Item3 - minValue) / yRange); y1 = Math.Clamp(y1, _axisStartY, _axisEndY); y2 = Math.Clamp(y2, _axisStartY, _axisEndY); points1.Add(new Point(x, y1)); points2.Add(new Point(x, y2)); } // 更新曲线 if (Friction1Polyline != null) { Friction1Polyline.Points = points1; } if (Friction2Polyline != null) { Friction2Polyline.Points = points2; } } private double CalculateNiceInterval(double range) { if (range <= 0) return 10; double[] niceIntervals = { 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000 }; double targetInterval = range / 8; // 大约8个刻度 double bestInterval = 10; double minDiff = double.MaxValue; foreach (double interval in niceIntervals) { double numTicks = range / interval; if (numTicks >= 4 && numTicks <= 12) // 4-12个刻度比较合适 { double diff = Math.Abs(interval - targetInterval); if (diff < minDiff) { minDiff = diff; bestInterval = interval; } } } return bestInterval; } private double CalculateNiceTimeInterval(double totalSeconds) { if (totalSeconds <= 0) return 10; // 可能的刻度间隔值(按从小到大排序) double[] possibleIntervals = { 1, 2, 5, 10, 15, 20, 30, 60, 120, 300, 600, 900, 1200, 1800 }; // 我们希望有大约5-10个刻度 double targetTicks = 8; double targetInterval = totalSeconds / targetTicks; // 找到第一个大于等于目标间隔的合适间隔 foreach (double interval in possibleIntervals) { // 检查这个间隔是否会产生合理的刻度数(4-12个) double numTicks = totalSeconds / interval; if (numTicks >= 4 && numTicks <= 12 && interval >= targetInterval) { return interval; } } // 如果没找到合适的,返回最后一个间隔 return possibleIntervals[possibleIntervals.Length - 1]; } private void DrawXAxisTimeTicks(double totalSeconds) { if (CurveCanvas == null || totalSeconds <= 0) return; // 计算合适的刻度间隔 double interval = CalculateNiceTimeInterval(totalSeconds); // 关键修改:使用浮点数循环,但要精确控制 double currentTime = 0; int tickCount = 0; // 先计算实际需要绘制的刻度数 int maxTicks = 15; // 最多15个刻度,避免重叠 while (currentTime <= totalSeconds && tickCount < maxTicks) { double x = _axisStartX + (_axisWidth * currentTime / totalSeconds); x = Math.Clamp(x, _axisStartX, _axisEndX); // 绘制刻度线 var tickLine = new Line { X1 = x, Y1 = _axisEndY, X2 = x, Y2 = _axisEndY + Math.Max(6, _canvasHeight * 0.02), Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#BDC3C7")), StrokeThickness = 1.5, Tag = "DynamicElement" }; CurveCanvas.Children.Add(tickLine); // 绘制时间标签 - 关键修改:显示实际时间值,与报表一致 string timeText = FormatTimeLabel(currentTime, totalSeconds); var tickText = new TextBlock { Text = timeText, FontSize = 14, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#7F8C8D")), Tag = "DynamicElement" }; // 测量文本宽度以居中显示 tickText.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); double textWidth = tickText.DesiredSize.Width; Canvas.SetLeft(tickText, x - textWidth / 2); Canvas.SetTop(tickText, _axisEndY + Math.Max(10, _canvasHeight * 0.025)); CurveCanvas.Children.Add(tickText); // 增加时间和刻度计数 currentTime += interval; tickCount++; } // 关键修改:确保最后一个刻度显示总时间(与报表一致) if (totalSeconds > 0) { // 检查是否已经绘制了最后一个刻度 bool hasLastTick = false; if (currentTime - interval < totalSeconds && Math.Abs(currentTime - totalSeconds) > 0.001) { hasLastTick = false; } if (!hasLastTick) { double lastX = _axisEndX; // 关键:使用实际的总时间,与报表一致 string lastText = FormatTimeLabel(totalSeconds, totalSeconds); var lastTickLine = new Line { X1 = lastX, Y1 = _axisEndY, X2 = lastX, Y2 = _axisEndY + Math.Max(6, _canvasHeight * 0.02), Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#BDC3C7")), StrokeThickness = 1.5, Tag = "DynamicElement" }; CurveCanvas.Children.Add(lastTickLine); var lastTickText = new TextBlock { Text = lastText, FontSize = 14, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#7F8C8D")), Tag = "DynamicElement" }; lastTickText.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); double lastTextWidth = lastTickText.DesiredSize.Width; Canvas.SetLeft(lastTickText, lastX - lastTextWidth / 2); Canvas.SetTop(lastTickText, _axisEndY + Math.Max(10, _canvasHeight * 0.025)); CurveCanvas.Children.Add(lastTickText); } } } // 修改 FormatTimeLabel 方法,显示实际数值 private string FormatTimeLabel(double seconds, double totalSeconds) { // 关键修改:显示实际的小数值,与报表一致 if (totalSeconds < 60) { // 如果小于1秒,显示2位小数;否则显示1位小数 if (seconds < 1) return $"{seconds:F2}秒"; return $"{seconds:F1}秒"; } else if (totalSeconds < 3600) { double minutes = seconds / 60; return $"{minutes:F1}分"; } else { double hours = seconds / 3600; return $"{hours:F2}小时"; } } private void ClearAllDynamicElements() { if (CurveCanvas == null || CurveCanvas.Children == null) return; var elementsToRemove = new List(); foreach (object element in CurveCanvas.Children) { if (element is FrameworkElement fe && fe.Tag != null && fe.Tag.ToString() == "DynamicElement") { elementsToRemove.Add(fe); } } foreach (var element in elementsToRemove) { CurveCanvas.Children.Remove(element); } } private void TimeRangeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { //if (TimeRangeComboBox.SelectedItem is ComboBoxItem item && int.TryParse(item.Tag.ToString(), out int newRange)) //{ // _currentTimeRange = newRange; //} } private void ClearCurveButton_Click(object sender, RoutedEventArgs e) { ClearCurve(); } public void ClearCurve() { if (_sharedDataQueue != null) { // ConcurrentQueue doesn't support Clear(), dequeue all items safely while (_sharedDataQueue.TryDequeue(out _)) { } } if (Friction1Polyline != null) { Friction1Polyline.Points.Clear(); } if (Friction2Polyline != null) { Friction2Polyline.Points.Clear(); } ClearAllDynamicElements(); _lastTotalSeconds = 0; _lastMinValue = 0; _lastMaxValue = 0; } } }