读者只要按照本文步骤新建 WinForms 项目,并复制对应代码,就可以绘制出下面几种常用图表:
折线图

柱状图

饼图

散点图

一、开发环境
本文使用环境:
Visual Studio 2022 .NET 8 Windows Forms C#
也可以使用 .NET 6 或 .NET 7,只要是 Windows Forms 项目即可。
二、新建 WinForms 项目
打开 Visual Studio,创建项目:
项目类型:Windows 窗体应用 项目名称:DrawWinformChart 框架:.NET 8
或者使用命令行创建:
dotnet new winforms -n DrawWinformChart -f net8.0-windows cd DrawWinformChart
项目文件 DrawWinformChart.csproj 应该类似这样:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
三、最终项目结构
按照本文完成后,项目结构如下:
DrawWinformChart │ ├─ Controls │ └─ SimpleChartControl.cs │ ├─ Models │ └─ ChartDataPoint.cs │ ├─ Form1.cs ├─ Program.cs └─ DrawWinformChart.csproj
说明:
| 文件 | 作用 |
|---|---|
ChartDataPoint.cs |
定义图表数据 |
SimpleChartControl.cs |
自定义图表控件,负责绘制图表 |
Form1.cs |
创建界面,准备数据,调用绘图方法 |
Program.cs |
WinForms 程序入口 |
四、创建图表数据类
先创建文件夹:
Models
然后在 Models 文件夹中新建文件:
ChartDataPoint.cs
写入以下代码:
namespace DrawWinformChart.Models
{
/// <summary>
/// 图表中的一个数据点。
///
/// 折线图、柱状图、饼图主要使用 Label 和 Value。
/// 散点图会同时使用 X 和 Value。
/// </summary>
public class ChartDataPoint
{
/// <summary>
/// 数据名称。
/// 例如“一月”、“产品A”、“线上商城”等。
/// </summary>
public string Label { get; set; }
/// <summary>
/// 横坐标。
/// 主要用于散点图。
/// </summary>
public double X { get; set; }
/// <summary>
/// 数据值。
/// 折线图中表示 Y 值。
/// 柱状图中表示柱子高度。
/// 饼图中表示占比数值。
/// 散点图中表示纵坐标。
/// </summary>
public double Value { get; set; }
/// <summary>
/// 普通图表使用这个构造方法。
/// </summary>
public ChartDataPoint(string label, double value)
{
Label = label;
Value = value;
}
/// <summary>
/// 散点图使用这个构造方法。
/// </summary>
public ChartDataPoint(string label, double x, double value)
{
Label = label;
X = x;
Value = value;
}
}
}
这个类可以理解为“图表的一条数据”。
例如:
new ChartDataPoint("一月", 120)
表示:
一月的数据值是 120
五、创建自定义图表控件
创建文件夹:
Controls
然后在 Controls 文件夹中新建文件:
SimpleChartControl.cs
写入以下完整代码:
using System.Drawing.Drawing2D;
using DrawWinformChart.Models;
namespace DrawWinformChart.Controls
{
/// <summary>
/// 一个简单的 WinForms 图表控件。
///
/// 外部调用 DrawLineChart、DrawBarChart、DrawPieChart、DrawScatterChart 方法,
/// 控件内部保存数据,然后在 OnPaint 方法中完成绘制。
/// </summary>
public class SimpleChartControl : UserControl
{
/// <summary>
/// 当前要绘制的图表类型。
/// 可选值:none、line、bar、pie、scatter。
/// </summary>
private string _chartType = "none";
/// <summary>
/// 图表标题。
/// </summary>
private string _title = string.Empty;
/// <summary>
/// 图表数据。
/// </summary>
private List<ChartDataPoint> _data = new();
public SimpleChartControl()
{
// 开启双缓冲,可以减少图表刷新时的闪烁。
DoubleBuffered = true;
// 当控件大小发生变化时,让控件自动重绘。
SetStyle(ControlStyles.ResizeRedraw, true);
BackColor = Color.White;
}
/// <summary>
/// 绘制折线图。
/// </summary>
public void DrawLineChart(List<ChartDataPoint> data, string title)
{
_chartType = "line";
_title = title;
_data = data;
Invalidate();
}
/// <summary>
/// 绘制柱状图。
/// </summary>
public void DrawBarChart(List<ChartDataPoint> data, string title)
{
_chartType = "bar";
_title = title;
_data = data;
Invalidate();
}
/// <summary>
/// 绘制饼图。
/// </summary>
public void DrawPieChart(List<ChartDataPoint> data, string title)
{
_chartType = "pie";
_title = title;
_data = data;
Invalidate();
}
/// <summary>
/// 绘制散点图。
/// </summary>
public void DrawScatterChart(List<ChartDataPoint> data, string title)
{
_chartType = "scatter";
_title = title;
_data = data;
Invalidate();
}
/// <summary>
/// WinForms 控件真正绘图的入口。
/// </summary>
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics g = e.Graphics;
// 抗锯齿,让线条和圆形更平滑。
g.SmoothingMode = SmoothingMode.AntiAlias;
// 清空背景。
g.Clear(Color.White);
if (_data.Count == 0)
{
DrawCenterText(g, "请选择左侧图表类型");
return;
}
DrawTitle(g);
if (_chartType == "line")
{
DrawLineChartCore(g);
}
else if (_chartType == "bar")
{
DrawBarChartCore(g);
}
else if (_chartType == "pie")
{
DrawPieChartCore(g);
}
else if (_chartType == "scatter")
{
DrawScatterChartCore(g);
}
}
/// <summary>
/// 绘制折线图。
/// </summary>
private void DrawLineChartCore(Graphics g)
{
if (_data.Count == 1)
{
DrawCenterText(g, "折线图至少需要两个数据点");
return;
}
Rectangle plotArea = GetPlotArea();
double maxValue = GetMaxValue();
DrawAxis(g, plotArea, maxValue);
PointF[] points = new PointF[_data.Count];
for (int i = 0; i < _data.Count; i++)
{
float x = plotArea.Left + i * plotArea.Width / (float)(_data.Count - 1);
float y = plotArea.Bottom - (float)(_data[i].Value / maxValue * plotArea.Height);
points[i] = new PointF(x, y);
}
using Pen linePen = new(Color.RoyalBlue, 3);
g.DrawLines(linePen, points);
for (int i = 0; i < points.Length; i++)
{
DrawPoint(g, points[i], Color.RoyalBlue);
DrawBottomLabel(g, _data[i].Label, points[i].X, plotArea.Bottom + 8);
}
}
/// <summary>
/// 绘制柱状图。
/// </summary>
private void DrawBarChartCore(Graphics g)
{
Rectangle plotArea = GetPlotArea();
double maxValue = GetMaxValue();
DrawAxis(g, plotArea, maxValue);
float itemWidth = plotArea.Width / (float)_data.Count;
float barWidth = itemWidth * 0.55F;
for (int i = 0; i < _data.Count; i++)
{
float barHeight = (float)(_data[i].Value / maxValue * plotArea.Height);
float x = plotArea.Left + i * itemWidth + (itemWidth - barWidth) / 2;
float y = plotArea.Bottom - barHeight;
RectangleF bar = new(x, y, barWidth, barHeight);
using Brush brush = new SolidBrush(GetColor(i));
g.FillRectangle(brush, bar);
DrawBottomLabel(g, _data[i].Label, x + barWidth / 2, plotArea.Bottom + 8);
DrawValueLabel(g, _data[i].Value, x + barWidth / 2, y - 22);
}
}
/// <summary>
/// 绘制饼图。
/// </summary>
private void DrawPieChartCore(Graphics g)
{
double total = _data.Sum(item => item.Value);
if (total <= 0)
{
DrawCenterText(g, "饼图数据总和必须大于 0");
return;
}
int diameter = Math.Min(Width / 2, Height - 120);
Rectangle pieRect = new(60, 80, diameter, diameter);
float startAngle = -90;
for (int i = 0; i < _data.Count; i++)
{
float sweepAngle = (float)(_data[i].Value / total * 360);
using Brush brush = new SolidBrush(GetColor(i));
g.FillPie(brush, pieRect, startAngle, sweepAngle);
startAngle += sweepAngle;
}
DrawPieLegend(g, pieRect.Right + 40, pieRect.Top, total);
}
/// <summary>
/// 绘制散点图。
/// </summary>
private void DrawScatterChartCore(Graphics g)
{
Rectangle plotArea = GetPlotArea();
double maxValue = GetMaxValue();
double maxX = _data.Max(item => item.X);
if (maxX <= 0)
{
maxX = 1;
}
DrawAxis(g, plotArea, maxValue);
for (int i = 0; i < _data.Count; i++)
{
float x = plotArea.Left + (float)(_data[i].X / maxX * plotArea.Width);
float y = plotArea.Bottom - (float)(_data[i].Value / maxValue * plotArea.Height);
DrawPoint(g, new PointF(x, y), Color.SeaGreen);
}
}
/// <summary>
/// 获取绘图区。
/// 这里没有使用整个控件区域,因为要给标题、坐标轴文字留空间。
/// </summary>
private Rectangle GetPlotArea()
{
return new Rectangle(70, 70, Width - 110, Height - 130);
}
/// <summary>
/// 获取最大值。
/// 乘以 1.2 是为了让图表顶部留出一点空白。
/// </summary>
private double GetMaxValue()
{
double maxValue = _data.Max(item => item.Value);
if (maxValue <= 0)
{
maxValue = 1;
}
return maxValue * 1.2;
}
/// <summary>
/// 绘制图表标题。
/// </summary>
private void DrawTitle(Graphics g)
{
using Font font = new("Microsoft YaHei UI", 14, FontStyle.Bold);
using Brush brush = new SolidBrush(Color.FromArgb(40, 40, 40));
SizeF size = g.MeasureString(_title, font);
float x = (Width - size.Width) / 2;
g.DrawString(_title, font, brush, x, 20);
}
/// <summary>
/// 绘制坐标轴和横向网格线。
/// </summary>
private void DrawAxis(Graphics g, Rectangle plotArea, double maxValue)
{
using Pen axisPen = new(Color.FromArgb(80, 80, 80), 1);
using Pen gridPen = new(Color.FromArgb(225, 225, 225), 1);
using Font font = new("Microsoft YaHei UI", 9);
using Brush brush = new SolidBrush(Color.FromArgb(60, 60, 60));
g.DrawLine(axisPen, plotArea.Left, plotArea.Top, plotArea.Left, plotArea.Bottom);
g.DrawLine(axisPen, plotArea.Left, plotArea.Bottom, plotArea.Right, plotArea.Bottom);
for (int i = 0; i <= 5; i++)
{
float y = plotArea.Bottom - i * plotArea.Height / 5F;
double value = maxValue * i / 5;
g.DrawLine(gridPen, plotArea.Left, y, plotArea.Right, y);
g.DrawString(value.ToString("0"), font, brush, 25, y - 8);
}
}
/// <summary>
/// 绘制数据点。
/// </summary>
private void DrawPoint(Graphics g, PointF point, Color color)
{
using Brush brush = new SolidBrush(color);
g.FillEllipse(brush, point.X - 5, point.Y - 5, 10, 10);
}
/// <summary>
/// 绘制 X 轴下方文字。
/// </summary>
private void DrawBottomLabel(Graphics g, string text, float centerX, float y)
{
using Font font = new("Microsoft YaHei UI", 9);
using Brush brush = new SolidBrush(Color.FromArgb(60, 60, 60));
SizeF size = g.MeasureString(text, font);
g.DrawString(text, font, brush, centerX - size.Width / 2, y);
}
/// <summary>
/// 绘制柱子上方的数值。
/// </summary>
private void DrawValueLabel(Graphics g, double value, float centerX, float y)
{
using Font font = new("Microsoft YaHei UI", 9);
using Brush brush = new SolidBrush(Color.FromArgb(60, 60, 60));
string text = value.ToString("0");
SizeF size = g.MeasureString(text, font);
g.DrawString(text, font, brush, centerX - size.Width / 2, y);
}
/// <summary>
/// 绘制饼图右侧图例。
/// </summary>
private void DrawPieLegend(Graphics g, int x, int y, double total)
{
using Font font = new("Microsoft YaHei UI", 9);
using Brush textBrush = new SolidBrush(Color.FromArgb(60, 60, 60));
for (int i = 0; i < _data.Count; i++)
{
using Brush colorBrush = new SolidBrush(GetColor(i));
g.FillRectangle(colorBrush, x, y + i * 28, 14, 14);
double percent = _data[i].Value / total;
string text = $"{_data[i].Label} {percent:P1}";
g.DrawString(text, font, textBrush, x + 22, y + i * 28 - 2);
}
}
/// <summary>
/// 在控件中间绘制提示文字。
/// </summary>
private void DrawCenterText(Graphics g, string text)
{
using Font font = new("Microsoft YaHei UI", 11);
using Brush brush = new SolidBrush(Color.Gray);
SizeF size = g.MeasureString(text, font);
float x = (Width - size.Width) / 2;
float y = (Height - size.Height) / 2;
g.DrawString(text, font, brush, x, y);
}
/// <summary>
/// 根据序号获取颜色。
/// 柱状图和饼图有多个分类,所以需要多种颜色。
/// </summary>
private Color GetColor(int index)
{
Color[] colors =
{
Color.RoyalBlue,
Color.Orange,
Color.SeaGreen,
Color.IndianRed,
Color.MediumPurple,
Color.Teal
};
return colors[index % colors.Length];
}
}
}
六、替换 Form1.cs
为了让教程更容易复现,这里不依赖设计器拖控件,而是直接用代码创建界面。
打开 Form1.cs,替换为以下完整代码:
using DrawWinformChart.Controls;
using DrawWinformChart.Models;
namespace DrawWinformChart
{
public partial class Form1 : Form
{
private readonly SimpleChartControl chartControl;
private readonly Label descriptionLabel;
public Form1()
{
InitializeComponent();
Text = "WinForms 常用图表绘制";
StartPosition = FormStartPosition.CenterScreen;
Size = new Size(1000, 620);
MinimumSize = new Size(860, 520);
TableLayoutPanel mainLayout = new();
mainLayout.Dock = DockStyle.Fill;
mainLayout.ColumnCount = 2;
mainLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 180));
mainLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
Controls.Add(mainLayout);
Panel menuPanel = new();
menuPanel.Dock = DockStyle.Fill;
menuPanel.BackColor = Color.FromArgb(245, 247, 250);
menuPanel.Padding = new Padding(16);
mainLayout.Controls.Add(menuPanel, 0, 0);
Label menuTitleLabel = new();
menuTitleLabel.Text = "常用图表";
menuTitleLabel.Font = new Font("Microsoft YaHei UI", 10.5F, FontStyle.Bold);
menuTitleLabel.Location = new Point(16, 16);
menuTitleLabel.AutoSize = true;
menuPanel.Controls.Add(menuTitleLabel);
Button lineButton = CreateMenuButton("折线图", 46);
Button barButton = CreateMenuButton("柱状图", 94);
Button pieButton = CreateMenuButton("饼图", 142);
Button scatterButton = CreateMenuButton("散点图", 190);
lineButton.Click += (sender, e) => ShowLineChart();
barButton.Click += (sender, e) => ShowBarChart();
pieButton.Click += (sender, e) => ShowPieChart();
scatterButton.Click += (sender, e) => ShowScatterChart();
menuPanel.Controls.Add(lineButton);
menuPanel.Controls.Add(barButton);
menuPanel.Controls.Add(pieButton);
menuPanel.Controls.Add(scatterButton);
TableLayoutPanel rightLayout = new();
rightLayout.Dock = DockStyle.Fill;
rightLayout.Padding = new Padding(16);
rightLayout.RowCount = 2;
rightLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
rightLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, 72));
mainLayout.Controls.Add(rightLayout, 1, 0);
chartControl = new SimpleChartControl();
chartControl.Dock = DockStyle.Fill;
chartControl.BorderStyle = BorderStyle.FixedSingle;
rightLayout.Controls.Add(chartControl, 0, 0);
descriptionLabel = new Label();
descriptionLabel.Dock = DockStyle.Fill;
descriptionLabel.TextAlign = ContentAlignment.MiddleLeft;
descriptionLabel.Font = new Font("Microsoft YaHei UI", 10);
descriptionLabel.ForeColor = Color.FromArgb(45, 45, 45);
rightLayout.Controls.Add(descriptionLabel, 0, 1);
ShowLineChart();
}
private static Button CreateMenuButton(string text, int top)
{
Button button = new();
button.Text = text;
button.Location = new Point(16, top);
button.Size = new Size(148, 36);
button.UseVisualStyleBackColor = true;
return button;
}
private void ShowLineChart()
{
var data = new List<ChartDataPoint>
{
new("一月", 120),
new("二月", 150),
new("三月", 132),
new("四月", 180),
new("五月", 210),
new("六月", 195)
};
chartControl.DrawLineChart(data, "折线图:月度销售趋势");
descriptionLabel.Text = "折线图适合观察数据变化趋势。调用方式:chartControl.DrawLineChart(data, title)。";
}
private void ShowBarChart()
{
var data = new List<ChartDataPoint>
{
new("产品A", 88),
new("产品B", 126),
new("产品C", 98),
new("产品D", 156),
new("产品E", 112)
};
chartControl.DrawBarChart(data, "柱状图:产品销量对比");
descriptionLabel.Text = "柱状图适合比较分类数据。调用方式:chartControl.DrawBarChart(data, title)。";
}
private void ShowPieChart()
{
var data = new List<ChartDataPoint>
{
new("线上商城", 45),
new("线下门店", 30),
new("代理渠道", 15),
new("其他", 10)
};
chartControl.DrawPieChart(data, "饼图:销售渠道占比");
descriptionLabel.Text = "饼图适合展示占比。调用方式:chartControl.DrawPieChart(data, title)。";
}
private void ShowScatterChart()
{
var data = new List<ChartDataPoint>
{
new("点1", 1, 12),
new("点2", 2, 18),
new("点3", 3, 16),
new("点4", 4, 28),
new("点5", 5, 24),
new("点6", 6, 33),
new("点7", 7, 31)
};
chartControl.DrawScatterChart(data, "散点图:投入与产出关系");
descriptionLabel.Text = "散点图适合观察两个数值之间的关系。调用方式:chartControl.DrawScatterChart(data, title)。";
}
}
}
七、运行项目
编译:
dotnet build
运行:
dotnet run
运行后会看到一个 WinForms 窗口。
左侧有四个按钮:
折线图 柱状图 饼图 散点图
点击不同按钮,右侧会绘制对应图表。
八、核心调用方式
1. 绘制折线图
var data = new List<ChartDataPoint>
{
new("一月", 120),
new("二月", 150),
new("三月", 132),
new("四月", 180),
new("五月", 210),
new("六月", 195)
};
chartControl.DrawLineChart(data, "折线图:月度销售趋势");
2. 绘制柱状图
var data = new List<ChartDataPoint>
{
new("产品A", 88),
new("产品B", 126),
new("产品C", 98),
new("产品D", 156),
new("产品E", 112)
};
chartControl.DrawBarChart(data, "柱状图:产品销量对比");
3. 绘制饼图
var data = new List<ChartDataPoint>
{
new("线上商城", 45),
new("线下门店", 30),
new("代理渠道", 15),
new("其他", 10)
};
chartControl.DrawPieChart(data, "饼图:销售渠道占比");
4. 绘制散点图
var data = new List<ChartDataPoint>
{
new("点1", 1, 12),
new("点2", 2, 18),
new("点3", 3, 16),
new("点4", 4, 28),
new("点5", 5, 24),
new("点6", 6, 33),
new("点7", 7, 31)
};
chartControl.DrawScatterChart(data, "散点图:投入与产出关系");
九、绘图原理简单说明
1. 为什么调用 DrawLineChart 后会刷新图表
因为方法内部调用了:
Invalidate();
Invalidate() 的意思是告诉 WinForms:
这个控件需要重新绘制
然后 WinForms 会自动执行 OnPaint 方法。
2. OnPaint 是什么
OnPaint 是控件绘图的入口。
代码里 根据 _chartType 判断当前要画哪种图:
if (_chartType == "line")
{
DrawLineChartCore(g);
}
else if (_chartType == "bar")
{
DrawBarChartCore(g);
}
3. 为什么 Y 坐标要反着计算
WinForms 的坐标系是这样的:
左上角是原点 越往右,X 越大 越往下,Y 越大
但是图表中数值越大,点应该越靠上。
所以计算 Y 坐标时使用:
float y = plotArea.Bottom - (float)(_data[i].Value / maxValue * plotArea.Height);
十、常见问题
1. 复制代码后提示找不到 ChartDataPoint
检查是否添加了命名空间:
using DrawWinformChart.Models;
2. 复制代码后提示找不到 SimpleChartControl
检查是否添加了命名空间:
using DrawWinformChart.Controls;
3. 如果项目名称不是 DrawWinformChart 怎么办
如果你的项目名不是 DrawWinformChart,需要把代码中的命名空间改成你的项目名称。
例如项目名是 MyChartDemo,则:
namespace DrawWinformChart.Models
要改成:
namespace MyChartDemo.Models
其它文件中的命名空间也要对应修改。
十一、总结
本文从零实现了一个简单的 WinForms 图表绘制示例。
实现内容:
- 自定义图表数据类
- 自定义图表控件
- 折线图绘制
- 柱状图绘制
- 饼图绘制
- 散点图绘制
- 点击按钮切换图表
最终调用方式:
chartControl.DrawLineChart(data, "折线图标题"); chartControl.DrawBarChart(data, "柱状图标题"); chartControl.DrawPieChart(data, "饼图标题"); chartControl.DrawScatterChart(data, "散点图标题");
对于初学者来说,建议先掌握这几个概念:
Graphics OnPaint Invalidate Rectangle 坐标换算
理解这些以后,再学习 WinForms Chart 控件、ScottPlot 或 LiveCharts 会更容易。












