跳转至

绘制图表

在 WinForms 应用中进行数据可视化,使用功能强大且易于集成的第三方库是最高效的选择。本章将详细介绍 ScottPlot,一个专为 .NET 设计的优秀开源绘图库,并提供多种实用场景的详细代码示例。

为何选择 ScottPlot?

  • 现代且易用: API 设计简洁直观,学习曲线平缓。
  • 高性能: 对大数据量和实时数据展示有很好的优化,性能卓越。
  • 交互性强: 内置支持鼠标缩放、平移等交互功能。
  • 集成方便: 提供专门的 ScottPlot.WinForms 包,可以像使用普通 Panel 一样在设计器中拖拽和布局。
  • 开源免费: 可用于商业项目。

入门指南

1. 安装

通过 Visual Studio 的 NuGet 包管理器控制台安装 ScottPlot.WinForms

Install-Package ScottPlot.WinForms

安装后,请重新编译你的项目FormsPlot 控件即会出现在 Visual Studio 的工具箱中。

2. 添加图表控件

从工具箱中拖拽一个 FormsPlot 控件到你的窗体上。它就是一个标准的 WinForms 控件,可以设置 Dock, Anchor 等属性来控制其布局。假设其 Name 属性为 formsPlot1

3. 官方文档

ScottPlot 提供了非常详尽的官方文档和示例,你可以在其官网上找到:

ScottPlot 5.0 Cookbook (官方示例)


基础图表类型

曲线图/散点图 (Scatter Plot)

这是最常见的图表类型,用于展示数据点和趋势。

// 1. 准备数据
double[] days = { 1, 2, 3, 4, 5, 6, 7 };
double[] temperatures = { 12, 15, 17, 16, 18, 17, 20 };

// 2. 在 FormsPlot 控件上添加一个散点图
var scatter = formsPlot1.Plot.Add.Scatter(days, temperatures);

// 3. (可选) 自定义线条和标记点的样式
scatter.Color = ScottPlot.Colors.DarkBlue;
scatter.LineWidth = 2;
scatter.MarkerSize = 10;
scatter.MarkerShape = ScottPlot.MarkerShape.FilledCircle;
scatter.Label = "每日气温"; // 用于图例显示

// 4. (可选) 自定义图表标题和轴标签
formsPlot1.Plot.Title("一周气温趋势");
formsPlot1.Plot.XLabel("日期 (天)");
formsPlot1.Plot.YLabel("平均气温 (摄氏度)");
formsPlot1.Plot.ShowLegend(); // 显示图例

// 5. 刷新图表以应用更改
formsPlot1.Refresh();

ScottPlot Line Chart

柱状图 (Bar Plot)

柱状图非常适合比较不同类别的数据。

// 1. 准备数据
double[] sales = { 150, 320, 280, 410, 380 };
string[] productNames = { "苹果", "香蕉", "樱桃", "葡萄", "橙子" };

// 2. 创建柱状图
formsPlot1.Plot.Add.Bars(productNames, sales);

// 3. 自定义
formsPlot1.Plot.Title("水果销量对比");
formsPlot1.Plot.YLabel("销量 (公斤)");

// 4. 刷新
formsPlot1.Refresh();

ScottPlot Bar Chart

饼图 (Pie Chart)

饼图用于展示各部分占整体的比例。

// 1. 准备数据
var slices = new ScottPlot.PieSlice[]
{
    new() { Value = 30, FillColor = ScottPlot.Colors.Red, Label = "市场部" },
    new() { Value = 40, FillColor = ScottPlot.Colors.Blue, Label = "研发部" },
    new() { Value = 25, FillColor = ScottPlot.Colors.Green, Label = "销售部" },
    new() { Value = 5, FillColor = ScottPlot.Colors.Gray, Label = "行政部" },
};

// 2. 添加饼图
var pie = formsPlot1.Plot.Add.Pie(slices);

// 3. (可选) 自定义
pie.ShowLabels = true; // 在每个扇区上显示标签
formsPlot1.Plot.Title("各部门人员占比");

// 4. 刷新
formsPlot1.Refresh();

交互与动态更新

鼠标交互

ScottPlot 默认启用鼠标交互: - 左键拖拽: 平移图表。 - 右键拖拽: 缩放图表。 - 滚轮滚动: 缩放图表。

