C# 使用 System.Drawing 绘制柱状图、折线图与扇形图

一只小鹿
一只小鹿
发布时间:2026-10-13 | 6人浏览过
原创

本文将展示如何利用 C# 的 System.Drawing 命名空间,在不依赖第三方控件的情况下,直接通过代码动态生成柱状图、折线图和扇形图。适用于需要从数据库读取数据并在页面中实时渲染统计报表的场景。

结果预览与原理简介

C# 提供了强大的 System.Drawing 命名空间(即 GDI+),允许开发人员通过对象如 Graphics、Pen、Brush 和 Bitmap 来绘制各种图形。对于统计图,核心逻辑通常包括:定义画布尺寸与背景、计算数据对应的像素坐标、绘制网格与标签、以及最后将内存中的图像输出为 JPEG 格式响应给前端。

以下三个部分分别对应三种最经典的统计图表。代码中使用了 SQL Server 作为数据源进行演示,实际应用中可替换为其他数据获取方式。

准备条件

  • 开发环境:Visual Studio,支持 .NET Framework(注意 System.Drawing 在非 Windows 环境下可能存在兼容性问题)。
  • 引用命名空间:需引入 System.Drawing, System.Drawing.Imaging, System.Data.SqlClient 等。
  • 依赖项:文中代码假设存在下拉框控件(如 ddlTaget, ddlYear)用于筛选查询参数,以及数据库连接字符串配置。

第一步:绘制动态柱状图

柱状图适合用于对比不同类别的数据量。本例展示了如何对比“报名人数”与“通过人数”,并将两组柱子并列显示以便直观比较。

  1. 初始化画布与画笔
    创建 Bitmap 实例作为画布,并使用 Graphics.FromImage() 获取绘图上下文。设置背景色并定义用于绘制的 Pen 和 Brush。
int height = 500, width = 700;
Bitmap image = new Bitmap(width, height);
Graphics g = Graphics.FromImage(image);
// ... 后续操作 ...
  1. 绘制网格线与坐标轴标签
    使用循环绘制横向与纵向的辅助线,并遍历数组在相应位置绘制 X 轴(时间周期)和 Y 轴(数值刻度)的文字标签。
// 示例:绘制X轴标签
String[] n = { "第一期", "第二期", "...", "全年统计" };
for (int i = 0; i < n.Length; i++)
{
    g.DrawString(n[i].ToString(), font, Brushes.Blue, x, 348);
    x += 78;
}
  1. 填充数据并绘制柱体
    从数据库读取数据存入数组后,根据当前 X 坐标和柱体高度(基于数据值转换)调用 FillRectangle 绘制实心矩形。每个柱体上方还需绘制具体数值标签。
  1. 输出图像
    将绘制好的 Bitmap 保存至 MemoryStream,并清除响应流内容后以 “image/Jpeg” 类型二进制写出。

以下是柱状图的完整实现代码,包含了标题、边框、图例及完整的双柱对比绘制逻辑:

