曲线回读代码位置修改
This commit is contained in:
@@ -1,71 +1,381 @@
|
|||||||
using Logger;
|
using Logger;
|
||||||
using MahApps.Metro.Controls;
|
|
||||||
using Model.Entity;
|
|
||||||
using Model.Models;
|
|
||||||
using NLog.Targets;
|
|
||||||
using OxyPlot;
|
using OxyPlot;
|
||||||
using Service.Interface;
|
using OxyPlot.Axes;
|
||||||
|
using OxyPlot.Legends;
|
||||||
|
using OxyPlot.Series;
|
||||||
|
using Prism.Commands;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Data;
|
using System.IO;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Threading;
|
|
||||||
|
|
||||||
using UIShare.GlobalVariable;
|
|
||||||
using UIShare.UIViewModel;
|
|
||||||
using UIShare.ViewModelBase;
|
using UIShare.ViewModelBase;
|
||||||
|
|
||||||
namespace CurveModule.ViewModels
|
namespace CurveModule.ViewModels
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 曲线回读:加载 RecordView 导出的 CSV 文件,按 (MonitorName, Scope) 分组绘制曲线。
|
||||||
|
/// </summary>
|
||||||
public class CurveRecallViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
public class CurveRecallViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
||||||
{
|
{
|
||||||
#region 属性
|
#region 属性
|
||||||
public bool KeepAlive => true;
|
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
|
#endregion
|
||||||
|
|
||||||
#region 命令
|
#region 命令
|
||||||
|
public ICommand LoadCsvCommand { get; }
|
||||||
|
public ICommand ResetViewCommand { get; }
|
||||||
|
public ICommand ClearCommand { get; }
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 私有字段
|
#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
|
#endregion
|
||||||
|
|
||||||
public CurveRecallViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
public CurveRecallViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||||
{
|
{
|
||||||
|
Plot = BuildEmptyPlot();
|
||||||
|
LoadCsvCommand = new DelegateCommand(OnLoadCsv);
|
||||||
|
ResetViewCommand = new DelegateCommand(OnResetView);
|
||||||
|
ClearCommand = new DelegateCommand(OnClear);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region 命令处理
|
#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
|
#endregion
|
||||||
|
|
||||||
#region 辅助方法
|
#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
|
#endregion
|
||||||
#region 生命周期
|
|
||||||
|
|
||||||
|
#region 生命周期
|
||||||
public override void OnNavigatedTo(NavigationContext navigationContext)
|
public override void OnNavigatedTo(NavigationContext navigationContext)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
public LineSeries? Series { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,158 @@
|
|||||||
<UserControl x:Class="CurveModule.Views.CurveRecallView"
|
<UserControl x:Class="CurveModule.Views.CurveRecallView"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
mc:Ignorable="d"
|
xmlns:prism="http://prismlibrary.com/"
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
xmlns:oxy="http://oxyplot.org/wpf"
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||||
d:DesignHeight="1080" d:DesignWidth="1920">
|
mc:Ignorable="d"
|
||||||
<Grid>
|
d:DesignHeight="700"
|
||||||
|
d:DesignWidth="1200">
|
||||||
</Grid>
|
<UserControl.Resources>
|
||||||
|
<converters:LessThanConverter x:Key="LessThanConverter"/>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Border Background="#F5F7FA">
|
||||||
|
<Grid x:Name="RootGrid" Margin="8">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Row0:标题 -->
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="曲线回读"
|
||||||
|
FontSize="20" FontWeight="Bold"
|
||||||
|
Margin="4,0,0,8"/>
|
||||||
|
|
||||||
|
<!-- Row1:工具栏 -->
|
||||||
|
<Border Grid.Row="1"
|
||||||
|
Background="White"
|
||||||
|
BorderBrush="#DDD" BorderThickness="1"
|
||||||
|
CornerRadius="4" Padding="8" Margin="0,0,0,6">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="📂 加载 CSV"
|
||||||
|
Command="{Binding LoadCsvCommand}"
|
||||||
|
Padding="12,4"/>
|
||||||
|
<Button Content="↺ 复原视图"
|
||||||
|
Command="{Binding ResetViewCommand}"
|
||||||
|
Padding="12,4" Margin="6,0,0,0"
|
||||||
|
ToolTip="按数据范围重置坐标轴缩放/平移"/>
|
||||||
|
<Button Content="✕ 清空"
|
||||||
|
Command="{Binding ClearCommand}"
|
||||||
|
Padding="12,4" Margin="6,0,0,0"
|
||||||
|
ToolTip="清空已加载的曲线数据"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Row2:主体 -->
|
||||||
|
<Grid Grid.Row="2">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition>
|
||||||
|
<ColumnDefinition.Style>
|
||||||
|
<Style TargetType="ColumnDefinition">
|
||||||
|
<Setter Property="Width" Value="260"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding ActualWidth, ElementName=RootGrid, Converter={StaticResource LessThanConverter}, ConverterParameter=600}" Value="True">
|
||||||
|
<Setter Property="Width" Value="0"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</ColumnDefinition.Style>
|
||||||
|
</ColumnDefinition>
|
||||||
|
<ColumnDefinition>
|
||||||
|
<ColumnDefinition.Style>
|
||||||
|
<Style TargetType="ColumnDefinition">
|
||||||
|
<Setter Property="Width" Value="6"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding ActualWidth, ElementName=RootGrid, Converter={StaticResource LessThanConverter}, ConverterParameter=600}" Value="True">
|
||||||
|
<Setter Property="Width" Value="0"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</ColumnDefinition.Style>
|
||||||
|
</ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 左侧:通道列表 -->
|
||||||
|
<Border Grid.Column="0"
|
||||||
|
Background="White"
|
||||||
|
BorderBrush="#DDD" BorderThickness="1"
|
||||||
|
CornerRadius="4">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border Grid.Row="0" Background="#ECEFF4" Padding="8,4">
|
||||||
|
<TextBlock Text="曲线通道(勾选显示/隐藏)" FontWeight="Bold"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<ListBox Grid.Row="1"
|
||||||
|
ItemsSource="{Binding Channels}"
|
||||||
|
SelectedItem="{Binding SelectedChannel}"
|
||||||
|
BorderThickness="0">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Margin="2">
|
||||||
|
<DockPanel>
|
||||||
|
<CheckBox IsChecked="{Binding IsDisplayed}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
ToolTip="勾选=在图表上显示,取消=隐藏"/>
|
||||||
|
<Border Width="10" Height="10"
|
||||||
|
CornerRadius="2"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="4,0,6,0">
|
||||||
|
<Border.Background>
|
||||||
|
<SolidColorBrush Color="SteelBlue"/>
|
||||||
|
</Border.Background>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="{Binding DisplayName}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
|
</DockPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2"
|
||||||
|
Text="{Binding StatusMessage}"
|
||||||
|
FontSize="11" Foreground="#666"
|
||||||
|
Margin="4,2" TextWrapping="Wrap"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<GridSplitter Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="Transparent"/>
|
||||||
|
|
||||||
|
<!-- 右侧:OxyPlot 图表 -->
|
||||||
|
<Border Grid.Column="2"
|
||||||
|
Background="White"
|
||||||
|
BorderBrush="#DDD" BorderThickness="1"
|
||||||
|
CornerRadius="4">
|
||||||
|
<oxy:PlotView Model="{Binding Plot}"
|
||||||
|
Background="Transparent"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Row3:状态栏 -->
|
||||||
|
<Border Grid.Row="3"
|
||||||
|
Background="#ECEFF4"
|
||||||
|
Padding="8,4" Margin="0,6,0,0"
|
||||||
|
CornerRadius="2">
|
||||||
|
<TextBlock Text="{Binding StatusMessage}"
|
||||||
|
Foreground="#444"
|
||||||
|
FontSize="12"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ namespace MonitorModule
|
|||||||
{
|
{
|
||||||
containerRegistry.RegisterForNavigation<MonitorView>("MonitorView");
|
containerRegistry.RegisterForNavigation<MonitorView>("MonitorView");
|
||||||
containerRegistry.RegisterForNavigation<RecordView>("RecordView");
|
containerRegistry.RegisterForNavigation<RecordView>("RecordView");
|
||||||
containerRegistry.RegisterForNavigation<CurveRecallView>("CurveRecallView");
|
|
||||||
containerRegistry.RegisterDialog<ValueLimitView>("ValueLimitView");
|
containerRegistry.RegisterDialog<ValueLimitView>("ValueLimitView");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,381 +0,0 @@
|
|||||||
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 MonitorModule.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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
<UserControl x:Class="MonitorModule.Views.CurveRecallView"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
|
||||||
xmlns:oxy="http://oxyplot.org/wpf"
|
|
||||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
d:DesignHeight="700"
|
|
||||||
d:DesignWidth="1200">
|
|
||||||
<UserControl.Resources>
|
|
||||||
<converters:LessThanConverter x:Key="LessThanConverter"/>
|
|
||||||
</UserControl.Resources>
|
|
||||||
|
|
||||||
<Border Background="#F5F7FA">
|
|
||||||
<Grid x:Name="RootGrid" Margin="8">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- Row0:标题 -->
|
|
||||||
<TextBlock Grid.Row="0"
|
|
||||||
Text="曲线回读"
|
|
||||||
FontSize="20" FontWeight="Bold"
|
|
||||||
Margin="4,0,0,8"/>
|
|
||||||
|
|
||||||
<!-- Row1:工具栏 -->
|
|
||||||
<Border Grid.Row="1"
|
|
||||||
Background="White"
|
|
||||||
BorderBrush="#DDD" BorderThickness="1"
|
|
||||||
CornerRadius="4" Padding="8" Margin="0,0,0,6">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<Button Content="📂 加载 CSV"
|
|
||||||
Command="{Binding LoadCsvCommand}"
|
|
||||||
Padding="12,4"/>
|
|
||||||
<Button Content="↺ 复原视图"
|
|
||||||
Command="{Binding ResetViewCommand}"
|
|
||||||
Padding="12,4" Margin="6,0,0,0"
|
|
||||||
ToolTip="按数据范围重置坐标轴缩放/平移"/>
|
|
||||||
<Button Content="✕ 清空"
|
|
||||||
Command="{Binding ClearCommand}"
|
|
||||||
Padding="12,4" Margin="6,0,0,0"
|
|
||||||
ToolTip="清空已加载的曲线数据"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Row2:主体 -->
|
|
||||||
<Grid Grid.Row="2">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition>
|
|
||||||
<ColumnDefinition.Style>
|
|
||||||
<Style TargetType="ColumnDefinition">
|
|
||||||
<Setter Property="Width" Value="260"/>
|
|
||||||
<Style.Triggers>
|
|
||||||
<DataTrigger Binding="{Binding ActualWidth, ElementName=RootGrid, Converter={StaticResource LessThanConverter}, ConverterParameter=600}" Value="True">
|
|
||||||
<Setter Property="Width" Value="0"/>
|
|
||||||
</DataTrigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
</ColumnDefinition.Style>
|
|
||||||
</ColumnDefinition>
|
|
||||||
<ColumnDefinition>
|
|
||||||
<ColumnDefinition.Style>
|
|
||||||
<Style TargetType="ColumnDefinition">
|
|
||||||
<Setter Property="Width" Value="6"/>
|
|
||||||
<Style.Triggers>
|
|
||||||
<DataTrigger Binding="{Binding ActualWidth, ElementName=RootGrid, Converter={StaticResource LessThanConverter}, ConverterParameter=600}" Value="True">
|
|
||||||
<Setter Property="Width" Value="0"/>
|
|
||||||
</DataTrigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
</ColumnDefinition.Style>
|
|
||||||
</ColumnDefinition>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- 左侧:通道列表 -->
|
|
||||||
<Border Grid.Column="0"
|
|
||||||
Background="White"
|
|
||||||
BorderBrush="#DDD" BorderThickness="1"
|
|
||||||
CornerRadius="4">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Border Grid.Row="0" Background="#ECEFF4" Padding="8,4">
|
|
||||||
<TextBlock Text="曲线通道(勾选显示/隐藏)" FontWeight="Bold"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<ListBox Grid.Row="1"
|
|
||||||
ItemsSource="{Binding Channels}"
|
|
||||||
SelectedItem="{Binding SelectedChannel}"
|
|
||||||
BorderThickness="0">
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<StackPanel Margin="2">
|
|
||||||
<DockPanel>
|
|
||||||
<CheckBox IsChecked="{Binding IsDisplayed}"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
ToolTip="勾选=在图表上显示,取消=隐藏"/>
|
|
||||||
<Border Width="10" Height="10"
|
|
||||||
CornerRadius="2"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="4,0,6,0">
|
|
||||||
<Border.Background>
|
|
||||||
<SolidColorBrush Color="SteelBlue"/>
|
|
||||||
</Border.Background>
|
|
||||||
</Border>
|
|
||||||
<TextBlock Text="{Binding DisplayName}"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
TextTrimming="CharacterEllipsis"/>
|
|
||||||
</DockPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="2"
|
|
||||||
Text="{Binding StatusMessage}"
|
|
||||||
FontSize="11" Foreground="#666"
|
|
||||||
Margin="4,2" TextWrapping="Wrap"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<GridSplitter Grid.Column="1"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Background="Transparent"/>
|
|
||||||
|
|
||||||
<!-- 右侧:OxyPlot 图表 -->
|
|
||||||
<Border Grid.Column="2"
|
|
||||||
Background="White"
|
|
||||||
BorderBrush="#DDD" BorderThickness="1"
|
|
||||||
CornerRadius="4">
|
|
||||||
<oxy:PlotView Model="{Binding Plot}"
|
|
||||||
Background="Transparent"/>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Row3:状态栏 -->
|
|
||||||
<Border Grid.Row="3"
|
|
||||||
Background="#ECEFF4"
|
|
||||||
Padding="8,4" Margin="0,6,0,0"
|
|
||||||
CornerRadius="2">
|
|
||||||
<TextBlock Text="{Binding StatusMessage}"
|
|
||||||
Foreground="#444"
|
|
||||||
FontSize="12"/>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace MonitorModule.Views
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// CurveRecallView.xaml 的交互逻辑
|
|
||||||
/// </summary>
|
|
||||||
public partial class CurveRecallView : UserControl
|
|
||||||
{
|
|
||||||
public CurveRecallView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -358,18 +358,6 @@ namespace ZLGUSBCANFD
|
|||||||
/// <param name="循环间隔毫秒">0 = 只发送一次;>0 = 按指定间隔循环发送(毫秒)</param>
|
/// <param name="循环间隔毫秒">0 = 只发送一次;>0 = 按指定间隔循环发送(毫秒)</param>
|
||||||
/// <param name="是否使用CANFD">1 = CANFD,0 = CAN</param>
|
/// <param name="是否使用CANFD">1 = CANFD,0 = CAN</param>
|
||||||
/// <returns>是否成功启动/发送</returns>
|
/// <returns>是否成功启动/发送</returns>
|
||||||
public virtual bool 发送DBC定义报文(
|
|
||||||
uint 通道号,
|
|
||||||
uint 帧ID,
|
|
||||||
Dictionary<string, double> 信号物理值字典,
|
|
||||||
int 循环间隔毫秒 = 0,
|
|
||||||
int 是否使用CANFD = 1)
|
|
||||||
{
|
|
||||||
if (通道号 >= _maxChannels || _channelHandles[通道号] == IntPtr.Zero) return false;
|
|
||||||
if (!_isDbcLoadedArray[通道号]) throw new InvalidOperationException($"通道 {通道号} 的 DBC 协议未加载,无法执行打包发送指令。");
|
|
||||||
if (循环间隔毫秒 < 0) throw new ArgumentOutOfRangeException(nameof(循环间隔毫秒), "循环间隔必须大于或等于 0。");
|
|
||||||
if (信号物理值字典 == null || 信号物理值字典.Count == 0)
|
|
||||||
throw new ArgumentException("信号物理值字典不能为空。", nameof(信号物理值字典));
|
|
||||||
|
|
||||||
if (循环间隔毫秒 == 0)
|
if (循环间隔毫秒 == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user