你可以通过修改 formsPlot1.Interaction 对象的属性来禁用或自定义这些行为。

获取鼠标坐标

可以订阅 MouseMove 事件来获取鼠标在图表数据坐标系中的实时位置。

formsPlot1.MouseMove += (s, e) =>
{
    // 从屏幕像素坐标转换为图表数据坐标
    Pixel mousePixel = new(e.Location.X, e.Location.Y);
    Coordinates mouseCoords = formsPlot1.Plot.GetCoordinates(mousePixel);

    // 在窗口标题栏或 StatusStrip 中显示坐标
    this.Text = $"X: {mouseCoords.X:F2}, Y: {mouseCoords.Y:F2}";
};

实时更新图表

要实现动态实时更新的图表,可以使用 System.Windows.Forms.Timer。为了获得最佳性能,我们应该直接修改绘图对象的数据源,而不是每次都 Clear()Add()

注意: 对于高频更新(如每秒数百次),应使用 Signal 图表类型,它为大数据和实时渲染做了极致优化。以下示例适用于低频更新场景。

using ScottPlot.Plottables;

public partial class RealTimeForm : Form
{
    private System.Windows.Forms.Timer _updateTimer = new();
    private Scatter _realtimePlot;
    private List<double> _xData = new();
    private List<double> _yData = new();
    private int _nextDataIndex = 0;

    public RealTimeForm()
    {
        InitializeComponent();

        // 1. 初始化一个空的绘图对象,并传入可变的数据源 (List<T>)
        _realtimePlot = formsPlot1.Plot.Add.Scatter(_xData, _yData);
        _realtimePlot.Color = ScottPlot.Colors.Red;
        formsPlot1.Plot.Title("实时数据");

        // 2. 设置定时器
        _updateTimer.Interval = 500; // 0.5秒
        _updateTimer.Tick += (s, e) => UpdateData();
        _updateTimer.Start();
    }

    private void UpdateData()
    {
        // 3. 向数据源添加新数据
        _xData.Add(_nextDataIndex++);
        _yData.Add(new Random().NextDouble() * 100);

        // (可选) 如果数据量过大,可以移除旧数据
        if (_xData.Count > 100)
        {
            _xData.RemoveAt(0);
            _yData.RemoveAt(0);
        }

        // 4. 自动调整坐标轴范围以适应新数据
        formsPlot1.Plot.Axes.AutoScale();

        // 5. 刷新图表
        formsPlot1.Refresh();
    }
}

高级定制

自定义轴刻度标签

如果希望坐标轴显示文字标签而不是数字,可以自定义刻度生成器。

double[] positions = { 0, 1, 2, 3 };
double[] values = { 10, 25, 18, 32 };

// 创建标签数组
var labels = new string[] { "春季", "夏季", "秋季", "冬季" };

formsPlot1.Plot.Add.Bars(positions, values);

// 创建一个自定义的刻度生成器
var customTickGenerator = new ScottPlot.TickGenerators.NumericManual();
for (int i = 0; i < positions.Length; i++)
{
    customTickGenerator.Add(positions[i], labels[i]);
}

// 将其应用到X轴
formsPlot1.Plot.Axes.Bottom.TickGenerator = customTickGenerator;

// 确保标签不旋转
formsPlot1.Plot.Axes.Bottom.TickLabelStyle.Rotation = 0;

formsPlot1.Refresh();

多Y轴(次坐标轴)

当需要在一张图上展示两种不同单位的数据时(例如温度和降雨量),可以使用次Y轴。

// 1. 准备两组数据
double[] temp = { 20, 22, 24, 23, 25 };
double[] rainfall = { 5, 2, 10, 15, 8 };
double[] days = { 1, 2, 3, 4, 5 };

// 2. 创建第二个Y轴
var yAxis2 = formsPlot1.Plot.Axes.AddRightAxis("降雨量 (mm)");

// 3. 绘制第一组数据(使用默认左侧Y轴)
var tempPlot = formsPlot1.Plot.Add.Scatter(days, temp);
formsPlot1.Plot.Axes.Left.Label.Text = "温度 (°C)";

// 4. 绘制第二组数据,并将其关联到新的Y轴
var rainPlot = formsPlot1.Plot.Add.Bars(days, rainfall);
rainPlot.Axes.YAxis = yAxis2; // 关键步骤

formsPlot1.Refresh();