private void CreateBarChart()
{
    int height = 500, width = 700;
    Bitmap image = new Bitmap(width, height);
    Graphics g = Graphics.FromImage(image);

    try
    {
        // 1. 初始化背景与字体
        g.Clear(Color.White);
        Font font = new Font("Arial", 10, FontStyle.Regular);
        Font font1 = new Font("宋体", 20, FontStyle.Bold);
        LinearGradientBrush brush = new LinearGradientBrush(new Rectangle(0, 0, image.Width, image.Height), Color.Blue, Color.BlueViolet, 1.2f, true);

        // 绘制背景与标题
        g.FillRectangle(Brushes.WhiteSmoke, 0, 0, width, height);
        g.DrawString(this.ddlTaget.SelectedItem.Text + " " + this.ddlYear.SelectedItem.Text + " 成绩统计柱状图", font1, brush, new PointF(70, 30));
        g.DrawRectangle(new Pen(Color.Blue), 0, 0, image.Width - 1, image.Height - 1);

        Pen mypen = new Pen(brush, 1);

        // 2. 绘制网格
        // 横向线条
        int x = 100;
        for (int i = 0; i < 14; i++) { g.DrawLine(mypen, x, 80, x, 340); x += 40; }
        Pen mypen1 = new Pen(Color.Blue, 2);
        x = 60;
        g.DrawLine(mypen1, x, 80, x, 340);

        // 纵向线条
        int y = 106;
        for (int i = 0; i < 9; i++) { g.DrawLine(mypen, 60, y, 620, y); y += 26; }
        g.DrawLine(mypen1, 60, y, 620, y);

        // 3. 绘制坐标轴文字
        String[] n = { "第一期", "第二期", "第三期", "第四期", "上半年", "下半年", "全年统计" };
        x = 78;
        for (int i = 0; i < 7; i++) { g.DrawString(n[i], font, Brushes.Blue, x, 348); x += 78; }

        String[] m = { "250","225", "200", "175", "150", "125", "100", "75", "50", "25", "0" };
        y = 72;
        for (int i = 0; i < 11; i++) { g.DrawString(m[i], font, Brushes.Blue, 25, y); y += 26; }

        // 4. 数据读取与处理(此处仅示意结构,实际需替换为你的SQL逻辑)
        // int[] Count1 = new int[7]; // 报名数据
        // int[] Count2 = new int[7]; // 通过数据
        // SqlConnection Con = new SqlConnection(...);
        // ... 填充数据 ... 

        // 为演示代码完整性,这里模拟绘制第一个周期的逻辑
        int[] Count1 = new int[7] { 100, 110, 120, 130, 210, 250, 450 };
        int[] Count2 = new int[7] { 80, 90, 100, 110, 170, 200, 350 };

        // 5. 绘制柱状体
        x = 80;
        Font font2 = new Font("Arial", 10, FontStyle.Bold);
        SolidBrush mybrush = new SolidBrush(Color.Red);
        SolidBrush mybrush2 = new SolidBrush(Color.Green);

        // 循环绘制每一期数据 (此处演示第一期)
        // 报名人数 (红色)
        g.FillRectangle(mybrush, x, 340 - Count1[0], 20, Count1[0]);
        g.DrawString(Count1[0].ToString(), font2, Brushes.Red, x, 340 - Count1[0] - 15);

        // 通过人数 (绿色)
        x += 20;
        g.FillRectangle(mybrush2, x, 340 - Count2[0], 20, Count2[0]);
        g.DrawString(Count2[0].ToString(), font2, Brushes.Green, x, 340 - Count2[0] - 15);

        // 后续月份依此类推...
        
        // 6. 绘制图例
        g.DrawRectangle(new Pen(Brushes.Blue), 170, 400, 250, 50);
        g.FillRectangle(Brushes.Red, 270, 410, 20, 10);
        g.DrawString("报名人数", font2, Brushes.Red, 292, 408);
        g.FillRectangle(Brushes.Green, 270, 430, 20, 10);
        g.DrawString("通过人数", font2, Brushes.Green, 292, 428);

        // 7. 输出图像
        System.IO.MemoryStream ms = new System.IO.MemoryStream();
        image.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
        Response.ClearContent();
        Response.ContentType = "image/Jpeg";
        Response.BinaryWrite(ms.ToArray());
    }
    finally
    {
        g.Dispose();
        image.Dispose();
    }
}

第二步:绘制折线统计图

折线图主要用于显示数据随时间变化的趋势。与柱状图类似,区别在于使用 DrawLines 方法连接代表数值的坐标点,并通过 Point[] 数组管理坐标数据。

核心步骤如下:

  1. 计算坐标点数组:根据读取到的数据值,计算每个时间点(第一期至全年)对应的 X 轴横坐标(固定间隔递增)和 Y 轴纵坐标(340减去数据值)。
  2. 绘制折线:创建 Point[] 数组并填入坐标,使用 g.DrawLines(pen, points) 绘制线条。
  3. 添加数值标记:在每个数据点附近绘制具体的数值文本。

折线图完整代码如下:

