PyCharm+Tkinter学生成绩管理系统实战教程
2026/9/17 1:36:34 网站建设 项目流程

简介:这是一份面向Python初学者与高校课程设计学生的学生成绩管理系统实战项目,基于Tkinter构建图形化界面,使用PyCharm开发,完整覆盖数据录入、删除、修改、查询、排序及平均分计算等核心功能。资源包共815个文件,主体为740个Python源码文件(含主程序、模块化功能脚本及GUI组件),辅以24个可执行exe文件(便于无环境运行)、10个配置与说明txt文档,以及少量虚拟环境相关文件(如activate.bat、pyvenv.cfg)和依赖元数据,整体压缩包仅3.35MB,轻量易部署。已有595人学习下载,适合期末实训、小组协作开发或Tkinter GUI入门实践。读者可直接运行项目体验完整业务流程,获取规范的面向对象设计结构、健壮的异常处理逻辑、清晰的变量命名与注释风格,以及基于文件持久化的本地数据管理方案。

1. 用 PyCharm + Tkinter 写一个真正能跑起来的学生成绩管理系统,不是“Hello World”式演示

很多学生交 Python 期末作业时卡在第一步:PyCharm 新建项目后,Tkinter 窗口一闪而过、按钮点不动、数据存不进文件、增删改查逻辑一写就报AttributeError: 'NoneType' object has no attribute 'insert'。这不是代码写得不够“炫”,而是缺了关键骨架——一个带完整生命周期管理的主窗口类、可持久化的成绩数据结构、以及符合教学场景的最小可行交互流。这个系统不追求 Web 级并发或数据库集群,但必须满足三个硬要求:① 启动即显示带学号/姓名/语文/数学/英语字段的表格;② 点“添加”能校验空值和分数范围(0–100),成功后实时刷新表格;③ 关闭窗口时自动将当前全部成绩保存到scores.json文件,下次启动自动加载。它面向的是刚学完函数和类、正要接触 GUI 和文件 I/O 的大二学生,目标是让代码从“语法正确”走向“行为可靠”。


2. 搭建 PyCharm 项目结构与 Tkinter 主窗口骨架

2.1 创建符合教学规范的项目目录与依赖管理

PyCharm 中新建项目时,不要选“Pure Python”模板,而应手动创建标准结构:根目录下设src/(源码)、data/(数据文件)、docs/(可选说明)。右键src→ “Mark Directory as” → “Sources Root”,确保后续import路径清晰。Tkinter 是 Python 标准库,无需pip install,但为防环境异常,执行终端命令验证:

python -c "import tkinter as tk; print(tk.Tk().winfo_class())"

提示:若报错ModuleNotFoundError: No module named 'tkinter',说明 Python 安装时未勾选tcl/tk组件。Windows 用户需重新运行安装包,勾选“Add Python to PATH”和“tcl/tk and IDLE”;macOS 用户用brew install python-tk;Linux 用户执行sudo apt-get install python3-tk(Ubuntu/Debian)或sudo yum install python3-tkinter(CentOS/RHEL)。

2.2 设计可复用的主窗口类ScoreManagerApp

Tkinter 窗口不能写成过程式脚本(如直接root = Tk()后一堆.pack()),否则逻辑耦合、无法测试、扩展性差。必须封装为类,且遵循“单一职责”:__init__只做初始化,create_widgets专管控件布局,load_datasave_data分离 I/O 操作。以下是核心骨架(保存为src/app.py):

