简介:课程作业用的C#学生成绩管理系统完整源码包,面向正在学习C#编程、数据库应用开发或需要完成类似课程设计的初学者。系统以Access数据库为后端,借助数据访问组件完成学生、课程、成绩三类信息的增删改查,并实现界面交互、数据验证与异常处理;覆盖面向对象编程、窗体控件使用、关系数据库设计、SQL命令等核心知识点,同时包含学号唯一性校验、成绩范围判断等实际业务规则,适合作为项目练手和对照参考。资源共90个文件,以31个C#源代码文件、11个界面资源文件、4个依赖库、3个可执行程序和1个Access数据库文件为主,另含解决方案与项目工程文件、图标和配置文件,压缩包约3.35MB,整体结构清晰,可在开发环境里直接打开运行与调试。已有962人学习下载,通过查看源码和数据库表结构,可以快速理解业务逻辑与表关系,也可在此基础上扩展报表统计、登录验证或权限管理,完成自己的课程作业或期末设计。
1. C#学生成绩管理系统:课程作业里最该拆开看的一个完整闭环
学生成绩管理系统这个题目,几乎每个学 .NET 的人都在课程作业里撞见过。它看起来简单——无非是学生对课程、课程对成绩的三张表增删改查,但真把一个能交差的系统写完,你会发现它把 C# 语法、Access 数据库设计、ADO.NET 数据访问、Windows Forms 界面绑定、异常处理全串起来了。这个项目最好的地方在于:它不像那些只练语法的小 demo,它是一个有业务逻辑、有数据关联、有界面交互的完整闭环,做完一遍,你对"一个软件到底怎么从数据库走到界面上"会有实感。
这套资源适合两类人:一是正在做课程设计、需要一份能跑通且有注释参考的源码的学生;二是工作后用不上 WinForms、想快速捡回 ADO.NET 和 Access 这套老技术栈的开发者。我不打算给你讲一堆概念,直接按"数据库怎么建、连接怎么写、界面怎么绑、坑在哪"来拆,照着做,你也能把这套系统复现出来。
2. 数据从哪来:Access 三张表的设计与关联关系
2.1 为什么课程作业选 Access 而不是 SQL Server
很多人一上来就问:为什么不用 SQL Server?课程作业用 Access 是有现实原因的。Access 是文件型数据库,一个 .accdb 文件就能跑,不需要单独安装数据库服务,交作业时把数据库文件一起打包就行,老师打开就能验收。而 SQL Server 还得附加数据库、配账号权限,光环境问题就能劝退一批人。
从学习角度讲,Access 支持标准的 SQL 语法,SELECT、INSERT、UPDATE、DELETE 都能跑,主键、外键、索引这些约束也都有。你用 Access 练会的 SQL 语句,换到 SQL Server 和 MySQL 上基本是平移的。项目中用 ADO.NET 操作 Access 和操作 SQL Server 的代码结构也几乎一样,只是连接串和 Provider 不同。
2.2 建表语句与字段约束
这个系统核心是三张表:学生表、课程表、成绩表。字段设计直接参考了项目正文里提到的范式要求,我建议你按下面的结构来建:
-- 学生表 CREATE TABLE Student ( StudentID TEXT(10) PRIMARY KEY, StudentName TEXT(20) NOT NULL, Gender TEXT(2) CHECK (Gender IN ('男', '女')), BirthDate DATETIME, ClassName TEXT(30) ); -- 课程表 CREATE TABLE Course ( CourseID TEXT(10) PRIMARY KEY, CourseName TEXT(30) NOT NULL, Credit SINGLE, TeacherName TEXT(20) ); -- 成绩表:通过外键关联学生和课程 CREATE TABLE Score ( StudentID TEXT(10) NOT NULL, CourseID TEXT(10) NOT NULL, Score SINGLE CHECK (Score >= 0 AND Score <= 100), PRIMARY KEY (StudentID, CourseID), FOREIGN KEY (StudentID) REFERENCES Student(StudentID), FOREIGN KEY (CourseID) REFERENCES Course(CourseID) );StudentID 和 CourseID 都用 TEXT 类型而不是数字自增主键,这是 Access 场景下的常见做法。因为学号和课程编号本身就有业务含义(比如 2024001 代表某个年级的学生),直接用文本做主键,查询时条件写起来直观,也避免了自增 ID 在数据导入导出时对不上号的问题。
Score 表的主键是 (StudentID, CourseID) 联合主键,这保证了同一个学生同一门课只能有一条成绩记录,从数据库层面杜绝了重复录入。外键约束则是为了保证你删一个学生时,不会留下一堆孤立的成绩数据——虽然 Access 的外键约束执行得没有 SQL Server 那么严格,但建上总比不建强。
2.3 数据类型选择的三个注意点
- 文本字段给足长度:StudentID 设 TEXT(10),别用 TEXT(255) 或默认长度,否则后面写验证代码时还得处理多余空格。
- 成绩字段用 SINGLE 而不是 INTEGER:成绩可能有小数点(比如 85.5),INTEGER 会直接截断,SINGLE 够用且节省空间。
- 日期字段用 DATETIME:Access 里 DATE 类型实际存储也是 DATETIME,你写代码取出来的是 DateTime 对象,前端显示时再格式化。
3. ADO.NET 连接 Access:连接串、三种数据访问方式与增删改查实现
3.1 连接串怎么配才不出错
Access 数据库的连接串是这个项目里第一个坑。很多人直接把网上的代码抄过来,结果报"未找到提供程序"或"Microsoft.ACE.OLEDB.12.0 未注册"。这是因为 Access 数据库有两种格式:旧版 .mdb 用 Jet 4.0,新版 .accdb 用 ACE 12.0,两个的 Provider 名字不一样。
// 适用于 .accdb 文件(Access 2007 及以上版本) string connStr = @"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=|DataDirectory|StudentScore.accdb;Persist Security Info=False;"; // 适用于 .mdb 文件(Access 2003 及以下版本) string connStrOld = @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|StudentScore.mdb;";|DataDirectory| 是个好用的占位符,它会自动替换成程序运行目录下的数据文件夹。你把 .accdb 文件放在项目根目录下的 Data 文件夹里,然后在 App.config 里配置一下,代码里就不用写绝对路径了——这样作业拷到别的电脑上也能跑。
连接 Access 不需要用户名密码,Persist Security Info=False 意思是每次连接都用默认的安全机制。如果你的 Access 文件设置了数据库密码,这行得改成 True,并在连接串里加上 Password=xxx。
3.2 用 OleDbDataAdapter 填充 DataTable:查询的正确姿势
ADO.NET 操作 Access 有三件套:OleDbConnection、OleDbCommand、OleDbDataAdapter。最常见的查询写法是用 DataAdapter 把结果填进 DataTable,然后绑给 DataGridView 显示:
using System.Data.OleDb; // 1. 创建连接 using (OleDbConnection conn = new OleDbConnection(connStr)) { // 2. 准备 SQL 语句 string sql = "SELECT StudentID, StudentName, Gender, ClassName FROM Student"; // 3. 创建 DataAdapter 并填充 DataTable OleDbDataAdapter adapter = new OleDbDataAdapter(sql, conn); DataTable dt = new DataTable(); adapter.Fill(dt); // 4. 绑定到 DataGridView dataGridView1.DataSource = dt; }这段代码逻辑很直白:先创建连接对象,然后准备 SQL,DataAdapter 内部会自动打开连接、执行命令、把结果填充到 DataTable、再关闭连接——Fill 方法帮你把连接生命周期管理好了。你不需要手动 Open/Close,这比用 OleDbCommand 手动执行要省心。
如果你的查询需要参数(比如按学生姓名模糊搜索),别直接拼接字符串,用参数化查询:
string sql = "SELECT StudentID, StudentName, Gender, ClassName FROM Student WHERE StudentName LIKE @name"; adapter.SelectCommand = new OleDbCommand(sql, conn); adapter.SelectCommand.Parameters.AddWithValue("@name", "%" + txtKeyword.Text.Trim() + "%");注意 OleDb 的参数占位符是 @name,不是 SQL Server 的 @name 或 MySQL 的 ?,这是新手最容易搞混的地方。另外,OleDb 不支持命名参数批量添加的某些写法,你用 AddWithValue 逐个加最稳。
3.3 增删改:ExecuteNonQuery 与事务边界
增删改操作不走 DataAdapter,用 OleDbCommand 的 ExecuteNonQuery 方法。它返回受影响的行数,判断执行是否成功:
// 新增学生 string sql = "INSERT INTO Student (StudentID, StudentName, Gender, BirthDate, ClassName) VALUES (?, ?, ?, ?, ?)"; using (OleDbConnection conn = new OleDbConnection(connStr)) using (OleDbCommand cmd = new OleDbCommand(sql, conn)) { // OleDb 的参数顺序严格按 SQL 中 ? 的出现顺序 cmd.Parameters.AddWithValue("@p1", txtStudentID.Text.Trim()); cmd.Parameters.AddWithValue("@p2", txtStudentName.Text.Trim()); cmd.Parameters.AddWithValue("@p3", cmbGender.Text); cmd.Parameters.AddWithValue("@p4", DateTime.Parse(dtpBirthDate.Value.ToString("yyyy-MM-dd"))); cmd.Parameters.AddWithValue("@p5", txtClassName.Text.Trim()); conn.Open(); int rows = cmd.ExecuteNonQuery(); if (rows > 0) { MessageBox.Show("学生信息添加成功"); RefreshStudentList(); // 刷新 DataGridView } }这里有个关键点:OleDbCommand 的参数占位符用的是 ?(问号),而不是 @name,但 AddWithValue 里你写的参数名其实会被忽略,真正起作用的是参数添加的顺序。也就是说,你第一个 AddWithValue 对应 SQL 里第一个 ?,第二个对应第二个 ?,以此类推。这是个很容易翻车的地方——顺序写错,数据就像串台一样乱插。
更新和删除的写法类似,只是 SQL 不同:
string updateSql = "UPDATE Student SET StudentName=?, Gender=?, ClassName=? WHERE StudentID=?"; string deleteSql = "DELETE FROM Student WHERE StudentID=?";删除操作建议先判断该学生是否已有成绩记录,否则会把成绩表里的关联数据一起搞出孤儿记录。一种常见做法是删除前先查 Score 表:
string checkSql = "SELECT COUNT(*) FROM Score WHERE StudentID=@id"; // 如果 count > 0,提示用户先删除成绩记录这里我用的是 @id 而不是 ?,是想提醒你:OleDbCommand 两种写法混用没问题但不要混在同一条 SQL 里。实际项目中我一般统一用 ?,因为这是 OleDb 最原生的写法,碰上复杂 SQL 时不至于迷糊。
3.4 DataSet 与 DataTable:该用哪个
项目正文里提到了 DataSet、DataTable、DataAdapter 这些组件,很多初学者分不清。简单说:DataTable 是一张内存表,DataSet 是 DataTable 的集合,可以装多张表还保持它们之间的关联关系。课程作业这个场景,界面主要是单表展示,用 DataTable 就够了;如果你做的是那种左侧选学生、右侧自动显示该学生成绩的联动界面,可以一次性把 Student 和 Score 两张表 Fill 进 DataSet,通过 DataRelation 建立关联,省得来回查数据库。
但我的建议是:这种小系统别把 DataSet 玩得太花,每次操作直接查数据库反而逻辑更清楚。DataSet 的优势在大批量离线操作时才明显,课程作业用不上。
4. 界面层到数据层:DataGridView 绑定与增删改查的完整串联
4.1 界面控件职责分配
Windows Forms 界面按功能分三个区域,每个区域用到的控件职责很明确:
| 功能区域 | 核心控件 | 职责说明 |
|---|---|---|
| 数据展示 | DataGridView | 展示学生/课程/成绩列表,支持选中行回填 |
| 数据录入 | TextBox、ComboBox、DateTimePicker | 输入学号、姓名、性别、出生日期等信息 |
| 操作触发 | Button | 增、删、改、查四个操作入口 |
性别用 ComboBox 而不是 TextBox,是因为性别只有两个合法值,ComboBox 限制输入范围能减少数据验证的代码量。出生日期用 DateTimePicker 而不是手写日期,可以避免用户输入非法日期格式导致的异常。
4.2 DataGridView 选中行回填到输入框
这个交互很常见:用户点列表里的某一行,上面的输入框自动填上这一行的数据,改完点"修改"按钮就更新。核心用的是 DataGridView 的 CellClick 事件:
private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0) return; // 点列头时不做处理 DataGridViewRow row = dataGridView1.Rows[e.RowIndex]; txtStudentID.Text = row.Cells["StudentID"].Value.ToString(); txtStudentName.Text = row.Cells["StudentName"].Value.ToString(); cmbGender.Text = row.Cells["Gender"].Value.ToString(); dtpBirthDate.Value = Convert.ToDateTime(row.Cells["BirthDate"].Value); txtClassName.Text = row.Cells["ClassName"].Value.ToString(); }在数据绑定完成后,把 DataGridView 的 SelectionMode 设为 FullRowSelect,这样用户点任意单元格都能选中整行,回填的数据才是完整的。否则你只取了某一个单元格的值,其他文本框还是空的。
4.3 一个完整的成绩录入流程
成绩录入是三个表联动最典型的场景:界面上选择学生、选择课程、填分数,保存时往 Score 表插数据。我一般把学生和课程分别放到两个 ComboBox 里,下拉选项的数据源直接查表:
// 加载学生下拉框 string studentSql = "SELECT StudentID, StudentName FROM Student"; OleDbDataAdapter stuAdapter = new OleDbDataAdapter(studentSql, conn); DataTable stuTable = new DataTable(); stuAdapter.Fill(stuTable); cmbStudent.DataSource = stuTable; cmbStudent.DisplayMember = "StudentName"; // 下拉框显示姓名 cmbStudent.ValueMember = "StudentID"; // 实际用的是学号关键在这里:DisplayMember 和 ValueMember 必须分开设置。DisplayMember 决定用户看到什么,ValueMember 决定代码拿到什么。很多初学者只设置了 DisplayMember,结果取到的值变成 "System.Data.DataRowView",就是因为 ValueMember 没设。
录入成绩时,取两个 ComboBox 的 SelectedValue 再插入 Score 表:
string sql = "INSERT INTO Score (StudentID, CourseID, Score) VALUES (?, ?, ?)"; cmd.Parameters.AddWithValue("@p1", cmbStudent.SelectedValue.ToString()); cmd.Parameters.AddWithValue("@p2", cmbCourse.SelectedValue.ToString()); cmd.Parameters.AddWithValue("@p3", txtScore.Text.Trim());还有一个小细节:两个下拉框的 SelectedValue 在用户没选择时可能是 null,先做个判空,否则你会在 NULL 引用异常上多花十分钟。
4.4 查询结果的联动显示:按学生查成绩
按学生查成绩是这套系统里查询功能里最有代表性的一段逻辑。先按学号查 Student 表拿学生信息,再按学号查 Score 表和 Course 表联表查出成绩列表:
SELECT Score.Score, Course.CourseName, Course.Credit FROM Score INNER JOIN Course ON Score.CourseID = Course.CourseID WHERE Score.StudentID = ?这段 SQL 用 INNER JOIN 把成绩表和课程表关联起来,查出来的结果直接绑定到 DataGridView。你问为什么不用三表 JOIN?因为学生姓名已经在界面上单独显示了,不必要再查一遍。课程作业里做 JOIN 是加分项,说明你理解了表间关系,而不只是会单表查询。
5. 避坑与常见问题:Access 项目里四类高频翻车现场
5.1 "未在本地计算机上注册 Microsoft.ACE.OLEDB.12.0 提供程序"
现象:程序一跑连接就抛异常,报错信息写着未找到 ACE.OLEDB.12.0 提供程序。原因:你的电脑没装 Access Database Engine,或者装了 64 位版本但项目编译成了 x86(或反过来)。解决:去微软官网下载 AccessDatabaseEngine 安装包,注意位数必须和项目编译平台一致。项目编译成 AnyCPU 时,在 x64 系统上默认以 x64 运行,如果你装的是 32 位驱动就会报错。最稳的做法是把项目平台目标改成 x86,然后装 32 位的 ACE 驱动——兼容性最好,因为 Office 默认也是 32 位。
5.2 参数顺序错乱导致数据写入错误的列
现象:往 Student 表插入数据,学号和姓名写反了,或者性别跑到姓名字段里。原因:OleDbCommand 的参数按 ? 占位符的顺序对应,如果你把 Parameters.Add 的顺序写错,数据就串了。解决:写完 SQL 后数一下有几个 ? 占位符,然后再数一遍你 Add 了多少个参数,两者的顺序必须一一对应。每次写完我都在参数前加注释,标明这个参数对应 SQL 里的哪个位置,这个习惯能避免大多数串位问题。
5.3 修改学生信息时提示"索引或主键不能包含 Null 值"
现象:点修改按钮后程序报错,说是主键不能为空。原因:你把 StudentID 列也放进了 UPDATE 语句的 SET 子句里,而这个字段在主键约束下不允许为空,或者你把主键值也改了导致冲突。解决:UPDATE 语句只更新允许修改的字段,主键字段只出现在 WHERE 子句里做定位条件。学号不想让学生改,就把学号文本框设置成只读(ReadOnly = true),从源头避免这个问题。
5.4 删除有成绩的学生时没有提示,事后数据对不上
现象:删掉一个学生后,查成绩报表发现总数对不上,Score 表里还留着这个学生的记录。原因:删除代码没处理外键关联,或者 Access 没有启用参照完整性。解决:删除前先查 Score 表判断有无关联数据。如果有,要么禁止删除并提示用户先清成绩,要么在删除学生记录的同时级联删除成绩记录——课程作业里我建议用后者,代码直观,老师也容易看懂:
using (OleDbConnection conn = new OleDbConnection(connStr)) { conn.Open(); OleDbTransaction trans = conn.BeginTransaction(); try { string deleteScoreSql = "DELETE FROM Score WHERE StudentID=?"; OleDbCommand cmd1 = new OleDbCommand(deleteScoreSql, conn, trans); cmd1.Parameters.AddWithValue("@p1", studentId); cmd1.ExecuteNonQuery(); string deleteStudentSql = "DELETE FROM Student WHERE StudentID=?"; OleDbCommand cmd2 = new OleDbCommand(deleteStudentSql, conn, trans); cmd2.Parameters.AddWithValue("@p1", studentId); cmd2.ExecuteNonQuery(); trans.Commit(); } catch { trans.Rollback(); MessageBox.Show("删除失败,数据已回滚"); } }这里用到了事务,两条 DELETE 要么都成功要么都失败,不会出现删了学生还留成绩的中间状态。这个写法在课程作业里属于加分项,能体现你对数据一致性的理解。
5.5 Access 数据库文件被占用无法修改
现象:调试运行一次后,再次运行程序提示数据库文件被锁定,无法写入。原因:上一个程序实例没有完全释放连接资源,Access 文件被锁定。解决:所有的 OleDbConnection 都用 using 包裹,确保用完立即释放。另外检查是不是调试时程序没有完全退出,任务管理器里把残留的进程结束掉。Access 单文件架构就是有这个问题,不像 SQL Server 有独立的进程管理连接,所以你的代码里必须手动管理连接生命周期。
6. 把代码组织成能复用的小工具:参数化查询与统一数据访问类
做到这一步,系统功能基本齐全了。但如果你想把项目的代码质量往上提一个台阶,我建议做一件事:把数据库操作封装到一个独立的类里,而不是每次都在按钮事件里 new OleDbConnection、写重复 SQL。这个习惯在课程作业里看着是多余,但它是你从"写功能"走向"写代码"的分水岭。
我一般把数据访问封装成一个 ScoreDB 类,类里只暴露业务方法:
public class ScoreDB { private static readonly string connStr = @"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=Data\StudentScore.accdb;"; /// 查询学生列表(支持精确学号或模糊姓名) public DataTable GetStudents(string id = "", string name = "") { string sql = "SELECT StudentID, StudentName, Gender, BirthDate, ClassName FROM Student WHERE 1=1"; if (!string.IsNullOrEmpty(id)) sql += " AND StudentID LIKE @id"; if (!string.IsNullOrEmpty(name)) sql += " AND StudentName LIKE @name"; using (OleDbConnection conn = new OleDbConnection(connStr)) using (OleDbDataAdapter adapter = new OleDbDataAdapter(sql, conn)) { if (!string.IsNullOrEmpty(id)) adapter.SelectCommand.Parameters.AddWithValue("@id", "%" + id + "%"); if (!string.IsNullOrEmpty(name)) adapter.SelectCommand.Parameters.AddWithValue("@name", "%" + name + "%"); DataTable dt = new DataTable(); adapter.Fill(dt); return dt; } } /// 按学号查成绩(联表返回课程名和成绩) public DataTable GetScoresByStudent(string studentId) { string sql = @"SELECT Course.CourseName, Course.Credit, Score.Score FROM Score INNER JOIN Course ON Score.CourseID = Course.CourseID WHERE Score.StudentID = @id"; using (OleDbConnection conn = new OleDbConnection(connStr)) using (OleDbDataAdapter adapter = new OleDbDataAdapter(sql, conn)) { adapter.SelectCommand.Parameters.AddWithValue("@id", studentId); DataTable dt = new DataTable(); adapter.Fill(dt); return dt; } } }这段代码里有几个值得参考的细节。WHERE 1=1 是一个拼接技巧,它让后面跟的 AND 子句不需要判断是不是第一段条件,代码看起来整齐,也不会出现 WHERE 后面直接跟 AND 的语法错误。参数化查询统一用 @id、@name 占位而不是 ?,这是为了让 SQL 字符串可读性更高——OleDb 的 ? 占位方式在拼接动态 SQL 时容易看花眼,@占位在 SQL 文本里是显式的,你能直接看出哪个参数对哪个字段。
把数据访问封装成类之后,界面层的按钮事件代码变得非常干净。一个按钮事件就两三行:
private void btnSearch_Click(object sender, EventArgs e) { ScoreDB db = new ScoreDB(); DataTable dt = db.GetStudents(txtStudentID.Text.Trim(), txtStudentName.Text.Trim()); dataGridView1.DataSource = dt; }从那以后我每次写课程作业,都强制自己把数据访问和界面层分开,即使老师只要求一个能跑的程序。因为当你把 SQL 逻辑从界面事件里抽出来之后,你会发现程序的排错路径清晰很多:界面出问题去查事件,数据出问题去查 ScoreDB 类,不用把一个 300 行的按钮事件从头翻到尾。
最后多说一句:Access 和 ADO.NET 这套组合看着"老",但它的核心——连接管理、参数化查询、事务处理、DataTable 绑定——换到 SQL Server、MySQL 甚至 EF Core 里都是同一套思路。把这份课程作业拆透,你收获的不仅是一个能交差的项目,而是一条完整的数据库应用开发方法论。希望帮到你。
本文还有配套的精品资源,点击获取