private void CreateLineChart()
{
    int height = 480, width = 700;
    Bitmap image = new Bitmap(width, height);
    Graphics g = Graphics.FromImage(image);

    try
    {
        // 初始化
        g.Clear(Color.White);
        Font font = new Font("Arial", 9, FontStyle.Regular);
        Font font1 = new Font("宋体", 20, FontStyle.Regular);
        LinearGradientBrush brush = new LinearGradientBrush(new Rectangle(0, 0, image.Width, image.Height), Color.Blue, Color.Blue, 1.2f, true);
        g.FillRectangle(Brushes.AliceBlue, 0, 0, width, height);

        // 标题
        g.DrawString(this.ddlTaget.SelectedItem.Text + " " + this.ddlYear.SelectedItem.Text + " 成绩统计折线图", font1, new SolidBrush(Color.Blue), new PointF(85, 30));
        g.DrawRectangle(new Pen(Color.Blue), 0, 0, image.Width - 1, image.Height - 1);

        Pen mypen = new Pen(brush, 1);
        Pen mypen2 = new Pen(Color.Red, 2);

        // 绘制坐标网格(略,同柱状图逻辑)
        // ... 绘制网格代码 ...

        // 数据点数组(报名人数)
        int[] Count1 = new int[7] { 100, 110, 120, 130, 210, 250, 450 };
        Point[] points1 = new Point[7];
        
        // 填充坐标:X轴每隔80px一个刻度,Y轴从340处向下计算
        points1[0].X = 60;   points1[0].Y = 340 - Count1[0];
        points1[1].X = 140;  points1[1].Y = 340 - Count1[1];
        points1[2].X = 220;  points1[2].Y = 340 - Count1[2];
        points1[3].X = 300;  points1[3].Y = 340 - Count1[3];
        points1[4].X = 380;  points1[4].Y = 340 - Count1[4];
        points1[5].X = 460;  points1[5].Y = 340 - Count1[5];
        points1[6].X = 540;  points1[6].Y = 340 - Count1[6];

        // 绘制报名人数折线
        g.DrawLines(mypen2, points1);
        Font font3 = new Font("Arial", 10, FontStyle.Bold);
        for(int i=0; i<7; i++) {
            // 简单放置数字,实际项目中可能需要更复杂的定位逻辑避免重叠
             if(i==0) g.DrawString(Count1[i].ToString(), font3, Brushes.Red, 58, points1[i].Y - 20);
             else if(i<4) g.DrawString(Count1[i].ToString(), font3, Brushes.Red, 138+i*80, points1[i].Y - 20);
             else g.DrawString(Count1[i].ToString(), font3, Brushes.Red, 378+(i-4)*80, points1[i].Y - 20);
        }

        // 绘制通过人数折线(绿色,逻辑同上)
        int[] Count2 = new int[7] { 80, 90, 100, 110, 170, 200, 350 };
        Point[] points2 = new Point[7];
        for(int i=0; i<7; i++) points2[i] = new Point(points1[i].X, 340 - Count2[i]);
        
        Pen mypen3 = new Pen(Color.Green, 2);
        g.DrawLines(mypen3, points2);
        for(int i=0; i<7; i++) {
             if(i==0) g.DrawString(Count2[i].ToString(), font3, Brushes.Green, 61, points2[i].Y - 15);
             else if(i<4) g.DrawString(Count2[i].ToString(), font3, Brushes.Green, 131+i*80, points2[i].Y - 15);
             else g.DrawString(Count2[i].ToString(), font3, Brushes.Green, 381+(i-4)*80, points2[i].Y - 15);
        }

        // 图例
        g.DrawRectangle(new Pen(Brushes.Red), 180, 390, 250, 50);
        g.FillRectangle(Brushes.Red, 270, 402, 20, 10);
        g.DrawString("报名人数", font, Brushes.Red, 292, 400);
        g.FillRectangle(Brushes.Green, 270, 422, 20, 10);
        g.DrawString("通过人数", font, Brushes.Green, 292, 420);

        // 输出
        System.IO.MemoryStream ms = new System.IO.MemoryStream();
        image.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
        Response.ClearContent();
        Response.ContentType = "image/Jpeg";
        Response.BinaryWrite(ms.ToArray());
    }
    finally
    {
        g.Dispose();
        image.Dispose();
    }
}

第三步:绘制扇形统计图(饼图)

