Files
huadongmocaceshiyi/CurvePage.xaml.cs
2026-03-25 21:33:34 +08:00

695 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Tuple<DateTime, double, double>> _sharedDataQueue;
private DispatcherTimer _updateTimer;
private int _currentTimeRange = 60;
public Action<double, double> 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<Tuple<DateTime, double, double>> sharedDataQueue)
{
InitializeComponent();
_sharedDataQueue = sharedDataQueue ?? new System.Collections.Concurrent.ConcurrentQueue<Tuple<DateTime, double, double>>();
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<double> drawnValues = new HashSet<double>();
// 计算标签字体大小,根据画布大小动态调整
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<Tuple<DateTime, double, double>> 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<UIElement>();
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;
}
}
}