# 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("", 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("", 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)