```csharp
using System;
using System.Collections.Concurrent;
using System.Drawing;
using System.Windows.Forms;
using ScottPlot;
using ScottPlot.WinForms;
namespace ScottPlotRealTimeDemo
{
public partial class MainForm : Form
{
// 模拟数据生产者
private System.Windows.Forms.Timer _dataProducerTimer;
private ChartConsumer _chartConsumer;
public MainForm()
{
InitializeComponent();
SetupChartConsumer();
SetupDataProducer();
}
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.formsPlot1 = new ScottPlot.WinForms.FormsPlot();
this.btnStart = new Button();
this.btnStop = new Button();
this.lblStatus = new Label();
this.SuspendLayout();
// formsPlot1 配置
this.formsPlot1.Dock = DockStyle.Top;
this.formsPlot1.Location = new Point(0, 0);
this.formsPlot1.Name = "formsPlot1";
this.formsPlot1.Size = new Size(800, 400);
this.formsPlot1.TabIndex = 0;
// btnStart 配置
this.btnStart.Location = new Point(20, 420);
this.btnStart.Name = "btnStart";
this.btnStart.Size = new Size(100, 40);
this.btnStart.Text = "开始数据流";
this.btnStart.Click += BtnStart_Click;
// btnStop 配置
this.btnStop.Location = new Point(140, 420);
this.btnStop.Name = "btnStop";
this.btnStop.Size = new Size(100, 40);
this.btnStop.Text = "停止数据流";
this.btnStop.Enabled = false;
this.btnStop.Click += BtnStop_Click;
// lblStatus 配置
this.lblStatus.AutoSize = true;
this.lblStatus.Location = new Point(260, 430);
this.lblStatus.Name = "lblStatus";
this.lblStatus.Size = new Size(200, 20);
this.lblStatus.Text = "状态: 等待开始";
// MainForm 配置
this.ClientSize = new Size(800, 500);
this.Controls.Add(this.lblStatus);
this.Controls.Add(this.btnStop);
this.Controls.Add(this.btnStart);
this.Controls.Add(this.formsPlot1);
this.Text = "ScottPlot 实时数据可视化 Demo";
this.ResumeLayout(false);
}
private void SetupChartConsumer()
{
// 创建图表消费者,配置图表样式 [ref_1]
_chartConsumer = new ChartConsumer(
id: "实时温度监控",
formsPlot: formsPlot1,
title: "温度传感器实时数据",
seriesColor: Color.FromArgb(255, 50, 150, 255) // 蓝色系列
)
{
MaxDataPoints = 500, // 最多显示500个数据点
UiUpdateIntervalMs = 100 // UI每100ms更新一次 (10 FPS)
};
}
private void SetupDataProducer()
{
// 创建数据生产者定时器,模拟实时数据源
_dataProducerTimer = new System.Windows.Forms.Timer
{
Interval = 200, // 每200ms产生一个数据点
Enabled = false
};
_dataProducerTimer.Tick += (sender, e) =>
{
// 生成模拟数据:温度值 + 随机波动
var now = DateTime.Now;
double baseTemp = 25.0;
double randomFactor = (new Random().NextDouble() - 0.5) * 2.0; // -1到1的随机值
double currentTemp = baseTemp + randomFactor;
// 创建处理后的度量数据
var metric = new ProcessedMetric
{
Timestamp = now,
NormalizedValue = currentTemp,
RawValue = currentTemp,
SensorId = "TempSensor_001"
};
// 发送数据到消费者 [ref_1]
_chartConsumer.OnDataReceived(metric);
// 更新状态标签
lblStatus.Text = $"状态: 运行中 - 最新温度: {currentTemp:F2}°C ({now:HH:mm:ss})";
};
}
private void BtnStart_Click(object sender, EventArgs e)
{
_dataProducerTimer.Start();
btnStart.Enabled = false;
btnStop.Enabled = true;
lblStatus.Text = "状态: 数据流已启动";
}
private void BtnStop_Click(object sender, EventArgs e)
{
_dataProducerTimer.Stop();
_chartConsumer.OnCompleted(); // 通知消费者数据流结束
btnStart.Enabled = true;
btnStop.Enabled = false;
lblStatus.Text = "状态: 已停止";
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
// 确保资源被正确释放 [ref_3]
_dataProducerTimer?.Stop();
_chartConsumer?.Dispose();
base.OnFormClosing(e);
}
// 模拟的数据结构
public class ProcessedMetric
{
public DateTime Timestamp { get; set; }
public double NormalizedValue { get; set; }
public double RawValue { get; set; }
public string SensorId { get; set; }
}
// ScottPlot 图表消费者实现
public class ChartConsumer : IConsumer<ProcessedMetric>, IDisposable
{
public string Id { get; }
public string ChartTitle { get; set; }
public Color SeriesColor { get; set; }
public int MaxDataPoints { get; set; }
public int UiUpdateIntervalMs { get; set; }
private readonly FormsPlot _formsPlot;
private readonly Control _uiThreadControl;
private Plottables.DataLogger _dataLogger;
private readonly System.Windows.Forms.Timer _uiUpdateTimer;
private readonly BlockingCollection<(double x, double y)> _dataQueue;
private volatile bool _isDisposed = false;
public ChartConsumer(string id, FormsPlot formsPlot, string title = null, Color? seriesColor = null)
{
Id = id ?? throw new ArgumentNullException(nameof(id));
_formsPlot = formsPlot ?? throw new ArgumentNullException(nameof(formsPlot));
_uiThreadControl = formsPlot;
ChartTitle = title ?? "实时数据图表";
SeriesColor = seriesColor ?? Color.Blue;
MaxDataPoints = 1000;
UiUpdateIntervalMs = 50;
_dataQueue = new BlockingCollection<(double, double)>();
// 初始化图表
InitializeChartOnUiThread();
// 设置UI更新定时器
_uiUpdateTimer = new System.Windows.Forms.Timer { Interval = UiUpdateIntervalMs };
_uiUpdateTimer.Tick += OnUiTimerTick;
_uiUpdateTimer.Start();
}
private void InitializeChartOnUiThread()
{
if (_uiThreadControl.InvokeRequired)
{
_uiThreadControl.Invoke(new Action(InitializeChartOnUiThread));
return;
}
_formsPlot.Plot.Clear();
_formsPlot.Plot.Title(ChartTitle);
// 创建DataLogger并配置样式 [ref_1]
_dataLogger = _formsPlot.Plot.Add.DataLogger();
_dataLogger.Color = SeriesColor;
_dataLogger.LineWidth = 2;
_dataLogger.MarkerSize = 5;
// 配置坐标轴 [ref_1]
_formsPlot.Plot.Axes.DateTimeTicksBottom();
_formsPlot.Plot.Axes.SetLimitsY(20, 30); // 设置Y轴范围
_formsPlot.Plot.Axes.Margins(horizontal: 0.05, vertical: 0.1);
// 添加网格线
_formsPlot.Plot.Grid.MajorLineColor = Color.LightGray;
_formsPlot.Plot.Grid.MinorLineColor = Color.FromArgb(30, 200, 200, 200);
_formsPlot.Refresh();
}
public void OnDataReceived(ProcessedMetric data)
{
if (_isDisposed || data == null) return;
double x = data.Timestamp.ToOADate(); // 转换为OADate格式 [ref_1]
double y = data.NormalizedValue;
_dataQueue.TryAdd((x, y), TimeSpan.FromMilliseconds(1));
}
private void OnUiTimerTick(object sender, EventArgs e)
{
if (_isDisposed || _dataQueue.Count == 0) return;
bool dataAdded = false;
int maxBatchSize = 50; // 每次最多处理50个点
for (int i = 0; i < maxBatchSize && _dataQueue.TryTake(out var point); i++)
{
_dataLogger.Add(point.x, point.y);
dataAdded = true;
}
if (dataAdded)
{
// 自动调整坐标轴范围 [ref_1]
_formsPlot.Plot.Axes.AutoScale();
_formsPlot.Refresh();
}
}
public void OnCompleted()
{
if (_uiThreadControl.InvokeRequired)
{
_uiThreadControl.Invoke(new Action(OnCompleted));
return;
}
// 添加最终标记
_formsPlot.Plot.Add.Text("数据流结束", DateTime.Now.ToOADate(),
_dataLogger.Count > 0 ? _dataLogger.GetYs().Last() : 0);
_formsPlot.Refresh();
}
public void Dispose()
{
if (_isDisposed) return;
_isDisposed = true;
// 停止定时器
if (_uiUpdateTimer != null)
{
if (_uiThreadControl.InvokeRequired)
{
_uiThreadControl.Invoke(new Action(() =>
{
_uiUpdateTimer.Stop();
_uiUpdateTimer.Dispose();
}));
}
else
{
_uiUpdateTimer.Stop();
_uiUpdateTimer.Dispose();
}
}
// 清理队列
_dataQueue.CompleteAdding();
while (_dataQueue.TryTake(out _)) { }
_dataQueue.Dispose();
GC.SuppressFinalize(this);
}
}
// 消费者接口定义
public interface IConsumer<T>
{
string Id { get; }
void OnDataReceived(T data);
void OnCompleted();
}
// Windows Forms 控件声明
private ScottPlot.WinForms.FormsPlot formsPlot1;
private Button btnStart;
private Button btnStop;
private Label lblStatus;
private System.ComponentModel.IContainer components = null;
}
// 程序入口
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}
```
## Demo 功能详解
### 1. 项目结构与依赖
这是一个完整的 WinForms 应用程序,需要以下 NuGet 包支持:
- **ScottPlot.WinForms** (5.x 版本) - 图表绘制核心库 [ref_1]
- **System.Collections.Concurrent** - 线程安全集合(已包含在.NET中)
### 2. 核心功能模块
#### 2.1 主窗体 (`MainForm`)
```csharp
// 主要控件:
// - formsPlot1: ScottPlot 图表控件,用于实时数据可视化
// - btnStart/btnStop: 控制数据流的开始和停止
// - lblStatus: 显示当前运行状态和最新数据
```
#### 2.2 数据生产者模拟
```csharp
// 使用 Timer 模拟实时数据源:
// - 每 200ms 生成一个温度数据点
// - 包含基础值 (25°C) 和随机波动 (±1°C)
// - 时间戳精确到当前时间
```
#### 2.3 图表消费者 (`ChartConsumer`)
这是 Demo 的核心组件,实现了以下功能:
| 功能特性 | 实现方式 | 技术要点 |
|---------|---------|---------|
| **线程安全数据传递** | 使用 `BlockingCollection` 缓冲数据 | 生产者和消费者线程解耦,避免UI阻塞 [ref_2] |
| **高效UI更新** | 定时器批量处理 + 条件刷新 | 减少 `Refresh()` 调用频率,提升性能 |
| **实时坐标轴** | 自动缩放 + 时间轴格式化 | 使用 `DateTimeTicksBottom()` 显示时间 [ref_1] |
| **资源管理** | 实现 `IDisposable` 接口 | 正确释放定时器和集合资源 [ref_3] |
| **配置灵活性** | 通过构造函数参数配置 | 可自定义标题、颜色、更新频率等 |
#### 2.4 数据流处理流程
```mermaid
graph TD
A[数据生产者定时器] --> B[生成模拟温度数据]
B --> C[创建 ProcessedMetric 对象]
C --> D[调用 OnDataReceived]
D --> E[数据进入 BlockingCollection]
E --> F[UI 更新定时器]
F --> G{队列有数据?}
G -->|是| H[批量取出数据点]
H --> I[添加到 DataLogger]
I --> J[自动调整坐标轴]
J --> K[刷新图表显示]
G -->|否| L[跳过本次更新]
K --> M[更新状态标签]
```
### 3. 关键代码说明
#### 3.1 ScottPlot 图表初始化
```csharp
// 创建 DataLogger 对象(用于高效追加数据)[ref_1]
_dataLogger = _formsPlot.Plot.Add.DataLogger();
// 配置时间轴(X轴显示日期时间)[ref_1]
_formsPlot.Plot.Axes.DateTimeTicksBottom();
// 设置图表样式
_dataLogger.Color = SeriesColor; // 线条颜色
_dataLogger.LineWidth = 2; // 线宽
_dataLogger.MarkerSize = 5; // 数据点大小
```
#### 3.2 线程安全的数据传递
```csharp
// 生产者端:非阻塞添加数据
_dataQueue.TryAdd((x, y), TimeSpan.FromMilliseconds(1));
// 消费者端:批量取出数据
while (_dataQueue.TryTake(out var point, TimeSpan.Zero))
{
_dataLogger.Add(point.x, point.y);
dataAdded = true;
}
```
#### 3.3 性能优化策略
```csharp
// 1. 批量处理:每次最多处理50个数据点
int maxBatchSize = 50;
// 2. 条件刷新:只有新数据时才刷新图表
if (dataAdded)
{
_formsPlot.Plot.Axes.AutoScale();
_formsPlot.Refresh(); // 仅在此处调用刷新
}
// 3. 控制更新频率:通过定时器间隔控制
UiUpdateIntervalMs = 100; // 10 FPS 的UI更新
```
### 4. 使用与扩展指南
#### 4.1 运行 Demo
1. 创建新的 WinForms 项目
2. 安装 ScottPlot.WinForms NuGet 包
3. 将上述代码复制到项目中
4. 运行程序,点击"开始数据流"按钮
#### 4.2 自定义配置
```csharp
// 创建消费者时自定义参数
_chartConsumer = new ChartConsumer(
id: "CPU使用率监控",
formsPlot: formsPlot1,
title: "CPU使用率实时监控",
seriesColor: Color.Red
)
{
MaxDataPoints = 1000, // 最多显示1000个点
UiUpdateIntervalMs = 200 // 每200ms更新一次UI (5 FPS)
};
```
#### 4.3 扩展功能建议
1. **多数据源支持**:修改 `ChartConsumer` 支持多个数据序列
2. **数据持久化**:添加数据保存功能,支持导出为CSV或图像
3. **报警功能**:当数据超过阈值时显示警告
4. **历史回放**:添加暂停、回放历史数据功能
5. **样式主题**:支持深色/浅色主题切换
### 5. 常见问题解决
| 问题现象 | 可能原因 | 解决方案 |
|---------|---------|---------|
| 图表不更新 | UI线程阻塞 | 确保所有图表操作都在UI线程执行 [ref_2] |
| 内存持续增长 | 数据点未清理 | 设置合适的 `MaxDataPoints` 或实现循环缓冲区 |
| 数据点显示异常 | 时间格式问题 | 确认使用 `ToOADate()` 转换时间戳 [ref_1] |
| 界面卡顿 | 刷新频率过高 | 调整 `UiUpdateIntervalMs` 参数 |
这个 Demo 展示了如何在 WinForms 中使用 ScottPlot 实现高性能的实时数据可视化,采用了生产者-消费者模式确保线程安全,并通过批量处理和条件刷新优化了性能表现 [ref_1]。代码结构清晰,易于扩展和集成到实际项目中。