扇形图常用于展示各部分占整体的比例。在 C# 中,直接使用 FillPie 方法即可绘制。需要注意 currentdegree 角度的累加,以确保扇形能正确拼接成完整的圆。

主要逻辑包括:

  1. 确定绘制区域:计算饼图的矩形区域(Rectangle),通常位于画布中央偏上位置。
  2. 计算角度:根据每个数据项(如“通过人数”、“未通过人数”)占总数的百分比,乘以 360 度得到起始角度和扫过角度。
  3. 绘制扇区:调用 objgraphics.FillPie(brush, rectangle, startAngle, sweepAngle) 依次绘制各个扇区。
  4. 添加统计信息:在图表下方或侧边绘制详细的统计数据文本框。

扇形统计图完整代码如下:

private void CreatePieChart()
{
    // 数据库连接及数据读取(省略具体SQL细节)
    // ... 读取 Total, count[0](总数), count[1](通过数) 等 ... 

    float Total = 450.0f; // 示例总数
    float passCount = 350.0f; // 示例通过数
    
    // 布局参数
    int width = 350;
    int height = width + 200; // 额外高度用于放图例
    int titleHeight = 40;
    
    Rectangle pieRect = new Rectangle(0, titleHeight, width, width);

    Bitmap objbitmap = new Bitmap(width, height);
    Graphics objgraphics = Graphics.FromImage(objbitmap);
    
    // 清理背景
    objgraphics.FillRectangle(new SolidBrush(Color.White), 0, 0, width, height);
    objgraphics.FillRectangle(new SolidBrush(Color.Beige), pieRect);

    // 颜色定义
    ArrayList colors = new ArrayList();
    Random rnd = new Random();
    colors.Add(new SolidBrush(Color.FromArgb(rnd.Next(255), rnd.Next(255), rnd.Next(255)))); // 未通过颜色
    colors.Add(new SolidBrush(Color.FromArgb(rnd.Next(255), rnd.Next(255), rnd.Next(255)))); // 通过颜色

    // 绘制扇形
    float currentdegree = 0.0f;
    
    // 1. 绘制通过人数扇区
    float passSweep = (passCount / Total) * 360;
    objgraphics.FillPie((SolidBrush)colors[1], pieRect, currentdegree, passSweep);
    currentdegree += passSweep;

    // 2. 绘制未通过人数扇区
    float failCount = Total - passCount;
    float failSweep = (failCount / Total) * 360;
    objgraphics.FillPie((SolidBrush)colors[0], pieRect, currentdegree, failSweep);

    // 标题
    SolidBrush blackbrush = new SolidBrush(Color.Black);
    Font fonttitle = new Font("verdana", 10, FontStyle.Bold);
    StringFormat sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
    objgraphics.DrawString("机关单位成绩统计饼状图", fonttitle, blackbrush,
        new Rectangle(0, 0, width, titleHeight), sf);

    // 图例与详细信息
    Font fontlegend = new Font("verdana", 9);
    objgraphics.DrawString("----------------统计信息------------------", 
        new SolidBrush(Color.Blue), 20, height - 150);
    objgraphics.DrawString("统计单位: 示例单位", fontlegend, blackbrush, 20, height - 130);
    objgraphics.DrawString("总人数: " + Convert.ToInt32(Total), fontlegend, blackbrush, 20, height - 110);
    
    // 绘制小方块标识
    objgraphics.FillRectangle((SolidBrush)colors[1], 5, height - 90, 10, 10);
    objgraphics.DrawString("通过人数: " + Convert.ToInt32(passCount), fontlegend, blackbrush, 20, height - 90);
    
    objgraphics.FillRectangle((SolidBrush)colors[0], 5, height - 70, 10, 10);
    objgraphics.DrawString("未通过: " + Convert.ToInt32(failCount), fontlegend, blackbrush, 20, height - 70);

    // 输出
    objbitmap.Save(Response.OutputStream, System.Drawing.Imaging.ImageFormat.Jpeg);
    objgraphics.Dispose();
    objbitmap.Dispose();
}

以上就是 C# 利用 System.Drawing 绘制柱状图、折线图和扇形图的详细内容,更多关于 C# 图表开发优化的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表