Files
ADP/CurveModule/ViewModels/CurveRecallViewModel.cs
2026-07-28 14:35:33 +08:00

382 lines
13 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 Logger;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using Prism.Commands;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Input;
using UIShare.ViewModelBase;
namespace CurveModule.ViewModels
{
/// <summary>
/// 曲线回读:加载 RecordView 导出的 CSV 文件,按 (MonitorName, Scope) 分组绘制曲线。
/// </summary>
public class CurveRecallViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region
public bool KeepAlive => true;
public PlotModel Plot { get; }
/// <summary>已加载的曲线通道列表</summary>
public ObservableCollection<CurveRecallChannelVM> Channels { get; } = new();
private CurveRecallChannelVM? _selectedChannel;
public CurveRecallChannelVM? SelectedChannel
{
get => _selectedChannel;
set => SetProperty(ref _selectedChannel, value);
}
private string _statusMessage = "请加载 CSV 文件以回读曲线";
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
#endregion
#region
public ICommand LoadCsvCommand { get; }
public ICommand ResetViewCommand { get; }
public ICommand ClearCommand { get; }
#endregion
#region
// 颜色调色盘(与 MonitorView 一致)
private static readonly OxyColor[] _palette =
{
OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen,
OxyColors.DarkOrange, OxyColors.MediumPurple, OxyColors.Goldenrod,
OxyColors.Teal, OxyColors.Crimson, OxyColors.OliveDrab
};
private int _colorIndex;
#endregion
public CurveRecallViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
Plot = BuildEmptyPlot();
LoadCsvCommand = new DelegateCommand(OnLoadCsv);
ResetViewCommand = new DelegateCommand(OnResetView);
ClearCommand = new DelegateCommand(OnClear);
}
#region
/// <summary>
/// 打开 CSV 文件对话框,解析数据并按 (MonitorName, Scope) 分组绘制曲线。
/// </summary>
private void OnLoadCsv()
{
try
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*",
Title = "选择要加载的监测数据 CSV 文件"
};
if (dlg.ShowDialog() != true) return;
var records = ParseCsv(dlg.FileName);
if (records.Count == 0)
{
StatusMessage = "CSV 文件为空或格式不正确";
return;
}
// 按 (MonitorName, Scope) 分组
var groups = records.GroupBy(r => (r.MonitorName, r.Scope))
.OrderBy(g => g.Key.MonitorName)
.ThenBy(g => g.Key.Scope);
// 清空旧数据
ClearPlotAndChannels();
foreach (var group in groups)
{
var color = _palette[_colorIndex % _palette.Length];
_colorIndex++;
string displayName = string.IsNullOrEmpty(group.Key.Scope)
? group.Key.MonitorName
: $"{group.Key.MonitorName} [{group.Key.Scope}]";
var channel = new CurveRecallChannelVM
{
MonitorName = group.Key.MonitorName,
Scope = group.Key.Scope,
DisplayName = displayName,
Color = color,
IsDisplayed = true
};
// 创建 LineSeries 并填充数据点
channel.Series = new LineSeries
{
Title = displayName,
Color = color,
StrokeThickness = 1.5
};
// 按时间排序
var sortedPoints = group.OrderBy(r => r.CreateTime);
foreach (var point in sortedPoints)
{
channel.Series.Points.Add(new DataPoint(point.CreateTime.ToOADate(), point.MonitorValue));
}
channel.PropertyChanged += OnChannelPropertyChanged;
Plot.Series.Add(channel.Series);
Channels.Add(channel);
}
Plot.InvalidatePlot(true);
StatusMessage = $"已加载 {Path.GetFileName(dlg.FileName)},共 {Channels.Count} 条曲线,{records.Count} 个数据点";
LoggerHelper.Info($"曲线回读:加载 {dlg.FileName}{Channels.Count} 条曲线,{records.Count} 个数据点");
}
catch (Exception ex)
{
LoggerHelper.Error($"加载 CSV 失败:{ex.Message}");
StatusMessage = $"加载失败:{ex.Message}";
}
}
private void OnResetView()
{
Plot.ResetAllAxes();
Plot.InvalidatePlot(false);
StatusMessage = "视图已复原";
}
private void OnClear()
{
ClearPlotAndChannels();
Plot.InvalidatePlot(true);
StatusMessage = "已清空,请重新加载 CSV 文件";
}
#endregion
#region CSV
/// <summary>
/// 解析 RecordView 导出的 CSV 文件,返回监测记录列表。
/// </summary>
private static List<MonitorRecord> ParseCsv(string path)
{
var results = new List<MonitorRecord>();
using var sr = new StreamReader(path, Encoding.UTF8);
// 读取表头
string? headerLine = sr.ReadLine();
if (headerLine == null) return results;
var headers = ParseCsvLine(headerLine);
int monitorNameIdx = headers.FindIndex(h => h.Trim() == "MonitorName");
int monitorValueIdx = headers.FindIndex(h => h.Trim() == "MonitorValue");
int scopeIdx = headers.FindIndex(h => h.Trim() == "Scope");
int createTimeIdx = headers.FindIndex(h => h.Trim() == "CreateTime");
if (monitorNameIdx < 0 || monitorValueIdx < 0 || scopeIdx < 0 || createTimeIdx < 0)
return results;
int maxIdx = Math.Max(monitorNameIdx, Math.Max(monitorValueIdx, Math.Max(scopeIdx, createTimeIdx)));
string? line;
while ((line = sr.ReadLine()) != null)
{
if (string.IsNullOrWhiteSpace(line)) continue;
var fields = ParseCsvLine(line);
if (fields.Count <= maxIdx) continue;
if (!double.TryParse(fields[monitorValueIdx].Trim(), out double value)) continue;
if (!DateTime.TryParse(fields[createTimeIdx].Trim(), out DateTime createTime)) continue;
results.Add(new MonitorRecord
{
MonitorName = fields[monitorNameIdx].Trim(),
MonitorValue = value,
Scope = fields[scopeIdx].Trim(),
CreateTime = createTime
});
}
return results;
}
/// <summary>
/// 解析单行 CSV正确处理引号转义。
/// </summary>
private static List<string> ParseCsvLine(string line)
{
var fields = new List<string>();
bool inQuotes = false;
var sb = new StringBuilder();
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (inQuotes)
{
if (c == '"')
{
if (i + 1 < line.Length && line[i + 1] == '"')
{
sb.Append('"');
i++;
}
else
{
inQuotes = false;
}
}
else
{
sb.Append(c);
}
}
else
{
if (c == '"')
{
inQuotes = true;
}
else if (c == ',')
{
fields.Add(sb.ToString());
sb.Clear();
}
else
{
sb.Append(c);
}
}
}
fields.Add(sb.ToString());
return fields;
}
#endregion
#region OxyPlot
/// <summary>
/// 构建空的 OxyPlot 模型(与 MonitorView 完全一致的配置)。
/// </summary>
private static PlotModel BuildEmptyPlot()
{
var pm = new PlotModel
{
Title = "曲线回读",
PlotAreaBorderColor = OxyColors.LightGray,
Background = OxyColors.White
};
pm.Axes.Add(new DateTimeAxis
{
Position = AxisPosition.Bottom,
Title = "时间",
StringFormat = "HH:mm:ss",
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None
});
pm.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = "值",
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None
});
pm.Legends.Add(new Legend
{
LegendPosition = LegendPosition.RightTop,
LegendBackground = OxyColor.FromAColor(200, OxyColors.White),
LegendBorder = OxyColors.LightGray
});
return pm;
}
#endregion
#region
private void OnChannelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(CurveRecallChannelVM.IsDisplayed)) return;
if (sender is not CurveRecallChannelVM channel) return;
if (channel.IsDisplayed)
{
if (channel.Series != null && !Plot.Series.Contains(channel.Series))
{
Plot.Series.Add(channel.Series);
}
}
else
{
if (channel.Series != null)
{
Plot.Series.Remove(channel.Series);
}
}
Plot.InvalidatePlot(true);
}
private void ClearPlotAndChannels()
{
foreach (var ch in Channels)
{
ch.PropertyChanged -= OnChannelPropertyChanged;
}
Plot.Series.Clear();
Channels.Clear();
_colorIndex = 0;
}
#endregion
#region
public override void OnNavigatedTo(NavigationContext navigationContext)
{
}
public void Dispose()
{
ClearPlotAndChannels();
}
#endregion
}
/// <summary>
/// CSV 监测记录(单行数据)。
/// </summary>
internal class MonitorRecord
{
public string MonitorName { get; set; } = string.Empty;
public double MonitorValue { get; set; }
public string Scope { get; set; } = string.Empty;
public DateTime CreateTime { get; set; }
}
/// <summary>
/// 曲线回读通道MonitorName + Scope 唯一标识一条曲线。
/// </summary>
public class CurveRecallChannelVM : Prism.Mvvm.BindableBase
{
public string MonitorName { get; set; } = string.Empty;
public string Scope { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public OxyColor Color { get; set; } = OxyColors.SteelBlue;
private bool _isDisplayed = true;
public bool IsDisplayed
{
get => _isDisplayed;
set => SetProperty(ref _isDisplayed, value);
}
public LineSeries? Series { get; set; }
}
}