一、实践名称
学生成绩分析系统课程设计
二、实践目的
- 掌握数据帧的创建和切片方法;
- 掌握数据的预处理方法;
- 掌握Matplotlib的常用绘图方法;
- 掌握GUI窗体的创建方法;
- 掌握常用控件的使用;
- 掌握信号/槽编程方法;
- 掌握多窗体设计方法;
- 掌握进行项目功能测试、边界测试、黑盒测试和白盒测试的基本方法;
- 让学生在不断试错与纠错中成长,学会分析与解决实际工作中遇到的问题,提升抗挫能力。
- DataFrame的工作原理;
- DataFrame的常用数据处理函数和数据处理方法;
- 面向对象的编程方法;
- Matplotlib的工作原理;
- Matplotlib绘制柱状图的基本方法;Python GUI设计的基本方法;
- Python GUI 设计中的常用控件的属性;
- Python GUI 设计中的事件响应机制;
- 测试的基本原理。
本次开发基于python来做一个数据分析开发系统 ,前提是有一个data.csv文件,其他文件也可以自行修改
登录界面,可以登录,注册,通过电话号码找回密码等操作。账号密码自动会存在一个文本框里
class LoginWindow(tk.Toplevel):
def __init__(self, master=None):
super().__init__(master)
self.title("登录")
self.geometry("250x150")
self.username_label = tk.Label(self, text="用户名:")
self.password_label = tk.Label(self, text="密码:")
self.username_entry = tk.Entry(self)
self.password_entry = tk.Entry(self, show="*")
self.login_button = tk.Button(self, text="登录", command=self.login)
self.register_button = tk.Button(self, text="注册", command=self.collect_phone)
self.forgot_password_button = tk.Button(self, text="找回密码", command=self.find)
self.username_label.grid(row=0, column=0, padx=5, pady=5)
self.password_label.grid(row=1, column=0, padx=5, pady=5)
self.username_entry.grid(row=0, column=1, padx=5, pady=5)
self.password_entry.grid(row=1, column=1, padx=5, pady=5)
self.login_button.grid(row=2, column=0, padx=5, pady=5)
self.register_button.grid(row=2, column=1, padx=5, pady=5)
self.forgot_password_button.grid(row=3, column=0, columnspan=2, padx=5, pady=5)
self.users_file = "users.txt"
if not os.path.exists(self.users_file):
with open(self.users_file, "w") as file:
file.write("{}")
self.protocol("WM_DELETE_WINDOW", self.close_app)
def login(self):
username = self.username_entry.get()
password = self.password_entry.get()
users = self.load_users()
if username in users and users[username]["password"] == password:
messagebox.showinfo("登录成功", "登录成功!")
self.destroy()
self.master.has_permission = True
elif username == "admin" and password == "admin":
messagebox.showinfo("登录成功", "登录成功!")
self.destroy()
self.master.has_permission = True
else:
messagebox.showerror("登录失败", "用户名或密码错误")
def load_users(self):
try:
with open(self.users_file, "r") as file:
users = json.load(file)
except (FileNotFoundError, json.JSONDecodeError):
users = {}
return users
def find(self):
username = simpledialog.askstring("找回密码", "请输入用户名:")
phone = simpledialog.askstring("找回密码", "请输入电话号码:")
users = self.load_users()
if username in users and users[username]["phone"] == phone:
password = users[username]["password"]
messagebox.showinfo("找回密码", f"您的密码是: {password}")
else:
messagebox.showerror("找回密码失败", "用户名或电话号码不匹配")
def save_users(self, users):
with open(self.users_file, "w") as file:
json.dump(users, file)
def collect_phone(self):
username = self.username_entry.get()
password = self.password_entry.get()
if not username or not password:
messagebox.showerror("注册失败", "请输入用户名和密码")
return
phone = simpledialog.askstring("输入电话号码", "请输入电话号码:")
self.register(phone)
def register(self, phone):
username = self.username_entry.get()
password = self.password_entry.get()
if not username or not password:
messagebox.showerror("注册失败", "请输入用户名和密码")
elif not phone:
messagebox.showerror("注册失败", "请输入电话号码")
else:
users = self.load_users()
if username in users:
messagebox.showerror("注册失败", "用户名已被注册")
else:
users[username] = {"password": password, "phone": phone}
self.save_users(users)
messagebox.showinfo("注册成功", "注册成功!")
def close_app(self):
self.master.destroy() # 关闭整个应用程序
以上是登入这方面的类,给大家参考。
进入系统后,有三个页面,第一个是数据操作区,分析图展示,成绩汇总分析。
self.title("学生成绩分析系统")
self.has_permission = False
self.tabControl = ttk.Notebook(self)
self.undo_stack = []
self.student_vars = [] # 用于存储学生选择的变量
self.subject_vars = []
self.colors = ['blue', 'green', 'red', 'cyan', 'magenta', 'yellow', 'black', 'purple', 'pink', 'brown']
self.colorss = ['orange', 'blue', 'red']
self.tab1 = ttk.Frame(self.tabControl)
self.tab2 = ttk.Frame(self.tabControl)
self.tab3 = ttk.Frame(self.tabControl)
self.tab4 = ttk.Frame(self.tabControl)
self.tabControl.add(self.tab1, text="数据操作区")
self.tabControl.add(self.tab2, text="分析图展示")
self.tabControl.add(self.tab3, text="成绩汇总分析")
self.tabControl.pack(expand=1, fill="both")
# Tab 1 - 数据操作
self.tree = ttk.Treeview(self.tab1, columns=[f"col{i}" for i in range(4)], show="headings", selectmode="browse")
self.tree.grid(row=0, column=0, columnspan=5, padx=5, pady=5)
self.tree.heading("#0", text="ID")
self.tree.heading("#1", text="姓名")
self.tree.heading("#2", text="语文")
self.tree.heading("#3", text="数学")
self.tree.heading("#4", text="英语")
# 设置列的宽度
self.tree.column("#0", width=50, anchor='center')
self.tree.column("#1", width=100, anchor='center')
self.tree.column("#2", width=100, anchor='center')
self.tree.column("#3", width=100, anchor='center')
self.tree.column("#4", width=100, anchor='center')
# 设置数据居中对齐的样式
self.tree.tag_configure('center', anchor='center')
self.btn_load = tk.Button(self.tab1, text="读取", command=self.load_data)
self.btn_add = tk.Button(self.tab1, text="添加", command=self.show_add_window)
self.btn_modify = tk.Button(self.tab1, text="修改", command=lambda: self.modify_data())
self.btn_delete = tk.Button(self.tab1, text="删除", command=self.delete_data)
self.btn_save = tk.Button(self.tab1, text="保存", command=self.save_data)
self.back = tk.Button(self.tab1, text="撤回", command=self.undo)
self.exsave = tk.Button(self.tab1, text="另存为", command=self.undo)
self.btn_load.grid(row=1, column=0, padx=5, pady=5)
self.btn_add.grid(row=1, column=1, padx=5, pady=5)
self.btn_modify.grid(row=1, column=2, padx=5, pady=5)
self.btn_delete.grid(row=1, column=3, padx=5, pady=5)
self.btn_save.grid(row=1, column=4, padx=5, pady=5)
self.back.grid(row=1, column=5, padx=5, pady=5)
# Tab 2 - 柱状图展示
self.students_frame = tk.Frame(self.tab2)
self.subjects_frame = tk.Frame(self.tab2)
self.students_frame.grid(row=1, column=1, padx=5, pady=5)
self.subjects_frame.grid(row=2, column=1, padx=5, pady=5)
self.students_label = Label(self.students_frame, text="学生")
self.students_label.grid(row=0, column=0, padx=5, pady=5)
self.subject_label = Label(self.subjects_frame, text="学科")
self.subject_label.grid(row=1, column=0, padx=5, pady=5)
self.button7 = tk.Button(self.tab2, text="绘制柱状图", command=self.draw_bar_chart)
self.button7.grid(row=3, column=1, padx=5, pady=5)
self.plot_area = tk.Label(self.tab2)
self.plot_area.grid(row=1, columnspan=3, padx=5, pady=5)
# Tab 3 - 成绩分析
self.btn_avg_score = tk.Button(self.tab3, text="课程平均分", command=self.show_avg_score)
self.btn_max_score = tk.Button(self.tab3, text="课程最高分", command=self.show_max_score)
self.btn_personal_max_min = tk.Button(self.tab3, text="个人最高分", command=self.show_personal_max_min)
self.btn_avg_score.pack(pady=5)
self.btn_max_score.pack(pady=5)
self.btn_personal_max_min.pack(pady=5)
第一个是用来读取数据后进行增删改查等操作,以及点击保存后才会保存和撤回删除这个操作。代码如下
def load_data(self):
file_path = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")])
if file_path:
data = pd.read_csv(file_path)
self.tree.delete(*self.tree.get_children())
with open(file_path, "r", newline="", encoding="utf-8") as csvfile:
reader = csv.reader(csvfile)
first_line = next(reader)
first_line_str = ",".join(first_line)
self.tree.heading("#0", text=first_line_str)
self.tree.heading("#1", text=first_line_str[0:2])
self.tree.heading("#2", text=first_line_str[3:5])
self.tree.heading("#3", text=first_line_str[6:8])
self.tree.heading("#4", text=first_line_str[9:11])
def add_data(self):
# 在文本框中添加数据
cursor_id = self.tree.selection()
if cursor_id:
last_id = self.tree.index(cursor_id[-1]) + 1
else:
last_id = len(self.tree.get_children())
data_1 = self.entry_1.get()
data_2 = self.entry_2.get()
data_3 = self.entry_3.get()
data_4 = self.entry_4.get()
# 在树形列表中插入新行
last_id = len(self.tree.get_children())
self.tree.insert("", "end", text=last_id + 1, values=[data_1, data_2, data_3, data_4])
# self.tree.insert("", last_id, text=last_id, values=["", "", "", ""])
def show_add_window(self):
# 创建添加窗口
self.add_window = tk.Toplevel(self.master)
self.add_window.title("添加数据")
self.check_label1_1 = tk.Label(self.add_window, text="姓名", )
self.check_label2_2 = tk.Label(self.add_window, text="语文", )
self.check_label3_3 = tk.Label(self.add_window, text="数学", )
self.check_label4_4 = tk.Label(self.add_window, text="英语", )
self.check_label1_1.grid(row=0, column=0, padx=5, pady=5)
self.check_label2_2.grid(row=0, column=1, padx=5, pady=5)
self.check_label3_3.grid(row=0, column=2, padx=5, pady=5)
self.check_label4_4.grid(row=0, column=3, padx=5, pady=5)
# 创建输入框
self.entry_1 = tk.Entry(self.add_window)
self.entry_2 = tk.Entry(self.add_window)
self.entry_3 = tk.Entry(self.add_window)
self.entry_4 = tk.Entry(self.add_window)
self.entry_1.grid(row=2, column=0, padx=5, pady=5)
self.entry_2.grid(row=2, column=1, padx=5, pady=5)
self.entry_3.grid(row=2, column=2, padx=5, pady=5)
self.entry_4.grid(row=2, column=3, padx=5, pady=5)
# 创建添加按钮
self.btn_add_data = tk.Button(self.add_window, text="添加", command=self.add_data)
self.btn_add_data.grid(row=3, column=1, padx=5, pady=5)
def modify_data(self):
math_var = tk.StringVar()
messagebox.showinfo("提示", "双击修改数据")
# 编辑选中的行
def edit_item():
# 获取选中行的数据
selected_item = self.tree.selection()[0]
selected_data = self.tree.item(selected_item)['values']
# 创建一个新的窗口
edit_window = tk.Toplevel(self.tab1)
edit_window.title("编辑")
# 添加标签和文本框
math_label = tk.Label(edit_window, text="数学:")
math_label.grid(row=0, column=0, padx=5, pady=5)
math_var = tk.StringVar(value=str(selected_data[2]))
math_entry = tk.Entry(edit_window, textvariable=math_var)
math_entry.grid(row=0, column=1, padx=5, pady=5)
chinese_label = tk.Label(edit_window, text="语文:")
chinese_label.grid(row=1, column=0, padx=5, pady=5)
chinese_var = tk.StringVar(value=str(selected_data[1]))
chinese_entry = tk.Entry(edit_window, textvariable=chinese_var)
chinese_entry.grid(row=1, column=1, padx=5, pady=5)
english_label = tk.Label(edit_window, text="英语:")
english_label.grid(row=2, column=0, padx=5, pady=5)
english_var = tk.StringVar(value=str(selected_data[3]))
english_entry = tk.Entry(edit_window, textvariable=english_var)
english_entry.grid(row=2, column=1, padx=5, pady=5)
name_label = tk.Label(edit_window, text="姓名:")
name_label.grid(row=3, column=0, padx=5, pady=5)
name_var = tk.StringVar(value=str(selected_data[0]))
name_entry = tk.Entry(edit_window, textvariable=name_var)
name_entry.grid(row=3, column=1, padx=5, pady=5)
# 保存数据
def save_data():
new_math_score = math_var.get()
new_chinese_score = chinese_var.get()
new_english_score = english_var.get()
new_name = name_var.get()
self.tree.set(selected_item, column=0, value=new_name)
self.tree.set(selected_item, column=1, value=new_chinese_score)
self.tree.set(selected_item, column=2, value=new_math_score)
self.tree.set(selected_item, column=3, value=new_english_score)
edit_window.destroy()
# 添加一个按钮用于保存修改后的数据
save_button = tk.Button(edit_window, text="保存", command=save_data)
save_button.grid(row=4, column=1, padx=5, pady=5)
# 双击树型列表中的一行时编辑该行的数据
def on_double_click(event):
edit_item()
self.tree.bind("<Double-Button-1>", on_double_click)
def delete_data(self):
cur_item = self.tree.focus()
if not cur_item:
messagebox.showwarning("提示", "请先选择一行数据")
return
# 使用 item 方法获取当前选中项的相关信息
cur_text = self.tree.item(cur_item)["text"]
cur_values = self.tree.item(cur_item)["values"]
# 使用 delete 方法删除当前选中项
self.tree.delete(cur_item)
messagebox.showinfo("提示", f"已删除数据:{cur_text}, {cur_values}")
self.undo_stack.append((cur_item, cur_text, cur_values))
def save_data(self):
with open('data.csv', mode='w', newline='', encoding="utf-8") as file:
writer = csv.writer(file)
# 写入表头
columns = [self.tree.heading(column)["text"] for column in self.tree["columns"]]
writer.writerow(columns)
# 写入数据
for item_id in self.tree.get_children():
row_data = []
for column in self.tree["columns"]:
cell_data = self.tree.set(item_id, column)
row_data.append(cell_data)
writer.writerow(row_data)
第二部分就是把从文件读取学生的姓名和学科的姓名然后去根据下拉列表的多选去绘制柱状图
class CustomComboPicker(Frame):
def __init__(self, master, values=[], **kwargs):
super().__init__(master, **kwargs)
self.values = values
self.selected_values = []
self.combo_frame = Frame(self)
self.combo_frame.grid(row=0, column=3, padx=5, pady=5)
self.combo_button = Button(self.combo_frame, text="请选择", command=self.show_dropdown)
self.combo_button.grid(row=0, column=3, padx=5, pady=5)
self.combo_label = Label(self.combo_frame, text="")
# self.combo_label.pack(side=BOTTOM)
self.dropdown_window = None
if self.dropdown_window:
self.hide_dropdown()
else:
self.dropdown_window = Toplevel(self)
self.dropdown_window.geometry("+%d+%d" % (self.winfo_rootx() + self.winfo_width() // 2 - 75,
self.winfo_rooty() + self.winfo_height() // 2 - 100))
self.dropdown_window.overrideredirect(True)
self.listbox = Listbox(self.dropdown_window, selectmode=MULTIPLE)
self.listbox.pack(fill=BOTH, expand=True)
for value in self.values:
self.listbox.insert(END, value)
self.listbox.bind("<FocusOut>", self.hide_dropdown)
if self.dropdown_window:
selected_indices = self.listbox.curselection()
if selected_indices:
selected_values = [self.listbox.get(index) for index in selected_indices]
self.selected_values = selected_values
self.combo_label.configure(text=", ".join(selected_values))
messagebox.showinfo("选中的数据", ", ".join(selected_values))
else:
self.selected_values = []
self.combo_label.configure(text="")
self.dropdown_window.destroy()
self.dropdown_window = None
def get_selected_values(self):
return self.selected_values
绘图部分
def draw_bar_chart(self):
# 获取选中的学生和科目
self.subject_picker.hide_dropdown()
self.student_picker.hide_dropdown()
selected_subjects = self.subject_picker.get_selected_values()
selected_students = self.student_picker.get_selected_values()
data = {} # 存储选中学生的成绩数据
with open('data.csv', 'r', encoding="utf-8") as file:
reader = csv.DictReader(file) # 使用DictReader读取csv文件
for row in reader:
student_name = row['姓名'] # 获取学生姓名
if student_name in selected_students: # 如果学生在选中的学生列表中
scores = []
for subject, score in row.items():
if subject != '姓名' and subject in selected_subjects: # 如果科目不是姓名,并且在选中的科目列表中
scores.append(int(score)) # 将分数转换为整数并添加到分数列表中
data[student_name] = scores # 将学生和对应的分数列表存储到data字典中
if data: # 如果有有效数据
students = list(data.keys()) # 获取学生列表
num_students = len(students) # 学生数量
subjects = selected_subjects # 获取科目列表
num_subjects = len(subjects) # 科目数量
# 创建图形和坐标轴对象
fig, ax = plt.subplots()
bar_width = 0.2
index = 0
for i, student in enumerate(students):
scores = data[student] # 获取学生对应的成绩列表
x_pos = [index + j * bar_width for j in range(num_subjects)]
bars = ax.bar(x_pos, scores, bar_width, label=student,
color=self.colors[i % len(self.colors)]) # 设置柱子颜色
index += 1
# 添加成绩标签
for j, score in enumerate(scores):
ax.annotate(str(score), xy=(x_pos[j], score), va='bottom', ha='center')
# 添加科目标签
ax.set_xticks([index - bar_width / 2 + j * bar_width for j in range(num_subjects)])
ax.set_xticklabels(subjects)
# 创建图例
handles = []
for i, subject in enumerate(selected_subjects):
patch = plt.Rectangle((0, 0), 1, 1, color=self.colorss[i % len(self.colors)], label=subject)
handles.append(patch)
ax.legend(handles=handles)
# 设置图形属性
ax.set_xlabel('学生')
ax.set_ylabel('分数')
ax.set_title('成绩柱状图')
ax.set_xticks([index + bar_width / 2 for index in range(num_students)])
ax.set_xticklabels(students)
canvas = FigureCanvasTkAgg(fig, master=self.tab2)
canvas.draw()
canvas.get_tk_widget().grid(row=4, column=1, padx=10, pady=10)
else:
print("没有选择学生或科目")
最后选不同的学科和人去算平均分和最高分
def show_avg_score(self):
self.avg_window = tk.Toplevel(self.master)
self.avg_window.title("平均值")
window_width = 300
window_height = 150
screen_width = self.avg_window.winfo_screenwidth()
screen_height = self.avg_window.winfo_screenheight()
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
self.avg_window.geometry(f"{window_width}x{window_height}+{x}+{y}")
# 下拉列表
subject_var = tk.StringVar() # 存储所选科目
subject_var.set("语文") # 默认选择语文
subject_options = ["语文", "数学", "英语"]
subject_menu = tk.OptionMenu(self.avg_window, subject_var, *subject_options)
subject_menu.pack()
# 显示结果的标签
result_label = tk.Label(self.avg_window, text="")
result_label.pack()
def calculate_average():
subject = subject_var.get()
with open('data.csv', 'r', encoding="utf-8") as file:
reader = csv.DictReader(file)
scores = [int(row[subject]) for row in reader]
average = sum(scores) / len(scores)
result_label.config(text=f"{subject}平均值:{average:.2f}")
result_label.pack(side="bottom")
# 计算按钮
calculate_button = tk.Button(self.avg_window, text="计算", command=calculate_average)
calculate_button.pack(side="top")
def show_max_score(self):
self.max_window = tk.Toplevel(self.master)
self.max_window.title("最高分")
window_width = 300
window_height = 150
screen_width = self.max_window.winfo_screenwidth()
screen_height = self.max_window.winfo_screenheight()
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
self.max_window.geometry(f"{window_width}x{window_height}+{x}+{y}")
# 下拉列表
subject_var = tk.StringVar() # 存储所选科目
subject_var.set("语文") # 默认选择语文
subject_options = ["语文", "数学", "英语"]
subject_menu = tk.OptionMenu(self.max_window, subject_var, *subject_options)
subject_menu.pack()
# 显示结果的标签
result_label = tk.Label(self.max_window, text="")
result_label.pack()
def calculate_max_score():
subject = subject_var.get()
with open('data.csv', 'r', encoding="utf-8") as file:
reader = csv.DictReader(file)
scores = [int(row[subject]) for row in reader]
MAX = max(scores)
result_label.config(text=f"{subject}最高分:{MAX}")
result_label.pack(side="bottom")
# 计算按钮
calculate_button = tk.Button(self.max_window, text="计算", command=calculate_max_score)
calculate_button.pack(side="top")
def show_personal_max_min(self):
self.show_personal_max = tk.Toplevel(self.master)
self.show_personal_max.title("最高分")
window_width = 300
window_height = 150
screen_width = self.show_personal_max.winfo_screenwidth()
screen_height = self.show_personal_max.winfo_screenheight()
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
self.show_personal_max.geometry(f"{window_width}x{window_height}+{x}+{y}")
with open('data.csv', 'r', encoding="utf-8") as file:
reader = csv.DictReader(file)
names = set(row['姓名'] for row in reader)
self.name_combobox = Combobox(self.show_personal_max, values=list(names))
self.name_combobox.pack()
self.max_score_label = tk.Label(self.show_personal_max, text="")
self.max_score_label.pack(side="bottom")
button = tk.Button(self.show_personal_max, text="确定", command=self.show_max_score1)
label= tk.Label(self.show_personal_max, text="请选择学生")
label.pack(side="top")
button.pack(side="top")
def show_max_score1(self):
name = self.name_combobox.get()
with open('data.csv', 'r', encoding="utf-8") as file:
reader = csv.DictReader(file)
max_score_subject = None
max_score = 0
for row in reader:
student_name = row['姓名']
if student_name == name:
for subject, score in row.items():
if subject != '姓名':
score = int(score)
if score > max_score:
max_score = score
max_score_subject = subject
if max_score_subject:
self.max_score_label["text"] = f"{max_score_subject}: {max_score}"
else:
self.max_score_window = tk.Tk()
self.window_width = 300
self.window_height = 200
self.screen_width = self.max_score_window.winfo_screenwidth()
self.screen_height = self.max_score_window.winfo_screenheight()
self.x = (self.screen_width - self.window_width) // 2
self.y = (self.screen_height - self.window_height) // 2
self.max_score_window.geometry(f"{self.window_width}x{self.window_height}+{self.x}+{self.y}")
label = tk.Label(self.max_score_window, text="没有找到成绩")
label.pack()
大家可以自行去解读需要部分的代码,如果有什么不懂的地方可以交流