[Python] 纯文本查看 复制代码
import tkinter as tk from tkinter import messagebox, ttk, filedialog import sqlite3 import random import uuid from datetime import datetime import logging import pandas as pd import pyttsx3 # 设置日志 logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') class EnglishLearningApp: def __init__(self, root): self.root = root self.root.title("儿童英语词汇学习软件V1.0~~~qzwsa 2025.04.28 ") self.conn = sqlite3.connect("vocabulary.db") self.create_tables() # 初始年级列表 - 不再硬编码,从数据库加载 self.default_grades = ["三年级", "四年级上册", "四年级下册", "五年级上册", "五年级下册", "六年级上册", "六年级下册"] self.valid_grades = self.load_grades_from_db() self.populate_vocabulary() self.lines = [] self.correct_pairs = [] self.current_test_data = [] self.test_score = 0 self.chinese_coords = [] self.english_coords = [] self.chinese_labels = [] self.english_labels = [] self.chinese_points = [] # 存储中文连接点 self.english_points = [] # 存储英文连接点 self.canvas_width = 600 # 初始画布宽度 self.canvas_height = 400 # 初始画布高度 self.min_canvas_width = 400 # 最小画布宽度 self.min_canvas_height = 300 # 最小画布高度 self.resizing = False # 调整大小标志 self.result_marks = [] # 存储答题结果标志(√ 或 ×) # 帮助文档内容 self.help_content = { "核心功能介绍": """儿童英语词汇学习软件 欢迎使用本软件!这是一款专为儿童设计的英语词汇学习工具,旨在帮助学生通过趣味连线测试学习和巩固英语词汇。软件适合小学三年级及以上学生使用,界面全中文,操作简单。 核心功能: 1. **连线测试**:通过连线将中文词汇与对应的英文词汇配对,测试词汇掌握情况。 2. **语音支持**:点击词汇前的圆点可播放中文或英文发音,帮助学习正确发音。 3. **词汇管理**:支持添加、删除和从文件导入词汇,方便定制学习内容。 4. **测试记录**:记录每次测试的成绩和答题详情,支持导出为 Excel 或 CSV 文件。 5. **画布调整**:可拖动画布右下角调整大小,适应不同屏幕。 本软件使用 SQLite 数据库存储词汇和测试记录,跨平台支持 Windows、Mac 和 Linux。""", "操作指南": """操作指南 1. 开始测试 - **选择年级**:在"年级"下拉菜单中选择年级(如"四年级下册")。 - **设置题目数量**:输入测试的词汇数量(默认 5)。 - **设置每题分数**:输入每题的分数(默认 10)。 - **点击"开始测试"**:进入测试界面,左侧显示中文词汇,右侧显示英文词汇。 2. 连线测试 - **播放发音**: - 点击中文词汇前的红色圆点,播放中文发音(如"友好的")。 - 点击英文词汇前的蓝色圆点,播放英文发音(如"nice")。 - **连线操作**: - 点击中文词汇前的红色圆点(播放中文发音),拖动鼠标到对应的英文词汇前的蓝色圆点,松开鼠标(播放英文发音)。 - 连线显示为蓝色,表示已连接。 - **提交答案**: - 连线完成后,点击"提交答案"。 - 正确答案以红色连线显示(稍向下偏移)。 - 每题左侧显示绿色"√"(正确)或红色"×"(错误)。 - 底部显示总得分(如"得分:30/50")。 3. 管理词汇 - 点击"管理词汇"按钮,打开词汇管理窗口。 - **添加词汇**: - 点击"添加词汇",输入年级、中文、英文,点击"保存"。 - 可以输入新的年级名称,系统会自动将其添加到年级列表中。 - **删除词汇**: - 选中词汇,点击"删除选中词汇"。 - **导入词汇**: - 点击"从文件导入",选择 Excel 或 CSV 文件。 - 文件需包含"年级"、"中文"、"英文"三列。 - 系统会自动识别新的年级,并添加到年级列表中。 4. 查看和导出测试记录 - 点击"查看测试记录",显示所有测试的列表(测试ID、时间、年级、题目数、得分、总分)。 - **查看详情**: - 双击某次测试,查看答题详情(中文、用户选择的英文、是否正确、正确英文)。 - **导出记录**: - 按住 Ctrl(Windows)或 Command(Mac)键选择多条测试记录。 - 点击"导出选中测试",选择保存为 Excel 或 CSV 文件。 5. 调整画布大小 - 在测试界面,拖动画布右下角的灰色小方块。 - 画布最小尺寸为 400x300,最大不超过窗口大小。 - 调整后,词汇和连线会自动适配新尺寸。""", "常见问题": """常见问题 1. 为什么点击圆点没有声音? - **可能原因**: - 语音功能初始化失败(启动时会显示警告)。 - 系统音量关闭或未安装语音包。 - Linux 系统使用的 espeak 中文发音质量较差。 - **解决方法**: - 检查系统音量,确保未静音。 - Windows:确保安装中文语音包(设置 > 时间和语言 > 语音)。 - Linux:安装 espeak(sudo apt-get install espeak)或尝试其他语音引擎(如 festival)。 - 重启软件,查看是否有"语音功能初始化失败"提示。 2. 为什么无法导入词汇? - **可能原因**: - 文件格式错误,缺少"年级"、"中文"、"英文"列。 - 文件编码不正确(如 CSV 文件使用 GBK 编码)。 - 词汇已存在。 - **解决方法**: - 确保文件为 Excel(.xlsx/.xls)或 CSV 格式,包含正确列名。 - CSV 文件建议使用 UTF-8 编码,若为 GBK,可联系管理员调整代码。 - 系统支持导入任意年级的词汇,年级名称会自动添加到下拉列表中。 3. 为什么画布调整后内容显示异常? - **可能原因**: - 调整过程中窗口大小变化。 - **解决方法**: - 调整画布后,松开鼠标,软件会自动重新绘制词汇和连线。 - 确保画布大小不小于 400x300。 4. 如何确保测试记录导出成功? - **可能原因**: - 未选择测试记录。 - 保存路径无效或权限不足。 - **解决方法**: - 选择至少一条测试记录(可多选)。 - 确保保存路径可写,选择常用文件夹(如桌面)。 - 检查导出的 Excel/CSV 文件是否包含"测试信息"和"测试详情"。 5. 为什么连线后得分不正确? - **可能原因**: - 连线错误(中文未连接到正确英文)。 - **解决方法**: - 提交答案后,检查红色连线(正确答案)和绿色"√"/红色"×"。 - 查看测试记录详情,确认每个词汇的正确英文。 6. 如果删除了某个年级的词汇,会影响历史记录吗? - **回答**:不会。历史记录保存的是测试时使用的年级文本,而不是引用年级列表。即使删除了某个年级的所有词汇,历史记录仍然可以正常显示。""" } # 初始化语音引擎 self.tts_engine = None try: self.tts_engine = pyttsx3.init() self.tts_engine.setProperty('rate', 200) # 语速 self.tts_engine.setProperty('volume', 1.0) # 音量 logging.info("语音引擎初始化成功") except Exception as e: messagebox.showwarning("警告", f"语音功能初始化失败:{str(e)}\n语音功能将不可用。") logging.error(f"语音引擎初始化失败:{str(e)}") self.setup_ui() def load_grades_from_db(self): """从数据库加载所有年级""" cursor = self.conn.cursor() cursor.execute("SELECT DISTINCT grade FROM vocabulary") grades = [row[0] for row in cursor.fetchall()] # 如果数据库为空,使用默认年级列表 if not grades: grades = self.default_grades return grades def update_grade_menu(self): """更新年级下拉菜单""" # 重新从数据库加载年级 self.valid_grades = self.load_grades_from_db() # 更新下拉菜单 current_value = self.grade_var.get() self.grade_menu['values'] = self.valid_grades # 尝试保持当前选择,如果不在列表中则选择第一个 if current_value in self.valid_grades: self.grade_var.set(current_value) elif self.valid_grades: self.grade_var.set(self.valid_grades[0]) def create_tables(self): cursor = self.conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS vocabulary ( id INTEGER PRIMARY KEY AUTOINCREMENT, grade TEXT, chinese TEXT, english TEXT ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS test_results ( test_id TEXT, timestamp TEXT, grade TEXT, question_count INTEGER, score INTEGER, total_score INTEGER ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS test_questions ( test_id TEXT, chinese TEXT, english TEXT, is_correct INTEGER ) ''') self.conn.commit() logging.info("数据库表创建完成") def populate_vocabulary(self): cursor = self.conn.cursor() cursor.execute("SELECT COUNT(*) FROM vocabulary") if cursor.fetchone()[0] == 0: vocabulary_data = [ ("四年级下册", "友好的", "nice"), ("四年级下册", "聪明的", "clever"), ("四年级下册", "淘气的", "naughty"), ("三年级", "我", "I"), ("三年级", "你好(嗨)", "hello(hi)"), ("三年级", "再见", "goodbye(bye - bye)"), # 添加一些其他年级的初始词汇 ("五年级上册", "科学", "science"), ("五年级上册", "计算机", "computer"), ("六年级下册", "未来", "future"), ("六年级下册", "宇宙", "universe"), ] cursor.executemany("INSERT INTO vocabulary (grade, chinese, english) VALUES (?, ?, ?)", vocabulary_data) self.conn.commit() logging.info("词汇表数据已初始化") def setup_ui(self): self.settings_frame = tk.Frame(self.root) self.settings_frame.pack(pady=10) tk.Label(self.settings_frame, text="年级:").grid(row=0, column=0) self.grade_var = tk.StringVar() grades = self.valid_grades self.grade_menu = ttk.Combobox(self.settings_frame, textvariable=self.grade_var, values=grades) self.grade_menu.grid(row=0, column=1) self.grade_menu.current(0) tk.Label(self.settings_frame, text="题目数量:").grid(row=0, column=2) self.question_count_var = tk.StringVar(value="5") tk.Entry(self.settings_frame, textvariable=self.question_count_var, width=5).grid(row=0, column=3) tk.Label(self.settings_frame, text="每题分数:").grid(row=0, column=4) self.points_var = tk.StringVar(value="10") tk.Entry(self.settings_frame, textvariable=self.points_var, width=5).grid(row=0, column=5) tk.Button(self.settings_frame, text="开始测试", command=self.start_test).grid(row=0, column=6, padx=10) tk.Button(self.settings_frame, text="管理词汇", command=self.open_vocabulary_manager).grid(row=0, column=7) tk.Button(self.settings_frame, text="查看测试记录", command=self.view_test_history).grid(row=0, column=8) # 添加帮助按钮 tk.Button(self.settings_frame, text="帮助", command=self.show_help).grid(row=0, column=9, padx=10) self.canvas_frame = tk.Frame(self.root) self.canvas_frame.pack(pady=10) self.canvas = tk.Canvas(self.canvas_frame, width=self.canvas_width, height=self.canvas_height, bg="white") self.canvas.pack() self.submit_button = tk.Button(self.root, text="提交答案", command=self.submit_test, state=tk.DISABLED) self.submit_button.pack(pady=10) self.result_label = tk.Label(self.root, text="") self.result_label.pack() # 绑定画布事件 self.canvas.bind("<Button-1>", self.start_line) self.canvas.bind("<B1-Motion>", self.draw_line) self.canvas.bind("<ButtonRelease-1>", self.end_line) # 绑定调整大小事件 self.canvas.bind("<Button-1>", self.start_resize, add="+") self.canvas.bind("<B1-Motion>", self.do_resize, add="+") self.canvas.bind("<ButtonRelease-1>", self.end_resize, add="+") # 绘制调整大小区域 self.draw_resize_handle() logging.info("界面初始化完成") def draw_resize_handle(self): # 在画布右下角绘制调整大小区域(10x10 像素矩形) self.canvas.delete("resize_handle") self.canvas.create_rectangle( self.canvas_width - 10, self.canvas_height - 10, self.canvas_width, self.canvas_height, fill="gray", tags="resize_handle" ) def start_resize(self, event): # 检查是否点击调整大小区域 if (self.canvas_width - 10 <= event.x <= self.canvas_width and self.canvas_height - 10 <= event.y <= self.canvas_height): self.resizing = True self.resize_start_x = event.x self.resize_start_y = event.y logging.debug(f"开始调整画布大小:起始坐标=({event.x}, {event.y})") # 在Windows中,阻止事件继续传播给其他处理函数 return "break" return None # 允许事件继续传播 def do_resize(self, event): if self.resizing: # 获取主窗口尺寸 max_width = self.root.winfo_width() - 20 # 减去边框估计值 max_height = self.root.winfo_height() - 100 # 减去标题栏和界面其他部分的估计值 # 计算新宽度和高度 new_width = max(self.min_canvas_width, min(max_width, event.x)) new_height = max(self.min_canvas_height, min(max_height, event.y)) # 更新画布大小 self.canvas.config(width=new_width, height=new_height) self.canvas_width = new_width self.canvas_height = new_height # 更新起始坐标以支持连续拖动 self.resize_start_x = event.x self.resize_start_y = event.y # 重新绘制调整大小区域 self.draw_resize_handle() # 如果测试正在进行,重新绘制内容 if self.current_test_data: self.redraw_canvas() logging.debug(f"调整画布大小:新尺寸=({new_width}, {new_height}), 最大尺寸=({max_width}, {max_height})") return "break" # 阻止事件继续传播 return None def end_resize(self, event): if self.resizing: self.resizing = False logging.debug("结束调整画布大小") return "break" # 阻止事件继续传播 return None def redraw_canvas(self): # 重新绘制画布内容(词汇、连接点、连线、答题结果标志) self.canvas.delete("all") self.chinese_points = [] self.english_points = [] self.chinese_labels = [] self.english_labels = [] self.result_marks = [] # 获取当前词汇 chinese_words = [ch for _, ch, _ in self.lines] if self.lines else [ch for ch, _ in self.current_test_data] english_words = [eng for _, _, eng in self.lines] if self.lines else [eng for _, eng in self.current_test_data] if not self.lines: random.shuffle(chinese_words) random.shuffle(english_words) # 动态计算间距 item_count = len(chinese_words) vertical_spacing = max(60, self.canvas_height // (item_count + 1)) # 绘制中文词汇和连接点 for i, word in enumerate(chinese_words): x, y = 70, 50 + i * vertical_spacing point_id = self.canvas.create_oval(x-10, y-10, x+10, y+10, fill="red") label_id = self.canvas.create_text(x+20, y, text=word, font=("Arial", 12), anchor="w") self.chinese_labels.append((label_id, word)) self.chinese_points.append((point_id, word, x, y)) # 绘制英文词汇和连接点 for i, word in enumerate(english_words): x, y = self.canvas_width - 70, 50 + i * vertical_spacing point_id = self.canvas.create_oval(x-10, y-10, x+10, y+10, fill="blue") label_id = self.canvas.create_text(x-20, y, text=word, font=("Arial", 12), anchor="e") self.english_labels.append((label_id, word)) self.english_points.append((point_id, word, x, y)) # 重新绘制用户连线 for line_id, chinese, english in self.lines: chinese_y = english_y = None for _, word, x, y in self.chinese_points: if word == chinese: chinese_y = y break for _, word, x, y in self.english_points: if word == english: english_y = y break if chinese_y and english_y: self.canvas.create_line(70, chinese_y, self.canvas_width - 70, english_y, fill="blue") # 重新绘制正确答案连线和答题结果标志(如果已提交) if not self.submit_button["state"] == tk.NORMAL: # 绘制正确答案连线 for chinese, english in self.correct_pairs: chinese_y = english_y = None for _, word, x, y in self.chinese_points: if word == chinese: chinese_y = y break for _, word, x, y in self.english_points: if word == english: english_y = y break if chinese_y and english_y: self.canvas.create_line(70, chinese_y + 2, self.canvas_width - 70, english_y + 2, fill="red", width=2) # 绘制答题结果标志 for _, chinese, english in self.lines: is_correct = (chinese, english) in self.correct_pairs chinese_y = None for _, word, x, y in self.chinese_points: if word == chinese: chinese_y = y break if chinese_y: mark = "√" if is_correct else "×" color = "green" if is_correct else "red" mark_id = self.canvas.create_text(50, chinese_y, text=mark, font=("Arial", 14, "bold"), fill=color) self.result_marks.append((mark_id, chinese, mark)) logging.debug(f"绘制答题结果:{chinese} -> {mark} ({color})") # 重新绘制调整大小区域 self.draw_resize_handle() def speak(self, text, is_english=False): """播放指定文本的语音,is_english=True 时使用英文发音""" if not self.tts_engine: logging.warning("语音引擎未初始化,跳过语音播放") return try: # 设置语言 voices = self.tts_engine.getProperty('voices') selected_voice = None for voice in voices: if is_english and 'english' in voice.name.lower(): selected_voice = voice.id break elif not is_english and ('chinese' in voice.name.lower() or 'zh' in voice.name.lower()): selected_voice = voice.id break if selected_voice: self.tts_engine.setProperty('voice', selected_voice) else: logging.warning(f"未找到{'英文' if is_english else '中文'}语音,尝试使用默认语音") # 播放语音 self.tts_engine.say(text) self.tts_engine.runAndWait() logging.debug(f"语音播放:文本={text}, 语言={'英文' if is_english else '中文'}") except Exception as e: logging.error(f"语音播放失败:文本={text}, 错误={str(e)}") def play_word_sound(self, word, is_english=False): """播放词汇的发音""" self.speak(word, is_english=is_english) def start_test(self): try: question_count = int(self.question_count_var.get()) points_per_question = int(self.points_var.get()) grade = self.grade_var.get() except ValueError: messagebox.showerror("错误", "请输入有效的题目数量和每题分数。") return cursor = self.conn.cursor() cursor.execute("SELECT chinese, english FROM vocabulary WHERE grade = ?", (grade,)) words = cursor.fetchall() if len(words) < question_count: messagebox.showerror("错误", f"{grade}的词汇量不足以生成{question_count}道题目。") return self.current_test_data = random.sample(words, question_count) self.correct_pairs = [(ch, eng) for ch, eng in self.current_test_data] self.test_score = 0 self.points_per_question = points_per_question self.test_id = str(uuid.uuid4()) self.lines = [] self.chinese_coords = [] self.english_coords = [] self.chinese_labels = [] self.english_labels = [] self.chinese_points = [] self.english_points = [] self.result_marks = [] self.canvas.delete("all") self.submit_button.config(state=tk.NORMAL) self.result_label.config(text="") logging.info(f"开始测试:年级={grade},题目数量={question_count}") # 打乱中文和英文词汇 chinese_words = [ch for ch, eng in self.current_test_data] english_words = [eng for ch, eng in self.current_test_data] random.shuffle(chinese_words) random.shuffle(english_words) logging.info(f"中文词汇顺序: {chinese_words}") logging.info(f"英文词汇顺序: {english_words}") # 动态计算间距 item_count = len(chinese_words) vertical_spacing = max(60, self.canvas_height // (item_count + 1)) # 在画布左侧放置中文词汇和连接点 for i, word in enumerate(chinese_words): x, y = 70, 50 + i * vertical_spacing point_id = self.canvas.create_oval(x-10, y-10, x+10, y+10, fill="red") label_id = self.canvas.create_text(x+20, y, text=word, font=("Arial", 12), anchor="w") self.chinese_labels.append((label_id, word)) self.chinese_points.append((point_id, word, x, y)) # 在画布右侧放置英文词汇和连接点 for i, word in enumerate(english_words): x, y = self.canvas_width - 70, 50 + i * vertical_spacing point_id = self.canvas.create_oval(x-10, y-10, x+10, y+10, fill="blue") label_id = self.canvas.create_text(x-20, y, text=word, font=("Arial", 12), anchor="e") self.english_labels.append((label_id, word)) self.english_points.append((point_id, word, x, y)) # 绘制调整大小区域 self.draw_resize_handle() def start_line(self, event): # 检查是否点击调整大小区域,如果是则不处理连线 if (self.canvas_width - 10 <= event.x <= self.canvas_width and self.canvas_height - 10 <= event.y <= self.canvas_height): # 由调整大小处理程序处理 return None # 如果正在调整大小,不启动连线 if hasattr(self, 'resizing') and self.resizing: return None # 检查是否有测试在进行 if not self.chinese_points or not self.english_points: logging.warning("未初始化连接点,可能是未开始测试") return None # 防止同时存在多条连线 self.canvas.delete("temp_line") self.current_line = None # 标记是否找到了起点 found_start_point = False # 检查是否点击中文圆点 for i, (point_id, word, x, y) in enumerate(self.chinese_points): if abs(event.x - x) < 15 and abs(event.y - y) < 15: # 播放中文发音 self.play_word_sound(word, is_english=False) # 初始化连线 self.current_line = {"start": (x, y), "start_word": word, "start_index": i} logging.debug(f"开始连线:起点词汇={word}, 坐标=({x}, {y})") found_start_point = True break # 如果没有找到中文起点,检查是否点击英文圆点(仅播放语音) if not found_start_point: for _, word, x, y in self.english_points: if abs(event.x - x) < 15 and abs(event.y - y) < 15: # 仅播放英文发音,不初始化连线 self.play_word_sound(word, is_english=True) logging.debug(f"单独点击英文词汇:{word}, 坐标=({x}, {y})") break def draw_line(self, event): # 如果正在调整大小或没有当前线条,不绘制连线 if (hasattr(self, 'resizing') and self.resizing) or not self.current_line: return None # 清除先前的临时线条并绘制新的临时线条 self.canvas.delete("temp_line") self.canvas.create_line( self.current_line["start"][0], self.current_line["start"][1], event.x, event.y, tags="temp_line", fill="blue", width=2 ) def end_line(self, event): # 如果正在调整大小或没有当前线条,不结束连线 if (hasattr(self, 'resizing') and self.resizing) or not self.current_line: return None # 清除临时线条 self.canvas.delete("temp_line") # 标记是否找到了终点 found_end_point = False # 检查是否在英文圆点上释放鼠标 for i, (_, word, x, y) in enumerate(self.english_points): if abs(event.x - x) < 15 and abs(event.y - y) < 15: # 播放英文发音 self.play_word_sound(word, is_english=True) start_word = self.current_line["start_word"] # 检查是否已经存在相同起点的连线,如果有则删除 lines_to_remove = [] for j, (line_id, ch, _) in enumerate(self.lines): if ch == start_word: self.canvas.delete(line_id) lines_to_remove.append(j) # 从后往前删除,避免索引错误 for j in sorted(lines_to_remove, reverse=True): self.lines.pop(j) # 创建新连线 line_id = self.canvas.create_line( self.current_line["start"][0], self.current_line["start"][1], x, y, fill="blue", width=2 ) self.lines.append((line_id, start_word, word)) logging.debug(f"连线完成:{start_word} -> {word}") found_end_point = True break # 无论是否找到终点,都重置当前线条 self.current_line = None def submit_test(self): self.test_score = 0 test_questions = [] for _, chinese, english in self.lines: is_correct = (chinese, english) in self.correct_pairs if is_correct: self.test_score += self.points_per_question test_questions.append((self.test_id, chinese, english, 1 if is_correct else 0)) # 保存测试结果 cursor = self.conn.cursor() cursor.execute(''' INSERT INTO test_results (test_id, timestamp, grade, question_count, score, total_score) VALUES (?, ?, ?, ?, ?, ?) ''', ( self.test_id, datetime.now().strftime("%Y-%m-%d %H:%M:%S"), self.grade_var.get(), len(self.current_test_data), self.test_score, len(self.current_test_data) * self.points_per_question )) cursor.executemany(''' INSERT INTO test_questions (test_id, chinese, english, is_correct) VALUES (?, ?, ?, ?) ''', test_questions) self.conn.commit() logging.info(f"测试提交:得分={self.test_score},总分={len(self.current_test_data) * self.points_per_question}") logging.info(f"用户连线记录: {[(ch, eng, is_correct) for _, ch, eng in self.lines]}") # 显示正确答案连线和答题结果标志 for i, (chinese, english) in enumerate(self.correct_pairs): chinese_y = english_y = None for _, word, x, y in self.chinese_points: if word == chinese: chinese_y = y break for _, word, x, y in self.english_points: if word == english: english_y = y break if chinese_y and english_y: x1, y1 = 70, chinese_y x2, y2 = self.canvas_width - 70, english_y self.canvas.create_line(x1, y1+2, x2, y2+2, fill="red", width=2) # 绘制答题结果标志 for _, chinese, english in self.lines: is_correct = (chinese, english) in self.correct_pairs chinese_y = None for _, word, x, y in self.chinese_points: if word == chinese: chinese_y = y break if chinese_y: mark = "√" if is_correct else "×" color = "green" if is_correct else "red" mark_id = self.canvas.create_text(50, chinese_y, text=mark, font=("Arial", 14, "bold"), fill=color) self.result_marks.append((mark_id, chinese, mark)) logging.debug(f"绘制答题结果:{chinese} -> {mark} ({color})") self.result_label.config(text=f"得分:{self.test_score}/{len(self.current_test_data) * self.points_per_question}") self.submit_button.config(state=tk.DISABLED) self.draw_resize_handle() def open_vocabulary_manager(self): vocab_window = tk.Toplevel(self.root) vocab_window.title("管理词汇") vocab_window.geometry("600x400") tree = ttk.Treeview(vocab_window, columns=("ID", "年级", "中文", "英文"), show="headings") tree.heading("ID", text="ID") tree.heading("年级", text="年级") tree.heading("中文", text="中文") tree.heading("英文", text="英文") tree.pack(fill="both", expand=True) cursor = self.conn.cursor() cursor.execute("SELECT id, grade, chinese, english FROM vocabulary") for row in cursor.fetchall(): tree.insert("", "end", values=row) def add_vocab(): add_window = tk.Toplevel(vocab_window) add_window.title("添加词汇") tk.Label(add_window, text="年级:").grid(row=0, column=0, sticky="w", padx=5, pady=5) # 使用组合框,但允许用户输入自定义值 grade_var = tk.StringVar() grade_combo = ttk.Combobox(add_window, textvariable=grade_var, values=self.valid_grades) grade_combo.grid(row=0, column=1, padx=5, pady=5) tk.Label(add_window, text="中文:").grid(row=1, column=0, sticky="w", padx=5, pady=5) chinese_var = tk.StringVar() tk.Entry(add_window, textvariable=chinese_var).grid(row=1, column=1, padx=5, pady=5) tk.Label(add_window, text="英文:").grid(row=2, column=0, sticky="w", padx=5, pady=5) english_var = tk.StringVar() tk.Entry(add_window, textvariable=english_var).grid(row=2, column=1, padx=5, pady=5) def save(): grade = grade_var.get().strip() chinese = chinese_var.get().strip() english = english_var.get().strip() if not grade: messagebox.showerror("错误", "请输入年级。") return if not chinese or not english: messagebox.showerror("错误", "中文和英文不能为空。") return cursor.execute("SELECT COUNT(*) FROM vocabulary WHERE grade = ? AND chinese = ? AND english = ?", (grade, chinese, english)) if cursor.fetchone()[0] > 0: messagebox.showwarning("警告", "该词汇已存在。") return cursor.execute("INSERT INTO vocabulary (grade, chinese, english) VALUES (?, ?, ?)", (grade, chinese, english)) self.conn.commit() tree.insert("", "end", values=(cursor.lastrowid, grade, chinese, english)) add_window.destroy() # 检查是否是新年级,如果是则更新年级列表 if grade not in self.valid_grades: self.valid_grades.append(grade) self.update_grade_menu() logging.info(f"添加词汇:{grade} - {chinese} - {english}") tk.Button(add_window, text="保存", command=save).grid(row=3, column=0, columnspan=2, pady=10) # 居中显示窗口 add_window.update_idletasks() width = add_window.winfo_width() height = add_window.winfo_height() x = (add_window.winfo_screenwidth() // 2) - (width // 2) y = (add_window.winfo_screenheight() // 2) - (height // 2) add_window.geometry(f"+{x}+{y}") def delete_vocab(): selected = tree.selection() if selected: item = tree.item(selected[0]) vocab_id = item["values"][0] cursor.execute("DELETE FROM vocabulary WHERE id = ?", (vocab_id,)) self.conn.commit() tree.delete(selected[0]) logging.info(f"删除词汇:ID={vocab_id}") # 更新年级列表(如果某个年级没有词汇了,应该从列表中移除) self.update_grade_menu() def import_vocab(): file_path = filedialog.askopenfilename( filetypes=[("Excel 文件", "*.xlsx *.xls"), ("CSV 文件", "*.csv"), ("所有文件", "*.*")] ) if not file_path: return try: # 读取文件 if file_path.endswith('.csv'): df = pd.read_csv(file_path, encoding='utf-8', encoding_errors='ignore') else: df = pd.read_excel(file_path) # 规范化列名 df.columns = df.columns.str.strip().str.lower() # 映射可能的列名 column_map = { 'grade': ['grade', '年级', '年级名称'], 'chinese': ['chinese', '中文', '中文词汇'], 'english': ['english', '英文', '英文词汇'] } required_columns = {} for key, aliases in column_map.items(): for alias in aliases: if alias in df.columns: required_columns[key] = alias break if key not in required_columns: messagebox.showerror("错误", f"文件缺少必需列:{key}") return # 提取数据 data = df[[required_columns['grade'], required_columns['chinese'], required_columns['english']]].copy() data.columns = ['grade', 'chinese', 'english'] # 数据验证 errors = [] valid_data = [] new_grades = set() for i, row in data.iterrows(): grade = str(row['grade']).strip() chinese = str(row['chinese']).strip() english = str(row['english']).strip() if not grade: errors.append(f"第{i+2}行:年级为空") continue if not chinese: errors.append(f"第{i+2}行:中文为空") continue if not english: errors.append(f"第{i+2}行:英文为空") continue # 检查重复 cursor.execute("SELECT COUNT(*) FROM vocabulary WHERE grade = ? AND chinese = ? AND english = ?", (grade, chinese, english)) if cursor.fetchone()[0] > 0: logging.info(f"跳过重复词汇:{grade} - {chinese} - {english}") continue valid_data.append((grade, chinese, english)) # 记录新年级 if grade not in self.valid_grades: new_grades.add(grade) # 插入数据库 if valid_data: cursor.executemany("INSERT INTO vocabulary (grade, chinese, english) VALUES (?, ?, ?)", valid_data) self.conn.commit() # 刷新词汇列表 for row in valid_data: cursor.execute("SELECT id FROM vocabulary WHERE grade = ? AND chinese = ? AND english = ?", row) vocab_id = cursor.fetchone()[0] tree.insert("", "end", values=(vocab_id, *row)) # 更新年级列表 if new_grades: self.valid_grades.extend(new_grades) self.update_grade_menu() messagebox.showinfo("成功", f"成功导入 {len(valid_data)} 条词汇,新增 {len(new_grades)} 个年级。") logging.info(f"从文件 {file_path} 导入 {len(valid_data)} 条词汇,新增年级:{new_grades}") else: messagebox.showwarning("警告", "没有有效的新词汇可导入。") # 显示错误 if errors: messagebox.showerror("导入错误", "\n".join(errors[:5]) + ("\n..." if len(errors) > 5 else "")) except Exception as e: messagebox.showerror("错误", f"导入失败:{str(e)}") logging.error(f"导入文件 {file_path} 失败:{str(e)}") button_frame = tk.Frame(vocab_window) button_frame.pack(side="left", padx=5, pady=5) tk.Button(button_frame, text="添加词汇", command=add_vocab).pack(side="left", padx=5) tk.Button(button_frame, text="删除选中词汇", command=delete_vocab).pack(side="left", padx=5) tk.Button(button_frame, text="从文件导入", command=import_vocab).pack(side="left", padx=5) def view_test_history(self): history_window = tk.Toplevel(self.root) history_window.title("测试记录") history_window.geometry("600x400") tree = ttk.Treeview(history_window, columns=("测试ID", "时间", "年级", "题目数", "得分", "总分"), show="headings") tree.heading("测试ID", text="测试ID") tree.heading("时间", text="时间") tree.heading("年级", text="年级") tree.heading("题目数", text="题目数") tree.heading("得分", text="得分") tree.heading("总分", text="总分") tree.pack(fill="both", expand=True) cursor = self.conn.cursor() cursor.execute("SELECT test_id, timestamp, grade, question_count, score, total_score FROM test_results") for row in cursor.fetchall(): tree.insert("", "end", values=row) def show_details(event): selected = tree.selection() if selected: test_id = tree.item(selected[0])["values"][0] grade = tree.item(selected[0])["values"][2] details_window = tk.Toplevel(history_window) details_window.title("测试详情") details_tree = ttk.Treeview(details_window, columns=("中文", "用户选择的英文", "是否正确", "正确英文"), show="headings") details_tree.heading("中文", text="中文") details_tree.heading("用户选择的英文", text="用户选择的英文") details_tree.heading("是否正确", text="是否正确") details_tree.heading("正确英文", text="正确英文") details_tree.column("中文", width=100) details_tree.column("用户选择的英文", width=100) details_tree.column("是否正确", width=80) details_tree.column("正确英文", width=100) details_tree.pack(fill="both", expand=True) # 获取用户连线记录 cursor.execute("SELECT chinese, english, is_correct FROM test_questions WHERE test_id = ?", (test_id,)) user_answers = cursor.fetchall() # 获取正确答案 cursor.execute("SELECT chinese, english FROM vocabulary WHERE grade = ? AND chinese IN (SELECT chinese FROM test_questions WHERE test_id = ?)", (grade, test_id)) correct_pairs = {ch: eng for ch, eng in cursor.fetchall()} # 填充详情表格 for chinese, user_english, is_correct in user_answers: correct_english = correct_pairs.get(chinese, "未知") details_tree.insert("", "end", values=(chinese, user_english, "是" if is_correct else "否", correct_english)) logging.info(f"查看测试详情:test_id={test_id}, 用户答案={user_answers}") def export_test_results(): selected = tree.selection() if not selected: messagebox.showerror("错误", "请先选择至少一个测试记录。") return # 获取保存文件路径 file_path = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel 文件", "*.xlsx"), ("CSV 文件", "*.csv"), ("所有文件", "*.*")], title="保存测试结果" ) if not file_path: return try: test_info_data = [] details_data = [] test_ids = [] cursor = self.conn.cursor() # 遍历所有选中的测试记录 for item in selected: test_id = tree.item(item)["values"][0] grade = tree.item(item)["values"][2] test_ids.append(test_id) # 获取测试总体信息 cursor.execute("SELECT test_id, timestamp, grade, question_count, score, total_score FROM test_results WHERE test_id = ?", (test_id,)) test_info = cursor.fetchone() test_info_data.append({ '测试ID': test_info[0], '时间': test_info[1], '年级': test_info[2], '题目数': test_info[3], '得分': test_info[4], '总分': test_info[5] }) # 获取测试详情 cursor.execute("SELECT chinese, english, is_correct FROM test_questions WHERE test_id = ?", (test_id,)) user_answers = cursor.fetchall() cursor.execute("SELECT chinese, english FROM vocabulary WHERE grade = ? AND chinese IN (SELECT chinese FROM test_questions WHERE test_id = ?)", (grade, test_id)) correct_pairs = {ch: eng for ch, eng in cursor.fetchall()} for chinese, user_english, is_correct in user_answers: details_data.append({ '测试ID': test_info[0], '时间': test_info[1], '年级': test_info[2], '中文': chinese, '用户选择的英文': user_english, '是否正确': '是' if is_correct else '否', '正确英文': correct_pairs.get(chinese, '未知') }) # 创建 DataFrame test_info_df = pd.DataFrame(test_info_data) details_df = pd.DataFrame(details_data) # 导出文件 if file_path.endswith('.xlsx'): with pd.ExcelWriter(file_path, engine='openpyxl') as writer: test_info_df.to_excel(writer, sheet_name='测试信息', index=False) details_df.to_excel(writer, sheet_name='测试详情', index=False) else: # CSV test_info_df.to_csv(file_path, encoding='utf-8', index=False) with open(file_path, 'a', encoding='utf-8') as f: f.write('\n--- 测试详情 ---\n') details_df.to_csv(file_path, mode='a', encoding='utf-8', index=False) messagebox.showinfo("成功", f"成功导出 {len(test_ids)} 条测试记录到 {file_path}") logging.info(f"测试结果导出:test_ids={test_ids}, 文件={file_path}, 详情列={list(details_df.columns)}") except Exception as e: messagebox.showerror("错误", f"导出失败:{str(e)}") logging.error(f"导出测试结果失败:test_ids={test_ids}, 错误={str(e)}") tree.bind("<Double-1>", show_details) # 添加导出按钮 button_frame = tk.Frame(history_window) button_frame.pack(side="left", padx=5, pady=5) tk.Button(button_frame, text="导出选中测试", command=export_test_results).pack(side="left", padx=5) def show_help(self): """显示帮助窗口""" help_window = tk.Toplevel(self.root) help_window.title("帮助") help_window.geometry("800x600") help_window.minsize(600, 400) # 创建笔记本控件以显示不同的帮助页面 notebook = ttk.Notebook(help_window) notebook.pack(fill="both", expand=True, padx=10, pady=10) # 为每个帮助主题创建一个标签页 for title, content in self.help_content.items(): frame = ttk.Frame(notebook) notebook.add(frame, text=title) # 创建可滚动的文本区域 text_frame = ttk.Frame(frame) text_frame.pack(fill="both", expand=True) scrollbar = ttk.Scrollbar(text_frame) scrollbar.pack(side="right", fill="y") text = tk.Text(text_frame, wrap="word", font=("微软雅黑", 10), padx=10, pady=10) text.pack(side="left", fill="both", expand=True) # 连接滚动条和文本区域 scrollbar.config(command=text.yview) text.config(yscrollcommand=scrollbar.set) # 插入帮助内容 text.insert("1.0", content) # 设置为只读模式 text.config(state="disabled") # 添加关闭按钮 close_button = ttk.Button(help_window, text="关闭", command=help_window.destroy) close_button.pack(pady=10) # 居中显示窗口 help_window.update_idletasks() width = help_window.winfo_width() height = help_window.winfo_height() x = (help_window.winfo_screenwidth() // 2) - (width // 2) y = (help_window.winfo_screenheight() // 2) - (height // 2) help_window.geometry(f"{width}x{height}+{x}+{y}") logging.info("打开帮助窗口") def __del__(self): self.conn.close() if self.tts_engine: self.tts_engine.stop() logging.info("语音引擎已关闭") logging.info("数据库连接已关闭") if __name__ == "__main__": root = tk.Tk() app = EnglishLearningApp(root) root.mainloop()