import json import os from tkinter import Tk, Frame, Label, Entry, Button, ttk, messagebox class ScoreManagerApp: def __init__(self, root): self.root = root self.root.title("学生成绩管理系统") self.root.geometry("800x500") self.root.resizable(False, False) # 禁止缩放,避免布局错乱 # 初始化数据存储(内存中) self.scores = [] # 结构:[{"id": "2023001", "name": "张三", "chinese": 85, "math": 92, "english": 88}] # 创建 UI 组件 self.create_widgets() # 加载历史数据 self.load_data() def create_widgets(self): # 顶部输入区 input_frame = Frame(self.root) input_frame.pack(pady=10, padx=20, fill="x") Label(input_frame, text="学号:").grid(row=0, column=0, sticky="e", padx=(0, 5)) self.id_entry = Entry(input_frame, width=12) self.id_entry.grid(row=0, column=1, padx=(0, 15)) Label(input_frame, text="姓名:").grid(row=0, column=2, sticky="e", padx=(0, 5)) self.name_entry = Entry(input_frame, width=12) self.name_entry.grid(row=0, column=3, padx=(0, 15)) Label(input_frame, text="语文:").grid(row=0, column=4, sticky="e", padx=(0, 5)) self.chinese_entry = Entry(input_frame, width=8) self.chinese_entry.grid(row=0, column=5, padx=(0, 15)) Label(input_frame, text="数学:").grid(row=0, column=6, sticky="e", padx=(0, 5)) self.math_entry = Entry(input_frame, width=8) self.math_entry.grid(row=0, column=7, padx=(0, 15)) Label(input_frame, text="英语:").grid(row=0, column=8, sticky="e", padx=(0, 5)) self.english_entry = Entry(input_frame, width=8) self.english_entry.grid(row=0, column=9, padx=(0, 10)) Button(input_frame, text="添加", command=self.add_score).grid(row=0, column=10, padx=(10, 0)) # 表格区(使用 ttk.Treeview 实现专业表格) tree_frame = Frame(self.root) tree_frame.pack(pady=10, padx=20, fill="both", expand=True) # 定义列 columns = ("id", "name", "chinese", "math", "english", "total", "average") self.tree = ttk.Treeview(tree_frame, columns=columns, show="headings", height=12) # 设置列标题和宽度 self.tree.heading("id", text="学号") self.tree.column("id", width=100, anchor="center") self.tree.heading("name", text="姓名") self.tree.column("name", width=80, anchor="center") self.tree.heading("chinese", text="语文") self.tree.column("chinese", width=60, anchor="center") self.tree.heading("math", text="数学") self.tree.column("math", width=60, anchor="center") self.tree.heading("english", text="英语") self.tree.column("english", width=60, anchor="center") self.tree.heading("total", text="总分") self.tree.column("total", width=60, anchor="center") self.tree.heading("average", text="平均分") self.tree.column("average", width=70, anchor="center") # 添加滚动条 scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview) self.tree.configure(yscrollcommand=scrollbar.set) self.tree.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # 底部操作按钮 btn_frame = Frame(self.root) btn_frame.pack(pady=10) Button(btn_frame, text="删除选中", command=self.delete_selected).pack(side="left", padx=5) Button(btn_frame, text="清空全部", command=self.clear_all).pack(side="left", padx=5) Button(btn_frame, text="计算统计", command=self.show_statistics).pack(side="left", padx=5) def load_data(self): """从 data/scores.json 加载数据,文件不存在则初始化空列表""" data_path = os.path.join(os.path.dirname(__file__), "..", "data", "scores.json") try: with open(data_path, "r", encoding="utf-8") as f: self.scores = json.load(f) self.refresh_tree() # 加载后立即刷新表格 except FileNotFoundError: self.scores = [] # 自动创建 data 目录 os.makedirs(os.path.dirname(data_path), exist_ok=True) with open(data_path, "w", encoding="utf-8") as f: json.dump([], f, ensure_ascii=False, indent=2) except json.JSONDecodeError: messagebox.showwarning("警告", "数据文件损坏,已重置为空数据") self.scores = [] with open(data_path, "w", encoding="utf-8") as f: json.dump([], f, ensure_ascii=False, indent=2) def save_data(self): """保存当前 scores 到 data/scores.json""" data_path = os.path.join(os.path.dirname(__file__), "..", "data", "scores.json") try: with open(data_path, "w", encoding="utf-8") as f: json.dump(self.scores, f, ensure_ascii=False, indent=2) except Exception as e: messagebox.showerror("错误", f"保存失败:{str(e)}") def refresh_tree(self): """清空并重绘 Treeview 表格""" for item in self.tree.get_children(): self.tree.delete(item) for score in self.scores: total = score["chinese"] + score["math"] + score["english"] average = round(total / 3, 1) self.tree.insert("", "end", values=( score["id"], score["name"], score["chinese"], score["math"], score["english"], total, average ))

注意:ttk.Treeview比原始Listbox更适合成绩表——支持多列、排序、选中行高亮。anchor="center"确保数字居中对齐,避免视觉混乱。os.path.join构建跨平台路径,ensure_ascii=False保证中文不被转义。

