You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

685 lines
28 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# student/ui/gui_ui.py
import tkinter as tk
from tkinter import ttk, messagebox
from datetime import datetime, date
import re
from student.model.student import Student
from typing import List, Optional
from ..bll.student_bll import StudentBLL
from ..model.student import Student
from ..util.validator import Validator
class StudentGUI:
"""学生信息管理系统的图形用户界面"""
def __init__(self, root: tk.Tk, bll: StudentBLL):
self.root = root
self.root.title("学生信息管理系统")
self.root.geometry("900x600")
self.root.minsize(800, 500)
self.bll = bll
# 设置中文字体
self.style = ttk.Style()
self.style.configure("TLabel", font=("SimHei", 10))
self.style.configure("TButton", font=("SimHei", 10))
self.style.configure("TTreeview", font=("SimHei", 10))
# 创建主框架
self.main_frame = ttk.Frame(self.root)
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建顶部导航栏
self.create_navigation_bar()
# 创建主内容区域
self.content_frame = ttk.Frame(self.main_frame)
self.content_frame.pack(fill=tk.BOTH, expand=True, pady=10)
# 默认显示学生列表
self.show_student_list()
def create_navigation_bar(self):
"""创建顶部导航栏"""
nav_frame = ttk.Frame(self.main_frame, height=40)
nav_frame.pack(fill=tk.X, side=tk.TOP)
# 添加导航按钮
buttons = [
("添加学生", self.show_add_student),
("修改学生", self.show_update_student),
("删除学生", self.show_delete_student),
("查询学生", self.show_search_student),
("统计信息", self.show_statistics),
("数据导入导出", self.show_import_export)
]
for text, command in buttons:
btn = ttk.Button(nav_frame, text=text, command=command)
btn.pack(side=tk.LEFT, padx=5, pady=5)
def clear_content_frame(self):
"""清空内容区域"""
for widget in self.content_frame.winfo_children():
widget.destroy()
def show_student_list(self, students: Optional[List[Student]] = None):
"""显示学生列表"""
self.clear_content_frame()
if students is None:
students = self.bll.get_all_students()
# 创建搜索框
search_frame = ttk.Frame(self.content_frame)
search_frame.pack(fill=tk.X, pady=5)
ttk.Label(search_frame, text="搜索:").pack(side=tk.LEFT, padx=5)
search_var = tk.StringVar()
search_entry = ttk.Entry(search_frame, textvariable=search_var, width=30)
search_entry.pack(side=tk.LEFT, padx=5)
def search_students():
keyword = search_var.get()
if keyword:
search_results = []
search_results.extend(self.bll.search_students_by_name(keyword))
search_results.extend(self.bll.search_students_by_class(keyword))
search_results.extend(self.bll.search_students_by_major(keyword))
# 去重
unique_students = []
ids = set()
for s in search_results:
if s.id_card not in ids:
unique_students.append(s)
ids.add(s.id_card)
self.show_student_list(unique_students)
else:
self.show_student_list()
ttk.Button(search_frame, text="搜索", command=search_students).pack(side=tk.LEFT, padx=5)
# 创建表格
columns = ("name", "id_card", "stu_id", "gender", "age", "class_name", "major")
tree = ttk.Treeview(self.content_frame, columns=columns, show="headings")
# 设置列标题
tree.heading("name", text="姓名")
tree.heading("id_card", text="身份证号")
tree.heading("stu_id", text="学号")
tree.heading("gender", text="性别")
tree.heading("age", text="年龄")
tree.heading("class_name", text="班级")
tree.heading("major", text="专业")
# 设置列宽
tree.column("name", width=80)
tree.column("id_card", width=150)
tree.column("stu_id", width=100)
tree.column("gender", width=50)
tree.column("age", width=50)
tree.column("class_name", width=100)
tree.column("major", width=100)
# 添加数据
for student in students:
gender_text = "" if student.gender else "" if student.gender is not None else ""
age_text = str(student.age) if student.age is not None else ""
tree.insert("", tk.END, values=(
student.name,
student.id_card,
student.stu_id,
gender_text,
age_text,
student.class_name or "",
student.major or ""
))
# 添加滚动条
scrollbar = ttk.Scrollbar(self.content_frame, orient=tk.VERTICAL, command=tree.yview)
tree.configure(yscroll=scrollbar.set)
# 布局
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
tree.pack(fill=tk.BOTH, expand=True)
# 双击查看详情
def on_double_click(event):
item = tree.selection()
if item:
id_card = tree.item(item[0])["values"][1] # 身份证号在第二列
student = self.bll.get_student_by_id(id_card)
if student:
self.show_student_details(student)
tree.bind("<Double-1>", on_double_click)
def show_student_details(self, student: Student):
"""显示学生详情"""
self.clear_content_frame()
# 创建详情表单
detail_frame = ttk.LabelFrame(self.content_frame, text=f"学生详情 - {student.name}")
detail_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 左侧信息
left_frame = ttk.Frame(detail_frame)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)
# 右侧信息
right_frame = ttk.Frame(detail_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10)
# 基本信息
info_items = [
("姓名:", student.name),
("身份证号:", student.id_card),
("学号:", student.stu_id),
("性别:", "" if student.gender else "" if student.gender is not None else "未设置"),
("年龄:", str(student.age) if student.age is not None else "未知"),
("出生日期:", str(student.birthday) if student.birthday else "未知"),
("身高:", f"{student.height} cm" if student.height else "未设置"),
("体重:", f"{student.weight} kg" if student.weight else "未设置"),
("入学日期:", str(student.enrollment_date) if student.enrollment_date else "未设置"),
("班级:", student.class_name if student.class_name else "未设置"),
("专业:", student.major if student.major else "未设置"),
("电子邮箱:", student.email if student.email else "未设置"),
("联系电话:", student.phone if student.phone else "未设置")
]
# 显示信息
for i, (label, value) in enumerate(info_items):
if i < len(info_items) // 2: # 前半部分放左侧
ttk.Label(left_frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=5)
ttk.Label(left_frame, text=value).grid(row=i, column=1, sticky=tk.W, pady=5)
else: # 后半部分放右侧
ttk.Label(right_frame, text=label).grid(row=i - len(info_items) // 2, column=0, sticky=tk.W, pady=5)
ttk.Label(right_frame, text=value).grid(row=i - len(info_items) // 2, column=1, sticky=tk.W, pady=5)
# 返回按钮
ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=10)
def show_add_student(self):
"""显示添加学生表单"""
self.clear_content_frame()
form_frame = ttk.LabelFrame(self.content_frame, text="添加学生")
form_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 表单字段
fields = {
"name": tk.StringVar(),
"id_card": tk.StringVar(),
"stu_id": tk.StringVar(),
"gender": tk.StringVar(value="未设置"),
"height": tk.StringVar(),
"weight": tk.StringVar(),
"enrollment_date": tk.StringVar(),
"class_name": tk.StringVar(),
"major": tk.StringVar(),
"email": tk.StringVar(),
"phone": tk.StringVar()
}
# 错误提示标签和输入框引用
error_labels = {}
entries = {}
# 验证函数
def validate_field(field_name, event=None):
value = fields[field_name].get()
error_msg = ""
# 根据字段类型进行验证
if field_name == "name":
if not value:
error_msg = "姓名不能为空"
elif field_name == "id_card":
if not value:
error_msg = "身份证号不能为空"
elif len(value) != 18:
error_msg = "身份证号必须为18位"
elif not value[:17].isdigit():
error_msg = "前17位必须是数字"
elif not (value[17].isdigit() or value[17].upper() == 'X'):
error_msg = "最后一位必须是数字或X"
elif field_name == "stu_id":
if not value:
error_msg = "学号不能为空"
elif field_name == "height":
if value and not value.isdigit():
error_msg = "请输入数字"
elif field_name == "weight":
if value and not value.replace('.', '', 1).isdigit():
error_msg = "请输入数字"
elif field_name == "enrollment_date":
if value:
try:
datetime.strptime(value, '%Y-%m-%d')
except ValueError:
error_msg = "格式应为YYYY-MM-DD"
elif field_name == "email":
if value and not re.match(r"[^@]+@[^@]+\.[^@]+", value):
error_msg = "邮箱格式不正确"
elif field_name == "phone":
if value and not re.match(r"^1[3-9]\d{9}$", value):
error_msg = "手机号格式不正确"
# 更新错误标签和输入框样式
if error_labels.get(field_name):
error_labels[field_name].config(text=error_msg, foreground="red")
if entries.get(field_name):
if error_msg:
entries[field_name].configure(style="Error.TEntry")
else:
entries[field_name].configure(style="TEntry")
return error_msg
# 创建表单
row = 0
for field, var in fields.items():
# 字段标签
label_text = {
"name": "姓名:",
"id_card": "身份证号:",
"stu_id": "学号:",
"height": "身高 (cm):",
"weight": "体重 (kg):",
"enrollment_date": "入学日期 (YYYY-MM-DD):",
"class_name": "班级:",
"major": "专业:",
"email": "电子邮箱:",
"phone": "联系电话:"
}.get(field, f"{field}:")
ttk.Label(form_frame, text=label_text).grid(row=row, column=0, sticky=tk.W, pady=5)
if field == "gender":
# 性别选择(单选按钮)
gender_frame = ttk.Frame(form_frame)
gender_frame.grid(row=row, column=1, sticky=tk.W, pady=5)
for i, option in enumerate(["", "", "未设置"]):
ttk.Radiobutton(gender_frame, text=option, variable=var, value=option).pack(side=tk.LEFT, padx=5)
# 性别不需要错误提示
error_labels[field] = ttk.Label(form_frame, text="")
error_labels[field].grid(row=row, column=2, sticky=tk.W, pady=5)
else:
# 创建输入框
entry = ttk.Entry(form_frame, textvariable=var)
entry.grid(row=row, column=1, sticky=tk.EW, pady=5)
entries[field] = entry
# 绑定验证事件
var.trace_add("write", lambda name, index, mode, f=field: validate_field(f))
entry.bind("<FocusOut>", lambda event, f=field: validate_field(f, event))
# 添加错误提示标签
error_labels[field] = ttk.Label(form_frame, text="", foreground="red")
error_labels[field].grid(row=row, column=2, sticky=tk.W, pady=5)
# 添加标准说明
standard_text = {
"id_card": "(18位数字最后一位可为X)",
"enrollment_date": "(例如: 2023-09-01)",
"email": "(例如: example@mail.com)",
"phone": "(例如: 13800138000)"
}.get(field, "")
if standard_text:
ttk.Label(form_frame, text=standard_text, foreground="gray").grid(row=row, column=3, sticky=tk.W,
pady=5)
row += 1
# 设置列权重,使输入框可以拉伸
form_frame.columnconfigure(1, weight=1)
# 定义错误样式
style = ttk.Style()
style.configure("Error.TEntry", fieldbackground="#ffe6e6", foreground="#000000")
# 提交按钮
def submit_form():
# 验证所有字段
errors = {}
for field in fields:
if field != "gender": # 性别不需要验证
error = validate_field(field)
if error:
errors[field] = error
if errors:
messagebox.showerror("输入错误", "请修正以下错误:\n\n" + "\n".join(errors.values()))
return
# 获取表单数据
name = fields["name"].get()
id_card = fields["id_card"].get()
stu_id = fields["stu_id"].get()
gender_text = fields["gender"].get()
gender = None
if gender_text == "":
gender = True
elif gender_text == "":
gender = False
height = fields["height"].get()
height = int(height) if height else None
weight = fields["weight"].get()
weight = float(weight) if weight else None
enrollment_date = fields["enrollment_date"].get()
enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() if enrollment_date else None
class_name = fields["class_name"].get()
major = fields["major"].get()
email = fields["email"].get()
phone = fields["phone"].get()
# 创建学生对象
student = Student(
name=name,
id_card=id_card,
stu_id=stu_id,
gender=gender,
height=height,
weight=weight,
enrollment_date=enrollment_date,
class_name=class_name,
major=major,
email=email,
phone=phone
)
# 添加学生
success, message = self.bll.add_student(student)
if success:
messagebox.showinfo("成功", "学生添加成功!")
self.show_student_list()
else:
messagebox.showerror("错误", f"添加失败: {message}")
# 创建按钮
button_frame = ttk.Frame(self.content_frame)
button_frame.pack(fill=tk.X, pady=10)
ttk.Button(button_frame, text="提交", command=submit_form).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="返回", command=self.show_student_list).pack(side=tk.LEFT, padx=5)
def show_update_student(self):
"""显示修改学生表单"""
self.clear_content_frame()
ttk.Label(self.content_frame, text="请输入要修改的学生身份证号:").pack(pady=10)
id_card_var = tk.StringVar()
ttk.Entry(self.content_frame, textvariable=id_card_var).pack(fill=tk.X, padx=20, pady=5)
def search_student():
id_card = id_card_var.get()
student = self.bll.get_student_by_id(id_card)
if student:
self._show_update_form(student)
else:
messagebox.showerror("错误", "未找到该学生!")
ttk.Button(self.content_frame, text="查找学生", command=search_student).pack(pady=10)
ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=5)
def _show_update_form(self, student: Student):
"""显示实际的修改表单"""
self.clear_content_frame()
form_frame = ttk.LabelFrame(self.content_frame, text=f"修改学生信息 - {student.name}")
form_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 表单字段
fields = {
"name": tk.StringVar(value=student.name),
"id_card": tk.StringVar(value=student.id_card),
"stu_id": tk.StringVar(value=student.stu_id),
"gender": tk.StringVar(value="" if student.gender else "" if student.gender is not None else "未设置"),
"height": tk.StringVar(value=str(student.height) if student.height is not None else ""),
"weight": tk.StringVar(value=str(student.weight) if student.weight is not None else ""),
"enrollment_date": tk.StringVar(value=str(student.enrollment_date) if student.enrollment_date else ""),
"class_name": tk.StringVar(value=student.class_name or ""),
"major": tk.StringVar(value=student.major or ""),
"email": tk.StringVar(value=student.email or ""),
"phone": tk.StringVar(value=student.phone or "")
}
# 性别选项
gender_options = ["", "", "未设置"]
# 创建表单
row = 0
for field, var in fields.items():
if field == "id_card": # 身份证号不可修改
ttk.Label(form_frame, text="身份证号:").grid(row=row, column=0, sticky=tk.W, pady=5)
ttk.Label(form_frame, text=student.id_card).grid(row=row, column=1, sticky=tk.W, pady=5)
elif field == "gender":
ttk.Label(form_frame, text="性别:").grid(row=row, column=0, sticky=tk.W, pady=5)
gender_frame = ttk.Frame(form_frame)
gender_frame.grid(row=row, column=1, sticky=tk.W, pady=5)
for i, option in enumerate(gender_options):
ttk.Radiobutton(gender_frame, text=option, variable=var, value=option).pack(side=tk.LEFT, padx=5)
else:
label_text = {
"name": "姓名:",
"stu_id": "学号:",
"height": "身高 (cm):",
"weight": "体重 (kg):",
"enrollment_date": "入学日期 (YYYY-MM-DD):",
"class_name": "班级:",
"major": "专业:",
"email": "电子邮箱:",
"phone": "联系电话:"
}.get(field, f"{field}:")
ttk.Label(form_frame, text=label_text).grid(row=row, column=0, sticky=tk.W, pady=5)
ttk.Entry(form_frame, textvariable=var).grid(row=row, column=1, sticky=tk.EW, pady=5)
row += 1
# 设置列权重,使输入框可以拉伸
form_frame.columnconfigure(1, weight=1)
# 提交按钮
def submit_form():
# 获取表单数据
name = fields["name"].get()
stu_id = fields["stu_id"].get()
gender_text = fields["gender"].get()
gender = None
if gender_text == "":
gender = True
elif gender_text == "":
gender = False
height = fields["height"].get()
height = int(height) if height else None
weight = fields["weight"].get()
weight = float(weight) if weight else None
enrollment_date = fields["enrollment_date"].get()
enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() if enrollment_date else None
class_name = fields["class_name"].get()
major = fields["major"].get()
email = fields["email"].get()
phone = fields["phone"].get()
# 更新学生对象
student.name = name
student.stu_id = stu_id
student.gender = gender
student.height = height
student.weight = weight
student.enrollment_date = enrollment_date
student.class_name = class_name
student.major = major
student.email = email
student.phone = phone
# 更新学生
success, message = self.bll.update_student(student)
if success:
messagebox.showinfo("成功", "学生信息更新成功!")
self.show_student_list()
else:
messagebox.showerror("错误", f"更新失败: {message}")
button_frame = ttk.Frame(self.content_frame)
button_frame.pack(fill=tk.X, pady=10)
ttk.Button(button_frame, text="提交", command=submit_form).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="返回", command=self.show_student_list).pack(side=tk.LEFT, padx=5)
def show_delete_student(self):
"""显示删除学生界面"""
self.clear_content_frame()
ttk.Label(self.content_frame, text="请输入要删除的学生身份证号:").pack(pady=10)
id_card_var = tk.StringVar()
ttk.Entry(self.content_frame, textvariable=id_card_var).pack(fill=tk.X, padx=20, pady=5)
def delete_student():
id_card = id_card_var.get()
student = self.bll.get_student_by_id(id_card)
if not student:
messagebox.showerror("错误", "未找到该学生!")
return
confirm = messagebox.askyesno("确认", f"确定要删除学生 {student.name} 吗?")
if confirm:
if self.bll.delete_student_by_id(id_card):
messagebox.showinfo("成功", "学生删除成功!")
self.show_student_list()
else:
messagebox.showerror("错误", "删除失败!")
button_frame = ttk.Frame(self.content_frame)
button_frame.pack(fill=tk.X, pady=10)
ttk.Button(button_frame, text="删除", command=delete_student).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="返回", command=self.show_student_list).pack(side=tk.LEFT, padx=5)
def show_search_student(self):
"""显示查询学生界面"""
self.clear_content_frame()
search_frame = ttk.LabelFrame(self.content_frame, text="查询学生")
search_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 查询方式
search_type = tk.StringVar(value="name")
ttk.Radiobutton(search_frame, text="按姓名", variable=search_type, value="name").grid(row=0, column=0,
sticky=tk.W, pady=5)
ttk.Radiobutton(search_frame, text="按班级", variable=search_type, value="class").grid(row=0, column=1,
sticky=tk.W, pady=5)
ttk.Radiobutton(search_frame, text="按专业", variable=search_type, value="major").grid(row=0, column=2,
sticky=tk.W, pady=5)
# 查询关键词
ttk.Label(search_frame, text="关键词:").grid(row=1, column=0, sticky=tk.W, pady=5)
keyword_var = tk.StringVar()
ttk.Entry(search_frame, textvariable=keyword_var).grid(row=1, column=1, sticky=tk.EW, pady=5)
search_frame.columnconfigure(1, weight=1)
def perform_search():
keyword = keyword_var.get()
if not keyword:
messagebox.showwarning("警告", "请输入关键词!")
return
search_method = search_type.get()
if search_method == "name":
students = self.bll.search_students_by_name(keyword)
elif search_method == "class":
students = self.bll.search_students_by_class(keyword)
else: # major
students = self.bll.search_students_by_major(keyword)
if not students:
messagebox.showinfo("提示", "未找到匹配的学生!")
self.show_student_list()
else:
self.show_student_list(students)
ttk.Button(search_frame, text="查询", command=perform_search).grid(row=1, column=2, padx=5, pady=5)
ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=10)
def show_statistics(self):
"""显示统计信息"""
self.clear_content_frame()
stats_frame = ttk.LabelFrame(self.content_frame, text="统计信息")
stats_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 学生总数
total_students = self.bll.get_total_students()
ttk.Label(stats_frame, text=f"学生总数: {total_students}").grid(row=0, column=0, sticky=tk.W, pady=5)
# 性别比例
gender_ratio = self.bll.get_gender_ratio()
ttk.Label(stats_frame, text=f"男生人数: {gender_ratio['male']} ({gender_ratio['male_ratio']:.2%})").grid(row=1,
column=0,
sticky=tk.W,
pady=5)
ttk.Label(stats_frame, text=f"女生人数: {gender_ratio['female']} ({gender_ratio['female_ratio']:.2%})").grid(
row=2, column=0, sticky=tk.W, pady=5)
# 各专业人数
major_frame = ttk.LabelFrame(stats_frame, text="各专业人数")
major_frame.grid(row=3, column=0, sticky=tk.NSEW, pady=10)
major_count = self.bll.get_students_by_major()
row = 0
for major, count in major_count.items():
ttk.Label(major_frame, text=f"{major}: {count}").grid(row=row, column=0, sticky=tk.W, pady=2)
row += 1
# 平均身高体重
avg_height = self.bll.calculate_average_height()
avg_weight = self.bll.calculate_average_weight()
ttk.Label(stats_frame, text=f"平均身高: {avg_height:.2f} cm").grid(row=0, column=1, sticky=tk.W, pady=5)
ttk.Label(stats_frame, text=f"平均体重: {avg_weight:.2f} kg").grid(row=1, column=1, sticky=tk.W, pady=5)
# 设置列权重
stats_frame.columnconfigure(0, weight=1)
stats_frame.columnconfigure(1, weight=1)
stats_frame.rowconfigure(3, weight=1)
ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=10)
def show_import_export(self):
"""显示数据导入导出界面"""
self.clear_content_frame()
ttk.Label(self.content_frame, text="数据导入导出功能开发中...").pack(pady=20)
ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=10)