调整拼音显示
This commit is contained in:
@@ -41,7 +41,6 @@ class RollCallApp:
|
||||
|
||||
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 = "华文中宋"
|
||||
@@ -50,11 +49,13 @@ class RollCallApp:
|
||||
self.font_size_standard = 14
|
||||
self.font_size_counter = 18
|
||||
self.font_size_display = 42
|
||||
self.font_size_pinyin_display = 28 # New font size for Pinyin
|
||||
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_pinyin_display_tuple = (self.font_family_display, self.font_size_pinyin_display, "bold") # New font tuple for Pinyin
|
||||
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)
|
||||
@@ -142,11 +143,25 @@ class RollCallApp:
|
||||
|
||||
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)
|
||||
|
||||
# Changed from tk.Label to tk.Text
|
||||
self.name_display = tk.Text(self.name_display_border_frame,
|
||||
# Default font for the widget, actual text parts will use tags
|
||||
font=self.font_display_tuple,
|
||||
bg=self.COLOR_NAME_DISPLAY_DEFAULT_BG,
|
||||
fg=self.COLOR_NAME_DISPLAY_DEFAULT_FG,
|
||||
wrap=tk.NONE, # Prevent wrapping for a single line display
|
||||
# height=1, # Let it auto-size based on font and pady
|
||||
relief="flat", bd=0,
|
||||
pady=15, # Internal padding, similar to ipady of Label
|
||||
tabs=("1c", "center") # Attempt to center, though tags are better
|
||||
)
|
||||
self.name_display.tag_configure("main_name", font=self.font_display_tuple)
|
||||
self.name_display.tag_configure("pinyin", font=self.font_pinyin_display_tuple)
|
||||
self.name_display.tag_configure("center", justify='center')
|
||||
self.name_display.pack(fill=tk.X, padx=1, pady=1) # Pack with small pad to show border
|
||||
self._set_name_display_text("请先加载名单", is_placeholder=True)
|
||||
# self.name_display.config(state=tk.DISABLED) # state set in _set_name_display_text
|
||||
|
||||
flash_control_frame = ttk.Frame(master, style="TFrame")
|
||||
flash_control_frame.pack(pady=15, fill=tk.X, padx=30)
|
||||
@@ -200,7 +215,7 @@ class RollCallApp:
|
||||
# --- 底部版权信息 ---
|
||||
self.footer_label = ttk.Label(
|
||||
master,
|
||||
text="Ver. 2.2 @杨昱幸. All Rights Reserved.",
|
||||
text="Ver. 2.3 @杨昱幸. All Rights Reserved.",
|
||||
style="Footer.TLabel",
|
||||
anchor='center'
|
||||
)
|
||||
@@ -208,8 +223,53 @@ class RollCallApp:
|
||||
|
||||
self.update_counter()
|
||||
|
||||
def _set_name_display_text(self, text_content, fg_color=None, bg_color=None, is_placeholder=False):
|
||||
self.name_display.config(state=tk.NORMAL)
|
||||
self.name_display.delete("1.0", tk.END)
|
||||
|
||||
current_bg_color = bg_color if bg_color is not None else self.COLOR_NAME_DISPLAY_DEFAULT_BG
|
||||
current_fg_color = fg_color if fg_color is not None else self.COLOR_NAME_DISPLAY_DEFAULT_FG
|
||||
|
||||
self.name_display.config(bg=current_bg_color)
|
||||
# Foreground for Text widget tags is better controlled by the tag itself if varied,
|
||||
# or can be set on the widget if uniform. Here, tags will control fg for main/pinyin.
|
||||
# For simplicity, let's assume main_name and pinyin will use the passed fg_color.
|
||||
self.name_display.tag_configure("main_name", font=self.font_display_tuple, foreground=current_fg_color)
|
||||
self.name_display.tag_configure("pinyin", font=self.font_pinyin_display_tuple, foreground=current_fg_color)
|
||||
# Suffix color will also be current_fg_color by using main_name tag.
|
||||
|
||||
if is_placeholder: # Simple string for placeholders
|
||||
self.name_display.insert(tk.END, str(text_content), ("center", "main_name"))
|
||||
else:
|
||||
# text_content is expected to be a tuple: (base_str, pinyin_str, suffix_str)
|
||||
base_str, pinyin_str_val, suffix_str = text_content
|
||||
self.name_display.insert(tk.END, base_str, ("center", "main_name"))
|
||||
self.name_display.insert(tk.END, pinyin_str_val, ("center", "pinyin"))
|
||||
if suffix_str:
|
||||
self.name_display.insert(tk.END, suffix_str, ("center", "main_name"))
|
||||
|
||||
# Calculate required height for one line of the largest font + padding
|
||||
# This is tricky. For now, rely on Text widget's auto-sizing with its internal pady.
|
||||
# The height of the Text widget should be fixed to one line of the largest font.
|
||||
# We might need to set a fixed height for self.name_display_border_frame or self.name_display itself.
|
||||
# One approach is to ensure self.name_display shows one line effectively.
|
||||
# If font_display_tuple size 42, linespace is X. pady=15. Total height=X+2*15.
|
||||
# Set Text widget height to 1 to ensure it only shows one line, line height determined by tallest char.
|
||||
# The `Text` widget's `height` parameter is in lines of its default font.
|
||||
# If we want to ensure it visually fits one line of mixed (taller) fonts, it's complex.
|
||||
# For now, we'll assume `wrap=tk.NONE` and `pady` manage this.
|
||||
# After some testing, Text widget default height of 10 lines is too much.
|
||||
# Setting height=1 for text widget:
|
||||
font_obj = tk.font.Font(family=self.font_family_display, size=self.font_size_display, weight="bold")
|
||||
linespace = font_obj.metrics("linespace")
|
||||
widget_height_pixels = linespace + 2 * self.name_display.cget("pady") # pady is internal padding
|
||||
# The Text widget's height is in lines of its *default* font. This is complex.
|
||||
# Simplest is to set height=1 to force single-line behavior and let Tk handle line height.
|
||||
self.name_display.config(height=1) # Try forcing to 1 line height.
|
||||
|
||||
self.name_display.config(state=tk.DISABLED)
|
||||
|
||||
|
||||
# ... (load_file, _reset_data_core, etc. methods remain unchanged) ...
|
||||
def load_file(self):
|
||||
filepath = filedialog.askopenfilename(
|
||||
title="请选择学生名单文件 (学号,姓名)",
|
||||
@@ -234,7 +294,7 @@ class RollCallApp:
|
||||
student_id_str = parts[0].strip()
|
||||
name = parts[1].strip()
|
||||
if student_id_str and name:
|
||||
pinyin_list = pinyin(name, style=Style.NORMAL)
|
||||
pinyin_list = pinyin(name, style=Style.TONE)
|
||||
pinyin_str = "".join([item[0] for item in pinyin_list])
|
||||
student_info = (student_id_str, name, pinyin_str)
|
||||
loaded_students.append(student_info)
|
||||
@@ -250,7 +310,10 @@ class RollCallApp:
|
||||
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._set_name_display_text("名单已加载,准备点名",
|
||||
fg_color=self.COLOR_NAME_DISPLAY_DEFAULT_FG,
|
||||
bg_color=self.COLOR_NAME_DISPLAY_DEFAULT_BG,
|
||||
is_placeholder=True)
|
||||
self.master.update_idletasks()
|
||||
|
||||
self.update_counter()
|
||||
@@ -294,7 +357,10 @@ class RollCallApp:
|
||||
|
||||
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._set_name_display_text("请先加载名单",
|
||||
fg_color=self.COLOR_NAME_DISPLAY_DEFAULT_FG,
|
||||
bg_color=self.COLOR_NAME_DISPLAY_DEFAULT_BG,
|
||||
is_placeholder=True)
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
self.mark_absent_button.config(state=tk.DISABLED)
|
||||
self.export_button.config(state=tk.DISABLED)
|
||||
@@ -311,7 +377,10 @@ class RollCallApp:
|
||||
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)
|
||||
self._set_name_display_text("所有学生已点完!",
|
||||
fg_color=self.COLOR_NAME_DISPLAY_SELECTED_FG,
|
||||
bg_color=self.COLOR_NAME_DISPLAY_SELECTED_BG,
|
||||
is_placeholder=True)
|
||||
messagebox.showinfo("提示", "所有学生均已点名。")
|
||||
self.start_button.config(state=tk.DISABLED); self.mark_absent_button.config(state=tk.DISABLED)
|
||||
return
|
||||
@@ -325,7 +394,7 @@ class RollCallApp:
|
||||
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)
|
||||
# Background and text color for flashing will be set by _flash_name via _set_name_display_text
|
||||
self._flash_name()
|
||||
|
||||
duration_ms = self.flash_duration_var.get() * 1000
|
||||
@@ -338,22 +407,22 @@ class RollCallApp:
|
||||
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)
|
||||
base_text = f"{display_student[0]},{display_student[1]},"
|
||||
pinyin_text = display_student[2]
|
||||
|
||||
self._set_name_display_text((base_text, pinyin_text, ""),
|
||||
fg_color=self.COLOR_NAME_DISPLAY_FLASH_FG,
|
||||
bg_color=self.COLOR_NAME_DISPLAY_FLASH_BG)
|
||||
|
||||
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 not self.is_flashing: return
|
||||
|
||||
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
|
||||
@@ -362,12 +431,11 @@ class RollCallApp:
|
||||
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.is_flashing:
|
||||
self.is_flashing = False
|
||||
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)
|
||||
@@ -375,7 +443,10 @@ class RollCallApp:
|
||||
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._set_name_display_text("所有学生已点完!",
|
||||
fg_color=self.COLOR_NAME_DISPLAY_SELECTED_FG,
|
||||
bg_color=self.COLOR_NAME_DISPLAY_SELECTED_BG,
|
||||
is_placeholder=True)
|
||||
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)
|
||||
@@ -384,33 +455,40 @@ class RollCallApp:
|
||||
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()
|
||||
base_text = f"{self.current_student_info[0]},{self.current_student_info[1]},"
|
||||
pinyin_text = self.current_student_info[2]
|
||||
suffix_text = ""
|
||||
|
||||
if self.remaining_students:
|
||||
self.start_button.config(state=tk.NORMAL)
|
||||
else: # No more students left to call
|
||||
if not self.remaining_students: # No more students left to call
|
||||
suffix_text = " (最后一位)"
|
||||
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)
|
||||
else:
|
||||
self.start_button.config(state=tk.NORMAL)
|
||||
|
||||
self._set_name_display_text((base_text, pinyin_text, suffix_text),
|
||||
fg_color=self.COLOR_NAME_DISPLAY_SELECTED_FG,
|
||||
bg_color=self.COLOR_NAME_DISPLAY_SELECTED_BG)
|
||||
self.update_counter()
|
||||
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
|
||||
if self.current_student_info in self.called_students:
|
||||
self.called_students.remove(self.current_student_info)
|
||||
if self.current_student_info not in self.absent_students: # Avoid duplicates
|
||||
if self.current_student_info not in self.absent_students:
|
||||
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.
|
||||
self.update_counter()
|
||||
base_text = f"{self.current_student_info[0]},{self.current_student_info[1]},"
|
||||
pinyin_text = self.current_student_info[2]
|
||||
suffix_text = " [未到]"
|
||||
self._set_name_display_text((base_text, pinyin_text, suffix_text),
|
||||
fg_color=self.COLOR_NAME_DISPLAY_ABSENT_FG,
|
||||
bg_color=self.COLOR_NAME_DISPLAY_ABSENT_BG)
|
||||
self.mark_absent_button.config(state=tk.DISABLED)
|
||||
self.export_button.config(state=tk.NORMAL)
|
||||
|
||||
def export_absent_list(self):
|
||||
if not self.absent_students:
|
||||
@@ -429,41 +507,43 @@ class RollCallApp:
|
||||
if not filepath: return
|
||||
|
||||
try:
|
||||
sorted_absent = sorted(self.absent_students, key=lambda x: x[0]) # Sort by student ID
|
||||
sorted_absent = sorted(self.absent_students, key=lambda x: x[0])
|
||||
with open(filepath, 'w', encoding='utf-8-sig', newline='') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(["学号", "姓名"]) # Header row
|
||||
writer.writerow(["学号", "姓名"])
|
||||
for student in sorted_absent:
|
||||
writer.writerow([student[0], student[1]]) # Student ID and Name
|
||||
writer.writerow([student[0], student[1]])
|
||||
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
|
||||
if not self.all_students:
|
||||
messagebox.showinfo("提示", "尚未加载学生名单,无数据可清空。")
|
||||
return
|
||||
|
||||
if messagebox.askyesno("确认", "确定要清空当前考勤数据吗?\n这将重置点名状态,但学生名单会保留。"):
|
||||
self._reset_data_core() # Clears called, absent, current_student, timers
|
||||
self._reset_data_core()
|
||||
|
||||
# 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)
|
||||
self._set_name_display_text("考勤数据已清空,可重新点名",
|
||||
fg_color=self.COLOR_NAME_DISPLAY_DEFAULT_FG,
|
||||
bg_color=self.COLOR_NAME_DISPLAY_DEFAULT_BG,
|
||||
is_placeholder=True)
|
||||
|
||||
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.mark_absent_button.config(state=tk.DISABLED)
|
||||
self.export_button.config(state=tk.DISABLED)
|
||||
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
|
||||
self.load_button.config(state=tk.NORMAL)
|
||||
|
||||
|
||||
# --- Main program entry point ---
|
||||
@@ -473,18 +553,23 @@ if __name__ == "__main__":
|
||||
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)")
|
||||
# csv is standard, no need to check typically
|
||||
# try:
|
||||
# import csv
|
||||
# except ImportError:
|
||||
# missing_libs.append("csv (standard library module)")
|
||||
|
||||
if missing_libs:
|
||||
temp_root = tk.Tk()
|
||||
temp_root.withdraw()
|
||||
# Temporary root for messagebox if main GUI cannot start
|
||||
temp_root_for_error = tk.Tk()
|
||||
temp_root_for_error.withdraw() # Hide the temp window
|
||||
messagebox.showerror("依赖缺失", f"请先安装或确保以下库可用:\n" + "\n".join(missing_libs))
|
||||
temp_root.destroy()
|
||||
exit()
|
||||
temp_root_for_error.destroy()
|
||||
sys.exit(1) # Use sys.exit for clarity
|
||||
|
||||
root = tk.Tk()
|
||||
# It's good practice to also import tk.font for metrics if needed, but here it's just for one line.
|
||||
import tkinter.font # Explicitly import for font metrics if used more extensively
|
||||
|
||||
app = RollCallApp(root)
|
||||
root.mainloop()
|
||||
Reference in New Issue
Block a user