|
|
import tkinter as tk
|
|
|
from tkinter import messagebox, ttk
|
|
|
import random
|
|
|
import sqlite3
|
|
|
import datetime
|
|
|
from points_system import PointsSystem
|
|
|
|
|
|
class AttendanceSystem:
|
|
|
def __init__(self):
|
|
|
# 创建主窗口
|
|
|
self.root = tk.Tk()
|
|
|
self.root.title("课堂点名系统 - 完整版")
|
|
|
self.root.geometry("800x600")
|
|
|
|
|
|
# 初始化积分系统
|
|
|
self.points_system = PointsSystem()
|
|
|
|
|
|
# 创建标签页
|
|
|
self.notebook = ttk.Notebook(self.root)
|
|
|
self.notebook.pack(fill='both', expand=True, padx=10, pady=10)
|
|
|
|
|
|
# 点名页面
|
|
|
self.create_attendance_tab()
|
|
|
|
|
|
# 积分排名页面
|
|
|
self.create_ranking_tab()
|
|
|
|
|
|
# 关于页面
|
|
|
self.create_about_tab()
|
|
|
|
|
|
# 初始加载数据
|
|
|
self.load_students()
|
|
|
|
|
|
def create_attendance_tab(self):
|
|
|
"""创建点名标签页"""
|
|
|
self.attendance_frame = ttk.Frame(self.notebook)
|
|
|
self.notebook.add(self.attendance_frame, text="课堂点名")
|
|
|
|
|
|
# 标题
|
|
|
title_label = tk.Label(self.attendance_frame, text="课堂随机点名系统",
|
|
|
font=("Arial", 18, "bold"))
|
|
|
title_label.pack(pady=10)
|
|
|
|
|
|
# 控制按钮框架
|
|
|
button_frame = tk.Frame(self.attendance_frame)
|
|
|
button_frame.pack(pady=10)
|
|
|
|
|
|
# 导入按钮
|
|
|
import_button = tk.Button(button_frame, text="导入学生名单",
|
|
|
font=("Arial", 12),
|
|
|
command=self.import_students,
|
|
|
bg="lightgreen", width=15)
|
|
|
import_button.grid(row=0, column=0, padx=5)
|
|
|
|
|
|
# 随机点名按钮
|
|
|
self.random_button = tk.Button(button_frame, text="随机点名",
|
|
|
font=("Arial", 12),
|
|
|
command=self.random_pick,
|
|
|
bg="lightblue", width=15)
|
|
|
self.random_button.grid(row=0, column=1, padx=5)
|
|
|
|
|
|
# 权重点名按钮
|
|
|
self.weighted_button = tk.Button(button_frame, text="权重点名",
|
|
|
font=("Arial", 12),
|
|
|
command=self.weighted_pick,
|
|
|
bg="orange", width=15)
|
|
|
self.weighted_button.grid(row=0, column=2, padx=5)
|
|
|
|
|
|
# 结果显示
|
|
|
self.result_label = tk.Label(self.attendance_frame, text="请先导入学生名单",
|
|
|
font=("Arial", 20, "bold"),
|
|
|
fg="red", height=3)
|
|
|
self.result_label.pack(pady=10)
|
|
|
|
|
|
# 积分操作按钮
|
|
|
points_frame = tk.Frame(self.attendance_frame)
|
|
|
points_frame.pack(pady=5)
|
|
|
|
|
|
tk.Button(points_frame, text="重复问题+0.5",
|
|
|
command=lambda: self.add_special_points("repeat_question"),
|
|
|
width=12).grid(row=0, column=0, padx=2)
|
|
|
|
|
|
tk.Button(points_frame, text="回答正确+2",
|
|
|
command=lambda: self.add_special_points("answer_correct"),
|
|
|
width=12).grid(row=0, column=1, padx=2)
|
|
|
|
|
|
tk.Button(points_frame, text="回答错误-1",
|
|
|
command=lambda: self.add_special_points("answer_wrong"),
|
|
|
width=12).grid(row=0, column=2, padx=2)
|
|
|
|
|
|
# 重置积分按钮
|
|
|
reset_frame = tk.Frame(self.attendance_frame)
|
|
|
reset_frame.pack(pady=5)
|
|
|
|
|
|
tk.Button(reset_frame, text="重置所有积分",
|
|
|
command=self.reset_points,
|
|
|
font=("Arial", 10), bg="lightcoral", width=15).pack()
|
|
|
|
|
|
# 学生名单显示
|
|
|
list_frame = tk.Frame(self.attendance_frame)
|
|
|
list_frame.pack(fill='both', expand=True, padx=10, pady=10)
|
|
|
|
|
|
tk.Label(list_frame, text="学生名单", font=("Arial", 12, "bold")).pack()
|
|
|
|
|
|
# 创建表格样式的名单显示
|
|
|
columns = ("学号", "姓名", "专业", "积分")
|
|
|
self.tree = ttk.Treeview(list_frame, columns=columns, show="headings", height=8)
|
|
|
|
|
|
for col in columns:
|
|
|
self.tree.heading(col, text=col)
|
|
|
self.tree.column(col, width=100)
|
|
|
|
|
|
self.tree.pack(fill='both', expand=True)
|
|
|
|
|
|
def create_ranking_tab(self):
|
|
|
"""创建积分排名标签页"""
|
|
|
self.ranking_frame = ttk.Frame(self.notebook)
|
|
|
self.notebook.add(self.ranking_frame, text="积分排名")
|
|
|
|
|
|
# 标题
|
|
|
tk.Label(self.ranking_frame, text="学生积分排行榜",
|
|
|
font=("Arial", 16, "bold")).pack(pady=10)
|
|
|
|
|
|
# 按钮框架
|
|
|
button_frame = tk.Frame(self.ranking_frame)
|
|
|
button_frame.pack(pady=5)
|
|
|
|
|
|
# 刷新按钮
|
|
|
tk.Button(button_frame, text="刷新排名",
|
|
|
command=self.show_ranking,
|
|
|
font=("Arial", 12), bg="lightyellow", width=10).pack(side='left', padx=5)
|
|
|
|
|
|
# 导出按钮
|
|
|
tk.Button(button_frame, text="导出CSV",
|
|
|
command=self.export_data,
|
|
|
font=("Arial", 12), bg="lightgreen", width=10).pack(side='left', padx=5)
|
|
|
|
|
|
# 显示图表按钮
|
|
|
tk.Button(button_frame, text="显示图表",
|
|
|
command=self.show_chart,
|
|
|
font=("Arial", 12), bg="lightcoral", width=10).pack(side='left', padx=5)
|
|
|
|
|
|
# 排名显示
|
|
|
self.ranking_text = tk.Text(self.ranking_frame, height=15, width=60,
|
|
|
font=("Arial", 11))
|
|
|
self.ranking_text.pack(padx=10, pady=10, fill='both', expand=True)
|
|
|
|
|
|
# 初始显示排名
|
|
|
self.show_ranking()
|
|
|
|
|
|
def create_about_tab(self):
|
|
|
"""创建关于页面"""
|
|
|
self.about_frame = ttk.Frame(self.notebook)
|
|
|
self.notebook.add(self.about_frame, text="关于")
|
|
|
|
|
|
# 标题
|
|
|
title_label = tk.Label(self.about_frame, text="课堂点名系统",
|
|
|
font=("Arial", 20, "bold"))
|
|
|
title_label.pack(pady=20)
|
|
|
|
|
|
# 版本信息
|
|
|
info_text = """
|
|
|
开发信息:
|
|
|
• 版本:v1.0
|
|
|
• 开发者:单人开发
|
|
|
• 开发时间:2024年11月
|
|
|
• 技术栈:Python + Tkinter + SQLite
|
|
|
|
|
|
系统功能:
|
|
|
✓ 学生名单导入
|
|
|
✓ 随机点名
|
|
|
✓ 权重点名(积分低优先)
|
|
|
✓ 积分管理系统
|
|
|
✓ 数据可视化
|
|
|
✓ CSV导出
|
|
|
|
|
|
积分规则:
|
|
|
• 正常点名:+1分
|
|
|
• 重复问题:+0.5分
|
|
|
• 回答正确:+2分
|
|
|
• 回答错误:-1分
|
|
|
• 权重点名:积分越低越容易被点
|
|
|
"""
|
|
|
|
|
|
info_label = tk.Label(self.about_frame, text=info_text,
|
|
|
font=("Arial", 11), justify="left")
|
|
|
info_label.pack(pady=10)
|
|
|
|
|
|
def import_students(self):
|
|
|
"""导入学生名单"""
|
|
|
test_students = [
|
|
|
("031902100", "张三", "计算机科学与技术"),
|
|
|
("031902101", "李四", "软件工程"),
|
|
|
("031902102", "王五", "计算机科学与技术"),
|
|
|
("031902103", "赵六", "软件工程"),
|
|
|
("031902104", "钱七", "计算机科学与技术"),
|
|
|
("031902105", "孙八", "软件工程"),
|
|
|
("031902106", "周九", "计算机科学与技术"),
|
|
|
("031902107", "吴十", "软件工程")
|
|
|
]
|
|
|
|
|
|
# 使用积分系统的方法来操作数据库
|
|
|
conn = sqlite3.connect('students.db')
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
# 清空现有数据
|
|
|
cursor.execute("DELETE FROM students")
|
|
|
|
|
|
# 插入测试数据
|
|
|
for student_id, name, major in test_students:
|
|
|
cursor.execute(
|
|
|
"INSERT INTO students (student_id, name, major) VALUES (?, ?, ?)",
|
|
|
(student_id, name, major)
|
|
|
)
|
|
|
|
|
|
conn.commit()
|
|
|
conn.close()
|
|
|
|
|
|
# 更新显示
|
|
|
self.load_students()
|
|
|
messagebox.showinfo("导入成功", f"成功导入 {len(test_students)} 名学生!")
|
|
|
self.show_ranking()
|
|
|
|
|
|
def load_students(self):
|
|
|
"""加载学生名单到表格"""
|
|
|
# 清空表格
|
|
|
for item in self.tree.get_children():
|
|
|
self.tree.delete(item)
|
|
|
|
|
|
# 直接从数据库读取
|
|
|
conn = sqlite3.connect('students.db')
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
cursor.execute("SELECT student_id, name, major, points FROM students")
|
|
|
students_data = cursor.fetchall()
|
|
|
conn.close()
|
|
|
|
|
|
for student_id, name, major, points in students_data:
|
|
|
self.tree.insert("", "end", values=(student_id, name, major, points))
|
|
|
|
|
|
# 更新结果显示
|
|
|
if students_data:
|
|
|
self.result_label.config(text="准备就绪,可以开始点名")
|
|
|
else:
|
|
|
self.result_label.config(text="请先导入学生名单")
|
|
|
|
|
|
def random_pick(self):
|
|
|
"""完全随机点名"""
|
|
|
if not self.points_system.has_students():
|
|
|
messagebox.showwarning("提示", "请先导入学生名单!")
|
|
|
return
|
|
|
|
|
|
# 直接从数据库读取
|
|
|
conn = sqlite3.connect('students.db')
|
|
|
cursor = conn.cursor()
|
|
|
cursor.execute("SELECT student_id, name FROM students")
|
|
|
students = cursor.fetchall()
|
|
|
conn.close()
|
|
|
|
|
|
if not students:
|
|
|
messagebox.showwarning("提示", "请先导入学生名单!")
|
|
|
return
|
|
|
|
|
|
student_id, name = random.choice(students)
|
|
|
self.show_pick_result(student_id, name, "random")
|
|
|
|
|
|
def weighted_pick(self):
|
|
|
"""权重随机点名(积分低的更容易被选)"""
|
|
|
if not self.points_system.has_students():
|
|
|
messagebox.showwarning("提示", "请先导入学生名单!")
|
|
|
return
|
|
|
|
|
|
result = self.points_system.get_weighted_random_student()
|
|
|
|
|
|
if not result:
|
|
|
messagebox.showwarning("提示", "请先导入学生名单!")
|
|
|
return
|
|
|
|
|
|
student_id, name, points = result
|
|
|
self.show_pick_result(student_id, name, "weighted")
|
|
|
|
|
|
def show_pick_result(self, student_id, name, pick_type):
|
|
|
"""显示点名结果"""
|
|
|
# 更新积分
|
|
|
points_added = self.points_system.add_points(student_id, "attendance")
|
|
|
|
|
|
# 显示结果
|
|
|
result_text = f"{name} ({student_id})"
|
|
|
if pick_type == "weighted":
|
|
|
result_text += "\n[权重模式:积分低优先]"
|
|
|
|
|
|
self.result_label.config(text=result_text)
|
|
|
|
|
|
# 刷新显示
|
|
|
self.load_students()
|
|
|
self.show_ranking()
|
|
|
|
|
|
messagebox.showinfo("点名结果",
|
|
|
f"被点到的同学是:{name}\n"
|
|
|
f"学号:{student_id}\n"
|
|
|
f"积分+{points_added}")
|
|
|
|
|
|
def add_special_points(self, points_type):
|
|
|
"""添加特殊积分"""
|
|
|
# 获取当前选中的学生
|
|
|
selection = self.tree.selection()
|
|
|
if not selection:
|
|
|
messagebox.showwarning("提示", "请先点名选择一个学生!")
|
|
|
return
|
|
|
|
|
|
# 获取选中学生的学号
|
|
|
item = self.tree.item(selection[0])
|
|
|
student_id = item['values'][0]
|
|
|
|
|
|
# 添加积分
|
|
|
points_added = self.points_system.add_points(student_id, points_type)
|
|
|
|
|
|
# 刷新显示
|
|
|
self.load_students()
|
|
|
self.show_ranking()
|
|
|
|
|
|
point_names = {
|
|
|
"repeat_question": "重复问题",
|
|
|
"answer_correct": "回答正确",
|
|
|
"answer_wrong": "回答错误"
|
|
|
}
|
|
|
|
|
|
messagebox.showinfo("积分更新",
|
|
|
f"{point_names[points_type]}\n"
|
|
|
f"积分{points_added:+}")
|
|
|
|
|
|
def reset_points(self):
|
|
|
"""重置积分确认"""
|
|
|
if not self.points_system.has_students():
|
|
|
messagebox.showwarning("提示", "请先导入学生名单!")
|
|
|
return
|
|
|
|
|
|
if messagebox.askyesno("确认", "确定要重置所有学生的积分吗?"):
|
|
|
self.points_system.reset_all_points()
|
|
|
self.load_students()
|
|
|
self.show_ranking()
|
|
|
messagebox.showinfo("成功", "所有积分已重置为0")
|
|
|
|
|
|
def show_ranking(self):
|
|
|
"""显示积分排名"""
|
|
|
ranking = self.points_system.get_ranking(10)
|
|
|
|
|
|
self.ranking_text.delete(1.0, tk.END)
|
|
|
|
|
|
if not ranking:
|
|
|
self.ranking_text.insert(1.0, "暂无数据,请先导入学生名单")
|
|
|
return
|
|
|
|
|
|
# 显示排名
|
|
|
header = "排名 姓名 学号 积分\n"
|
|
|
header += "=" * 35 + "\n"
|
|
|
self.ranking_text.insert(1.0, header)
|
|
|
|
|
|
for i, (name, student_id, points) in enumerate(ranking, 1):
|
|
|
rank_text = f"{i:2d} {name:8} {student_id} {points:3d}分\n"
|
|
|
self.ranking_text.insert(tk.END, rank_text)
|
|
|
|
|
|
def export_data(self):
|
|
|
"""导出数据到CSV"""
|
|
|
try:
|
|
|
filename = self.points_system.export_to_csv()
|
|
|
messagebox.showinfo("导出成功", f"数据已导出到:\n{filename}")
|
|
|
except Exception as e:
|
|
|
messagebox.showerror("导出失败", f"导出时出错:{str(e)}")
|
|
|
|
|
|
def show_chart(self):
|
|
|
"""显示积分图表"""
|
|
|
try:
|
|
|
# 尝试使用matplotlib
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
students_data = self.points_system.get_all_students_data()
|
|
|
if not students_data:
|
|
|
messagebox.showwarning("提示", "没有数据可显示")
|
|
|
return
|
|
|
|
|
|
names = [student[0] for student in students_data]
|
|
|
points = [student[1] for student in students_data]
|
|
|
|
|
|
# 创建图表
|
|
|
plt.figure(figsize=(12, 6))
|
|
|
|
|
|
# 柱状图
|
|
|
plt.subplot(1, 2, 1)
|
|
|
bars = plt.bar(names, points, color=['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'])
|
|
|
plt.title('学生积分柱状图', fontsize=14, fontweight='bold')
|
|
|
plt.xlabel('学生姓名')
|
|
|
plt.ylabel('积分')
|
|
|
plt.xticks(rotation=45)
|
|
|
|
|
|
# 在柱子上显示数值
|
|
|
for bar, point in zip(bars, points):
|
|
|
plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.1,
|
|
|
str(point), ha='center', va='bottom')
|
|
|
|
|
|
# 饼图(前5名)
|
|
|
plt.subplot(1, 2, 2)
|
|
|
top_names = names[:5]
|
|
|
top_points = points[:5]
|
|
|
|
|
|
# 如果数据不足5个,使用所有数据
|
|
|
if len(top_names) < 5:
|
|
|
top_names = names
|
|
|
top_points = points
|
|
|
|
|
|
colors = ['#FF9999', '#66B2FF', '#99FF99', '#FFD700', '#FFB6C1']
|
|
|
plt.pie(top_points, labels=top_names, autopct='%1.1f%%', colors=colors[:len(top_names)])
|
|
|
plt.title('积分分布饼图(前5名)', fontsize=14, fontweight='bold')
|
|
|
|
|
|
plt.tight_layout()
|
|
|
plt.show()
|
|
|
|
|
|
except ImportError:
|
|
|
# 如果matplotlib不可用,显示文本图表
|
|
|
self.show_text_chart()
|
|
|
|
|
|
def show_text_chart(self):
|
|
|
"""显示文本格式的图表(matplotlib不可用时)"""
|
|
|
students_data = self.points_system.get_all_students_data()
|
|
|
if not students_data:
|
|
|
messagebox.showwarning("提示", "没有数据可显示")
|
|
|
return
|
|
|
|
|
|
chart_window = tk.Toplevel(self.root)
|
|
|
chart_window.title("积分图表")
|
|
|
chart_window.geometry("600x400")
|
|
|
|
|
|
tk.Label(chart_window, text="积分排行榜 - 文本图表",
|
|
|
font=("Arial", 16, "bold")).pack(pady=10)
|
|
|
|
|
|
# 创建文本显示区域
|
|
|
text_widget = tk.Text(chart_window, font=("Consolas", 10), width=80, height=20)
|
|
|
text_widget.pack(padx=10, pady=10, fill='both', expand=True)
|
|
|
|
|
|
# 生成文本图表
|
|
|
chart_text = "积分柱状图(文本版):\n"
|
|
|
chart_text += "=" * 50 + "\n\n"
|
|
|
|
|
|
max_points = max(student[1] for student in students_data) if students_data else 1
|
|
|
|
|
|
for name, points in students_data:
|
|
|
# 计算柱状图长度(最大20个字符)
|
|
|
bar_length = int((points / max_points) * 20) if max_points > 0 else 0
|
|
|
bar = "█" * bar_length
|
|
|
chart_text += f"{name:6} [{points:2d}分] {bar}\n"
|
|
|
|
|
|
chart_text += f"\n导出时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
|
|
chart_text += f"总学生数:{len(students_data)}人"
|
|
|
|
|
|
text_widget.insert(1.0, chart_text)
|
|
|
text_widget.config(state='disabled') # 设为只读
|
|
|
|
|
|
def run(self):
|
|
|
"""启动程序"""
|
|
|
self.root.mainloop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
print("=== 课堂点名系统启动 ===")
|
|
|
app = AttendanceSystem()
|
|
|
app.run() |