ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WinForms学生成绩排名柱状图实现:基于Chart控件的可视化

WinForms学生成绩排名柱状图实现:基于Chart控件的可视化 学生管理系统写到这里成绩录入、成绩查询、按条件筛选这些能力基本都齐了。第 50 节开始进入“学生成绩图形排名”这一节先做前半部分用柱状图把同一场考试、同一科目的学生成绩按从高到低展示出来。为什么已经有 DataGridView 排序还要再做图表因为排名关心的是数据的相对关系分数排名不是简单地把名字按大小排一行还要让人一眼看出高分和低分之间的距离、前几名是否咬得很紧、后半段是否大面积重合。图形化的目的就是把这种不容易从数字里直接感知的信息用柱长度表现出来。这一节会完整走一遍成绩排名数据的准备、图表控件选择和 WinForms 界面渲染。完成后你可以在窗体中选择考试和科目点击查询后得到一张按分数降序排列的柱状图并且能在数据标签上直接看到每个学生的具体分数。整个实现不依赖第三方商业组件使用 .NET 自带的 Chart 控件就可以完成适合作为学生管理系统中第一个可视化模块。1. 成绩图形排名要解决的不只是把分数从高到低排列1.1 表格排名看得见数据却看不见差距如果把一次数学考试的成绩按分数降序放在表格里看到的是这样一组行名次学生分数1张三982李四963王五874赵六865孙七68从这张表能读出名次却很难立刻读出分数之间的距离。张三 98 分、李四 96 分只差 2 分李四和王五之间差 9 分王五和赵六之间又只差 1 分。如果只是靠肉眼看数字逐个做减法很快会疲劳尤其是班里 40 人、50 人时这种差距信息基本被淹没在表格里。柱状图能改变这个问题。柱状图的 Y 轴是分数柱子的高度和分数成比例98 和 96 的两根柱子几乎一样高而 96 和 87 的柱子会有明显的高度落差。这样成绩排名的“头部咬合”“中段分离”“尾部断档”就能被直观地看出来这是纯表格很难达到的表达效果。1.2 图形排名的功能边界和本节的交付目标“成绩图形排名”是一个比较大的能力集合通常可以往下拆成多个子功能同一次考试、同一个科目下的学生成绩排名柱状图。按分数段统计人数查看成绩分布情况。不同班级平均分对比图。同一个学生多次考试的成绩变化折线图。将图表导出为图片用于成绩分析报告或家长会展示。这一节只完成第一项也是后续几项的基础。本节的目标是在现有学生管理系统的 WinForms 窗体中增加一个成绩排名界面用户选择考试和科目后界面按分数降序把学生成绩渲染成柱状图。柱状图要包含标题、坐标轴、学生姓名标签和分数标签能正确处理无数据场景并为下一节扩展班级筛选和图表导出留出接口。这里先不急着处理饼图、折线图和雷达图。排名柱状图做完坐标轴语义、数据源结构、Series 绑定方式都会沉淀下来后面再扩展其他图表类型会顺利得多。2. 图表控件选型先判断到底需要什么级别的图表能力2.1 三种可选的绘图路线做 WinForms 图表常见的路线不止一种方案是否需要额外安装上手成本适合场景System.Windows.Forms.DataVisualization.Charting.NET Framework 自带现代 .NET 需要 NuGet 包低柱状图、折线图、饼图等常规统计图表ScottPlot需要 NuGet 包中科学计算、大量数据点、信号波形LiveCharts2需要 NuGet 包中高追求动画效果和现代视觉风格GDI 自绘柱状图不需要高需要完全自定义渲染逻辑从本项目的定位来看学生管理系统要画的是柱状图、饼图、折线图这类常规业务图表数据量不大重点是开发速度和可维护性因此优先选择 WinForms 自带的 Chart 控件。它有完整的ChartArea、Series、Legend、Axis模型能满足常规可视化需求也容易出现第三方控件版本不一致的问题。第三方图表库不是不能用但要额外评估几个点包的维护状态、依赖冲突、控件在工具箱中的注册方式、Graphic 渲染在高 DPI 下的表现。学习环境可以自由切换生产环境一旦选定后续升级成本较高应尽量在项目早期确定。2.2 Chart 控件的引用方式不同 .NET 版本差异较大如果项目跑在 .NET Framework 4.7.2 上Chart 控件已经包含在框架里操作路径是在解决方案中右键“引用”选择“添加引用”。在程序集列表中找到System.Windows.Forms.DataVisualization。在代码文件顶部添加using System.Windows.Forms.DataVisualization.Charting;。如果工具箱中没有 Chart 控件可以在工具箱空白处右键选择“选择项”在“.NET Framework 组件”中找到 Chart 并勾选。如果项目已经升级到 .NET 6 或 .NET 8 的 WinFormsSystem.Windows.Forms.DataVisualization不再默认包含在框架中需要单独引入 NuGet 包。实际使用中通常使用的社区维护包名是System.Windows.Forms.DataVisualization但具体版本要以项目实际引用的为准不要在没有确认的情况下直接使用网上任意版本号。引包之前的判断标准很简单先确认项目的目标框架。处理完引用后用一个最简单的空窗体放一个 Chart 控件拖动一下确认编译通过再进入数据层开发能避免后面把问题混在一起。2.3 环境假设和示例项目约定这一节后面的代码示例按常见学生管理系统项目约定展开具体环境如下项目配置开发工具Visual Studio 2019 或 2022目标框架.NET Framework 4.7.2UI 技术WinForms数据库SQL Server 或 SQL Server Express数据访问ADO.NET使用 System.Data.SqlClient图表控件System.Windows.Forms.DataVisualization.Charting如果你的项目是 .NET 6代码主体不变只需要调整团队中的包引用方式和连接字符串配置。示例中的表结构是教学约定不是唯一标准实际项目如果之前已经设计了不同的表名直接替换成真实表名即可关键是理解查询条件和图表 Series 之间的对应关系。3. 成绩数据准备从表结构到可排序查询3.1 分数表结构和查询关联字段要生成成绩排名图至少需要知道三个信息考试、科目、分数属于哪个学生。本系列前面已经常见表结构类似如下t_Student学生表包含StudentId、StudentNo、StudentName、ClassId。t_Exam考试表包含ExamId、ExamName。t_Subject科目表包含SubjectId、SubjectName。t_StudentScore成绩表包含Id、ExamId、SubjectId、StudentId、Score。成绩表的建表语句可以按下面的结构确认CREATE TABLE dbo.t_StudentScore ( Id INT IDENTITY(1,1) PRIMARY KEY, ExamId INT NOT NULL, SubjectId INT NOT NULL, StudentId INT NOT NULL, Score DECIMAL(5,1) NOT NULL );字段含义如下字段类型说明IdINT IDENTITY主键无业务含义ExamIdINT考试编号关联考试表SubjectIdINT科目编号关联科目表StudentIdINT学生编号关联学生表ScoreDECIMAL(5,1)成绩保留一位小数使用DECIMAL而不是FLOAT是为了避免浮点误差影响成绩计算和显示。成绩通常按一位小数或整数处理DECIMAL(5,1)已经足够。如果没有在成绩表上创建外键也不影响本节查询但建议后续在测试数据完整后补充外键和索引。查询排名的重点字段是ExamId SubjectId StudentId的组合条件。3.2 按考试、科目查询成绩的 SQL 写法排名图形的基础查询并不复杂核心 SQL 如下SELECT s.StudentId, s.StudentNo, s.StudentName, sc.Score FROM dbo.t_StudentScore sc INNER JOIN dbo.t_Student s ON sc.StudentId s.StudentId WHERE sc.ExamId ExamId AND sc.SubjectId SubjectId ORDER BY sc.Score DESC, s.StudentNo ASC;这里有两个容易忽略的细节。第一必须使用参数化查询不能把ExamId直接拼接成字符串。学生管理系统里经常处理用户从界面上选出来的值如果直接拼接 SQL一旦后续把输入框改成手输条件就可能引入 SQL 注入风险。参数化写法在 ADO.NET 中成本很低应该从一开始就固定下来。第二ORDER BY sc.Score DESC, s.StudentNo ASC的作用是给同分学生一个稳定顺序。如果只按Score排序两个学生分数相同时数据库返回顺序可能不确定而图表柱子的前后顺序不同会影响观感。加上StudentNo作为第二排序字段可以保证每次查询结果一致。3.3 排名和名次字段ROW_NUMBER 还是 RANK如果只做柱状图ORDER BY已经足够因为图表本身从左到右就是降序排列不需要额外产生数字名次。但很多业务场景要求在柱子上或数据标签里显示“第几名”。这时需要决定排名规则ROW_NUMBER()生成连续序号分数相同的两个人也会出现 1、2 的区别。RANK()生成并列序号两人并列第一下一个名次会跳过 2直接排到 3。DENSE_RANK()生成并列序号但不跳号两人并列第一下一名是 2。不需要排名的图表不要先查一个“假名次”再排序。数据源里保留StudentName和Score即可。如果后续界面要求支持并列排名可以改成下面的写法SELECT s.StudentId, s.StudentName, sc.Score, RANK() OVER (ORDER BY sc.Score DESC) AS RankNo FROM dbo.t_StudentScore sc INNER JOIN dbo.t_Student s ON sc.StudentId s.StudentId WHERE sc.ExamId ExamId AND sc.SubjectId SubjectId ORDER BY sc.Score DESC, s.StudentNo ASC;注意RANK()与查询结果的最终顺序不完全相关RankNo只是计算结果不能作为排序依据使用。4. 用 List 模型接住成绩查询结果并排好顺位4.1 查询条件对象 ScoreRankQuery进入 C# 编码前先定义两个简单类一个表示查询条件一个表示成绩排名项。这样可以把数据访问、业务逻辑和界面绑定解耦。public class ScoreRankQuery { public int ExamId { get; set; } public int SubjectId { get; set; } public int? ClassId { get; set; } public int? TopCount { get; set; } } public class ScoreRankItem { public string StudentName { get; set; } public decimal Score { get; set; } public string ScoreText Score.ToString(0.#); }ScoreRankQuery中的ClassId和TopCount可以暂时不用但设计时就把可选条件放进去后续扩展“按班查看”“只看前 10 名”时不需要重新定义数据访问方法。ScoreRankItem中的ScoreText属性是为柱状图数据标签准备的避免在 UI 层反复处理格式。4.2 数据访问方法 GetScoreRankList数据访问层方法如下public ListScoreRankItem GetScoreRankList(ScoreRankQuery query) { string sql SELECT s.StudentName, sc.Score FROM dbo.t_StudentScore sc INNER JOIN dbo.t_Student s ON sc.StudentId s.StudentId WHERE sc.ExamId ExamId AND sc.SubjectId SubjectId ORDER BY sc.Score DESC, s.StudentNo ASC;; var items new ListScoreRankItem(); using (var conn new SqlConnection(_connectionString)) using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(ExamId, query.ExamId); cmd.Parameters.AddWithValue(SubjectId, query.SubjectId); conn.Open(); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { items.Add(new ScoreRankItem { StudentName reader[StudentName].ToString(), Score Convert.ToDecimal(reader[Score]) }); } } } return items; }代码里有几个细节需要说明。_connectionString应该来自配置文件不要写死在代码里。学生管理系统开发到后期数据库地址、用户名、密码通常都不止一套测试环境和生产环境如果使用不同配置连接字符串写死会导致每次部署都要改代码。reader[Score]返回的是object必须用Convert.ToDecimal转换。如果成绩字段是DECIMAL类型这个步骤非常稳定。reader[StudentName].ToString()这里假设StudentName没有空值。如果你的表允许姓名为空应先做判空处理否则界面可能显示空柱子的标签。4.3 排序和 TopN 过滤放在 SQL 层而不是内存层有一种常见写法是先把所有学生成绩查出来然后在 C# 中内存排序return items.OrderByDescending(x x.Score).ToList();这种写法在分数记录几十条时没有问题但不符合良好的数据访问习惯。学生管理系统的成绩数据会随学期增长一个年级多个班、多次考试累积后记录数并不少。SQL 层的ORDER BY可以配合索引减少数据传输量内存排序则必须先取出全部数据再做处理两者适用场景不同。真正需要做“只看前 10 名”时应该直接在 SQL 层限制数据量SELECT TOP (TopCount) s.StudentName, sc.Score FROM dbo.t_StudentScore sc INNER JOIN dbo.t_Student s ON sc.StudentId s.StudentId WHERE sc.ExamId ExamId AND sc.SubjectId SubjectId ORDER BY sc.Score DESC, s.StudentNo ASC;需要注意的是SELECT TOP (TopCount)是 SQL Server 的写法如果项目用的是 MySQL 或其他数据库要改成对应的LIMIT语法。C# 代码中给参数赋值cmd.Parameters.AddWithValue(TopCount, query.TopCount ?? 10);如果TopCount为 null则默认为 10界面层可以自行决定什么时候显示这个筛选条件。5. WinForms 界面接入 Chart 并渲染成绩柱状图5.1 在窗体上加入 Chart 控件并初始化图表区新建一个窗体FrmScoreRank布局可以这样设计控件名称作用ComboBoxcmbExam选择考试ComboBoxcmbSubject选择科目ButtonbtnQuery触发查询和图表刷新ChartchartScore显示成绩排名柱状图在窗体加载时完成下拉框和图表控件的初始化。private void FrmScoreRank_Load(object sender, EventArgs e) { LoadExamList(); LoadSubjectList(); InitChart(); } private void InitChart() { chartScore.Series.Clear(); chartScore.ChartAreas.Clear(); chartScore.Titles.Clear(); chartScore.Legends.Clear(); var area new ChartArea(mainArea); chartScore.ChartAreas.Add(area); chartScore.Titles.Add(学生成绩排名); chartScore.Titles[0].Font new Font(Microsoft YaHei, 12f, FontStyle.Bold); }ChartArea是图表的绘图区。一次查询只画一张柱状图所以只建立一个名为mainArea的图表区即可。Titles的索引从 0 开始添加标题后再设置字体。如果窗体是在不同项目之间复制的记得检查chartScore控件的Dock属性或Anchor属性否则窗体尺寸变化时图表可能不会跟随缩放。5.2 根据查询结果动态生成 Series点击查询按钮后执行数据访问并将结果绑定到 Chart 的 Series 上private void btnQuery_Click(object sender, EventArgs e) { if (cmbExam.SelectedValue null || cmbSubject.SelectedValue null) { MessageBox.Show(请先选择考试和科目。); return; } var query new ScoreRankQuery { ExamId Convert.ToInt32(cmbExam.SelectedValue), SubjectId Convert.ToInt32(cmbSubject.SelectedValue) }; var items _scoreService.GetScoreRankList(query); if (items.Count 0) { MessageBox.Show(当前条件没有成绩数据。); RefreshChart(items); return; } RefreshChart(items); } private void RefreshChart(ListScoreRankItem items) { chartScore.Series.Clear(); var series new Series(成绩) { ChartType SeriesChartType.Column, IsValueShownAsLabel true, LabelFormat {0:F1} }; series.Points.DataBindXY( items.Select(x x.StudentName).ToArray(), items.Select(x (double)x.Score).ToArray() ); chartScore.Series.Add(series); AdjustChartArea(); }核心是DataBindXY方法。它接收两个数组第一个数组是 X 轴分类第二个数组是 Y 轴数值。这里用学生姓名作为 X 轴分类用成绩作为 Y 轴数值。LabelFormat {0:F1}是因为成绩保留了一位小数如果不设置格式double类型可能显示成多余的小数位。SeriesChartType.Column表示柱状图。使用ListScoreRankItem而不是 DataSet是为了让数据流动更清晰。在这类查询中DataSet 显得过重而且会增加绑定时的类型转换成本。5.3 坐标轴、数据标签和前三名颜色细节RefreshChart中调用的AdjustChartArea是图表显示效果的关键方法private void AdjustChartArea() { var area chartScore.ChartAreas[mainArea]; area.AxisX.Title 学生; area.AxisX.Interval 1; area.AxisX.LabelStyle.Angle -45; area.AxisX.LabelStyle.Interval 1; area.AxisY.Title 分数; area.AxisY.Minimum 0; area.AxisY.Maximum 100; area.AxisY.Interval 10; }这里必须解释清楚每个设置的意义。AxisY.Minimum 0很关键。考试成绩一般按百分制或 150 分制Y 轴从 0 开始柱子的高度比例才真实可信。如果不设置Chart 控件可能根据数据自动选择 Y 轴起点导致分数差距被视觉放大例如最低分 60 分的数据如果从 55 开始画会让人误以为 60 分很低。AxisX.LabelStyle.Angle -45是当学生数量较多时让姓名标签倾斜显示避免重叠。AxisX.Interval 1和AxisX.LabelStyle.Interval 1保证每个学生都显示一条分类标签。前三名高亮可以通过设置每个点的颜色实现private void HighlightTopStudents(Series series) { string[] topColors { #FFD700, #C0C0C0, #CD7F32 }; for (int i 0; i series.Points.Count; i) { if (i topColors.Length) { series.Points[i].Color ColorTranslator.FromHtml(topColors[i]); } else { series.Points[i].Color Color.FromArgb(70, 130, 220); } } }调用位置在chartScore.Series.Add(series);之后代码中需要对Series使用System.Drawing命名空间。前三名使用金、银、铜三色是常见处理能更快定位头部学生但这只是一个视觉约定具体配色可以按学校要求调整。如果学生数量非常多图表横轴会变得拥挤。此时可以在查询条件层增加一个“只看前 10 名”复选项而不是让 50 个名字全部挤在 Chart 上。实物使用中Chart 控件横向滚动不够灵活处理大数据量时优先做 TopN 筛选或改为分组统计图。6. 运行验证、问题排查和落地建议6.1 一次完整的验证流程代码如下完后不要直接以为图形能正常出来要按下面的路径验证向t_StudentScore插入一组测试数据至少包含两个考试、两个科目、多个学生的成绩分数要有高有低最好有相同分数。运行程序打开成绩排名窗体确认考试下拉框和科目下拉框有数据。选择一场考试和一个科目点击查询。观察柱子是否按分数从高到低排列。检查第一个柱子是否为最高分最后一个柱子是否为最低分。检查数据标签上的分数和数据库中的记录是否一致。切换到另一个科目确认图表数据会变化。选择一个没有成绩的考试科目组合确认页面会弹出友好提示而不是抛异常。预期画面是窗体顶部显示标题“学生成绩排名”横轴按学生姓名排列纵轴从 0 到 100柱体高度依次下降顶部标签显示具体分数。6.2 图表显示异常的排查链路图表不显示或显示异常时不要急着怀疑控件坏了按下面的顺序排查先检查查询结果集在GetScoreRankList方法返回后断点查看items.Count如果为 0后续所有图表代码都不会产生柱子。检查chartScore.ChartAreas是否存在Chart控件如果没有ChartAreaSeries 没有可绘制区域。检查series.Points数量DataBindXY的两个数组长度不一致或者某个数组里有 null会导致异常或显示错误。检查 Y 轴范围如果设置了AxisY.Maximum但分数超过该值柱子会超出绘图区如果Maximum小于实际分数效果也不正确。检查标签角度和间隔人数较多时横轴文字会叠在一起看起来像显示异常实际是布局参数问题。6.3 常见问题对照表问题现象常见原因处理建议Chart 控件在工具箱里找不到项目引用中缺少System.Windows.Forms.DataVisualization添加引用后重启工具箱或直接通过代码创建控件的引用点击查询后没有柱子查询结果为空或Series没有添加到chartScore.Series先检查数据集数量确认chartScore.Series.Add(series)已执行柱子高度比例看起来失真Y 轴最小值没有设置自动范围压缩了距离感设置AxisY.Minimum 0根据满分设置Maximum横轴学生名字全部叠在一起学生数量多标签没有倾斜或间隔过大设置AxisX.LabelStyle.Angle -45或限制 TopN重复点查询后柱子越来越多每次查询只新建 Series没有先Series.Clear()在查询开始处调用chartScore.Series.Clear()分数标签显示 98.00数值类型是 decimal/double 且未设置格式使用LabelFormat {0:F1}或{0:0.#}6.4 常见坑和落地检查清单这一节里最容易翻车的地方有三个。第一反复点击查询导致柱状图叠加。根因是代码中直接new Series后添加却忘了清理旧的 Series。正确处理是每次刷新前先调用chartScore.Series.Clear()。第二Y 轴范围不设置靠控件自动判断。图表的自动范围适合探索性绘图不适合成绩排名这种有明确语义的榜单。0 分和 100 分之间如果没有固定范围柱子长度就会失去参考意义。第三显示排名图只绑定分数没有检查同分情况。如果有两个学生都是 96 分柱子排序后的前后顺序可能不稳定但只要 SQL 中加入了StudentNo作为第二排序字段同分顺序就稳定了。成绩排名模块开发完可以按下面这个清单自查[ ] 查询 SQL 是否参数化[ ] 是否有稳定排序字段处理同分[ ] 查询结果为空时是否给出提示[ ] Chart 初始化是否清理了标题、图表区和 Series[ ] Y 轴是否设置了语义正确的起点和满点[ ] 数据标签格式是否统一[ ] 每列数据是否可以在 ToolTip 中查看考生名和分数[ ] 是否预留 TopN 参数用于学生较多时的横向空间优化7. 下一步图形排名下篇可以扩展的方向这一节的“上篇”完成了一条完整链路从成绩表数据准备到 C# 数据访问再到 WinForms Chart 柱状图渲染。你可以把它理解为排名图的主要骨架已经完成。下一部分值得优先扩展的方向有三个。第一按班级对比。回到ScoreRankQuery如果当前窗体不需要班级条件可以在查询 SQL 里继续增加AND s.ClassId ClassId并在界面上增加班级下拉框。这样图表就能从“整个年级某科排名”细化到“某个班的某科排名”。第二加入平均分参考线或分段统计图。柱状图展示的是个体排名分段统计图展示的是分布。在图表中添加一条线型 Series用SeriesChartType.Line表示班级平均分可以让读者既看到个体位置也看到整体水平。第三把图表导出为图片。Chart 控件本身支持保存图片chartScore.SaveImage(C:\temp\scoreRank.png, ChartImageFormat.Png);实际项目中不要把导出路径写死应该使用SaveFileDialog让用户选择目录。导出的图片还可以配合报表一起生成作为成绩分析材料。继续做“下篇”时建议先不要换图表库也不要急着加动画。先把同一个
返回列表