尧图精选

C#学生成绩管理系统从零实现:WinForms+SQL Server完整教程

🕒 发布时间:2026/10/1 3:03:33 📁 来源:尧图网络
简介面向C#初学者与数据库课程设计的学生成绩管理系统完整项目覆盖学生、课程、成绩三张表的关联管理包含登录、录入、查询、修改与删除等典型功能。压缩包共90个文件含31个C#源文件、11个资源文件、11个界面资源、配置文件以及可直接运行的EXE程序、Access数据库、项目解决方案与说明文档整体仅3.35MB已有962人学习下载。项目基于ADO.NET实现C#与Access数据库的交互涉及数据库范式设计、外键关联、CRUD操作、用户界面控件、异常处理、数据验证及SQL注入防范等核心知识点。学习本项目可快速理解窗体应用与关系型数据库的完整流程并借鉴源码中的界面布局、代码规范和排错思路。无论是完成课程作业还是入门.NET开发这份资源都能提供从设计到编码的系统参考帮助提升实际动手能力。1. 课程作业“C#学生成绩管理系统”一个题目卡住多少C#新手课程作业“C#学生成绩管理系统”大概是C#入门阶段出现频率最高的题目。网上C#基础教程、C#入门教学视频一抓一大把但真拿到这个题目从空的Visual Studio项目一路做到能运行、能答辩很多人会卡在同一个地方数据库连不上、DataGridView刷不出数据、老师随口问一句“你这段SQL是怎么设计的”就答不上来。这不是单个知识点不会而是缺一条从建库、连库到功能落地的完整路径。这篇笔记就是按我实际做过的方案把选型、建表、登录、学生管理、成绩统计、导出和常见翻车点一次讲透适合正在做课程设计或实训作业的同学直接照着复现。2. 为什么是WinForms和SQL Server选型理由、建表脚本与项目骨架2.1 界面层为什么选WinForms而不是WPF管理系统这类课程设计界面层的首选我一般推WinForms。原因很直接作业周期短WinForms的控件拖拽、双击事件、MessageBox弹窗这套交互逻辑几乎不用额外学习成本。新建项目后把Button、TextBox、DataGridView往窗体上一放事件处理函数就自动生成了剩下的事情就是把数据填进去。WPF不是不好但它带着MVVM、数据绑定、命令、依赖属性这一整套概念。对已经学过WPF的人当然没问题可如果是为了这次作业现学学习曲线会占掉你大半时间而且一旦绑定写错界面不刷新、数据不显示排查起来非常头疼。课程设计不是做产品稳定交付、能被老师问明白比界面技术新更重要。还有一个现实因素C#课程的教材和实验指导大多还以WinForms为宿主遇到问题时搜“C#学生成绩管理系统 WinForms”能参考的代码量远大于WPF。踩坑时找得到答案本身就是一种生产力。2.2 数据访问为什么用ADO.NET而不是EF Core数据访问层我选ADO.NET的SqlConnection、SqlCommand这套原生方案而不是EF Core。原因有两点第一课程里教的往往就是ADO.NET的这套流程作业答辩时老师问“你的数据是怎么查出来的”你能讲清楚Connection打开连接、Command执行SQL、DataAdapter填充DataTable这就够了第二ADO.NET的SQL语句是显式写出来的逻辑透明出问题能直接定位。EF Core是ORM框架虽然写起来更简短但对新手来说是个黑匣子——你调用一个ToList()背后生成的SQL是什么、延迟加载什么时候触发、导航属性为什么报了空引用每一个都是潜在扣分点。如果老师额外追问一句“EF怎么出来的这条SQL”没有实际项目经验很难接住。如果老师要求三层架构就按UI、BLL、DAL三层分如果老师没要求UI直接调DAL也可以。我见过很多作业硬凑一个BLL层里面的方法只是原封不动转发DAL的调用这种BLL在答辩时反而容易被问倒。正确的做法是让BLL承担校验逻辑比如分数必须在0到100之间、学号不能为空这些判断放BLL才有实际意义。2.3 数据库选型SQL Server Express还是SQLite数据库的选择直接影响你后面所有代码的写法这里用一个对比表说清楚。对比维度SQL Server Express含LocalDBSQLiteAccess安装成本LocalDB随VS安装几乎零配置单文件无需安装依赖旧版驱动新机器常缺SQL语法标准T-SQL教学常用大部分兼容个别函数不同语法老旧坑多并发能力好单机够用差答辩观感正规像完整项目可接受容易被质疑我一般推荐SQL Server Express的LocalDB模式它不需要你单独装一个完整数据库实例Visual Studio安装时通常自带。连接串写成Data Source(LocalDB)\MSSQLLocalDB就能用几乎不会出环境问题。如果老师的机器或你的电脑实在装不上SQL Server降级用SQLite。SQLite只需要拷一个.db文件连接串也简单代码层面改动有限后面避坑章节我会专门讲两者切换时的语法差异。2.4 三层结构与连接串项目骨架先搭好新建解决方案时按四个项目来组织。StudentManager.Model放实体类StudentManager.DAL放数据访问StudentManager.BLL放业务校验StudentManager.UI放WinForms窗体。课程设计规模小Model甚至可以不要但DAL和UI分开是底线否则所有代码堆在一个窗体文件里后期改一个功能要翻几百行代码非常痛苦。连接字符串我习惯放在UI项目的App.config里因为它是启动项目运行时能读到配置。?xml version1.0 encodingutf-8 ? configuration connectionStrings add nameStudentDB connectionStringData Source(LocalDB)\MSSQLLocalDB;AttachDbFilename|DataDirectory|StudentDB.mdf;Integrated SecurityTrue providerNameSystem.Data.SqlClient / /connectionStrings /configuration这段配置的核心是连接串里的三个部分。Data Source指定数据库实例LocalDB的实例名是固定的MSSQLLocalDBAttachDbFilename表示直接附加项目目录下的mdf文件配合|DataDirectory|可以在不同机器上迁移Integrated SecurityTrue表示用Windows账户登录不需要额外的SQL账号密码。如果同学们在机房用的是完整版SQL Server Express连接串通常改为Data Source.\SQLEXPRESS后面的Database名换成你自己建的库名。这一步配置错了后面所有查询都会报“无法连接服务器”。2.5 建表脚本学生、课程、成绩、账户四张表一次建完一个学生成绩管理系统最精简也要四张表。Account存登录账号Student存学生档案Course存课程Score存成绩。成绩表通过外键关联学生和课程。下面是完整的建库建表脚本可以直接在SQL Server Management Studio或Visual Studio的SQL Server对象资源管理器中执行。CREATE DATABASE StudentDB; GO USE StudentDB; GO CREATE TABLE Account ( Id INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(20) NOT NULL UNIQUE, Password NVARCHAR(50) NOT NULL, Role NVARCHAR(10) NOT NULL DEFAULT Teacher ); CREATE TABLE Student ( StudentNo NVARCHAR(10) PRIMARY KEY, Name NVARCHAR(20) NOT NULL, Gender NVARCHAR(2) NOT NULL, BirthDate DATE NULL, ClassName NVARCHAR(30) NULL, Phone NVARCHAR(11) NULL ); CREATE TABLE Course ( CourseNo NVARCHAR(10) PRIMARY KEY, CourseName NVARCHAR(30) NOT NULL, Credit DECIMAL(3,1) NOT NULL ); CREATE TABLE Score ( Id INT IDENTITY(1,1) PRIMARY KEY, StudentNo NVARCHAR(10) NOT NULL, CourseNo NVARCHAR(10) NOT NULL, Score DECIMAL(5,1) NOT NULL, CONSTRAINT UQ_Student_Course UNIQUE (StudentNo, CourseNo), CONSTRAINT FK_Score_Student FOREIGN KEY (StudentNo) REFERENCES Student(StudentNo), CONSTRAINT FK_Score_Course FOREIGN KEY (CourseNo) REFERENCES Course(CourseNo) ); INSERT INTO Account (UserName, Password, Role) VALUES (admin, 123456, Admin); INSERT INTO Student (StudentNo, Name, Gender, BirthDate, ClassName, Phone) VALUES (2024001, N张三, N男, 2005-03-12, N计科2401, 13800000000); INSERT INTO Course (CourseNo, CourseName, Credit) VALUES (C001, NC#程序设计, 3.0);建表脚本里有几个细节需要说明。Account表里的Role字段默认Teacher后面可以扩展成Admin和Teacher两种角色用于控制谁能删除数据谁只能录入。Score表加了一个UNIQUE约束禁止同一学生同一课程出现两条成绩记录这个约束直接影响后面成绩录入时先查重再更新的逻辑。所有中文字符串前面都加了N前缀这是SQL Server存储Unicode的标准写法。如果不加N遇到生僻字或特殊符号可能存成问号这是新手最容易忽视的细节。2.6 学号生成为什么作业里偏爱MAX1而不是IDENTITY学生表的学号我设计成NVARCHAR(10)而不是用自增ID这是课程设计里最常见的学号格式。实际开发中自增IDENTITY当然最省事但学号通常要求带年份前缀比如2024001表示2024级第001号学生。这种业务编号用IDENTITY做不了常见做法是查当前最大学号然后加一。-- 生成新学号年份前缀 三位流水号 DECLARE newNo VARCHAR(10); SELECT newNo 2024 RIGHT(000 CAST( ISNULL(MAX(CAST(RIGHT(StudentNo, 3) AS INT)), 0) 1 AS VARCHAR(3)), 3) FROM Student; SELECT newNo;这段SQL的逻辑是先从Student表中取出当前最大的学号用RIGHT取其后三位转成数字加一后再用RIGHT补零成三位最后拼上年份前缀。ISNULL处理空表的情况如果表里还没有数据MAX的结果是NULLISNULL把它变成0再加1得到的就是2024001。这个写法的问题很明显并发环境下两个用户同时新增学生可能生成同一个学号。但课程设计是单机程序不存在并发所以完全够用。如果老师要求更稳妥的方案可以在新增时用事务加锁但作业阶段没必要自己加复杂度。3. 登录与学生管理参数化查询、DataGridView和增删改查的落地写法3.1 SqlHelper把打开连接、执行SQL、返回结果封装成一行写WinForms窗体时如果每个按钮事件里都重复写连接、命令、适配器代码会非常冗余。我习惯先封装一个SqlHelper类放在DAL层后面所有窗体都调用它。这个类提供三个方法查询返回DataTable执行增删改返回受影响行数查询单个值返回object。using System; using System.Data; using System.Data.SqlClient; namespace StudentManager.DAL { public static class SqlHelper { private static readonly string connStr System.Configuration.ConfigurationManager .ConnectionStrings[StudentDB].ConnectionString; // 查询返回DataTable public static DataTable ExecuteQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } SqlDataAdapter adapter new SqlDataAdapter(cmd); DataTable dt new DataTable(); adapter.Fill(dt); return dt; } } // 增删改返回受影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } conn.Open(); return cmd.ExecuteNonQuery(); } } // 查询单个值登录时统计行数用 public static object ExecuteScalar(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } conn.Open(); return cmd.ExecuteScalar(); } } } }用using关键字包裹SqlConnection和SqlCommand是个好习惯作用是在方法退出时自动释放数据库连接和命令对象不需要手动Close。这三个方法里有一个容易被忽略的细节ExecuteQuery没有手动Open连接因为SqlDataAdapter.Fill方法内部会检查连接状态如果是关闭的就自动打开执行完再自动关闭但ExecuteNonQuery和ExecuteScalar必须手动Open。配置文件的读取用了ConfigurationManager需要在UI项目里引用System.Configuration程序集。如果编译时提示找不到ConfigurationManager就去NuGet里装System.Configuration.ConfigurationManager这是.NET Framework和.NET Core通用做法。3.2 登录窗体用参数化查询而不是拼接SQL登录窗体的核心逻辑很简单拿用户在文本框里输入的用户名和密码去Account表里查有没有匹配记录。这里最大的坑是SQL拼接写法。很多教材示例会写string sql SELECT COUNT(*) FROM Account WHERE UserName txtUser.Text 这种写法在作业里属于严重扣分项。拼接SQL有两个问题。第一是SQL注入风险用户输入一段精心构造的字符串可以绕过密码验证虽然课程设计不追求安全但老师问起来答不上为什么用参数化就是失分点。第二是引号和中文容易写错字符串拼接时少写个引号程序就崩。下面是我常用的登录按钮事件写法。private void btnLogin_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtUser.Text) || string.IsNullOrWhiteSpace(txtPassword.Text)) { MessageBox.Show(用户名和密码不能为空); return; } string sql SELECT COUNT(*) FROM Account WHERE UserNameuser AND Passwordpwd; SqlParameter[] parameters { new SqlParameter(user, txtUser.Text.Trim()), new SqlParameter(pwd, txtPassword.Text) }; int count Convert.ToInt32(SqlHelper.ExecuteScalar(sql, parameters)); if (count 0) { this.Hide(); MainForm mainForm new MainForm(txtUser.Text.Trim()); mainForm.Show(); } else { MessageBox.Show(用户名或密码错误); } }参数化的本质是把用户输入当作数据而不是SQL代码。user和pwd是SQL语句里的占位符真正的值通过SqlParameter传给SqlCommand数据库在处理时会把它们当作字面值比对永远不会被解释成可执行的SQL片段。登录成功后用this.Hide()把登录窗体隐藏而不是关闭主窗体用Show()非模态显示。这里有个隐患如果直接关掉登录窗体程序的主消息循环可能跟着退出。我通常在MainForm的FormClosing事件里加一行Environment.Exit(0)确保退出时所有窗口一起关闭不残留后台进程。把用户名从登录窗体传到主窗体最简单的方案是构造函数传参也就是上面代码中的new MainForm(txtUser.Text.Trim())。如果想做得复杂一点用C#委托和事件也能实现但课程设计没必要。3.3 学生列表DataGridView绑定DataTable中文别名的妙用学生管理窗体是整个系统最基础的页面功能包括列表展示、添加、修改、删除、搜索。列表展示这一步核心命令只有两行。string sql SELECT StudentNo AS 学号, Name AS 姓名, Gender AS 性别, BirthDate AS 出生日期, ClassName AS 班级, Phone AS 电话 FROM Student; DataTable dt SqlHelper.ExecuteQuery(sql, null); dataGridView1.DataSource dt;这里有两个关键写法。第一SQL查询里显式列出字段而不是SELECT *。SELECT *会把表结构原样展示列的顺序不可控以后加了字段会连不用的列一起显示显式列出字段能让查询结果稳定也方便控制列名。第二AS别名直接写成中文DataGridView绑定DataTable时会默认把列名当作表头这样表格显示出来的就是“学号”“姓名”“班级”省去了手工设置HeaderText的代码。绑定完成后如果后续执行了插入或更新操作需要重新执行一遍查询并重新赋值DataSource表格才会刷新。偷懒的做法是在每次操作后重新调用一下加载数据的方法。3.4 添加、修改、删除学生三条SQL与参数顺序新增学生的核心是INSERT语句。以下是一个完整的添加按钮事件。private void btnAdd_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtStudentNo.Text)) { MessageBox.Show(学号不能为空); return; } string sql INSERT INTO Student(StudentNo, Name, Gender, BirthDate, ClassName, Phone) VALUES(no, name, gender, birth, class, phone); SqlParameter[] parameters { new SqlParameter(no, txtStudentNo.Text.Trim()), new SqlParameter(name, txtName.Text.Trim()), new SqlParameter(gender, comboGender.Text), new SqlParameter(birth, dtpBirth.Value.Date), new SqlParameter(class, txtClass.Text.Trim()), new SqlParameter(phone, txtPhone.Text.Trim()) }; int rows SqlHelper.ExecuteNonQuery(sql, parameters); if (rows 0) { MessageBox.Show(添加成功); LoadStudentList(); } }参数数组里每个SqlParameter的名字要跟SQL语句里的占位符完全一致顺序无所谓因为SqlCommand按名字匹配参数。但有一个细节如果SQL里同一个参数名出现了两次比如UPDATE语句里SET和WHERE都用到no参数数组里只需要添加一个noSqlCommand会自动复用。修改学生的SQL是UPDATE一般按学号定位记录。string sql UPDATE Student SET Namename, Gendergender, BirthDatebirth, ClassNameclass, Phonephone WHERE StudentNono;删除学生的操作建议先弹一个确认框防止误删。确认后执行DELETE FROM Student WHERE StudentNono。注意一个连带影响如果某个学生已经有成绩记录直接删Student会触发外键约束报错。常见的处理方式是先确认“该学生有成绩记录删除将同时删除其成绩”然后在一个事务里先DELETE Score再DELETE Student或者用ON DELETE CASCADE的外键设计。课程设计阶段我给学生的建议是写清楚逻辑先删成绩再删学生。3.5 按学号或姓名搜索LIKE参数不能写在SQL里搜索框的实现用LIKE模糊查询。常见错误是把百分号拼接在SQL字符串里写成WHERE Name LIKE %kw%这样是查不到数据的因为kw被当成了字面量的一部分。正确的是把百分号拼在参数值里。private void btnSearch_Click(object sender, EventArgs e) { string keyword txtKeyword.Text.Trim(); string sql SELECT StudentNo AS 学号, Name AS 姓名, Gender AS 性别, ClassName AS 班级, Phone AS 电话 FROM Student WHERE StudentNo LIKE kw OR Name LIKE kw; SqlParameter p new SqlParameter(kw, % keyword %); DataTable dt SqlHelper.ExecuteQuery(sql, p); dataGridView1.DataSource dt; }LIKE语句里百分号是通配符%放在字符串前表示以关键字结尾放在字符串后表示以关键字开头两边都放表示包含。把%加在参数值里让数据库在执行时拿完整的匹配模式去比对这是参数化查询下做模糊搜索的标准姿势。这个搜索方法还有一个小问题关键字为空时会查出全部学生体验反而混乱可以在方法开头加个判断关键字为空时直接返回不执行查询。4. 成绩录入、统计分析与导出从录分数到一张Excel成绩单4.1 成绩录入下拉框选学生课程先查重再写入成绩录入窗体的交互设计通常是三个控件一个下拉框选学生一个下拉框选课程一个文本框输分数再加一个保存按钮。两个下拉框的数据源绑定是关键。// 绑定学生下拉框显示学号-姓名值存学号 DataTable students SqlHelper.ExecuteQuery( SELECT StudentNo, Name FROM Student, null); comboStudent.DataSource students; comboStudent.DisplayMember Name; comboStudent.ValueMember StudentNo; // 绑定课程下拉框显示课程名值存课程编号 DataTable courses SqlHelper.ExecuteQuery( SELECT CourseNo, CourseName FROM Course, null); comboCourse.DataSource courses; comboCourse.DisplayMember CourseName; comboCourse.ValueMember CourseNo;DisplayMember决定界面上显示的文字ValueMember决定SelectedValue返回的值。这两个属性是ComboBox绑定的核心很多同学只设置了DataSource就运行结果下拉框里显示System.Data.DataRowView就是因为漏掉了DisplayMember。保存成绩时因为Score表有UNIQUE约束必须先查询该学生该课程是否已经有成绩。private void btnSave_Click(object sender, EventArgs e) { if (comboStudent.SelectedValue null || comboCourse.SelectedValue null) { MessageBox.Show(请选择学生和课程); return; } if (!int.TryParse(txtScore.Text.Trim(), out int score) || score 0 || score 100) { MessageBox.Show(分数必须是0到100之间的整数); return; } string studentNo comboStudent.SelectedValue.ToString(); string courseNo comboCourse.SelectedValue.ToString(); string checkSql SELECT COUNT(*) FROM Score WHERE StudentNostudentNo AND CourseNocourseNo; SqlParameter[] checkParams { new SqlParameter(studentNo, studentNo), new SqlParameter(courseNo, courseNo) }; int exists Convert.ToInt32(SqlHelper.ExecuteScalar(checkSql, checkParams)); if (exists 0) { string updateSql UPDATE Score SET Scorescore WHERE StudentNostudentNo AND CourseNocourseNo; } else { string insertSql INSERT INTO Score(StudentNo, CourseNo, Score) VALUES(studentNo, courseNo, score); } // 执行增删改并提示成功然后刷新成绩列表 }分数校验用了int.TryParse加范围判断比直接Convert.ToInt32安全因为用户可能输入非数字内容TryParse不会抛异常。成绩字段在数据库里是DECIMAL(5,1)可以存小数分把成绩按整数处理是为了校验方便如果允许小数改成double.TryParse即可。这段先查再写的方式看起来多一次数据库往返但对课设系统完全没问题。它保证了成绩不会重复录入也为后续统计打下了数据干净的基础。4.2 统计排名一条GROUP BY代替循环累加成绩统计最常见的需求是算每个学生的总分、平均分和排名。新手容易写成在C#里用foreach循环累加所有成绩再算平均这种做法不仅代码长而且当数据量到几百条时界面会卡顿。正确做法是在SQL里一次完成。SELECT ROW_NUMBER() OVER (ORDER BY SUM(sc.Score) DESC) AS 排名, st.StudentNo AS 学号, st.Name AS 姓名, SUM(sc.Score) AS 总分, CAST(AVG(sc.Score) AS DECIMAL(5,1)) AS 平均分 FROM Score sc INNER JOIN Student st ON sc.StudentNo st.StudentNo GROUP BY st.StudentNo, st.Name ORDER BY 总分 DESC;这条SQL的逻辑拆开看有三个关键点。GROUP BY按学号和姓名分组把同一个学生的多条成绩记录合成一组SUM和AVG是聚合函数分别算组内总分和平均分ROW_NUMBER() OVER (ORDER BY SUM(sc.Score) DESC)按总分降序生成排名序号。这里必须注意GROUP BY的规则SELECT子句里出现的非聚合列必须出现在GROUP BY中。上面查询里学号和姓名都来自学生表按学号分组后姓名是跟着学号走的所以GROUP BY st.StudentNo, st.Name两项都要写上。如果只GROUP BY st.StudentNo而SELECT里又有st.NameSQL Server会直接报错。排名算法还有一个边界情况两个学生总分相同ROW_NUMBER会生成两个连续的排名比如并列第一会是1、2而不是1、1。如果老师要求并列排名改用RANK()函数它会为相同分数生成相同排名。4.3 按课程统计及格率CASE WHEN的聚合写法另一个高频需求是按课程统计考试人数、及格人数和及格率。及格率的计算用CASE WHEN配合SUM这是SQL里很经典的条件聚合写法。SELECT c.CourseName AS 课程名称, COUNT(*) AS 考试人数, SUM(CASE WHEN sc.Score 60 THEN 1 ELSE 0 END) AS 及格人数, CAST( SUM(CASE WHEN sc.Score 60 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) * 100 AS DECIMAL(5,1) ) AS 及格率 FROM Score sc INNER JOIN Course c ON sc.CourseNo c.CourseNo GROUP BY c.CourseName;CASE WHEN在SUM里的作用是把符合条件的记录标记为1、不符合标记为0SUM的结果就是满足条件的行数。及格率计算时乘1.0是关键因为整数除以整数在SQL Server里结果还是整数85除以100会得0乘1.0转成小数才是0.85。这张结果表很适合直接放进课程设计报告的实验结果章节。如果还要统计单科最高分、最低分同样是GROUP BY加MAX和MIN一条SQL就能带出来。4.4 导出ExcelCOM方式与CSV降级方案导出功能是演示时的加分项也是现场最容易翻车的一环。最直观的方案是调用Microsoft Excel的COM组件把DataGridView的内容循环写入Excel工作表。Microsoft.Office.Interop.Excel.Application app new Microsoft.Office.Interop.Excel.Application(); app.Visible false; app.DisplayAlerts false; Workbook workbook app.Workbooks.Add(); Worksheet sheet workbook.Worksheets[1]; // 写入表头 for (int j 0; j dataGridView1.Columns.Count; j) { sheet.Cells[1, j 1] dataGridView1.Columns[j].HeaderText; } // 写入数据行 for (int i 0; i dataGridView1.Rows.Count; i) { for (int j 0; j dataGridView1.Columns.Count; j) { sheet.Cells[i 2, j 1] dataGridView1.Rows[i].Cells[j].Value?.ToString() ?? ; } } workbook.SaveAs(D:\成绩单.xlsx); workbook.Close(); app.Quit();使用COM导出前需要在项目里添加引用在“添加引用”对话框里选择“COM”选项卡找到Microsoft Excel 16.0 Object Library。这个方法有三个必须注意的参数app.Visible要设置false否则导出过程中会闪出一个Excel窗口sheet.Cells的行列索引都是从1开始而DataGridView的索引从0开始所以写入表头时列要加1、写入数据时行要加2第一行表头占位最后app.Quit()必须在所有操作完成后调用不然Excel进程一直挂在后台。COM方式最怕现场环境没有安装桌面版Excel。稳妥的降级方案是导出CSV文件用StreamWriter写入逗号分隔的文本Excel可以直接打开。string filePath D:\成绩单.csv; using (StreamWriter writer new StreamWriter(filePath, false, new UTF8Encoding(true))) { // 写入表头 string[] header new string[dataGridView1.Columns.Count]; for (int j 0; j dataGridView1.Columns.Count; j) { header[j] dataGridView1.Columns[j].HeaderText; } writer.WriteLine(string.Join(,, header)); // 写入数据行 foreach (DataGridViewRow row in dataGridView1.Rows) { string[] cells new string[dataGridView1.Columns.Count]; for (int j 0; j dataGridView1.Columns.Count; j) { cells[j] row.Cells[j].Value?.ToString() ?? ; } writer.WriteLine(string.Join(,, cells)); } } MessageBox.Show(导出成功 filePath);CSV方案我作为兜底推荐。编码必须用带BOM的UTF-8也就是代码里的new UTF8Encoding(true)否则Excel用系统默认编码打开文件时中文姓名会变成乱码。不带BOM的UTF-8在记事本里正常在Excel里乱码这是导出功能最常见的翻车点。5. 避坑专章课程设计里最常见的5个翻车现场5.1 DataGridView不显示数据也不报错现象程序跑起来窗体正常弹出但DataGridView一片空白连列标题都没有。原因最常见的是没有给DataSource赋值或者绑定的DataTable是空的。还有一种隐蔽情况是SQL查询抛了异常但代码里的try-catch是空的异常被吞掉界面看起来就像什么都没发生。解决先用手工构造一个DataTable试绑定确认控件本身没毛病再检查查询语句和连接串。所有DAL调用处的catch块里至少放一个MessageBox.Show(ex.Message)让错误现形。空catch是调试时最大的敌人我自己早期课设时被这种“不报错但没数据”硬生生耗掉过两天时间。5.2 数据库文件被占用无法附加或删除现象在Visual Studio里尝试附加StudentDB.mdf文件系统提示“文件正在被另一进程使用”或者想删掉数据库文件重建时怎么也删不掉。原因上一轮调试的WinForms进程没有真正退出或者Visual Studio的SQL Server对象资源管理器一直保持着对mdf文件的连接。数据库文件不像普通文本文件被连接时是锁定状态任何外部操作都会被拒绝。解决先去任务管理器确认StudentManager.exe进程是否存在有就结束掉再把Visual Studio里所有打开的相关窗口关掉必要时整个解决方案关掉重新打开。最彻底的办法是用sqllocaldb stop命令停掉LocalDB实例释放所有连接。5.3 导出CSV后中文乱码现象程序导出的CSV文件用Excel打开中文姓名、班级、课程名全部显示成乱码但用记事本打开正常。原因StreamWriter默认用的编码不是UTF-8也不是系统中文编码导致Excel用错误编码解析文件。这是.NET里File.WriteAllText和StreamWriter最容易踩的隐形坑。解决创建StreamWriter时显式指定编码为new UTF8Encoding(true)写文件时带上BOM头。Excel识别到BOM后会主动用UTF-8解码中文就正常了。如果已经生成乱码文件重新导出一次即可不用改任何业务逻辑。5.4 Excel COM导出失败或进程残留现象点击导出按钮后没有任何反应或者导出一次成功但任务管理器里攒了一堆EXCEL.EXE进程再点导出就越点越卡。原因COM方式导出要求代码在STA线程运行WinForms主线程默认是STA但如果把导出逻辑放到了后台线程COM调用会失败。进程残留则是没有正确释放COM对象每次导出创建一个新的Excel ApplicationQuit了但ReleaseComObject没有调用。解决导出逻辑直接放在按钮点击事件里不要用Task.Run包起来。app.Quit()之后遍历所有用到的COM对象调用Marshal.FinalReleaseComObject最后GC.Collect()。如果现场没有Excel或者反复报COM错误直接切换到CSV导出功能和效果基本不受影响。5.5 换台电脑就连不上数据库现象程序在自己电脑上运行正常拷到老师电脑上演示时登录窗体一提交就报“无法连接到数据库”或“从服务器读取结果出错”。原因连接串里的Data Source指向的是你本机的LocalDB实例或你机器上装的SQL Server实例。换一台电脑实例名不存在自然连不上。这是课程设计现场最容易被低估的一步很多同学在机房演示时当场翻车。解决交作业前把数据库的建立步骤写清楚包括创建数据库、执行建表脚本、插入初始账号数据。如果项目使用了AttachDbFilename指向mdf文件记得把mdf和ldf两个文件一起拷到项目目录。最省心的做法是把连接串改成相对通用的形式并附一页部署说明文档让老师知道在机器上怎么把环境搭起来。6. 交作业前的自测与可加分扩展从“能跑”到“能讲”6.1 交作业前按这个清单过一遍我每次交课程设计前都会强制自己做一次“删库重来”测试删掉数据库重新执行建表脚本然后把登录、学生管理、成绩录入、统计、导出全部跑一遍。这能暴露一个很现实的问题——你的项目里是否有手工改过数据库导致代码和脚本不一致而这就是老师现场演示时最常检查的漏洞。按这个顺序自测一遍从建库脚本开始完整重建数据库登录admin账号新增一个学生修改他的班级信息再删掉他录入两门课程成绩看排名统计是否正确导出CSV确认中文不乱码最后检查任务管理器没有残留进程。断网环境跑一遍也很重要LocalDB是本地服务断网不影响但如果你的代码里有任何联网逻辑这一步就能发现。6.2 三个不用大改的加分扩展第一个扩展是权限控制。Account表里已经有Role字段登录时把这个值带进主窗体。删除学生和删除成绩的按钮事件开头加一个判断Role不是Admin就提示无权限并return几行代码就能让系统看起来完整很多。第二个扩展是操作日志。在SqlHelper的catch块里记录错误信息到日志文件演示时直接打开日志文件给老师看比嘴上解释“这里有个异常”有说服力得多。catch (Exception ex) { using (StreamWriter writer new StreamWriter(error.log, true)) { writer.WriteLine(${DateTime.Now} - {ex.Message}); writer.WriteLine(ex.StackTrace); } throw; }第三个扩展是数据量自测。用一段循环SQL往Score表插几百条随机成绩验证统计页面是否还能秒开。这条T-SQL可以在SQL Server Management Studio里直接跑。DECLARE i INT 1; WHILE i 500 BEGIN INSERT INTO Score(StudentNo, CourseNo, Score) VALUES(2024001, C001, 60 (i % 40)); SET i i 1; END;我自己的习惯是每写完一个模块就先跑一遍这个自测而不是攒到最后统一测。等到最后再做全套测试一旦发现问题排查范围会覆盖整个项目压力很大。课设阶段给自己留出从容的时间答辩现场才不会慌。希望这篇笔记能帮你把“C#学生成绩管理系统”从题目变成一份能跑、能讲、能演示的完整作业。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →