"""元音单词跟读英文书写练习 - Python TTS GUI版本功能:TTS语音朗读、手写画布、自动书写动画、主题切换依赖:pip install pyttsx3"""import tkinter as tkfrom tkinter import ttk, Canvasimport pyttsx3import threadingimport time============ 单词数据 ============WORD_DATA = {"短元音 /æ/ 字母a": [{"word": "cat", "phonetic": "/kæt/", "cn": "猫", "vowel_pos": [1]},{"word": "bag", "phonetic": "/bæɡ/", "cn": "包", "vowel_pos": [1]},{"word": "map", "phonetic": "/mæp/", "cn": "地图", "vowel_pos": [1]},{"word": "fat", "phonetic": "/fæt/", "cn": "胖的", "vowel_pos": [1]},],"短元音 /e/ 字母e": [{"word": "pen", "phonetic": "/pen/", "cn": "钢笔", "vowel_pos": [1]},{"word": "bed", "phonetic": "/bed/", "cn": "床", "vowel_pos": [1]},{"word": "egg", "phonetic": "/eɡ/", "cn": "鸡蛋", "vowel_pos": [0]},{"word": "leg", "phonetic": "/leɡ/", "cn": "腿", "vowel_pos": [1]},],"短元音 /ɪ/ 字母i": [{"word": "big", "phonetic": "/bɪɡ/", "cn": "大的", "vowel_pos": [1]},{"word": "pig", "phonetic": "/pɪɡ/", "cn": "猪", "vowel_pos": [1]},{"word": "fish", "phonetic": "/fɪʃ/", "cn": "鱼", "vowel_pos": [1]},{"word": "milk", "phonetic": "/mɪlk/", "cn": "牛奶", "vowel_pos": [1]},],"短元音 /ɒ/ 字母o": [{"word": "dog", "phonetic": "/dɒɡ/", "cn": "狗", "vowel_pos": [1]},{"word": "box", "phonetic": "/bɒks/", "cn": "盒子", "vowel_pos": [1]},{"word": "hot", "phonetic": "/hɒt/", "cn": "热的", "vowel_pos": [1]},{"word": "fox", "phonetic": "/fɒks/", "cn": "狐狸", "vowel_pos": [1]},],"短元音 /ʌ/ 字母u": [{"word": "cup", "phonetic": "/kʌp/", "cn": "杯子", "vowel_pos": [1]},{"word": "bus", "phonetic": "/bʌs/", "cn": "公交车", "vowel_pos": [1]},{"word": "sun", "phonetic": "/sʌn/", "cn": "太阳", "vowel_pos": [1]},{"word": "duck", "phonetic": "/dʌk/", "cn": "鸭子", "vowel_pos": [1]},],}============ 主题配色 ============THEMES = {"浅色": {"bg": "#f8fafc", "fg": "#1e293b","card_bg": "#ffffff", "card_border": "#e2e8f0","btn_read": "#2563eb", "btn_write": "#10b981","vowel_color": "#e53935",},"护眼绿": {"bg": "#f0fdf4", "fg": "#065f46","card_bg": "#ecfdf5", "card_border": "#a7f3d0","btn_read": "#059669", "btn_write": "#4f46e5","vowel_color": "#e53935",},"深色": {"bg": "#0f172a", "fg": "#f1f5f9","card_bg": "#1e293b", "card_border": "#475569","btn_read": "#3b82f6", "btn_write": "#22c55e","vowel_color": "#ff6b6b",},}自动书写动画颜色AUTO_DRAW_COLORS = ['#e74c3c', '#f39c12', '#27ae60', '#2980b9', '#8e44ad', '#1abc9c']class TTSEngine:"""TTS语音引擎封装,支持异步朗读"""def __init__(self): self._lock = threading.Lock() self._speaking = Falsedef speak(self, text): """每次朗读创建新引擎实例,避免 runAndWait 死锁""" if self._speaking: return def _run(): self._speaking = True try: engine = pyttsx3.init() engine.setProperty('rate', 140) engine.setProperty('volume', 1.0) # 尝试设置英文语音 voices = engine.getProperty('voices') for voice in voices: if 'english' in voice.name.lower() or 'en' in voice.languages: engine.setProperty('voice', voice.id) break engine.say(text) engine.runAndWait() engine.stop() except Exception: pass finally: self._speaking = False threading.Thread(target=_run, daemon=True).start()class WritingDialog(tk.Toplevel):"""手写练习弹窗 - 含画布与自动书写动画"""def __init__(self, parent, word, theme): super().__init__(parent) self.title("英文单词手写练习") self.word = word self.theme = theme self.geometry("520x480") self.resizable(False, False) self.configure(bg=theme["bg"]) self.transient(parent) self.grab_set() self.line_width = 4 self.drawing = False self.last_x = 0 self.last_y = 0 self.auto_drawing = False self._build_ui()def _build_ui(self): t = self.theme # 标题 tk.Label(self, text="英文单词手写练习", font=("Arial", 16, "bold"), bg=t["bg"], fg=t["fg"]).pack(pady=(12, 4)) # 目标单词 tk.Label(self, text=self.word, font=("Arial", 32, "bold"), bg=t["bg"], fg=t["btn_read"]).pack(pady=(0, 8)) # 画布 self.canvas = Canvas(self, width=480, height=260, bg="white", bd=2, relief="groove", cursor="pencil") self.canvas.pack(padx=16, pady=4) self.canvas.bind("<Button-1>", self._start_draw) self.canvas.bind("<B1-Motion>", self._draw_move) self.canvas.bind("<ButtonRelease-1>", self._stop_draw) # 工具栏 toolbar = tk.Frame(self, bg=t["bg"]) toolbar.pack(pady=8) btn_style = {"font": ("Arial", 11), "width": 10, "cursor": "hand2", "bd": 0} tk.Button(toolbar, text="自动书写", bg="#c0392b", fg="white", command=self._auto_draw, **btn_style).pack(side="left", padx=4) tk.Button(toolbar, text="清空画布", bg="#333333", fg="white", command=self._clear, **btn_style).pack(side="left", padx=4) tk.Button(toolbar, text="细笔", bg="#333333", fg="white", command=lambda: self._set_width(3), **btn_style).pack(side="left", padx=4) tk.Button(toolbar, text="粗笔", bg="#333333", fg="white", command=lambda: self._set_width(6), **btn_style).pack(side="left", padx=4) # 关闭按钮 tk.Button(self, text="关闭", bg="#666666", fg="white", font=("Arial", 12), width=20, command=self.destroy, cursor="hand2", bd=0).pack(pady=(8, 12))def _start_draw(self, event): self.drawing = True self.auto_drawing = False self.last_x = event.x self.last_y = event.ydef _draw_move(self, event): if not self.drawing: return self.canvas.create_line(self.last_x, self.last_y, event.x, event.y, width=self.line_width, fill="black", capstyle="round", joinstyle="round") self.last_x = event.x self.last_y = event.ydef _stop_draw(self, event): self.drawing = Falsedef _clear(self): self.canvas.delete("all") self.auto_drawing = Falsedef _set_width(self, w): self.line_width = wdef _auto_draw(self): """自动书写动画 - 逐字母渐显""" if self.auto_drawing: return self.auto_drawing = True self._clear() self.auto_drawing = True # _clear会重置,需重新设定 def _animate(): start_x = 40 base_y = 150 gap = 70 font_size = 48 for i, char in enumerate(self.word): if not self.auto_drawing: break color = AUTO_DRAW_COLORS[i % len(AUTO_DRAW_COLORS)] # 渐显动画:逐步增加字母透明度(用不同灰度模拟) for step in range(1, 11): if not self.auto_drawing: break tag = f"letter_{i}" self.canvas.delete(tag) # 模拟渐显:字体从浅到深 alpha = int(255 * step / 10) self.canvas.create_text( start_x + i * gap, base_y, text=char, font=("Arial", font_size, "bold"), fill=color, tags=tag ) time.sleep(0.03) time.sleep(0.2) self.auto_drawing = False threading.Thread(target=_animate, daemon=True).start()class VowelTTSApp:"""主应用程序"""def __init__(self): self.root = tk.Tk() self.root.title("元音单词跟读英文书写练习") self.root.geometry("900x700") self.root.minsize(700, 500) self.current_theme = "浅色" self.columns = 4 self.tts = TTSEngine() self._build_header() self._build_content() self._apply_theme()def _build_header(self): """顶部标题栏与设置""" self.header = tk.Frame(self.root) self.header.pack(fill="x", padx=16, pady=(12, 8)) tk.Label(self.header, text="元音单词跟读英文书写", font=("Arial", 20, "bold")).pack(side="left") settings = tk.Frame(self.header) settings.pack(side="right") # 主题切换 tk.Label(settings, text="主题:", font=("Arial", 11)).pack(side="left") self.theme_var = tk.StringVar(value="浅色") theme_combo = ttk.Combobox(settings, textvariable=self.theme_var, values=list(THEMES.keys()), width=8, state="readonly") theme_combo.pack(side="left", padx=(0, 12)) theme_combo.bind("<<ComboboxSelected>>", self._on_theme_change) # 列数切换 tk.Label(settings, text="每行:", font=("Arial", 11)).pack(side="left") self.col_var = tk.StringVar(value="4") col_combo = ttk.Combobox(settings, textvariable=self.col_var, values=["2", "3", "4"], width=4, state="readonly") col_combo.pack(side="left") col_combo.bind("<<ComboboxSelected>>", self._on_col_change)def _build_content(self): """构建可滚动的单词卡片区域""" # 滚动容器 container = tk.Frame(self.root) container.pack(fill="both", expand=True, padx=8, pady=4) self.scroll_canvas = Canvas(container, highlightthickness=0) scrollbar = ttk.Scrollbar(container, orient="vertical", command=self.scroll_canvas.yview) self.scroll_frame = tk.Frame(self.scroll_canvas) self.scroll_frame.bind("<Configure>", lambda e: self.scroll_canvas.configure( scrollregion=self.scroll_canvas.bbox("all"))) self.scroll_canvas.create_window((0, 0), window=self.scroll_frame, anchor="nw") self.scroll_canvas.configure(yscrollcommand=scrollbar.set) self.scroll_canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # 鼠标滚轮绑定 self.scroll_canvas.bind_all("<MouseWheel>", lambda e: self.scroll_canvas.yview_scroll( int(-1 * (e.delta / 120)), "units")) self._render_cards()def _render_cards(self): """渲染所有单词卡片""" for widget in self.scroll_frame.winfo_children(): widget.destroy() t = THEMES[self.current_theme] row_idx = 0 for group_name, words in WORD_DATA.items(): # 元音组标题 title_label = tk.Label(self.scroll_frame, text=group_name, font=("Arial", 14, "bold"), bg=t["bg"], fg=t["fg"], anchor="w") title_label.grid(row=row_idx, column=0, columnspan=self.columns, sticky="w", padx=8, pady=(16, 6)) row_idx += 1 # 单词卡片 for i, item in enumerate(words): col = i % self.columns card = self._create_card(self.scroll_frame, item, t) card.grid(row=row_idx + i // self.columns, column=col, padx=6, pady=6, sticky="nsew") # 设置列权重 for c in range(self.columns): self.scroll_frame.columnconfigure(c, weight=1) row_idx += (len(words) + self.columns - 1) // self.columns + 1def _create_card(self, parent, item, theme): """创建单个单词卡片""" t = theme card = tk.Frame(parent, bg=t["card_bg"], bd=1, relief="solid", highlightbackground=t["card_border"], highlightthickness=1, padx=10, pady=8) # 英文单词(元音字母标红) word_frame = tk.Frame(card, bg=t["card_bg"]) word_frame.pack(anchor="w") for idx, ch in enumerate(item["word"]): color = t["vowel_color"] if idx in item["vowel_pos"] else t["fg"] tk.Label(word_frame, text=ch, font=("Arial", 16, "bold"), bg=t["card_bg"], fg=color).pack(side="left") # 音标 tk.Label(card, text=item["phonetic"], font=("Arial", 11), bg=t["card_bg"], fg="#888888").pack(anchor="w") # 中文释义 tk.Label(card, text=item["cn"], font=("Arial", 12), bg=t["card_bg"], fg=t["fg"]).pack(anchor="w", pady=(2, 6)) # 按钮组 btn_frame = tk.Frame(card, bg=t["card_bg"]) btn_frame.pack(fill="x") tk.Button(btn_frame, text="读音", bg=t["btn_read"], fg="white", font=("Arial", 10, "bold"), bd=0, cursor="hand2", command=lambda w=item["word"]: self.tts.speak(w) ).pack(side="left", expand=True, fill="x", padx=(0, 4)) tk.Button(btn_frame, text="书写", bg=t["btn_write"], fg="white", font=("Arial", 10, "bold"), bd=0, cursor="hand2", command=lambda w=item["word"]: self._open_writing(w) ).pack(side="left", expand=True, fill="x", padx=(4, 0)) return carddef _open_writing(self, word): """打开手写练习弹窗""" WritingDialog(self.root, word, THEMES[self.current_theme])def _apply_theme(self): """应用当前主题""" t = THEMES[self.current_theme] self.root.configure(bg=t["bg"]) self.header.configure(bg=t["bg"]) for widget in self.header.winfo_children(): try: widget.configure(bg=t["bg"], fg=t["fg"]) except tk.TclError: pass self.scroll_canvas.configure(bg=t["bg"]) self.scroll_frame.configure(bg=t["bg"]) self._render_cards()def _on_theme_change(self, event=None): self.current_theme = self.theme_var.get() self._apply_theme()def _on_col_change(self, event=None): self.columns = int(self.col_var.get()) self._render_cards()def run(self): self.root.mainloop()if name == "main":app = VowelTTSApp()app.run()