全新2.2版本,内核用Python编写
This commit is contained in:
BIN
课堂点名小程序/rollcall_icon.png
Normal file
BIN
课堂点名小程序/rollcall_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
490
课堂点名小程序/课堂随机点名.py
Normal file
490
课堂点名小程序/课堂随机点名.py
Normal file
@@ -0,0 +1,490 @@
|
||||
import tkinter as tk
|
||||
# Add PhotoImage to the import
|
||||
from tkinter import ttk, filedialog, messagebox, PhotoImage
|
||||
import random
|
||||
# import time
|
||||
import os
|
||||
import csv
|
||||
from datetime import date
|
||||
from pypinyin import pinyin, Style
|
||||
import sys # Import sys for path joining
|
||||
|
||||
class RollCallApp:
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
master.title("杨昱幸老师的课堂点名系统")
|
||||
|
||||
# --- Set Window Icon ---
|
||||
try:
|
||||
# Determine the base path (works for scripts and frozen executables)
|
||||
if getattr(sys, 'frozen', False):
|
||||
base_path = sys._MEIPASS # PyInstaller temp folder
|
||||
else:
|
||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
icon_path = os.path.join(base_path, 'rollcall_icon.png') # Make sure 'rollcall_icon.png' exists here!
|
||||
|
||||
if os.path.exists(icon_path):
|
||||
# Use PhotoImage for broader compatibility (PNG, GIF)
|
||||
app_icon = PhotoImage(file=icon_path)
|
||||
master.iconphoto(True, app_icon) # The 'True' makes it the default icon
|
||||
else:
|
||||
print(f"Warning: Icon file not found at {icon_path}. Using default icon.")
|
||||
except tk.TclError as e:
|
||||
# Handle cases where the image format might be unsupported or file is corrupt
|
||||
print(f"Warning: Could not load icon '{icon_path}': {e}. Using default icon.")
|
||||
except Exception as e:
|
||||
# Catch any other potential errors during icon loading
|
||||
print(f"Warning: An unexpected error occurred while loading the icon: {e}. Using default icon.")
|
||||
# --- End Set Window Icon ---
|
||||
|
||||
|
||||
master.minsize(width=1100, height=700)
|
||||
|
||||
# ... (the rest of your __init__ code remains exactly the same) ...
|
||||
# --- Define Font Families (User specified) ---
|
||||
self.font_family_ui = "微软雅黑"
|
||||
self.font_family_display = "华文中宋"
|
||||
|
||||
# --- Define Font Sizes (Slightly adjusted for modern feel, easy to revert) ---
|
||||
self.font_size_standard = 14
|
||||
self.font_size_counter = 18
|
||||
self.font_size_display = 42
|
||||
self.font_size_title = 28
|
||||
|
||||
self.font_title_tuple = (self.font_family_ui, self.font_size_title, "bold")
|
||||
self.font_counter_tuple = (self.font_family_ui, self.font_size_counter)
|
||||
self.font_display_tuple = (self.font_family_display, self.font_size_display, "bold")
|
||||
self.font_standard_tuple = (self.font_family_ui, self.font_size_standard)
|
||||
self.font_button_tuple = (self.font_family_ui, self.font_size_standard, "bold")
|
||||
self.font_footer_tuple = (self.font_family_ui, 11)
|
||||
|
||||
|
||||
# --- Style and Colors ---
|
||||
self.style = ttk.Style(master)
|
||||
try:
|
||||
self.style.theme_use('clam')
|
||||
except tk.TclError:
|
||||
print("Clam theme not available, using default theme. Styling will still be applied.")
|
||||
|
||||
self.COLOR_BACKGROUND = "#ECEFF1"
|
||||
self.COLOR_TEXT = "#263238"
|
||||
self.COLOR_PRIMARY = "#007BFF"
|
||||
self.COLOR_PRIMARY_DARK = "#0056b3"
|
||||
self.COLOR_PRIMARY_HOVER = "#0069D9"
|
||||
self.COLOR_PRIMARY_TEXT = "#FFFFFF"
|
||||
self.COLOR_SECONDARY_BG = "#FFFFFF"
|
||||
self.COLOR_BORDER = "#B0BEC5"
|
||||
self.COLOR_DISABLED_FG = "#78909C"
|
||||
self.COLOR_DISABLED_BG = "#CFD8DC"
|
||||
|
||||
self.COLOR_NAME_DISPLAY_DEFAULT_BG = self.COLOR_SECONDARY_BG
|
||||
self.COLOR_NAME_DISPLAY_DEFAULT_FG = self.COLOR_TEXT
|
||||
self.COLOR_NAME_DISPLAY_FLASH_BG = "#FFF9C4" # Pale Yellow (Material Design Yellow 100)
|
||||
self.COLOR_NAME_DISPLAY_FLASH_FG = self.COLOR_TEXT
|
||||
self.COLOR_NAME_DISPLAY_SELECTED_BG = "#C8E6C9" # Pale Green (Material Design Green 100)
|
||||
self.COLOR_NAME_DISPLAY_SELECTED_FG = "#2E7D32" # Dark Green (Material Design Green 800)
|
||||
self.COLOR_NAME_DISPLAY_ABSENT_BG = "#FFCDD2" # Pale Red (Material Design Red 100)
|
||||
self.COLOR_NAME_DISPLAY_ABSENT_FG = "#C62828" # Dark Red (Material Design Red 800)
|
||||
|
||||
master.configure(bg=self.COLOR_BACKGROUND)
|
||||
|
||||
self.style.configure(".", background=self.COLOR_BACKGROUND, foreground=self.COLOR_TEXT,
|
||||
font=self.font_standard_tuple)
|
||||
self.style.configure("TFrame", background=self.COLOR_BACKGROUND)
|
||||
|
||||
self.style.configure("TLabel", background=self.COLOR_BACKGROUND, foreground=self.COLOR_TEXT)
|
||||
self.style.configure("Title.TLabel", font=self.font_title_tuple, foreground=self.COLOR_PRIMARY)
|
||||
self.style.configure("Counter.TLabel", font=self.font_counter_tuple)
|
||||
self.style.configure("Value.TLabel", font=self.font_standard_tuple)
|
||||
self.style.configure("Footer.TLabel", font=self.font_footer_tuple, foreground="#546E7A")
|
||||
|
||||
self.style.configure("TButton", font=self.font_button_tuple, padding=(12, 7), relief="raised")
|
||||
self.style.map("TButton",
|
||||
foreground=[('disabled', self.COLOR_DISABLED_FG),
|
||||
('pressed', self.COLOR_PRIMARY_TEXT),
|
||||
('active', self.COLOR_PRIMARY_TEXT),
|
||||
('!disabled', self.COLOR_PRIMARY_TEXT)],
|
||||
background=[('disabled', self.COLOR_DISABLED_BG),
|
||||
('pressed', self.COLOR_PRIMARY_DARK),
|
||||
('active', self.COLOR_PRIMARY_HOVER),
|
||||
('!disabled', self.COLOR_PRIMARY)],
|
||||
relief=[('pressed', 'sunken'), ('!pressed', 'raised')])
|
||||
|
||||
self.style.configure("TEntry", font=self.font_standard_tuple, fieldbackground=self.COLOR_SECONDARY_BG,
|
||||
relief="flat")
|
||||
self.style.map("TEntry",
|
||||
bordercolor=[('focus', self.COLOR_PRIMARY), ('!focus', self.COLOR_BORDER)],
|
||||
borderwidth=[('focus', 1), ('!focus', 1)],
|
||||
lightcolor=[('focus', self.COLOR_PRIMARY)])
|
||||
|
||||
self.style.configure("Horizontal.TScale", background=self.COLOR_BACKGROUND, troughcolor=self.COLOR_SECONDARY_BG)
|
||||
|
||||
# --- Data storage ---
|
||||
self.all_students = []
|
||||
self.remaining_students = []
|
||||
self.called_students = []
|
||||
self.absent_students = []
|
||||
self.current_student_info = None
|
||||
self.flash_timer = None
|
||||
self.stop_timer = None
|
||||
self.is_flashing = False
|
||||
self.file_path = tk.StringVar(value="尚未选择文件")
|
||||
self.flash_duration_var = tk.IntVar(value=1)
|
||||
self.flash_interval_var = tk.IntVar(value=25)
|
||||
|
||||
# --- UI Elements ---
|
||||
self.title_label = ttk.Label(master, text="杨昱幸老师的课堂点名系统", style="Title.TLabel")
|
||||
self.title_label.pack(pady=(25, 15))
|
||||
|
||||
self.counter_label = ttk.Label(master, text="已点名:0/0人 (总人数:0)", style="Counter.TLabel")
|
||||
self.counter_label.pack(pady=(0, 20))
|
||||
|
||||
self.name_display_border_frame = tk.Frame(master, background=self.COLOR_BORDER, relief="flat", bd=1)
|
||||
self.name_display_border_frame.pack(pady=10, padx=30, fill=tk.X)
|
||||
self.name_display = tk.Label(self.name_display_border_frame, text="请先加载名单", font=self.font_display_tuple,
|
||||
bg=self.COLOR_NAME_DISPLAY_DEFAULT_BG,
|
||||
fg=self.COLOR_NAME_DISPLAY_DEFAULT_FG,
|
||||
justify="center")
|
||||
self.name_display.pack(fill=tk.X, ipady=15, padx=1, pady=1)
|
||||
|
||||
flash_control_frame = ttk.Frame(master, style="TFrame")
|
||||
flash_control_frame.pack(pady=15, fill=tk.X, padx=30)
|
||||
|
||||
duration_frame = ttk.Frame(flash_control_frame) # Will inherit style from parent or "."
|
||||
duration_frame.pack(pady=8, anchor='center')
|
||||
ttk.Label(duration_frame, text="闪烁时间 (秒):").pack(side=tk.LEFT, padx=(0,10))
|
||||
self.duration_slider = ttk.Scale(duration_frame, from_=1, to=5, orient=tk.HORIZONTAL,
|
||||
variable=self.flash_duration_var, length=250,
|
||||
command=lambda v: self.flash_duration_var.set(int(float(v))),
|
||||
style="Horizontal.TScale")
|
||||
self.duration_slider.pack(side=tk.LEFT, padx=5)
|
||||
self.duration_label = ttk.Label(duration_frame, textvariable=self.flash_duration_var, width=3, style="Value.TLabel")
|
||||
self.duration_label.pack(side=tk.LEFT)
|
||||
self.duration_slider.config(state=tk.DISABLED)
|
||||
|
||||
interval_frame = ttk.Frame(flash_control_frame)
|
||||
interval_frame.pack(pady=8, anchor='center')
|
||||
ttk.Label(interval_frame, text="闪烁间隔 (毫秒):").pack(side=tk.LEFT, padx=(0,10))
|
||||
self.speed_interval_slider = ttk.Scale(interval_frame, from_=25, to=300, orient=tk.HORIZONTAL,
|
||||
variable=self.flash_interval_var, length=250,
|
||||
command=lambda v: self.flash_interval_var.set(int(float(v))),
|
||||
style="Horizontal.TScale")
|
||||
self.speed_interval_slider.pack(side=tk.LEFT, padx=5)
|
||||
self.interval_label = ttk.Label(interval_frame, textvariable=self.flash_interval_var, width=4, style="Value.TLabel")
|
||||
self.interval_label.pack(side=tk.LEFT)
|
||||
self.speed_interval_slider.config(state=tk.DISABLED)
|
||||
|
||||
# --- File Selection Area ---
|
||||
file_frame = ttk.Frame(master)
|
||||
file_frame.pack(pady=20, padx=30)
|
||||
ttk.Label(file_frame, text="学生名单 (学号,姓名.txt):").pack(side=tk.LEFT, padx=(0,10))
|
||||
self.file_entry = ttk.Entry(file_frame, textvariable=self.file_path, width=25, state='readonly')
|
||||
self.file_entry.pack(side=tk.LEFT, padx=5, ipady=2) # Small ipady for entry height consistency
|
||||
self.load_button = ttk.Button(file_frame, text="选择文件", command=self.load_file)
|
||||
self.load_button.pack(side=tk.LEFT, padx=(5,0))
|
||||
|
||||
# --- Control Button Area ---
|
||||
button_frame = ttk.Frame(master)
|
||||
button_frame.pack(pady=25)
|
||||
# Grid padx/pady for spacing between buttons
|
||||
self.start_button = ttk.Button(button_frame, text="开始点名", command=self.start_roll_call, state=tk.DISABLED)
|
||||
self.start_button.grid(row=0, column=0, padx=10, pady=8)
|
||||
self.mark_absent_button = ttk.Button(button_frame, text="标记未到场", command=self.mark_absent, state=tk.DISABLED)
|
||||
self.mark_absent_button.grid(row=0, column=1, padx=10, pady=8)
|
||||
self.export_button = ttk.Button(button_frame, text="导出未到场(CSV)", command=self.export_absent_list, state=tk.DISABLED)
|
||||
self.export_button.grid(row=1, column=0, padx=10, pady=8)
|
||||
self.clear_button = ttk.Button(button_frame, text="清空考勤数据", command=self.clear_data, state=tk.DISABLED)
|
||||
self.clear_button.grid(row=1, column=1, padx=10, pady=8)
|
||||
|
||||
# --- 底部版权信息 ---
|
||||
self.footer_label = ttk.Label(
|
||||
master,
|
||||
text="Ver. 2.2 @杨昱幸. All Rights Reserved.",
|
||||
style="Footer.TLabel",
|
||||
anchor='center'
|
||||
)
|
||||
self.footer_label.pack(side=tk.BOTTOM, fill=tk.X, pady=(15,10))
|
||||
|
||||
self.update_counter()
|
||||
|
||||
|
||||
# ... (load_file, _reset_data_core, etc. methods remain unchanged) ...
|
||||
def load_file(self):
|
||||
filepath = filedialog.askopenfilename(
|
||||
title="请选择学生名单文件 (学号,姓名)",
|
||||
filetypes=[("Text files", "*.txt")]
|
||||
)
|
||||
if not filepath: return
|
||||
|
||||
try:
|
||||
self._reset_data_full()
|
||||
self.file_path.set(os.path.basename(filepath))
|
||||
loaded_students = []
|
||||
invalid_lines = []
|
||||
line_num = 0
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line_num += 1
|
||||
original_line = line
|
||||
line = line.strip()
|
||||
if not line: continue
|
||||
parts = line.split(',', 1)
|
||||
if len(parts) == 2:
|
||||
student_id_str = parts[0].strip()
|
||||
name = parts[1].strip()
|
||||
if student_id_str and name:
|
||||
pinyin_list = pinyin(name, style=Style.NORMAL)
|
||||
pinyin_str = "".join([item[0] for item in pinyin_list])
|
||||
student_info = (student_id_str, name, pinyin_str)
|
||||
loaded_students.append(student_info)
|
||||
else: invalid_lines.append((line_num, "学号或姓名为空", original_line))
|
||||
else: invalid_lines.append((line_num, "格式错误", original_line))
|
||||
|
||||
if not loaded_students:
|
||||
messagebox.showwarning("警告", "文件未包含有效格式的学生信息 (学号,姓名)。")
|
||||
self._reset_ui_after_load_fail()
|
||||
return
|
||||
|
||||
self.all_students = loaded_students
|
||||
self.remaining_students = self.all_students[:]
|
||||
random.shuffle(self.remaining_students)
|
||||
|
||||
self.name_display.config(text="名单已加载,准备点名", fg=self.COLOR_NAME_DISPLAY_DEFAULT_FG, bg=self.COLOR_NAME_DISPLAY_DEFAULT_BG)
|
||||
self.master.update_idletasks()
|
||||
|
||||
self.update_counter()
|
||||
self.start_button.config(state=tk.NORMAL)
|
||||
self.clear_button.config(state=tk.NORMAL)
|
||||
self.export_button.config(state=tk.DISABLED)
|
||||
self.mark_absent_button.config(state=tk.DISABLED)
|
||||
self.duration_slider.config(state=tk.NORMAL)
|
||||
self.speed_interval_slider.config(state=tk.NORMAL)
|
||||
|
||||
info_msg = f"成功加载 {len(self.all_students)} 位学生!"
|
||||
if invalid_lines:
|
||||
info_msg += f"\n\n但有 {len(invalid_lines)} 行格式无效已被跳过:"
|
||||
for i, (ln, reason, orig) in enumerate(invalid_lines[:5]):
|
||||
info_msg += f"\n第{ln}行: {reason} -> '{orig.strip()}'"
|
||||
if len(invalid_lines) > 5: info_msg += "\n..."
|
||||
messagebox.showwarning("加载部分成功", info_msg)
|
||||
else:
|
||||
messagebox.showinfo("成功", info_msg)
|
||||
|
||||
except FileNotFoundError:
|
||||
messagebox.showerror("错误", f"文件未找到: {filepath}")
|
||||
self._reset_ui_after_load_fail()
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"加载文件时出错: {e}")
|
||||
self._reset_ui_after_load_fail()
|
||||
|
||||
def _reset_data_core(self):
|
||||
self.remaining_students = []
|
||||
self.called_students = []
|
||||
self.absent_students = []
|
||||
self.current_student_info = None
|
||||
if self.flash_timer: self.master.after_cancel(self.flash_timer); self.flash_timer = None
|
||||
if self.stop_timer: self.master.after_cancel(self.stop_timer); self.stop_timer = None
|
||||
self.is_flashing = False
|
||||
|
||||
def _reset_data_full(self):
|
||||
self._reset_data_core()
|
||||
self.all_students = []
|
||||
self.file_path.set("尚未选择文件")
|
||||
|
||||
def _reset_ui_after_load_fail(self):
|
||||
self.file_path.set("尚未选择文件")
|
||||
self.name_display.config(text="请先加载名单", fg=self.COLOR_NAME_DISPLAY_DEFAULT_FG, bg=self.COLOR_NAME_DISPLAY_DEFAULT_BG)
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
self.mark_absent_button.config(state=tk.DISABLED)
|
||||
self.export_button.config(state=tk.DISABLED)
|
||||
self.clear_button.config(state=tk.DISABLED)
|
||||
self.duration_slider.config(state=tk.DISABLED)
|
||||
self.speed_interval_slider.config(state=tk.DISABLED)
|
||||
self.update_counter()
|
||||
|
||||
def update_counter(self):
|
||||
total = len(self.all_students)
|
||||
called_count = len(self.called_students) + len(self.absent_students)
|
||||
self.counter_label.config(text=f"已点名:{called_count}/{total}人 (总人数:{total})")
|
||||
|
||||
def start_roll_call(self):
|
||||
if self.is_flashing: return
|
||||
if not self.remaining_students:
|
||||
self.name_display.config(text="所有学生已点完!", fg=self.COLOR_NAME_DISPLAY_SELECTED_FG, bg=self.COLOR_NAME_DISPLAY_SELECTED_BG)
|
||||
messagebox.showinfo("提示", "所有学生均已点名。")
|
||||
self.start_button.config(state=tk.DISABLED); self.mark_absent_button.config(state=tk.DISABLED)
|
||||
return
|
||||
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
self.load_button.config(state=tk.DISABLED)
|
||||
self.mark_absent_button.config(state=tk.DISABLED)
|
||||
self.clear_button.config(state=tk.DISABLED)
|
||||
self.export_button.config(state=tk.DISABLED)
|
||||
self.duration_slider.config(state=tk.DISABLED)
|
||||
self.speed_interval_slider.config(state=tk.DISABLED)
|
||||
|
||||
self.is_flashing = True
|
||||
self.name_display.config(bg=self.COLOR_NAME_DISPLAY_FLASH_BG, fg=self.COLOR_NAME_DISPLAY_FLASH_FG)
|
||||
self._flash_name()
|
||||
|
||||
duration_ms = self.flash_duration_var.get() * 1000
|
||||
self.stop_timer = self.master.after(duration_ms, self.stop_flashing)
|
||||
|
||||
def _flash_name(self):
|
||||
if not self.is_flashing or not self.remaining_students:
|
||||
if self.is_flashing:
|
||||
self.stop_flashing() # Cleanly stop if conditions change
|
||||
return
|
||||
|
||||
display_student = random.choice(self.remaining_students)
|
||||
display_text = f"{display_student[0]},{display_student[1]},{display_student[2]}"
|
||||
# Text color for flashing already set in start_roll_call
|
||||
self.name_display.config(text=display_text)
|
||||
|
||||
flash_interval_ms = self.flash_interval_var.get()
|
||||
self.flash_timer = self.master.after(flash_interval_ms, self._flash_name)
|
||||
|
||||
def stop_flashing(self):
|
||||
if not self.is_flashing: return # Already stopped or never started
|
||||
|
||||
if self.flash_timer:
|
||||
self.master.after_cancel(self.flash_timer)
|
||||
self.flash_timer = None
|
||||
# self.stop_timer is the timer for the duration of flashing.
|
||||
# If this method is called by that timer, self.stop_timer will be None automatically.
|
||||
# If called externally (e.g. by _flash_name), cancel it.
|
||||
if self.stop_timer:
|
||||
self.master.after_cancel(self.stop_timer)
|
||||
self.stop_timer = None
|
||||
|
||||
self.is_flashing = False
|
||||
self._select_final_student()
|
||||
|
||||
def _select_final_student(self):
|
||||
if self.is_flashing: # Should not happen if stop_flashing was called correctly
|
||||
self.is_flashing = False # Force stop
|
||||
if self.flash_timer: self.master.after_cancel(self.flash_timer); self.flash_timer = None
|
||||
if self.stop_timer: self.master.after_cancel(self.stop_timer); self.stop_timer = None
|
||||
|
||||
# Restore button states
|
||||
self.load_button.config(state=tk.NORMAL)
|
||||
self.clear_button.config(state=tk.NORMAL)
|
||||
can_enable_sliders = bool(self.all_students)
|
||||
self.duration_slider.config(state=tk.NORMAL if can_enable_sliders else tk.DISABLED)
|
||||
self.speed_interval_slider.config(state=tk.NORMAL if can_enable_sliders else tk.DISABLED)
|
||||
|
||||
if not self.remaining_students:
|
||||
self.name_display.config(text="所有学生已点完!", fg=self.COLOR_NAME_DISPLAY_SELECTED_FG, bg=self.COLOR_NAME_DISPLAY_SELECTED_BG)
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
self.mark_absent_button.config(state=tk.DISABLED)
|
||||
self.export_button.config(state=tk.NORMAL if self.absent_students else tk.DISABLED)
|
||||
return
|
||||
|
||||
self.current_student_info = self.remaining_students.pop(0)
|
||||
self.called_students.append(self.current_student_info)
|
||||
|
||||
display_text = f"{self.current_student_info[0]},{self.current_student_info[1]},{self.current_student_info[2]}"
|
||||
self.name_display.config(text=display_text, fg=self.COLOR_NAME_DISPLAY_SELECTED_FG, bg=self.COLOR_NAME_DISPLAY_SELECTED_BG)
|
||||
self.update_counter()
|
||||
|
||||
if self.remaining_students:
|
||||
self.start_button.config(state=tk.NORMAL)
|
||||
else: # No more students left to call
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
self.name_display.config(text=display_text + " (最后一位)", fg=self.COLOR_NAME_DISPLAY_SELECTED_FG, bg=self.COLOR_NAME_DISPLAY_SELECTED_BG)
|
||||
|
||||
self.mark_absent_button.config(state=tk.NORMAL)
|
||||
self.export_button.config(state=tk.NORMAL if self.absent_students else tk.DISABLED)
|
||||
|
||||
def mark_absent(self):
|
||||
if not self.current_student_info or self.is_flashing: return
|
||||
|
||||
if self.current_student_info in self.called_students: # Check if they were marked present first
|
||||
self.called_students.remove(self.current_student_info)
|
||||
if self.current_student_info not in self.absent_students: # Avoid duplicates
|
||||
self.absent_students.append(self.current_student_info)
|
||||
|
||||
self.update_counter() # Counter includes both called and absent
|
||||
display_text = f"{self.current_student_info[0]},{self.current_student_info[1]},{self.current_student_info[2]} [未到]"
|
||||
self.name_display.config(text=display_text, fg=self.COLOR_NAME_DISPLAY_ABSENT_FG, bg=self.COLOR_NAME_DISPLAY_ABSENT_BG)
|
||||
self.mark_absent_button.config(state=tk.DISABLED) # Prevent re-marking for current student
|
||||
self.export_button.config(state=tk.NORMAL) # Enable export since there's an absent student
|
||||
# else: Student might already be in absent_students or not selected, do nothing.
|
||||
|
||||
def export_absent_list(self):
|
||||
if not self.absent_students:
|
||||
messagebox.showinfo("提示", "当前没有未到场的学生记录。")
|
||||
return
|
||||
|
||||
today_str = date.today().strftime('%Y-%m-%d')
|
||||
default_filename = f"未到场名单_{today_str}.csv"
|
||||
|
||||
filepath = filedialog.asksaveasfilename(
|
||||
title="保存未到场名单 (CSV)",
|
||||
defaultextension=".csv",
|
||||
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
|
||||
initialfile=default_filename
|
||||
)
|
||||
if not filepath: return
|
||||
|
||||
try:
|
||||
sorted_absent = sorted(self.absent_students, key=lambda x: x[0]) # Sort by student ID
|
||||
with open(filepath, 'w', encoding='utf-8-sig', newline='') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(["学号", "姓名"]) # Header row
|
||||
for student in sorted_absent:
|
||||
writer.writerow([student[0], student[1]]) # Student ID and Name
|
||||
messagebox.showinfo("导出成功", f"未到场名单已成功导出为 CSV 文件:\n{filepath}")
|
||||
except Exception as e:
|
||||
messagebox.showerror("导出失败", f"导出文件时出错: {e}")
|
||||
|
||||
def clear_data(self):
|
||||
if not self.all_students: # No student list loaded
|
||||
messagebox.showinfo("提示", "尚未加载学生名单,无数据可清空。")
|
||||
return
|
||||
|
||||
if messagebox.askyesno("确认", "确定要清空当前考勤数据吗?\n这将重置点名状态,但学生名单会保留。"):
|
||||
self._reset_data_core() # Clears called, absent, current_student, timers
|
||||
|
||||
# Re-initialize remaining_students from all_students
|
||||
self.remaining_students = self.all_students[:]
|
||||
random.shuffle(self.remaining_students)
|
||||
|
||||
self.name_display.config(text="考勤数据已清空,可重新点名", fg=self.COLOR_NAME_DISPLAY_DEFAULT_FG, bg=self.COLOR_NAME_DISPLAY_DEFAULT_BG)
|
||||
|
||||
can_enable_controls = bool(self.all_students)
|
||||
self.start_button.config(state=tk.NORMAL if can_enable_controls else tk.DISABLED)
|
||||
self.mark_absent_button.config(state=tk.DISABLED) # No student selected yet
|
||||
self.export_button.config(state=tk.DISABLED) # Absent list is now empty
|
||||
self.clear_button.config(state=tk.NORMAL if can_enable_controls else tk.DISABLED)
|
||||
|
||||
self.duration_slider.config(state=tk.NORMAL if can_enable_controls else tk.DISABLED)
|
||||
self.speed_interval_slider.config(state=tk.NORMAL if can_enable_controls else tk.DISABLED)
|
||||
|
||||
self.update_counter()
|
||||
self.load_button.config(state=tk.NORMAL) # Load button should generally be active
|
||||
|
||||
|
||||
# --- Main program entry point ---
|
||||
if __name__ == "__main__":
|
||||
missing_libs = []
|
||||
try:
|
||||
import pypinyin
|
||||
except ImportError:
|
||||
missing_libs.append("pypinyin (`pip install pypinyin`)")
|
||||
try:
|
||||
import csv
|
||||
except ImportError: # csv is standard, but good practice if it were external
|
||||
missing_libs.append("csv (standard library module)")
|
||||
|
||||
if missing_libs:
|
||||
temp_root = tk.Tk()
|
||||
temp_root.withdraw()
|
||||
messagebox.showerror("依赖缺失", f"请先安装或确保以下库可用:\n" + "\n".join(missing_libs))
|
||||
temp_root.destroy()
|
||||
exit()
|
||||
|
||||
root = tk.Tk()
|
||||
app = RollCallApp(root)
|
||||
root.mainloop()
|
||||
Reference in New Issue
Block a user