2.3 编写主程序入口main.py

src/目录下新建main.py,内容极简但关键:

from tkinter import Tk from app import ScoreManagerApp if __name__ == "__main__": root = Tk() app = ScoreManagerApp(root) # 绑定窗口关闭事件,确保退出前保存数据 root.protocol("WM_DELETE_WINDOW", lambda: [app.save_data(), root.destroy()]) root.mainloop()

提示:root.protocol("WM_DELETE_WINDOW", ...)是 Tkinter 窗口关闭的“钩子”,比单纯root.destroy()多一层保障——它强制在进程退出前执行save_data(),避免学生忘记点“保存”导致数据丢失。这是教学项目中最易被忽略却最影响体验的细节。


3. 实现核心业务逻辑:添加、删除、统计与数据持久化

3.1 添加成绩的校验与插入逻辑

add_score方法需同时处理前端输入解析、业务规则校验、内存数据更新、UI 刷新四步。常见错误是直接self.scores.append(...)后忘记refresh_tree(),或未捕获ValueError导致输入非数字时程序崩溃。

def add_score(self): """添加一条成绩记录,含完整校验""" # 获取输入值 student_id = self.id_entry.get().strip() name = self.name_entry.get().strip() try: chinese = int(self.chinese_entry.get().strip()) math = int(self.math_entry.get().strip()) english = int(self.english_entry.get().strip()) except ValueError: messagebox.showerror("输入错误", "语文、数学、英语必须为整数!") return # 业务校验 if not student_id or not name: messagebox.showerror("输入错误", "学号和姓名不能为空!") return if len(student_id) < 6 or len(student_id) > 10: messagebox.showerror("输入错误", "学号长度应在6-10位!") return if not (0 <= chinese <= 100 and 0 <= math <= 100 and 0 <= english <= 100): messagebox.showerror("输入错误", "各科分数必须在0-100之间!") return # 检查学号是否重复 if any(s["id"] == student_id for s in self.scores): messagebox.showwarning("警告", f"学号 {student_id} 已存在!") return # 构造新记录并添加 new_score = { "id": student_id, "name": name, "chinese": chinese, "math": math, "english": english } self.scores.append(new_score) # 清空输入框,刷新表格 self.id_entry.delete(0, "end") self.name_entry.delete(0, "end") self.chinese_entry.delete(0, "end") self.math_entry.delete(0, "end") self.english_entry.delete(0, "end") self.refresh_tree()

逻辑说明:strip()去除首尾空格防止“张三 ”被误判为不同姓名;len(student_id)校验模拟真实学号规则;any(...)遍历检查重复学号——这是关系型思维在内存数据中的体现,比用dict键更直观易懂。

3.2 删除选中行与清空全部功能

Treeview的选中状态通过self.tree.selection()获取,返回的是项 ID 列表(如('I001', 'I002')),需映射回内存数据索引。直接delete项 ID 会导致内存与 UI 不一致,必须同步修改self.scores

def delete_selected(self): """删除 Treeview 中选中的行""" selected_items = self.tree.selection() if not selected_items: messagebox.showinfo("提示", "请先选择要删除的记录!") return # 获取选中行在 Treeview 中的索引(从0开始) indices_to_remove = [] for item in selected_items: # Treeview 的 item ID 是内部标识,需通过 item() 获取值 values = self.tree.item(item, "values") if values: # 根据学号匹配内存数据 student_id = values[0] for i, s in enumerate(self.scores): if s["id"] == student_id: indices_to_remove.append(i) break # 倒序删除(避免索引偏移) for i in sorted(indices_to_remove, reverse=True): self.scores.pop(i) self.refresh_tree() def clear_all(self): """清空所有成绩记录""" if messagebox.askyesno("确认", "确定要清空所有成绩?此操作不可撤销!"): self.scores.clear() self.refresh_tree()

注意:Treeviewitem()方法返回元组("2023001", "张三", 85, 92, 88, 269, 89.7)values[0]即学号。倒序pop是 Python 列表删除的通用技巧,避免因索引变化漏删。

3.3 计算统计信息并弹窗展示

show_statistics不仅计算总人数、各科平均分,还用messagebox.showinfo以固定格式呈现,避免学生自己拼接字符串出错:

