读者只要按照本文步骤新建 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 会更容易。

觉得上面的内容有用吗?快来点个赞吧!

点赞() 我要打赏

温馨提示 : 本站内容来自会员投稿以及互联网,所有源码及教程均为作者总结编辑,请大家在使用过程中提前做好备份,以免发生无法预知的错误,源码类教程请勿直接用于生产环境!

 可能感兴趣的文章