简介:一套基于Python的GUI学生信息管理系统源码,面向Python入门学习者、高校课程设计或毕业设计初步选题人群,解决学生信息手动管理不便的问题。程序采用窗口化界面,提供增加、修改、删除、查询与整页显示五类基础功能,分别以姓名、学号、年龄、手机号为核心字段,逻辑清晰,适合用来理解Tkinter界面布局与事件响应机制。资源压缩包共4个文件,全部为.py脚本,包体仅3KB,代码紧凑,便于逐行阅读与二次修改,可以在本地环境快速运行验证。目前已有6827人学习下载,说明该小工具在实际练习与课设场景中受到一定认可。通过这份源码,读者可以掌握如何将输入表单、按钮绑定、数据存储与刷新展示组合为一个完整小项目,同时也能在此基础上扩展排序、持久化或搜索等高阶功能,既可作为作业参考,也可作为GUI编程入门练习。
1. 学生信息管理系统用 Python 写 GUI,先想清楚数据放在哪
办公室的 Excel 越攒越多,每次想查一个学生的联系方式都要按 Ctrl+F 找半天。这种场景下,很多人第一反应是找一个用 Python 写的学生信息管理系统来接替。网上的 GUI 界面版教程,十有八九是先画窗口再塞功能;我习惯反过来,动手前先回答一个问题:数据放在哪、以什么结构存。学生信息管理系统表面上是界面问题,真正的复杂度在数据层。数据模型定清楚后,Tkinter、PyQt 都只是换一层皮。对想快速交付的 Python 开发者来说,这个顺序能省掉一多半返工。下面用零依赖的 Tkinter,把保存、查询、校验和退出处理逐层拆开。
2. 用 Tkinter 搭出学生信息管理系统的 GUI 界面骨架
2.1 为什么 Python 的 GUI 界面首选 Tkinter,而不是一开始就上 PyQt
先补一个背景:Tkinter 并不是老旧、只能做玩具的代名词,它是 Python 自带的标准库。Windows 下装完 Python 官方安装包就能直接 import;Linux 系统自带的 Python 有时没带图形支持,才需要apt install python3-tk补一个包。对学生信息管理系统这类内部工具,数据量就是几个班、几百人,控件就是表单加表格,Tkinter 的组件完全够用。课程设计、班级管理这类项目用 PyQt,反而会被信号槽、Qt Designer 和打包体积拖慢节奏;这些优势只在复杂桌面软件里才体现得出来。
选 Tkinter 还有一个实打实的好处:学习资料密度高。搜 Python GUI 界面相关话题,大量可参考的 python 代码和免费 python 源码大全都以 Tkinter 为例,遇到的坑基本都有人踩过,Python 入门阶段就能读懂全部源码,不需要额外学一套 Qt 的对象模型。下面是我做选型时的对照表:
| 对比项 | Tkinter | PyQt6 / PySide6 |
|---|---|---|
| 安装依赖 | Windows 自带,Linux 装 python3-tk | pip 额外安装,包体积大 |
| 布局体系 | pack / grid / place 三种 | 布局类加 QSS 样式表 |
| 表格组件 | ttk.Treeview 够用 | QTableView 功能更强 |
| 打包体积 | 单个 exe 十几 MB 量级 | 通常几十 MB 起 |
| 适用场景 | 本地工具、课程设计 | 商业软件、复杂界面 |
2.2 最小可运行代码:窗口、工具栏、Treeview 一起立起来
下面这段代码是整套系统的骨架,后面增删改查都往这个类里加。直接用python 文件名.py运行,不要从自带的 IDLE 里用 F5 跑,IDLE 会额外占用一个事件循环,偶尔出现界面卡住不刷新的假象。
import tkinter as tk from tkinter import ttk class StudentApp: def __init__(self, root): self.root = root self.root.title("学生信息管理系统") self.root.geometry("760x500") # 顶部工具栏:两个输入框加一个保存按钮 toolbar = ttk.Frame(root, padding=8) toolbar.pack(fill="x") self.sid_var = tk.StringVar() ttk.Label(toolbar, text="学号").pack(side="left") ttk.Entry(toolbar, textvariable=self.sid_var, width=12).pack(side="left", padx=4) self.name_var = tk.StringVar() ttk.Label(toolbar, text="姓名").pack(side="left", padx=(8, 0)) ttk.Entry(toolbar, textvariable=self.name_var, width=12).pack(side="left", padx=4) self.save_btn = ttk.Button(toolbar, text="保存", command=self.save_student) self.save_btn.pack(side="left", padx=8) # 中部数据表格:Treeview 加纵向滚动条 body = ttk.Frame(root) body.pack(fill="both", expand=True, padx=8, pady=8) columns = ("sid", "name", "score") self.tree = ttk.Treeview(body, columns=columns, show="headings") for col, text in zip(columns, ("学号", "姓名", "成绩")): self.tree.heading(col, text=text) self.tree.column(col, width=140, anchor="center") self.tree.pack(side="left", fill="both", expand=True) scrollbar = ttk.Scrollbar(body, orient="vertical", command=self.tree.yview) scrollbar.pack(side="right", fill="y") self.tree.configure(yscrollcommand=scrollbar.set) def save_student(self): print("待保存:", self.sid_var.get(), self.name_var.get()) if __name__ == "__main__": root = tk.Tk() app = StudentApp(root) root.mainloop()代码说明:
- 用
StringVar绑定输入框,而不是每次调用entry.get()。后续做编辑回填时,直接self.sid_var.set("20240101")就能让界面同步变化。 - 布局用的是
pack,因为从上往下摆放“工具栏 + 表格”最省事。表单字段多时改用grid做两列对齐;place用绝对坐标,一般不做首选。 show="headings"必须写,否则 Treeview 第一列会显示一个默认的空白树列。- pack 的顺序有讲究:先 pack 表格再 pack 滚动条,否则滚动条会占据错误的位置。
2.3 把系统写成类,而不是脚本
很多人写小工具习惯把所有代码堆在顶层:先建 root,再创建控件,最后 mainloop。30 行以内没问题,加到“删除”“编辑”“搜索”之后,函数之间要共享 Treeview 里的选中行、要共享学生列表,顶层变量越来越多,排错就困难了。
我的做法是:用类把窗口、控件和业务方法包在一起,所有共享状态挂在self上。回调方法写self.save_student就行,Tkinter 拿到的是一个可调用对象,不会在绑定阶段重复执行。这也方便把数据层单独拆成文件,界面代码只负责把结果展示到表格。另外记住一条边界:Tkinter 的控件更新只能发生在主线程,以后接数据库或后台任务时,子线程里直接改 Treeview 会静默失败,必须用root.after把更新动作调度回主线程。
3. 学生信息增删改查与输入校验:从回调到 JSON 存储
3.1 数据文件用 JSON 还是 SQLite
学生信息管理系统必须把数据存下来。第一版我推荐 JSON 文件:几百条学生记录,文件大小只有几十到几百 KB,读取和写入都是毫秒级。JSON 文件可以直接用文本编辑器打开,数据错了能当场改;SQLite 虽然也可以用标准库 sqlite3,但要写 SQL,还要装图形工具才看得舒服。我一般这样判断:
| 维度 | JSON 文件 | SQLite |
|---|---|---|
| 数据规模 | 适合几百到几千条 | 万级以上 |
| 可读性 | 文本可直接查看 | 需要数据库工具 |
| 并发能力 | 单进程安全 | 支持多进程 |
| 依赖程度 | 只用标准库 json | 标准库 sqlite3,但要写 SQL |
| 备份方式 | 复制文件即可 | 需要 db.backup 一类工具 |
如果后面数据量涨到几千人,或者要多台机器同时读写,再迁移到 SQLite 也不迟。数据模型不变,只是把读写函数换成数据库操作,这就是先定数据结构带来的好处。
3.2 读写函数:load_students 与 save_students
import json import os DATA_FILE = "students.json" def load_students(filepath=DATA_FILE): """读取学生列表,文件不存在时返回空列表。""" if not os.path.exists(filepath): return [] with open(filepath, "r", encoding="utf-8") as f: return json.load(f) def save_students(students, filepath=DATA_FILE): """把学生列表写入 JSON 文件。""" with open(filepath, "w", encoding="utf-8") as f: json.dump(students, f, ensure_ascii=False, indent=2)参数说明:
ensure_ascii=False:不写这个的话,中文会变成\u5f20这样的转义序列,文件能读回来,但直接打开看非常痛苦。indent=2:让 JSON 带缩进,方便查看和用 git 对比改动。encoding="utf-8":Windows 下如果不显式指定,默认可能是 GBK,特殊字符在读写之间容易出问题。- 返回值是一个
list[dict],每个 dict 固定包含sid、name、score三个键。取值用s["sid"],不要用s[0],以后加字段不会影响已有代码。
常见误用是把json.dump散落在各个界面回调里,哪次保存都自己写一遍。保存逻辑一多,输出格式稍微调整就要改很多地方。正确做法是界面层只操纵self.students这个内存列表,点保存时统一调一次save_students(self.students),这是增删改查逻辑能收敛的前提。
3.3 保存按钮回调:校验、更新内存、落盘、刷新界面
界面版代码和脚本版最大的区别,在于回调函数不返回结果,而是负责把界面和数据状态同步起来。保存回调按四步走:
from tkinter import messagebox def save_student(self): sid = self.sid_var.get().strip() name = self.name_var.get().strip() score_text = self.score_var.get().strip() if not self.validate_input(sid, name, score_text): return if self.editing_sid is None: # 新增 self.students.append({"sid": sid, "name": name, "score": float(score_text)}) else: # 更新:按原始学号定位,而不是按行号 for stu in self.students: if stu["sid"] == self.editing_sid: stu.update({"sid": sid, "name": name, "score": float(score_text)}) break save_students(self.students) self.refresh_tree() self.clear_form() self.editing_sid = None逻辑说明:editing_sid记录的是正在编辑的原始学号,不是 Treeview 当前选中行的行号。这样即使用户在搜索框里换了关键词,列表经过过滤后顺序变了,更新逻辑靠学号也依然能指向同一个人。refresh_tree负责清空表格再插入全部数据,是最直观也最容易排查问题的写法。注意在__init__里先初始化self.students = load_students()、self.editing_sid = None,否则第一次点保存就会报属性不存在的错误。
3.4 校验的三道关
def validate_input(self, sid, name, score_text): if not sid or not name: messagebox.showerror("输入错误", "学号和姓名不能为空") return False try: score = float(score_text) except ValueError: messagebox.showerror("输入错误", "成绩必须是数字") return False if not 0 <= score <= 100: messagebox.showerror("输入错误", "成绩范围是 0 到 100") return False if self.editing_sid is None and any(s["sid"] == sid for s in self.students): messagebox.showerror("输入错误", "学号已存在") return False return True第一道关是空值,第二道是类型转换,第三道是范围。学号重复检测有一个隐藏细节:新增时要查重,编辑时要跳过当前记录本身,否则用户没改学号也保存不了。用any()配合生成器扫描内存列表,不用为了一次校验重新读文件。
提示:不要把校验只写在按钮回调里。如果以后加 CSV 导入或批量录入,导入路径也要走一遍
validate_input,否则校验规则会被绕过。
4. 学生检索与编辑联动:Treeview 查询和双击回填的实现
4.1 一个搜索框跑通多字段过滤
数据量变大之后,“找到某个人”成为最常用的功能。我习惯在工具栏加一个搜索框,绑定回车事件,过滤直接在内存列表上做:
def search_student(self): keyword = self.search_var.get().strip().lower() self.refresh_tree(keyword) def refresh_tree(self, keyword=""): # 先清空表格 for row in self.tree.get_children(): self.tree.delete(row) for stu in self.students: if keyword: if not (keyword in stu["sid"].lower() or keyword in stu["name"].lower() or keyword in stu.get("class_name", "").lower()): continue self.tree.insert("", "end", values=( stu["sid"], stu["name"], stu.get("score", "") ))这里默认的是“包含”匹配,因为用户往往只记得半个名字或者学号后四位,模糊命中比精确匹配更友好。不同的匹配方式适用不同场景:
| 匹配方式 | 判断写法 | 适用场景 |
|---|---|---|
| 包含匹配 | keyword in value | 模糊搜索,最常用 |
| 精确匹配 | value == keyword | 学号唯一查询 |
| 正则匹配 | re.search(keyword, value) | 按学号区间等复杂模式过滤 |
| 忽略大小写 | keyword.lower() | 英文姓名、学号带字母 |
过滤逻辑放在refresh_tree里而不是search_student里,好处是新增、修改后调用refresh_tree()也能用上一次的关键词刷新结果。如果搜完没有数据,我不会弹 messagebox,而是在状态栏加一行“共找到 0 条”,否则每次搜索都弹窗会非常烦。
4.2 双击回填,把编辑和新增合并成一套流程
编辑功能最怕做成“再开一个编辑窗口”,不但要额外管理 Toplevel 生命周期,焦点切换也别扭。在这个学生信息管理系统里,最顺手的编辑方式是:双击表格某一行,数据回填到顶部输入框,保存按钮从“保存”变成“更新”:
self.tree.bind("<Double-1>", self.fill_form) def fill_form(self, event): selected = self.tree.selection() if not selected: return row = self.tree.item(selected[0])["values"] sid, name, score = row[0], row[1], str(row[2]) self.sid_var.set(sid) self.name_var.set(name) self.score_var.set(score) self.editing_sid = sid self.save_btn.config(text="更新")测试时最容易翻车的是这个场景:搜索过滤后双击一条记录,改完保存。如果拿self.tree.index(selected[0])去定位数据列表,搜索状态下这个索引已经不是原列表的位置了,更新会写到错误的人身上。所以在 3.3 里始终用editing_sid这个原始学号重新定位。用户改了学号也不怕,editing_sid保存的是进入编辑时的旧值,更新循环按旧学号逐条查找再stu.update(...),能同时处理“学号没变”和“学号被改掉”两种情况。
4.3 全量刷新何时变成性能问题
refresh_tree每次都先delete所有行再重新insert,几百行数据完全感觉不到延迟;到一万行以上时,Treeview 每插入一行都要做一次布局计算,全量刷新会出现肉眼可见的卡顿。这时候有两个改进方向:一是给 Treeview 关闭不必要的列自动宽度计算,提前用column()固定列宽;二是做增量刷新,用每行的iid关联到一个学生记录的主键,只更新发生变化的那几行。绝大多数学生信息管理系统到不了这个规模,知道边界就好,不用第一版就做增量。
5. 学生信息管理系统收尾必做的三件小事:中文编码、退出保存和备份
5.1 中文乱码多半不是界面问题
Tkinter 窗口里显示中文一般不会乱,乱码往往发生在 JSON 文件被其他编辑器打开、或者控制台 print 的时候。根治办法只有一条:所有文件读写显式指定encoding="utf-8",写 JSON 时加ensure_ascii=False。不要在脚本里调sys.setdefaultencoding,这个接口在 Python 3 里已经不存在了,就算能调也只影响解释器默认编码,救不了已经写坏的文件。
5.2 点右上角关闭按钮时,把数据存下来
如果保存只发生在“保存按钮”上,用户改完数据直接关窗口,改动就丢了。Tkinter 的常规做法是接管窗口关闭协议:
self.root.protocol("WM_DELETE_WINDOW", self.on_close) def on_close(self): save_students(self.students) self.root.destroy()这里用的是destroy()而不是quit()。quit()只是让mainloop返回,窗口资源还在;destroy()才会真正释放控件,避免关闭后 Python 进程不退出。
5.3 再加一层保险:写文件用“临时文件加替换”
保存过程如果遇到断电或异常,直接open(filepath, "w")会把原文件清空一半。我会改成临时文件加原子替换的写法:
def save_students_atomic(students, filepath=DATA_FILE): tmp_path = filepath + ".tmp" backup_path = filepath + ".bak" if os.path.exists(filepath): os.replace(filepath, backup_path) with open(tmp_path, "w", encoding="utf-8") as f: json.dump(students, f, ensure_ascii=False, indent=2) os.replace(tmp_path, filepath)os.replace在 Windows 和 Linux 上都可用,而且是原子操作:要么新文件完全落盘,要么保留旧文件,不会出现写一半的脏状态。加载入口也加一层兜底:load_students用try捕获json.JSONDecodeError,解析失败时自动改读.bak,并在状态栏标“已恢复备份”。
验证这套逻辑只需要两步:第一次运行后关窗口,第二次打开看表格是否还是刚才的数据;再用文本编辑器删掉 students.json 里的半个字段,重新启动程序,确认能自动回退到.bak而不报错。这两步通过后,这个 Python 实现的 GUI 界面版学生信息管理系统才算真正达到可交付状态。
本文还有配套的精品资源,点击获取