简介:这是一份面向Python初学者与课程设计学习者的学生成绩管理系统完整源码,采用Python编写并搭配GUI图形界面,帮助非编程背景用户也能直观完成成绩录入、查询与统计等操作。压缩包共19个文件,约14KB,以ui界面文件、xml配置、txt数据与说明文档为主,另含核心py脚本、iml工程文件及gitignore,结构轻量,便于直接运行与二次修改。系统围绕学生信息的增删改查展开,涵盖数据录入、按学号或姓名查询、平均分与最高最低分统计、CSV或Excel导入导出以及输入校验等常见模块,界面部分通过窗口、按钮、输入框和表格控件组织交互,数据层可结合SQLite完成持久化存储。目前已有17859人学习下载,适合作为Python GUI与数据库入门的练手项目,也能为课程设计或毕业设计提供可复用的界面布局、事件处理与数据管理思路。
1. 从一份“能跑起来”的 Python 学生成绩管理系统说起
很多同学做课程设计时,第一反应是“先把增删改查写出来再说”,结果命令行里跑得挺欢,一到答辩演示就翻车:老师问“能不能按班级筛一下”“能不能看个分数分布”,现场改代码根本来不及。Python 学生成绩管理系统加 GUI 可视化界面,解决的正是这个尴尬——它把数据操作和结果呈现拆成两层,底层用 Python 管数据,上层用图形界面把成绩变成能点、能筛、能看的表格和图表。这套东西适合三类人:正在做课程设计的学生、想练手桌面应用的初学者、以及需要给非技术同事做小工具的一线开发者。核心难点不在“增删改查”,而在界面与数据的同步、成绩统计的准确性,以及打包分发时那些玄学问题。下面按“先跑通最小闭环,再补统计与可视化,最后处理打包和踩坑”的顺序讲透。
2. 用 Python 搭出成绩管理的最小闭环:数据层怎么设计
2.1 为什么选 SQLite 而不是直接读写 CSV
常见做法有两种:一种是把成绩存成 CSV 或 JSON,另一种是上 SQLite。CSV 的好处是肉眼可见、改起来方便,但一旦涉及“按学号查人”“按课程算平均分”“删除某条记录”,你就得自己写遍历和去重逻辑,代码量不小还容易出 bug。SQLite 是 Python 标准库自带的轻量数据库,不需要额外装服务,单文件存储,支持 SQL 查询,正好匹配成绩管理这种“数据量不大但查询频繁”的场景。
我一般会建两张表:一张存学生基本信息,一张存成绩记录。学生表用学号做主键,成绩表用自增 ID 做主键、学号做外键。这样改学生姓名不会影响成绩记录,删学生时也能通过外键约束决定是级联删除还是禁止删除。
import sqlite3 def init_db(db_path="grades.db"): conn = sqlite3.connect(db_path) cur = conn.cursor() # 学生表:学号唯一,姓名和班级允许重复 cur.execute(""" CREATE TABLE IF NOT EXISTS students ( sid TEXT PRIMARY KEY, name TEXT NOT NULL, class_name TEXT ) """) # 成绩表:一条记录对应一个学生的一门课 cur.execute(""" CREATE TABLE IF NOT EXISTS scores ( id INTEGER PRIMARY KEY AUTOINCREMENT, sid TEXT NOT NULL, course TEXT NOT NULL, score REAL CHECK(score >= 0 AND score <= 100), FOREIGN KEY (sid) REFERENCES students(sid) ON DELETE CASCADE ) """) conn.commit() conn.close()这段代码做了三件事:建学生表、建成绩表、开启外键级联删除。CHECK(score >= 0 AND score <= 100)是数据库层面的兜底,防止界面传进来非法分数。ON DELETE CASCADE表示删学生时自动删掉他的成绩,避免留下孤儿记录。参数db_path默认是当前目录下的grades.db,打包时要注意路径问题,后面会讲。
2.2 增删改查封装成函数,界面层只调函数
界面代码最怕和 SQL 混在一起,改一个按钮逻辑要翻半天。我的习惯是单独写一个db.py,把所有数据库操作封装成函数,GUI 只负责收集输入和展示结果。
def add_student(conn, sid, name, class_name): try: conn.execute("INSERT INTO students VALUES (?, ?, ?)", (sid, name, class_name)) conn.commit() return True, "添加成功" except sqlite3.IntegrityError: return False, "学号已存在" def add_score(conn, sid, course, score): cur = conn.execute("SELECT 1 FROM students WHERE sid = ?", (sid,)) if cur.fetchone() is None: return False, "学号不存在" conn.execute("INSERT INTO scores (sid, course, score) VALUES (?, ?, ?)", (sid, course, score)) conn.commit() return True, "成绩录入成功" def query_scores(conn, class_name=None, course=None): sql = """SELECT s.sid, s.name, s.class_name, sc.course, sc.score FROM scores sc JOIN students s ON sc.sid = s.sid WHERE 1=1""" params = [] if class_name: sql += " AND s.class_name = ?" params.append(class_name) if course: sql += " AND sc.course = ?" params.append(course) return conn.execute(sql, params).fetchall()add_student用try/except捕获主键冲突,返回布尔值和提示语,界面直接拿提示语弹窗。query_scores用WHERE 1=1加动态条件,这是拼 SQL 的常用技巧,避免判断“是不是第一个条件”。参数全部用?占位,防止 SQL 注入,虽然桌面应用风险低,但习惯要养好。
2.3 用 Tkinter 搭主窗口:表格、输入框、按钮怎么摆
Tkinter 是 Python 自带 GUI 库,不用额外安装,适合课程设计和内部工具。主窗口用grid布局比pack更可控,尤其是表格和按钮混排时。
import tkinter as tk from tkinter import ttk, messagebox class GradeApp: def __init__(self, root, conn): self.conn = conn self.root = root root.title("学生成绩管理") # 输入区 tk.Label(root, text="学号").grid(row=0, column=0, padx=5, pady=5) self.sid_entry = tk.Entry(root) self.sid_entry.grid(row=0, column=1) tk.Label(root, text="课程").grid(row=0, column=2) self.course_entry = tk.Entry(root) self.course_entry.grid(row=0, column=3) tk.Label(root, text="分数").grid(row=0, column=4) self.score_entry = tk.Entry(root) self.score_entry.grid(row=0, column=5) tk.Button(root, text="录入成绩", command=self.on_add).grid(row=0, column=6, padx=5) # 表格区 self.tree = ttk.Treeview(root, columns=("sid", "name", "class", "course", "score"), show="headings") for col, text in zip(("sid", "name", "class", "course", "score"), ("学号", "姓名", "班级", "课程", "分数")): self.tree.heading(col, text=text) self.tree.column(col, width=90) self.tree.grid(row=1, column=0, columnspan=7, sticky="nsew") self.refresh() def on_add(self): sid = self.sid_entry.get().strip() course = self.course_entry.get().strip() try: score = float(self.score_entry.get()) except ValueError: messagebox.showerror("错误", "分数必须是数字") return ok, msg = add_score(self.conn, sid, course, score) messagebox.showinfo("结果", msg) if ok else messagebox.showerror("错误", msg) self.refresh() def refresh(self): for row in self.tree.get_children(): self.tree.delete(row) for record in query_scores(self.conn): self.tree.insert("", "end", values=record)on_add先做输入校验,分数转float失败就弹错误框,不往下走。refresh每次操作后清空表格重新查,数据量小时没问题,数据量大时应该只更新受影响的行,这是后面要提的优化点。Treeview的show="headings"隐藏首列索引,看起来更像正经表格。
3. 把成绩变成图表:Matplotlib 嵌入 Tkinter 的完整步骤
3.1 为什么选 Matplotlib 而不是 Pyecharts
桌面端可视化,Matplotlib 是首选:它能嵌进 Tkinter 的 Frame 里,不依赖浏览器,打包后也能离线跑。Pyecharts 生成的是 HTML,适合网页展示,但桌面应用里还得开浏览器,体验割裂。常见做法是用FigureCanvasTkAgg把 Matplotlib 的图嵌到 Tkinter 窗口,再配一个工具栏支持缩放和保存。
import matplotlib matplotlib.use("TkAgg") from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk def create_chart_frame(parent, conn): frame = tk.Frame(parent) fig = Figure(figsize=(5, 3), dpi=100) ax = fig.add_subplot(111) # 查询各课程平均分 data = conn.execute("SELECT course, AVG(score) FROM scores GROUP BY course").fetchall() if data: courses = [d[0] for d in data] avgs = [d[1] for d in data] ax.bar(courses, avgs, color="#4C72B0") ax.set_title("各课程平均分") ax.set_ylabel("平均分") ax.set_ylim(0, 100) canvas = FigureCanvasTkAgg(fig, master=frame) canvas.draw() canvas.get_tk_widget().pack(fill="both", expand=True) toolbar = NavigationToolbar2Tk(canvas, frame) toolbar.update() return frame, canvasmatplotlib.use("TkAgg")必须在导入pyplot之前设置,否则后端冲突会导致窗口卡死。Figure和FigureCanvasTkAgg是嵌入的关键,NavigationToolbar2Tk提供保存图片和缩放按钮。ax.set_ylim(0, 100)固定 Y 轴范围,避免平均分都在 80 以上时柱子看起来一样高,视觉上分不出差距。
3.2 分数分布直方图:bin 数量怎么定
平均分看整体,分布看个体。直方图的bins参数直接影响可读性:太少看不出分布形状,太多柱子碎成一片。成绩数据一般集中在 60 到 100 之间,我一般设bins=10,每 10 分一档,正好对应及格线、良好线、优秀线。
def draw_histogram(ax, conn, course=None): sql = "SELECT score FROM scores" params = [] if course: sql += " WHERE course = ?" params.append(course) scores = [row[0] for row in conn.execute(sql, params).fetchall()] if not scores: ax.text(0.5, 0.5, "暂无数据", ha="center", va="center") return ax.hist(scores, bins=10, range=(0, 100), color="#55A868", edgecolor="white") ax.set_xlabel("分数") ax.set_ylabel("人数") ax.set_title(f"{course or '全部课程'}分数分布") ax.axvline(60, color="red", linestyle="--", linewidth=1)range=(0, 100)固定横轴范围,防止数据集中在 80 到 90 时横轴自动缩放到 75 到 95,看起来像全员低分。axvline(60, ...)画一条及格线,答辩时老师一眼就能看出多少人挂科。edgecolor="white"让柱子之间有分隔,视觉上更清晰。
3.3 界面与图表联动:筛选条件变化时重绘
图表不能是死的,得跟着筛选条件变。做法是把绘图逻辑封装成函数,筛选框绑定trace或按钮命令,条件变化时清空Figure重画。
def on_filter_change(self, *args): course = self.course_filter.get() self.ax.clear() draw_histogram(self.ax, self.conn, course if course != "全部" else None) self.canvas.draw()self.ax.clear()清空旧图,self.canvas.draw()触发重绘。注意不要每次重建Figure,那样内存会涨,直接清ax重画就行。筛选框用ttk.Combobox,state="readonly"防止用户手输不存在的课程名。
4. 打包与分发:PyInstaller 把脚本变成 exe 的避坑清单
4.1 数据库路径:为什么打包后找不到 grades.db
开发时grades.db在脚本同目录,打包成 exe 后,程序运行目录可能变成临时解压目录,数据库文件路径就错了。正确做法是用sys._MEIPASS判断是否打包环境,把数据库放到 exe 同级目录或用户目录。
import sys, os def get_db_path(): if getattr(sys, "frozen", False): # 打包后:exe 所在目录 base = os.path.dirname(sys.executable) else: base = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base, "grades.db")sys.frozen是 PyInstaller 打包后自动设置的属性,sys.executable是 exe 路径。这样数据库始终在 exe 旁边,用户拷贝整个文件夹就能带走数据。
4.2 隐藏控制台窗口与图标设置
打包时加--noconsole去掉黑色命令行窗口,加--icon=app.ico设置图标。但注意:去掉控制台后,print调试信息就看不到了,建议用logging写文件。
pyinstaller --noconsole --icon=app.ico --name=成绩管理 main.py--name指定 exe 名称,中文名在某些系统上可能乱码,建议用英文名再手动改。--add-data可以打包图片等资源,格式是源路径;目标路径(Windows)或源路径:目标路径(macOS/Linux)。
4.3 杀毒软件误报与启动速度优化
PyInstaller 打包的 exe 经常被误报,原因是引导程序行为像自解压。缓解办法:用--onedir代替--onefile,文件夹形式误报率低;或者对 exe 做代码签名(个人开发者成本高,不推荐)。启动速度方面,--onefile每次运行都要解压,大项目能慢好几秒,--onedir启动快但文件多。课程设计演示用--onedir更稳。
5. 成绩管理系统常见问题排查:从数据错乱到界面卡死
5.1 现象:录入成绩后表格没更新,重启才显示
原因:refresh没被调用,或者调用了但查的是旧连接。解决:每次写操作后强制self.refresh(),并确认所有函数用的是同一个conn对象。如果用了多线程,注意 SQLite 连接不能跨线程共享。
5.2 现象:平均分算出来是 0 或者报错
原因:AVG(score)在没有任何记录时返回None,直接画图会报类型错误。解决:查询后判断if data and data[0][1] is not None,或者在 SQL 里用COALESCE(AVG(score), 0)。
5.3 现象:打包后图表不显示,报No module named 'matplotlib.backends.backend_tkagg'
原因:PyInstaller 没自动收集 Matplotlib 的后端模块。解决:加--hidden-import=matplotlib.backends.backend_tkagg,或者用--collect-all matplotlib强制收集全部子模块。
5.4 现象:删除学生后成绩还在,统计对不上
原因:建表时没开外键,或者开了但每次连接没执行PRAGMA foreign_keys = ON。SQLite 默认外键是关闭的,必须在connect后手动开启。
conn = sqlite3.connect(db_path) conn.execute("PRAGMA foreign_keys = ON")5.5 现象:界面点几下就卡死,尤其是数据量上千条时
原因:refresh每次全量查询并重建所有表格行,主线程被阻塞。解决:加分页或限制显示条数,查询用LIMIT;或者把查询放到后台线程,用root.after回主线程更新 UI。课程设计数据量小,分页足够。
6. 让成绩管理更耐用的三个进阶技巧
第一个技巧是用pandas做统计导出。SQLite 适合存储和查询,但算标准差、及格率、优秀率这些,pandas一行搞定,还能直接导出 Excel 给老师看。
import pandas as pd def export_report(conn, path="report.xlsx"): df = pd.read_sql("""SELECT s.class_name, sc.course, sc.score FROM scores sc JOIN students s ON sc.sid = s.sid""", conn) summary = df.groupby(["class_name", "course"])["score"].agg( 人数="count", 平均分="mean", 最高分="max", 最低分="min", 及格率=lambda x: (x >= 60).mean() ).reset_index() summary.to_excel(path, index=False) return pathgroupby后agg支持自定义 lambda,(x >= 60).mean()算及格率,比手写循环清爽得多。to_excel需要openpyxl,打包时记得--hidden-import=openpyxl。
第二个技巧是给表格加排序和搜索。Treeview的heading可以绑定命令,点击表头按该列排序;搜索框绑定KeyRelease事件,实时过滤。排序时注意分数列要按数值排,不能按字符串排,否则 “100” 会排在 “99” 前面。
第三个技巧是数据备份。每次启动时把grades.db复制一份带日期的备份,防止误删。用shutil.copy2保留元数据,备份文件放backup子目录,超过 10 份就删最旧的。这个习惯让我在几次演示前救回了被误清空的数据。
我自己的教训是:别等到答辩前一天才打包,--onefile和--onedir各打一次,在不同电脑上跑一遍,确认数据库路径和图表都正常。成绩管理系统的代码量不大,但路径、外键、打包这三个地方最容易翻车,提前踩一遍比现场救火强。希望帮到你。
本文还有配套的精品资源,点击获取