diff --git a/COFTester/MainWindow.xaml.cs b/COFTester/MainWindow.xaml.cs index b8b3f6e..de474eb 100644 --- a/COFTester/MainWindow.xaml.cs +++ b/COFTester/MainWindow.xaml.cs @@ -17,11 +17,13 @@ public partial class MainWindow : Window { InitializeComponent(); DataContext = _viewModel; + _viewModel.RealtimeChartInvalidated += ViewModel_RealtimeChartInvalidated; QueueRealtimeChartLayoutRefresh(); } protected override void OnClosed(EventArgs e) { + _viewModel.RealtimeChartInvalidated -= ViewModel_RealtimeChartInvalidated; _viewModel.Dispose(); base.OnClosed(e); } @@ -41,6 +43,7 @@ public partial class MainWindow : Window private void ShowRealtimeTab_Click(object sender, RoutedEventArgs e) { MainWorkspaceTabs.SelectedIndex = 0; + QueueRealtimeChartLayoutRefresh(); } private void ShowHistoryTab_Click(object sender, RoutedEventArgs e) @@ -63,6 +66,17 @@ public partial class MainWindow : Window QueueRealtimeChartLayoutRefresh(); } + private void ViewModel_RealtimeChartInvalidated(object? sender, EventArgs e) + { + if (!Dispatcher.CheckAccess()) + { + Dispatcher.BeginInvoke(new Action(QueueRealtimeChartLayoutRefresh), DispatcherPriority.Render); + return; + } + + QueueRealtimeChartLayoutRefresh(); + } + private void TableMotionButton_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (sender is not Button button || button.Tag is not string direction) diff --git a/COFTester/Services/RunExportService.cs b/COFTester/Services/RunExportService.cs index a3554bf..5284bef 100644 --- a/COFTester/Services/RunExportService.cs +++ b/COFTester/Services/RunExportService.cs @@ -354,7 +354,7 @@ public sealed class RunExportService worksheet.Cell(row, 3).Value = runs[index].CurveColorHex; worksheet.Cell(row, 4).Value = runs[index].ValidSamples.Count; worksheet.Cell(row, 5).Value = IsChartableRun(runs[index]) - ? "已绘制" + ? "已绘制总览与单图" : "采样点不足,未绘制曲线"; worksheet.Cell(row, 3).Style.Fill.BackgroundColor = ToXlColor(runs[index].CurveColorHex); worksheet.Cell(row, 3).Style.Font.FontColor = XLColor.White; @@ -445,11 +445,16 @@ public sealed class RunExportService var titleRef = BuildCellReference(sheetName, startRow, xColumn); var startDataRow = startRow + 2; var endDataRow = Math.Max(startDataRow, currentRow - 1); + var xValues = item.ValidSamples.Select(sample => sample.DisplacementMm).ToArray(); + var yValues = item.ValidSamples.Select(sample => sample.ForceN).ToArray(); var layout = new SeriesLayout( titleRef, BuildRangeReference(sheetName, startDataRow, xColumn, endDataRow, xColumn), BuildRangeReference(sheetName, startDataRow, yColumn, endDataRow, yColumn), - item.CurveColorHex); + item.CurveColorHex, + BuildCurveLabel(item), + xValues, + yValues); if (IsChartableRun(item)) { @@ -458,10 +463,26 @@ public sealed class RunExportService } var charts = new List(); - var perRunStartRow = Math.Max(10, runs.Count + 8); + var overviewStartRow = Math.Max(10, runs.Count + 8); + const int overviewChartHeight = 13; const int chartHeight = 11; const int chartWidth = 19; const int rowGap = 2; + var perRunStartRow = overviewStartRow; + + if (perRunSeries.Count > 0) + { + charts.Add(new ChartDefinition( + "历史实时摩擦曲线总览", + "位移 (mm)", + "力值 (N)", + true, + perRunSeries.Select(item => item.Series).ToArray(), + new ChartAnchor(0, overviewStartRow, chartWidth, overviewStartRow + overviewChartHeight), + "历史实时摩擦曲线总览")); + + perRunStartRow = overviewStartRow + overviewChartHeight + rowGap; + } for (var index = 0; index < perRunSeries.Count; index++) { @@ -492,7 +513,8 @@ public sealed class RunExportService private static int CalculateChartSheetLastRow(IReadOnlyList runs) { var chartCount = runs.Count(IsChartableRun); - return Math.Max(24, runs.Count + 22 + chartCount * 13); + var renderedChartCount = chartCount == 0 ? 0 : chartCount + 1; + return Math.Max(24, runs.Count + 22 + renderedChartCount * 13); } private static void ApplyDefaultWorksheetStyle(IXLWorksheet worksheet) @@ -695,11 +717,11 @@ public sealed class RunExportService var scatterSeries = new C.ScatterChartSeries( new C.Index { Val = (uint)index }, new C.Order { Val = (uint)index }, - new C.SeriesText(new C.StringReference(new C.Formula(series.TitleReference))), + new C.SeriesText(CreateStringReference(series.TitleReference, series.TitleText)), chartShape, new C.Marker(new C.Symbol { Val = C.MarkerStyleValues.None }), - new C.XValues(new C.NumberReference(new C.Formula(series.XValuesReference))), - new C.YValues(new C.NumberReference(new C.Formula(series.YValuesReference))), + new C.XValues(CreateNumberReference(series.XValuesReference, series.XValues)), + new C.YValues(CreateNumberReference(series.YValuesReference, series.YValues)), new C.Smooth { Val = false }); scatterChart.Append(scatterSeries); @@ -754,6 +776,28 @@ public sealed class RunExportService chartPart.ChartSpace.Save(); } + private static C.StringReference CreateStringReference(string formula, string value) + { + var stringCache = new C.StringCache(new C.PointCount { Val = 1U }); + stringCache.Append(new C.StringPoint(new C.NumericValue(value)) { Index = 0U }); + return new C.StringReference(new C.Formula(formula), stringCache); + } + + private static C.NumberReference CreateNumberReference(string formula, IReadOnlyList values) + { + var numberingCache = new C.NumberingCache(new C.FormatCode("General"), new C.PointCount { Val = (uint)values.Count }); + for (var index = 0; index < values.Count; index++) + { + numberingCache.Append(new C.NumericPoint( + new C.NumericValue(values[index].ToString("G17", CultureInfo.InvariantCulture))) + { + Index = (uint)index + }); + } + + return new C.NumberReference(new C.Formula(formula), numberingCache); + } + private static void AppendChartAnchor(Xdr.WorksheetDrawing worksheetDrawing, string chartRelationshipId, ChartAnchor anchor, string shapeName) { var graphicFrameId = worksheetDrawing.Descendants() @@ -952,7 +996,14 @@ public sealed class RunExportService int ExcludedRunCount, int ValidSampleCount); - private sealed record SeriesLayout(string TitleReference, string XValuesReference, string YValuesReference, string ColorHex); + private sealed record SeriesLayout( + string TitleReference, + string XValuesReference, + string YValuesReference, + string ColorHex, + string TitleText, + IReadOnlyList XValues, + IReadOnlyList YValues); private sealed record PerRunSeriesLayout(ExportCurveData Data, SeriesLayout Series); diff --git a/COFTester/ViewModels/MainViewModel.cs b/COFTester/ViewModels/MainViewModel.cs index 1859f88..716ad3d 100644 --- a/COFTester/ViewModels/MainViewModel.cs +++ b/COFTester/ViewModels/MainViewModel.cs @@ -258,6 +258,8 @@ public sealed class MainViewModel : ObservableObject, IDisposable _ = InitializeDeviceConnectionAsync(); } + public event EventHandler? RealtimeChartInvalidated; + public string StandardVersion => "静摩擦与动摩擦系数工控界面"; public TestRecipe Recipe { get; } @@ -735,12 +737,9 @@ public sealed class MainViewModel : ObservableObject, IDisposable _activeRecipeSnapshot = TestRecipeSnapshot.FromRecipe(Recipe); _activeRunStartedByPlc = startedByPlc; _displayedRecipeSnapshot = _activeRecipeSnapshot; - _isShowingHistoricalRun = false; + ClearHistoricalSelectionForLiveRun(); _currentRunSamples.Clear(); - _forceSamples.Clear(); - _peakLineSamples.Clear(); - _averageLineSamples.Clear(); - _currentPointSample.Clear(); + ClearRealtimeChartSamples(); ResetRealtimeSamplingMetrics(); CurrentForceN = 0; CurrentDisplacementMm = 0; @@ -763,13 +762,15 @@ public sealed class MainViewModel : ObservableObject, IDisposable _kineticBand.Xj = Recipe.TravelMm; _kineticBand.Yi = 0; _kineticBand.Yj = 1; + SetAxisLimits(GetEmptyChartXAxisMaxLimit(), EmptyChartYAxisMaxLimit); + NotifyRealtimeChartChanged(); _deviceDataReader.Initialize(Recipe); AddInfoEvent($"批次 {Recipe.BatchNumber} 第 {NextRunIndex} 轮开始。模式={Recipe.TestMode}, 水平速度={Recipe.SpeedMmPerMin:F0} mm/min"); } else if (isRecoveringActiveRun) { _activeRunStartedByPlc |= startedByPlc; - _isShowingHistoricalRun = false; + ClearHistoricalSelectionForLiveRun(); _displayedRecipeSnapshot = _activeRecipeSnapshot; AddInfoEvent($"试验通信恢复后继续运行,已保留 {_currentRunSamples.Count} 个采样点。"); } @@ -785,6 +786,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable InterlockMessage = "采样进行中。实时监控力值与摩擦系数。"; _timer.Start(); RaiseStatusProperties(); + NotifyRealtimeChartChanged(); } private async void Pause() @@ -1078,6 +1080,8 @@ public sealed class MainViewModel : ObservableObject, IDisposable _exportReportCommand.RaiseCanExecuteChanged(); } + NotifyRealtimeChartChanged(); + return true; } @@ -1206,6 +1210,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable ForceYAxes[0].MaxLimit = yMax; OnPropertyChanged(nameof(ForceXAxes)); OnPropertyChanged(nameof(ForceYAxes)); + NotifyRealtimeChartChanged(); } private static bool ShouldUpdateAxisLimit(double currentLimit, double nextLimit) @@ -1324,6 +1329,35 @@ public sealed class MainViewModel : ObservableObject, IDisposable _lastRealtimeChartRefreshAt = DateTime.MinValue; } + private void ClearRealtimeChartSamples() + { + _forceSamples.Clear(); + _peakLineSamples.Clear(); + _averageLineSamples.Clear(); + _currentPointSample.Clear(); + OnPropertyChanged(nameof(ForceSeries)); + } + + private void ClearHistoricalSelectionForLiveRun() + { + _isShowingHistoricalRun = false; + if (_selectedRunRecord is null) + { + return; + } + + _selectedRunRecord = null; + OnPropertyChanged(nameof(SelectedRunRecord)); + OnPropertyChanged(nameof(ExportReportSummary)); + OnPropertyChanged(nameof(AnalysisModeSummary)); + RaiseCommandStates(); + } + + private void NotifyRealtimeChartChanged() + { + RealtimeChartInvalidated?.Invoke(this, EventArgs.Empty); + } + private TestRecipeSnapshot GetDisplayRecipeSnapshot() { return _isShowingHistoricalRun && _displayedRecipeSnapshot is not null @@ -1652,7 +1686,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable private void LoadSamplesIntoChart(IReadOnlyList samples) { - _forceSamples.Clear(); + ClearRealtimeChartSamples(); foreach (var sample in samples) { _forceSamples.Add(new ObservablePoint(sample.DisplacementMm, sample.ForceN)); @@ -1667,14 +1701,13 @@ public sealed class MainViewModel : ObservableObject, IDisposable { ShowEmptyRealtimeChartFrame(); } + + NotifyRealtimeChartChanged(); } private void ShowEmptyRealtimeChartFrame() { - _forceSamples.Clear(); - _peakLineSamples.Clear(); - _averageLineSamples.Clear(); - _currentPointSample.Clear(); + ClearRealtimeChartSamples(); _currentPointSample.Add(new ObservablePoint(0, EmptyChartPointForceN)); SetAxisLimits(GetEmptyChartXAxisMaxLimit(), EmptyChartYAxisMaxLimit); _kineticBand.Xi = Recipe.TravelMm * 0.35; @@ -1682,6 +1715,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable _kineticBand.Yi = 0; _kineticBand.Yj = EmptyChartYAxisMaxLimit; OnPropertyChanged(nameof(ForceSections)); + NotifyRealtimeChartChanged(); } private void RefreshEmptyRealtimeChartFrameIfVisible() @@ -1704,10 +1738,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable private void ResetRealtimePresentation() { - _forceSamples.Clear(); - _peakLineSamples.Clear(); - _averageLineSamples.Clear(); - _currentPointSample.Clear(); + ClearRealtimeChartSamples(); ResetRealtimeSamplingMetrics(); CurrentForceN = 0;