def show_statistics(self): """计算并显示统计信息""" if not self.scores: messagebox.showinfo("统计结果", "暂无成绩数据") return total_students = len(self.scores) total_chinese = sum(s["chinese"] for s in self.scores) total_math = sum(s["math"] for s in self.scores) total_english = sum(s["english"] for s in self.scores) avg_chinese = round(total_chinese / total_students, 1) avg_math = round(total_math / total_students, 1) avg_english = round(total_english / total_students, 1) # 找最高分学生 top_student = max(self.scores, key=lambda x: x["chinese"] + x["math"] + x["english"]) top_total = top_student["chinese"] + top_student["math"] + top_student["english"] stats_text = ( f"学生总数:{total_students}人\n" f"语文平均分:{avg_chinese}\n" f"数学平均分:{avg_math}\n" f"英语平均分:{avg_english}\n" f"最高总分:{top_total}分({top_student['name']})" ) messagebox.showinfo("班级统计", stats_text)

提示:max(..., key=lambda)是 Python 高阶函数典型用法,比手写循环更简洁。round(..., 1)控制小数位,符合教学场景对精度的要求。

3.4 数据持久化机制与文件路径安全策略

save_dataload_data方法已定义,但需确保data/目录存在且路径可写。PyCharm 默认工作目录是项目根目录,因此os.path.join(os.path.dirname(__file__), "..", "data")是可靠写法。补充一个健壮性检查:

# 在 load_data 方法开头添加 data_dir = os.path.join(os.path.dirname(__file__), "..", "data") if not os.path.exists(data_dir): os.makedirs(data_dir)

为什么不用绝对路径?因为学生可能把项目拷贝到 U 盘或不同电脑,相对路径保证可移植性。os.makedirs(..., exist_ok=True)避免重复创建目录报错。


4. PyCharm 调试配置与常见运行问题排错

4.1 设置正确的运行配置与工作目录

PyCharm 中右键main.py→ “Run 'main'”,首次运行可能报错FileNotFoundError: [Errno 2] No such file or directory: 'data/scores.json'。这是因为默认工作目录是src/,而data/在项目根目录下。解决方法:

  1. 点击右上角运行配置下拉箭头 → “Edit Configurations…”
  2. 在 “Working directory” 栏输入$ProjectFileDir$(PyCharm 内置变量,指向项目根目录)
  3. 确认 “Add content root to PYTHONPATH” 已勾选

提示:$ProjectFileDir$比手动填路径更安全,避免因项目重命名或移动失效。若仍报错,可在 PyCharm 终端执行ls -la查看当前目录结构,确认data/存在。

4.2 三类高频报错的定位与修复

报错现象根本原因修复步骤
窗口一闪而逝main.py中缺少root.mainloop()或被异常中断检查main.py是否有root.mainloop();在ScoreManagerApp.__init__开头加print("App initialized")确认类实例化成功
Treeview 显示空白refresh_tree()未被调用,或self.scores为空但未触发load_data()__init__末尾加print(f"Loaded {len(self.scores)} records");检查data/scores.json文件内容是否为合法 JSON 数组
输入数字后报 ValueErrorEntry.get()返回空字符串""int("")抛异常add_scoretry块前加if not self.chinese_entry.get().strip(): return预检

4.3 使用 PyCharm 调试器逐行跟踪添加流程

调试是理解 GUI 生命周期的关键。设置断点在add_score方法第一行,点击虫子图标启动调试:

  • 观察self.id_entry.get()实时值,确认输入框内容被正确读取
  • 进入try块后,chinese变量值应为int类型,而非str
  • 步入self.scores.append(...)后,在 Variables 面板展开self.scores,确认新字典已加入列表
  • 继续步入self.refresh_tree(),观察self.tree.get_children()返回项数是否增加

技巧:在 Debug 模式下,右键变量 → “Evaluate Expression” 可临时执行len(self.scores)等表达式,比打断点更灵活。


5. 进阶技巧:为成绩表添加搜索与双击编辑功能

5.1 实现按学号/姓名模糊搜索

create_widgetsinput_frame末尾添加搜索框和按钮:

