using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Threading; using Modbus.Device; using Modbus; using OxyPlot; using OxyPlot.Axes; using OxyPlot.Legends; using OxyPlot.Series; using System.Configuration; using System.Net.Sockets; using System.Windows.Interop; using 口罩泄露定制款; namespace LineChartDemo { public partial class MainWindow : Window { #region 图表相关变量 private PlotModel _plotModel; private LineSeries _exhaleSeries; private LineSeries _inhaleSeries; private List _exhaleData = new List(); private List _inhaleData = new List(); private DispatcherTimer _dataTimer; private double _timeCounter = 0; private const int MAX_DATA_POINTS = 30; // 最多显示30个数据点 private Function ma; private DataChange c = new DataChange(); // Modbus通信 private TcpClient _tcpClient; private IModbusMaster _modbusMaster; // 坐标轴引用 private LinearAxis _xAxis; private LinearAxis _yAxis; // 性能优化相关 private bool _isResizing = false; private readonly object _lockObj = new object(); private CancellationTokenSource _resizeTokenSource; #endregion #region Modbus相关配置 private readonly ushort _OutBreathAddress = 0x1398; // D5016 private readonly ushort _InBreathAddress = 0x1396; // D5014 #endregion public MainWindow() { InitializeComponent(); RenderOptions.ProcessRenderMode = RenderMode.Default; InitializeModbusTcp(); InitializePlot(); InitializeEvents(); } #region 窗口大小变化事件 protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) { base.OnRenderSizeChanged(sizeInfo); if (!_isResizing) { _isResizing = true; _dataTimer?.Stop(); } _resizeTokenSource?.Cancel(); _resizeTokenSource = new CancellationTokenSource(); var token = _resizeTokenSource.Token; Task.Delay(50, token).ContinueWith(_ => { Dispatcher.Invoke(() => { if (!token.IsCancellationRequested) { _plotModel?.InvalidatePlot(true); _isResizing = false; if (StopBtn.IsEnabled) { _dataTimer?.Start(); } } }); }, token); } #endregion #region Modbus初始化 private void InitializeModbusTcp() { try { string plcIp = ConfigurationManager.AppSettings["PLC2_IP"]; int plcPort = int.Parse(ConfigurationManager.AppSettings["PLC2_Port"] ?? "502"); _tcpClient = new TcpClient(); var connectResult = _tcpClient.BeginConnect(plcIp, plcPort, null, null); var success = connectResult.AsyncWaitHandle.WaitOne(3000); if (!success || !_tcpClient.Connected) throw new Exception("连接PLC超时,请检查IP和端口"); _modbusMaster = ModbusIpMaster.CreateIp(_tcpClient); _modbusMaster.Transport.ReadTimeout = 1000; _modbusMaster.Transport.WriteTimeout = 1000; ma = new Function(_modbusMaster); StatusText.Text = "状态:Modbus连接成功"; StatusText.Foreground = new SolidColorBrush(Color.FromRgb(76, 175, 80)); } catch (Exception ex) { ShowError($"Modbus初始化失败: {ex.Message}"); StatusText.Text = "状态:连接失败"; StatusText.Foreground = new SolidColorBrush(Color.FromRgb(239, 83, 80)); } } private void ShowError(string message) => MessageBox.Show(message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); #endregion #region 图表初始化 private void InitializePlot() { _plotModel = new PlotModel { Title = "呼气流量与吸气流量监测(最新30个数据)", TitleFontSize = 18, TitleFontWeight = OxyPlot.FontWeights.Bold, Background = OxyColors.White, PlotAreaBackground = OxyColor.FromRgb(250, 252, 255), //AnimationDuration = TimeSpan.Zero }; var legend = new Legend { LegendPlacement = LegendPlacement.Outside, LegendPosition = LegendPosition.TopRight, FontSize = 14 }; _plotModel.Legends.Add(legend); _exhaleSeries = new LineSeries { Title = "呼气流量 (L/min)", Color = OxyColor.FromRgb(239, 83, 80), StrokeThickness = 2, MarkerType = MarkerType.None, MarkerSize = 4 }; _inhaleSeries = new LineSeries { Title = "吸气流量 (L/min)", Color = OxyColor.FromRgb(66, 165, 245), StrokeThickness = 2, MarkerType = MarkerType.None, MarkerSize = 4 }; _plotModel.Series.Add(_exhaleSeries); _plotModel.Series.Add(_inhaleSeries); _xAxis = new LinearAxis { Position = AxisPosition.Bottom, Title = "时间 (秒)", TitleFontSize = 14, AxislineColor = OxyColors.LightGray, MajorGridlineColor = OxyColor.FromRgb(220, 231, 243), MajorGridlineStyle = LineStyle.Solid, MinorGridlineStyle = LineStyle.None, FontSize = 12, Minimum = 0 }; _yAxis = new LinearAxis { Position = AxisPosition.Left, Title = "流量 (L/min)", TitleFontSize = 14, AxislineColor = OxyColors.LightGray, MajorGridlineColor = OxyColor.FromRgb(220, 231, 243), MajorGridlineStyle = LineStyle.Solid, MinorGridlineStyle = LineStyle.None, FontSize = 12, MajorStep = 5 }; _plotModel.Axes.Add(_xAxis); _plotModel.Axes.Add(_yAxis); PlotView.Model = _plotModel; } #endregion #region 事件与定时器初始化 private void InitializeEvents() { _dataTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; _dataTimer.Tick += async (s, e) => await UpdateData(); StartBtn.Click += (s, e) => { _dataTimer.Start(); _ = UpdateData(); StartBtn.IsEnabled = false; StopBtn.IsEnabled = true; StatusText.Text = "状态:正在监测"; StatusText.Foreground = new SolidColorBrush(Color.FromRgb(76, 175, 80)); _exhaleSeries.MarkerType = MarkerType.Circle; _inhaleSeries.MarkerType = MarkerType.Circle; }; StopBtn.Click += (s, e) => { _dataTimer.Stop(); StartBtn.IsEnabled = true; StopBtn.IsEnabled = false; StatusText.Text = "状态:已停止"; StatusText.Foreground = new SolidColorBrush(Color.FromRgb(156, 39, 176)); }; ClearBtn.Click += (s, e) => { lock (_lockObj) { _exhaleData.Clear(); _inhaleData.Clear(); _exhaleSeries.Points.Clear(); _inhaleSeries.Points.Clear(); _timeCounter = 0; AdjustYAxisRange(); AdjustXAxisRange(); _plotModel.InvalidatePlot(true); } }; } #endregion #region 数据更新与处理(核心:限制20个数据点) private async Task UpdateData() { if (_isResizing) return; if (!IsModbusConnected()) { StatusText.Text = "状态:Modbus已断开"; StatusText.Foreground = new SolidColorBrush(Color.FromRgb(239, 83, 80)); return; } try { var (exhaleValue, inhaleValue) = await Task.Run(() => ReadModbusData(), CancellationToken.None); lock (_lockObj) { // 添加新数据点 var exhalePoint = new DataPoint(_timeCounter, exhaleValue); var inhalePoint = new DataPoint(_timeCounter, inhaleValue); _exhaleData.Add(exhalePoint); _inhaleData.Add(inhalePoint); // 关键:只保留最新的30个数据点 if (_exhaleData.Count > MAX_DATA_POINTS) { // 移除最旧的数据点(超出20个的部分) int removeCount = _exhaleData.Count - MAX_DATA_POINTS; _exhaleData.RemoveRange(0, removeCount); _inhaleData.RemoveRange(0, removeCount); } _timeCounter += 0.5; // 更新折线数据 _exhaleSeries.Points.Clear(); _inhaleSeries.Points.Clear(); _exhaleData.ForEach(p => _exhaleSeries.Points.Add(p)); _inhaleData.ForEach(p => _inhaleSeries.Points.Add(p)); // 调整坐标轴范围(基于当前20个数据) AdjustYAxisRange(); AdjustXAxisRange(); } _plotModel.InvalidatePlot(false); } catch (Exception ex) { StatusText.Text = $"状态:数据错误 - {ex.Message}"; StatusText.Foreground = new SolidColorBrush(Color.FromRgb(239, 83, 80)); } } private (float exhale, float inhale) ReadModbusData() { try { ushort[] exhalationData = _modbusMaster?.ReadHoldingRegisters(0x01, _OutBreathAddress, 2); ushort[] inhalationData = _modbusMaster?.ReadHoldingRegisters(0x01, _InBreathAddress, 2); float exhaleValue = c.UshortToFloat(exhalationData[1], exhalationData[0]); float inhaleValue = c.UshortToFloat(inhalationData[1], inhalationData[0]); return ((float)Math.Round(exhaleValue, 4), (float)Math.Round(inhaleValue, 4)); } catch (Exception ex) { throw new Exception($"Modbus读取错误:{ex.Message}"); } } private void AdjustYAxisRange() { var allYValues = new List(); allYValues.AddRange(_exhaleData.Select(p => p.Y)); allYValues.AddRange(_inhaleData.Select(p => p.Y)); if (!allYValues.Any()) { // 无数据时默认范围(预留一定空间,避免后续数据突然增大时轴范围不足) _yAxis.Minimum = 0; _yAxis.Maximum = 10; // 初始值设大一些,应对可能的大数值 return; } double currentMin = allYValues.Min(); double currentMax = allYValues.Max(); // 计算当前数据的波动范围 double dataRange = currentMax - currentMin; // 1. 底部留固定余量(避免数据贴底) double bottomPadding = Math.Max(0.5, dataRange * 0.1); // 取两者较大值,确保至少0.5 double newMin = currentMin - bottomPadding; // 若数据全为正数,Y轴从0开始更合理(可选,根据业务场景) if (newMin < 0 && currentMin >= 0) newMin = 0; // 2. 顶部留动态余量(随数据增大自动扩展) double topPadding = Math.Max(1, dataRange * 0.2); // 顶部余量稍大,应对突发大值 double newMax = currentMax + topPadding; // 3. 确保Y轴范围不会“收缩”(避免数据忽大忽小时轴频繁跳动) // 仅当新范围的上限大于当前轴上限时,才更新上限 if (newMax > _yAxis.Maximum) { _yAxis.Maximum = newMax; } // 下限可根据当前数据动态收缩(但不小于0,若数据全为正) _yAxis.Minimum = newMin; // 强制设置轴的“弹性”,允许数据超出当前范围时自动扩展(OxyPlot特性) _yAxis.IsZoomEnabled = false; // 禁止用户手动缩放(避免干扰自动范围) } private void AdjustXAxisRange() { if (_exhaleData.Count == 0 && _inhaleData.Count == 0) { _xAxis.Maximum = 1; return; } // X轴范围基于最新20个数据的时间范围 double maxX = Math.Max( _exhaleData.DefaultIfEmpty().Max(p => p.X), _inhaleData.DefaultIfEmpty().Max(p => p.X) ); // 当数据满20个时,X轴范围固定为这20个数据的时间跨度 if (_exhaleData.Count >= MAX_DATA_POINTS) { double minX = _exhaleData.Min(p => p.X); _xAxis.Minimum = minX; _xAxis.Maximum = maxX + 0.5; // 右侧留一点余量 } else { _xAxis.Minimum = 0; _xAxis.Maximum = maxX < 1 ? 1 : maxX * 1.1; } } private bool IsModbusConnected() { return _tcpClient != null && _tcpClient.Connected && _modbusMaster != null; } #endregion #region 窗口加载与资源释放 private void Window_Loaded(object sender, RoutedEventArgs e) { try { string imagePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources/sleep2.jpg"); if (System.IO.File.Exists(imagePath)) { var bitmap = new BitmapImage(); bitmap.BeginInit(); bitmap.UriSource = new Uri(imagePath, UriKind.Absolute); bitmap.DecodePixelWidth = (int)this.Width; bitmap.CacheOption = BitmapCacheOption.OnLoad; bitmap.EndInit(); Background = new ImageBrush { ImageSource = bitmap, Stretch = Stretch.UniformToFill, //CacheOption = BitmapCacheOption.OnLoad }; } else { Console.WriteLine($"背景图片不存在: {imagePath}"); } // 获取屏幕工作区(排除任务栏) var workingArea = SystemParameters.WorkArea; // 计算居中位置 this.Left = workingArea.Left + (workingArea.Width - this.Width) / 2; this.Top = workingArea.Top + (workingArea.Height - this.Height) / 2; } catch (Exception ex) { Console.WriteLine($"加载背景失败: {ex.Message}"); Background = Brushes.White; } } protected override void OnClosed(EventArgs e) { base.OnClosed(e); _dataTimer?.Stop(); _tcpClient?.Close(); _resizeTokenSource?.Dispose(); } #endregion } }