using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using ScottPlot; using ScottPlot.WPF; using SSPCTester.Devices.Interfaces; namespace SSPCTester.UI.Controls; public partial class CurvePlotControl : UserControl { private readonly bool _isDesignMode; private WpfPlot? _plot; public CurvePlotControl() { InitializeComponent(); _isDesignMode = DesignerProperties.GetIsInDesignMode(this); if (_isDesignMode) { DesignPlaceholder.Visibility = Visibility.Visible; return; } Loaded += (_, _) => InitPlot(); } public static readonly DependencyProperty CurveProperty = DependencyProperty.Register( nameof(Curve), typeof(CurveData), typeof(CurvePlotControl), new PropertyMetadata(null, (d, e) => ((CurvePlotControl)d).Render())); public static readonly DependencyProperty IsRisingProperty = DependencyProperty.Register( nameof(IsRising), typeof(bool), typeof(CurvePlotControl), new PropertyMetadata(true, (d, e) => ((CurvePlotControl)d).Render())); public static readonly DependencyProperty VMaxProperty = DependencyProperty.Register( nameof(VMax), typeof(double), typeof(CurvePlotControl), new PropertyMetadata(28.0, (d, e) => ((CurvePlotControl)d).Render())); public static readonly DependencyProperty ViewStartMsProperty = DependencyProperty.Register( nameof(ViewStartMs), typeof(double?), typeof(CurvePlotControl), new PropertyMetadata(null, (d, e) => ((CurvePlotControl)d).Render())); public static readonly DependencyProperty ViewEndMsProperty = DependencyProperty.Register( nameof(ViewEndMs), typeof(double?), typeof(CurvePlotControl), new PropertyMetadata(null, (d, e) => ((CurvePlotControl)d).Render())); public CurveData? Curve { get => (CurveData?)GetValue(CurveProperty); set => SetValue(CurveProperty, value); } public bool IsRising { get => (bool)GetValue(IsRisingProperty); set => SetValue(IsRisingProperty, value); } public double VMax { get => (double)GetValue(VMaxProperty); set => SetValue(VMaxProperty, value); } public double? ViewStartMs { get => (double?)GetValue(ViewStartMsProperty); set => SetValue(ViewStartMsProperty, value); } public double? ViewEndMs { get => (double?)GetValue(ViewEndMsProperty); set => SetValue(ViewEndMsProperty, value); } private void InitPlot() { _plot ??= CreateRuntimePlot(); _plot.Plot.Title(""); _plot.Plot.XLabel("t (ms)"); _plot.Plot.YLabel("V"); _plot.Plot.Axes.AntiAlias(true); _plot.Plot.FigureBackground.Color = ScottPlot.Color.FromHex("#FBFCFD"); _plot.Plot.DataBackground.Color = ScottPlot.Color.FromHex("#FBFCFD"); _plot.Plot.Grid.MajorLineColor = ScottPlot.Color.FromHex("#DFE4EA"); Render(); } private WpfPlot CreateRuntimePlot() { var plot = new WpfPlot(); RootContainer.Children.Clear(); RootContainer.Children.Add(plot); return plot; } private void Render() { if (_isDesignMode || !IsLoaded || _plot == null) return; _plot.Plot.Clear(); var c = Curve; if (c == null || c.Voltage.Length == 0) { _plot.Refresh(); return; } var points = BuildViewPoints(c); if (points.Count == 0) { _plot.Refresh(); return; } var xs = new double[points.Count]; var ys = new double[points.Count]; // 以触发时刻为 0;x 单位 ms for (int i = 0; i < points.Count; i++) { xs[i] = points[i].TimeMs; ys[i] = points[i].Voltage; } var color = IsRising ? "#1A73C4" : "#D23B3B"; var sig = _plot.Plot.Add.ScatterLine(xs, ys, ScottPlot.Color.FromHex(color)); sig.LineWidth = 2; sig.MarkerSize = 0; // 阈值线 double threshold = IsRising ? VMax * 0.9 : VMax * 0.1; var hLine = _plot.Plot.Add.HorizontalLine(threshold); hLine.Color = ScottPlot.Color.FromHex("#98A1AD"); hLine.LinePattern = ScottPlot.LinePattern.Dashed; hLine.LineWidth = 1; var label = _plot.Plot.Add.Text($"{(IsRising ? 90 : 10)}%", xs[0], threshold); label.LabelFontColor = ScottPlot.Color.FromHex("#98A1AD"); label.LabelFontSize = 10; // 触发线 t=0 if (!HasViewWindow(out double start, out double end) || (start <= 0 && end >= 0)) { var vLine = _plot.Plot.Add.VerticalLine(0); vLine.Color = ScottPlot.Color.FromHex("#98A1AD"); vLine.LinePattern = ScottPlot.LinePattern.Dotted; vLine.LineWidth = 1; } _plot.Plot.Axes.AutoScale(); _plot.Refresh(); } private List<(double TimeMs, double Voltage)> BuildViewPoints(CurveData curve) { var points = new List<(double TimeMs, double Voltage)>(curve.Voltage.Length); bool hasWindow = HasViewWindow(out double start, out double end); for (int i = 0; i < curve.Voltage.Length; i++) { double timeMs = (curve.TimeAt(i) - curve.TriggerTimeSec) * 1000.0; if (!hasWindow || (timeMs >= start && timeMs <= end)) points.Add((timeMs, curve.Voltage[i])); } return points; } private bool HasViewWindow(out double start, out double end) { start = ViewStartMs ?? 0; end = ViewEndMs ?? 0; return ViewStartMs.HasValue && ViewEndMs.HasValue && double.IsFinite(start) && double.IsFinite(end) && end > start; } /// 导出为 PNG 字节。 public byte[] ExportPng(int width = 800, int height = 300) { if (_plot == null) return Array.Empty(); return _plot.Plot.GetImageBytes(width, height, ImageFormat.Png); } }