# 搜索区(追加在 input_frame 中) Label(input_frame, text="搜索:").grid(row=1, column=0, sticky="e", padx=(0, 5), pady=(10, 0)) self.search_entry = Entry(input_frame, width=20) self.search_entry.grid(row=1, column=1, columnspan=3, padx=(0, 15), pady=(10, 0)) Button(input_frame, text="搜索", command=self.search_scores).grid(row=1, column=4, padx=(10, 0), pady=(10, 0))

新增search_scores方法:

def search_scores(self): """按学号或姓名模糊搜索""" keyword = self.search_entry.get().strip() if not keyword: self.refresh_tree() # 清空搜索,显示全部 return filtered = [] for s in self.scores: if keyword in s["id"] or keyword in s["name"]: filtered.append(s) # 临时替换 scores 并刷新(注意:不修改原始数据) temp_scores = self.scores self.scores = filtered self.refresh_tree() # 恢复原始数据,避免影响其他操作 self.scores = temp_scores

注意:此处用临时替换而非深拷贝,因搜索是只读操作。若需长期筛选,应维护独立的filtered_scores属性。

5.2 双击表格行进入编辑模式

Treeview支持<<TreeviewSelect>>事件,但双击需绑定<Double-1>

# 在 create_widgets 的 tree_frame 部分末尾添加 self.tree.bind("<Double-1>", self.on_double_click) def on_double_click(self, event): """双击某行,在弹窗中编辑该学生信息""" item = self.tree.focus() # 获取焦点项 if not item: return values = self.tree.item(item, "values") if not values: return # 创建编辑弹窗 edit_window = Tk() edit_window.title("编辑成绩") edit_window.geometry("400x250") # 预填充数据 Label(edit_window, text="学号:").grid(row=0, column=0, sticky="e", padx=5, pady=5) id_var = Entry(edit_window, width=15) id_var.insert(0, values[0]) id_var.grid(row=0, column=1, padx=5, pady=5) Label(edit_window, text="姓名:").grid(row=1, column=0, sticky="e", padx=5, pady=5) name_var = Entry(edit_window, width=15) name_var.insert(0, values[1]) name_var.grid(row=1, column=1, padx=5, pady=5) Label(edit_window, text="语文:").grid(row=2, column=0, sticky="e", padx=5, pady=5) chinese_var = Entry(edit_window, width=8) chinese_var.insert(0, values[2]) chinese_var.grid(row=2, column=1, padx=5, pady=5) # ... 同理添加数学、英语输入框(略) def save_edit(): try: new_id = id_var.get().strip() new_name = name_var.get().strip() new_chinese = int(chinese_var.get().strip()) # ... 解析其他字段(略) # 更新内存数据 for s in self.scores: if s["id"] == values[0]: s.update({ "id": new_id, "name": new_name, "chinese": new_chinese, # ... 更新其他字段 }) break self.refresh_tree() edit_window.destroy() except ValueError: messagebox.showerror("错误", "请输入有效数字!") Button(edit_window, text="保存", command=save_edit).grid(row=5, column=0, columnspan=2, pady=10)

提示:双击编辑是 GUI 交互的进阶需求,代码量较大但逻辑清晰。重点在于self.tree.focus()获取当前选中项,s.update(...)原地修改字典,避免重建列表。

5.3 一键导出为 CSV 的实用功能

btn_frame中追加导出按钮:

Button(btn_frame, text="导出CSV", command=self.export_to_csv).pack(side="left", padx=5)

实现export_to_csv

def export_to_csv(self): """导出当前成绩为 CSV 文件""" if not self.scores: messagebox.showinfo("提示", "无数据可导出") return import csv from tkinter import filedialog filename = filedialog.asksaveasfilename( defaultextension=".csv", filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")] ) if not filename: return try: with open(filename, "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) # 写入表头 writer.writerow(["学号", "姓名", "语文", "数学", "英语", "总分", "平均分"]) # 写入数据 for s in self.scores: total = s["chinese"] + s["math"] + s["english"] avg = round(total / 3, 1) writer.writerow([ s["id"], s["name"], s["chinese"], s["math"], s["english"], total, avg ]) messagebox.showinfo("成功", f"已导出至:{filename}") except Exception as e: messagebox.showerror("错误", f"导出失败:{str(e)}")

注意:encoding="utf-8-sig"是 Windows Excel 识别中文 CSV 的关键,newline=""避免空行。filedialog.asksaveasfilename提供标准保存对话框,比硬编码路径更专业。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询