Vokabeltrainer

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Hubert80
User
Beiträge: 2
Registriert: Dienstag 29. April 2025, 15:19

Moin zusammen,

ich habe mal eine Vokabeltrainer gebaut, dies ist mein erstes "richtiges" Programm, daher bitte ich um etwas Nachsicht. Es läuft jedenfalls und ich bin mit dem Ergebnis sehr zufieden.
Mein Sohn nutzt es jedenfalls und lernt damit besser als mit seinem Schulbuch. Natürlich könnte man da noch viele Sachen verbessern und das werde ich bestimmt auch noch nach und nach, aber fürs
Erste ist das ok so. Am meisten Schmerzen hat der ganze GUI Kram verursacht...

Code: Alles auswählen

import tkinter as tk
from PIL import Image, ImageTk
import os
import random
import re
from difflib import SequenceMatcher
import tkinter.messagebox as messagebox

VOCAB_FILE = "vocab.csv"

def load_vocab_by_chapter():
    vocab_by_chapter = {}
    if not os.path.exists(VOCAB_FILE):
        return vocab_by_chapter
    with open(VOCAB_FILE, encoding="latin1") as f:
        for line in f:
            line = line.strip()
            if not line or line.count(",") < 2:
                continue
            chapter, word1, word2 = line.split(",", 2)
            chapter = chapter.strip().strip('"')
            word1 = word1.strip().strip('"')
            word2 = word2.strip().strip('"')
            if chapter not in vocab_by_chapter:
                vocab_by_chapter[chapter] = []
            vocab_by_chapter[chapter].append([word1, word2])
    return vocab_by_chapter

STYLE_PROFILES = {
    "Englisch": {
        "bg": "#FFFFFF",
        "fg_primary": "#00247D",
        "fg_secondary": "#CF142B",
        "button_bg": "#CF142B",
        "button_fg": "#FFFFFF",
        "flag_image": "union_jack.png"
    },
    "Spanisch": {
        "bg": "#FFFFF0",
        "fg_primary": "#AA151B",
        "fg_secondary": "#F1BF00",
        "button_bg": "#F1BF00",
        "button_fg": "#000000",
        "flag_image": "spanish_flag.png"
    }
}

class VocabTrainerApp:
    def __init__(self, root, selected_language="Englisch"):
        self.root = root
        self.root.title("Vokabeltrainer")
        self.root.geometry("600x550")
        self.selected_language = selected_language
        self.style = STYLE_PROFILES[selected_language]

        self.canvas = tk.Canvas(self.root, highlightthickness=0)
        self.canvas.pack(fill="both", expand=True)

        self.bg_image = None
        self.bg_label = self.canvas.create_image(0, 0, anchor="nw")

        self.vocab_by_chapter = load_vocab_by_chapter()
        self.current_vocab = []
        self.current_word = None
        self.correct_first_try = 0
        self.total_vocab = 0
        self.answered_words = {}

        self.chapter_var = tk.StringVar()
        self.direction_var = tk.StringVar(value="Deutsch_Sprache")

        self._create_widgets()
        self.switch_language(self.selected_language)

        self.root.bind("<Configure>", self._resize_background)
        self.root.after(100, self._load_background_image)

    def _load_background_image(self):
        width = self.canvas.winfo_width()
        height = self.canvas.winfo_height()

        if width < 10 or height < 10:
            self.root.after(100, self._load_background_image)
            return

        img_path = self.style.get("flag_image")
        if not os.path.exists(img_path):
            return

        try:
            image = Image.open(img_path)
            image = image.resize((width, height), resample=Image.LANCZOS)
            self.bg_image = ImageTk.PhotoImage(image)
            self.canvas.itemconfig(self.bg_label, image=self.bg_image)
            self.canvas.tag_lower(self.bg_label)
        except Exception as e:
            print(f"Fehler beim Laden des Bildes: {e}")

    def _resize_background(self, event):
        self._load_background_image()

    def _create_widgets(self):
        font_label = ("Arial", 11, "bold")
        font_button = ("Arial", 10, "bold")

        flag_frame = tk.Frame(self.root, bg=self.style["bg"])
        self.en_button = tk.Button(flag_frame, text="🇬🇧 Englisch", font=("Arial", 10),
                                   command=lambda: self.switch_language("Englisch"))
        self.en_button.grid(row=0, column=0, padx=10)
        self.es_button = tk.Button(flag_frame, text="🇪🇸 Spanisch", font=("Arial", 10),
                                   command=lambda: self.switch_language("Spanisch"))
        self.es_button.grid(row=0, column=1, padx=10)
        self.canvas.create_window(300, 50, window=flag_frame)

        chapter_label = tk.Label(self.root, text="Kapitel auswählen:", font=font_label, bg=self.style["bg"],
                                 fg=self.style["fg_primary"])
        self.canvas.create_window(300, 100, window=chapter_label)
        self.chapter_dropdown = tk.OptionMenu(self.root, self.chapter_var, "")
        self.canvas.create_window(300, 130, window=self.chapter_dropdown)

        direction_label = tk.Label(self.root, text="Richtung auswählen:", font=font_label, bg=self.style["bg"],
                                   fg=self.style["fg_primary"])
        self.canvas.create_window(300, 170, window=direction_label)
        self.direction_dropdown = tk.OptionMenu(self.root, self.direction_var, "Sprache_Deutsch", "Deutsch_Sprache")
        self.canvas.create_window(300, 200, window=self.direction_dropdown)

        button_frame = tk.Frame(self.root, bg=self.style["bg"])

        self.start_button = tk.Button(button_frame, text="Abfrage starten",
                                      font=font_button, bg=self.style["button_bg"], fg=self.style["button_fg"],
                                      command=self.start_test)
        self.start_button.pack(side="left", padx=10)

        self.view_vocab_button = tk.Button(button_frame, text="Vokabeln ansehen",
                                           font=font_button, bg=self.style["button_bg"], fg=self.style["button_fg"],
                                           command=self.show_vocab_list)
        self.view_vocab_button.pack(side="left", padx=10)

        self.multi_chapter_button = tk.Button(button_frame, text="Mehrere Kapitel auswählen",
                                              font=font_button, bg=self.style["button_bg"], fg=self.style["button_fg"],
                                              command=self.open_multi_chapter_selection)
        self.multi_chapter_button.pack(side="left", padx=10)

        self.canvas.create_window(300, 250, window=button_frame)

        self.question_label = tk.Label(self.root, text="Übersetze:", font=("Arial", 16, "bold"),
                                       bg=self.style["bg"], fg="#444444")
        self.canvas.create_window(300, 290, window=self.question_label)

        self.answer_entry = tk.Entry(self.root, font=("Arial", 12), insertbackground=self.style["bg"])
        self.canvas.create_window(300, 330, window=self.answer_entry)
        self.answer_entry.bind("<Return>", lambda event: self.check_answer())

        self.check_button = tk.Button(self.root, text="Antwort prüfen",
                                      font=font_button, bg=self.style["button_bg"], fg=self.style["button_fg"],
                                      command=self.check_answer)
        self.canvas.create_window(300, 370, window=self.check_button)

        self.feedback_label = tk.Label(self.root, text="Richtig oder Falsch?", font=("Arial", 12),
                                       bg=self.style["bg"], fg="#444444")
        self.canvas.create_window(300, 410, window=self.feedback_label)

        self.result_label = tk.Label(self.root, text="Ergebnis: ? im ersten Versuch richtig!",
                                     font=("Arial", 12, "italic"),
                                     bg=self.style["bg"], fg="#444444")
        self.canvas.create_window(300, 450, window=self.result_label)

    def reset_feedback_label(self):
        self.feedback_label.config(text="Richtig oder Falsch?", fg="#444444")

    def reset_feedback_and_next_question(self):
        self.reset_feedback_label()
        self.ask_next_question()

    def switch_language(self, language):
        self.selected_language = language
        self.style = STYLE_PROFILES.get(language, STYLE_PROFILES["Englisch"])
        self._load_background_image()

        prefix = "EN_" if language == "Englisch" else "ES_"
        filtered_chapters = [ch for ch in self.vocab_by_chapter.keys() if ch.startswith(prefix)]
        self.chapter_var.set(filtered_chapters[0] if filtered_chapters else "")

        menu = self.chapter_dropdown["menu"]
        menu.delete(0, "end")
        for chapter in filtered_chapters:
            menu.add_command(label=chapter, command=lambda value=chapter: self.chapter_var.set(value))

        self.question_label.config(bg=self.style["bg"], fg=self.style["fg_primary"])
        self.feedback_label.config(bg=self.style["bg"])
        self.result_label.config(bg=self.style["bg"], fg=self.style["fg_primary"])
        self.check_button.config(bg=self.style["button_bg"], fg=self.style["button_fg"])
        self.start_button.config(bg=self.style["button_bg"], fg=self.style["button_fg"])
        self.view_vocab_button.config(bg=self.style["button_bg"], fg=self.style["button_fg"])

    def start_test(self):
        chapter = self.chapter_var.get()
        direction = self.direction_var.get()
        if not chapter or chapter not in self.vocab_by_chapter:
            self.feedback_label.config(text="Bitte ein gültiges Kapitel auswählen.")
            return
        self.current_vocab = self.vocab_by_chapter[chapter][:]
        random.shuffle(self.current_vocab)
        self.total_vocab = len(self.current_vocab)
        self.answered_words = {}
        self.correct_first_try = 0
        self.direction_var.set(direction)
        self.ask_next_question()

    def ask_next_question(self):
        if not self.current_vocab:
            self.question_label.config(text="Abfrage abgeschlossen!")
            self.answer_entry.config(state="disabled")
            self.check_button.config(state="disabled")
            self.result_label.config(
                text=f"Ergebnis: {self.correct_first_try}/{self.total_vocab} beim ersten Versuch richtig!"
            )
            return

        self.current_word = self.current_vocab.pop(0)
        show_word = self.current_word[0] if self.direction_var.get() == "Sprache_Deutsch" else self.current_word[1]
        self.question_label.config(text=f"Übersetze: {show_word}")
        self.answer_entry.config(state="normal")
        self.answer_entry.delete(0, tk.END)
        self.answer_entry.focus()
        self.reset_feedback_label()

    @staticmethod
    def normalize_answer(text):
        text = text.lower().strip()
        text = re.sub(r"\(.*?\)", "", text)
        text = re.sub(r"\s+", " ", text)
        return text

    @staticmethod
    def extract_variants(text):
        text = VocabTrainerApp.normalize_answer(text)
        variants = re.split(r"[\/;,]", text)
        return [v.strip() for v in variants if v.strip()]

    def check_answer(self):
        user_input = self.answer_entry.get().strip()

        if not user_input:
            self.result_label.config(text="⚠️ Keine Eingabe erfolgt!", fg="red")
            return
        else:
            self.result_label.config(text="Ergebnis: ? im ersten Versuch richtig!", fg="#444444")

        user_input = user_input.lower()
        user_variants = VocabTrainerApp.extract_variants(user_input)

        correct_raw = self.current_word[1] if self.direction_var.get() == "Sprache_Deutsch" else self.current_word[0]
        correct_variants = VocabTrainerApp.extract_variants(correct_raw)

        match_found = any(u in correct_variants for u in user_variants) or any(
            c in user_variants for c in correct_variants)

        if match_found:
            if self.current_word[0] not in self.answered_words:
                self.correct_first_try += 1
                self.answered_words[self.current_word[0]] = True
            self.feedback_label.config(text="✅ Richtig!", fg="green")
        else:
            ratio = max(
                SequenceMatcher(None, u, c).ratio()
                for u in user_variants for c in correct_variants
            )
            if ratio >= 0.8:
                self.feedback_label.config(text=f"🟡 Fast richtig! Korrekt wäre: {correct_raw}", fg="orange")
            else:
                self.feedback_label.config(text=f"❌ Falsch! Korrekt wäre: {correct_raw}", fg="red")
                self.answered_words[self.current_word[0]] = False
                self.current_vocab.append(self.current_word)

        self.root.after(2000, self.reset_feedback_and_next_question)

    def open_multi_chapter_selection(self):
        selection_window = tk.Toplevel(self.root)
        selection_window.title("Mehrere Kapitel auswählen")
        selection_window.configure(bg="#F0F0F0")

        label = tk.Label(selection_window, text="Kapitel auswählen:", font=("Arial", 12, "bold"), bg="#F0F0F0")
        label.pack(pady=(10, 5))

        listbox = tk.Listbox(selection_window, selectmode="multiple", font=("Arial", 11), bg="white", width=40,
                             height=10)
        listbox.pack(padx=10, pady=(0, 10))

        prefix = "EN_" if self.selected_language == "Englisch" else "ES_"
        chapters = [ch for ch in self.vocab_by_chapter.keys() if ch.startswith(prefix)]

        for chapter in chapters:
            listbox.insert(tk.END, chapter)

        amount_frame = tk.Frame(selection_window, bg="#F0F0F0")
        amount_frame.pack(pady=(0, 10))

        amount_label = tk.Label(amount_frame, text="Anzahl Vokabeln:", font=("Arial", 11), bg="#F0F0F0")
        amount_label.pack(side="left")

        amount_var = tk.StringVar(value="20")
        amount_entry = tk.Entry(amount_frame, textvariable=amount_var, font=("Arial", 11), width=5)
        amount_entry.pack(side="left", padx=5)

        def start_multi_chapter_test():
            selected_indices = listbox.curselection()
            selected_chapters = [listbox.get(i) for i in selected_indices]

            if not selected_chapters:
                messagebox.showwarning("Keine Auswahl", "Bitte mindestens ein Kapitel auswählen!")
                return

            try:
                total_vocab_needed = int(amount_var.get())
                if total_vocab_needed <= 0:
                    raise ValueError
            except ValueError:
                messagebox.showerror("Ungültige Zahl", "Bitte eine gültige positive Zahl eingeben!")
                return

            selected_vocab = []
            vocab_per_chapter = total_vocab_needed // len(selected_chapters)

            for chapter in selected_chapters:
                chapter_vocab = self.vocab_by_chapter.get(chapter, [])
                random.shuffle(chapter_vocab)
                selected_vocab.extend(chapter_vocab[:vocab_per_chapter])

            if len(selected_vocab) < total_vocab_needed:
                remaining_vocab = []
                for chapter in selected_chapters:
                    remaining_vocab.extend(self.vocab_by_chapter.get(chapter, []))
                random.shuffle(remaining_vocab)
                selected_vocab.extend(remaining_vocab[:total_vocab_needed - len(selected_vocab)])

            self.current_vocab = selected_vocab[:total_vocab_needed]
            random.shuffle(self.current_vocab)
            self.total_vocab = len(self.current_vocab)
            self.answered_words = {}
            self.correct_first_try = 0
            self.ask_next_question()
            selection_window.destroy()

        confirm_button = tk.Button(selection_window, text="Auswahl starten", font=("Arial", 10, "bold"),
                                   bg="#4CAF50", fg="white", command=start_multi_chapter_test)
        confirm_button.pack(pady=(0, 10))

    def show_vocab_list(self):
        chapter = self.chapter_var.get()
        if not chapter or chapter not in self.vocab_by_chapter:
            self.feedback_label.config(text="Bitte ein gültiges Kapitel auswählen.")
            return

        vocab_list = self.vocab_by_chapter[chapter]
        direction = self.direction_var.get()

        vocab_window = tk.Toplevel(self.root)
        vocab_window.title("Vokabelliste")
        vocab_window.configure(bg="#E0E0E0")

        container = tk.Frame(vocab_window, bg="#E0E0E0")
        container.pack(fill="both", expand=True, padx=10, pady=10)

        canvas = tk.Canvas(container, bg="#E0E0E0", highlightthickness=0)
        scrollbar = tk.Scrollbar(container, orient="vertical", command=canvas.yview)
        scrollable_frame = tk.Frame(canvas, bg="#E0E0E0")

        scrollable_frame.bind(
            "<Configure>",
            lambda e: canvas.configure(
                scrollregion=canvas.bbox("all"),
                width=scrollable_frame.winfo_reqwidth()
            )
        )

        canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)

        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")

        for idx, word_pair in enumerate(vocab_list):
            left_word = word_pair[0] if direction == "Sprache_Deutsch" else word_pair[1]
            right_word = word_pair[1] if direction == "Sprache_Deutsch" else word_pair[0]

            lbl_left = tk.Label(scrollable_frame, text=left_word, font=("Arial", 12), anchor="w", bg="#E0E0E0", fg="#333333")
            lbl_arrow = tk.Label(scrollable_frame, text="➔", font=("Arial", 12, "bold"), bg="#E0E0E0", fg="#666666")
            lbl_right = tk.Label(scrollable_frame, text=right_word, font=("Arial", 12), anchor="w", bg="#E0E0E0", fg="#333333")

            lbl_left.grid(row=idx, column=0, sticky="w", padx=(5, 10), pady=2)
            lbl_arrow.grid(row=idx, column=1, sticky="w", padx=5)
            lbl_right.grid(row=idx, column=2, sticky="w", padx=(10, 5), pady=2)

        vocab_window.update_idletasks()
        window_width = scrollable_frame.winfo_reqwidth() + 30
        window_height = min(scrollable_frame.winfo_reqheight() + 30, 600)
        vocab_window.geometry(f"{window_width}x{window_height}")

if __name__ == "__main__":
    root = tk.Tk()
    app = VocabTrainerApp(root)
    root.mainloop()
Benutzeravatar
__blackjack__
User
Beiträge: 13919
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Hubert80: Anmerkungen zum Quelltext: ``as`` beim importieren ist zum umbenennen, `messagebox` wird aber gar nicht umbenannt.

Konstanten werden nach den Importen definiert. Funktionen davor nur wenn die Funktion benötigt wird um die Konstante zu erstellen, und das ist eher selten.

Das Hauptprogramm gehört in eine Funktion. So wie es jetzt ist, gibt es `root` und `app` als globale Variablen.

Man prüft nicht auf die Existenz von Dateien. Diese Prüfung ist im öffnen der Datei schon enthalten und zwischen der eigenen Prüfung und dem Öffnen kann die Datei ja immer noch gelöscht werden, also muss ein sauberes Programm da sowieso drauf reagieren können, also macht man besser das.

Wenn das keine CSV-Datei ist, sollte die nicht so heissen, oder es ist eine, dann sollte man da nicht selber Code basteln der weniger kann als das `csv`-Modul. CSV kann beispielsweise das Trennzeichen oder die Anführungszeichen auch *in* den Werten haben.

Das hier…

Code: Alles auswählen

                if chapter not in vocab_by_chapter:
                    vocab_by_chapter[chapter] = []
                vocab_by_chapter[chapter].append([word1, word2])
…wäre früher das hier gewesen…

Code: Alles auswählen

                vocab_by_chapter.setdefault(chapter, []).append([word1, word2])
…aber heute ist das ein Fall für `collections.defaultdict`.

Wenn es es für Zeichenketten mit einer bedeutung als Argument in `tkinter` eine Konstante gibt, dann sollte man die verwenden.

Bei Namen sollte die Reihenfolge der Namen stimmen. Der Name `font_label` bedeutet beispielsweise etwas anderes als `label_font`. Schriftgrössen würde ich nicht hart absolut vorgeben. Der Benutzer des Systems hat für seine Wahl ja einen Grund. Ich würde die relativ zur normalen Grösse machen.

Den Hack bei ``lambda`` Defaultwerte zu binden macht man seit `functools.partial()` nicht mehr. Das wurde genau für so etwas eingeführt.

Ohne Not ``pop(0)`` ist unschön. Die Liste ist ja gemischt worden, also macht es inhaltlich keinen Unterschied von welcher Seite man die Elemente weg nimmt, aber von der Laufzeit nimmt ``pop()`` von hinten ein Element weg und mehr passiert da in den allermeisten Fällen nicht, aber ``pop(0)`` nimmt von vorne ein Element und verschiebt dann *alle anderen* Elemente um eine Position nach vorne.

Bei ``re.split(r"[\/;,]", text)`` ist entweder ein Backslash (\) zu viel oder einer zu wenig, je nach dem was das konkret machen soll. Der normale Schrägstrich muss in regulären Ausdrücken jedenfalls nicht escaped werden. An keiner Stelle.

Es wird nirgends geprüft oder sichergestellt, dass das erste Wort innerhalb eines Kapitels eindeutig ist, der Code braucht das aber soweit ich das sehe, sonst könnte die Statistik falsch werden.

Das ist alles viel zu viel für diese eine Klasse, in der GUI, „eye candy“, Fenster die in Methoden erstellt werden sogar mit einer Funktion in einer Methode geschachtelt, und die Programmlogik steckt. Dadurch das man ein Programm mit 26 globalen Variablen einfach mal in eine Klasse verschiebt, wird das ja nicht übersichtlicher, testbarer, wartbarer. Auch Methoden in denen verschiedene Sachen „resetted“ werden sind in der Regel keine gute Idee, erst recht nicht wenn das in mehr als einer Methode wiederholt wird. Das wäre eigentlich Programmlogik die in einer Klasse ganz gut aufgehoben wäre und wo man dann einfach ein neues Objekt erstellt, statt mehrere einzelne Attribute auf einer Gott-Klasse zu initialisieren.
“I am Dyslexic of Borg, Your Ass will be Laminated” — unknown
Hubert80
User
Beiträge: 2
Registriert: Dienstag 29. April 2025, 15:19

@blackjack, ja was soll ich sagen, du sprichst hier mit einem Anfänger und teilweise verstehe ich deine Ausführungen noch gar nicht ohne diese nachschauen zu müssen, aber trotzdem vielen Dank für deine
Bewertung und Analyse. Ich gehe bestimmt einige deiner Punkte an und werde versuchen es besser zu lösen, aber grundsätzlich bin ich trotzdem stolz, dass Ding läuft und macht was es soll.

VG
nezzcarth
User
Beiträge: 1733
Registriert: Samstag 16. April 2011, 12:47

du sprichst hier mit einem Anfänger und teilweise verstehe ich deine Ausführungen noch gar
Die Sache ist halt, dass GUI-Programmierung aus meiner Sicht eher kein Anfänger-Thema ist. Um das sauber umsetzen zu können, werden mehrere fortgeschrittene Programmierkonzepte benötigt und man sollte daher in Python schon recht sattelfest sein. So kommen dann diese ausführlichen Vorschläge zusammen, von denen sich manche Anfänger evtl. etwas erschlagen/demotiviert, etc. fühlen.
Benutzeravatar
__blackjack__
User
Beiträge: 13919
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Hubert80: Ich weiss mit „Anfänger“ immer nicht so recht was ich dazu sagen soll: Das ist doch kein Grund die Sachen *nicht* anzusprechen, weil dann ändert sich das mit dem Anfängerstatus ja nicht wirklich, wenn man da aus Rücksicht nix sagen würde.

Das es macht was es soll, naja, im Rahmen was Du bisher ausprobiert hast, denn da waren ja nicht nur Punkte die das einfacher oder übersichtlicher machen dabei, sondern auch welche wo das Programm nicht immer das macht was es soll. Beispielsweise machen Kommas in den Vokabeln Probleme, und ich bin mir ziemlich sicher, dass merken vom jeweils ersten Wort der Paare in einem `set()` kann zu falschen Werten führen wenn die nicht eindeutig sind, also wenn das gleiche Wort da mehrfach vorkommt. Und dann ist die Frage bei dem einen regulären Ausdruck ob da ein Backslash zu viel oder zu wenig ist — wenn der zu wenig ist, dann trennt das `re.split()` nicht alles was es sollte. Also die Frage ist ob "a" und "b" in folgendem Beispiel getrennt werden sollen oder nicht:

Code: Alles auswählen

In [38]: re.split(r"[\/;,]", r"a\b/c;d,e")
Out[38]: ['a\\b', 'c', 'd', 'e']
Ansonsten kannst Du die Punkte ja abarbeiten und wenn etwas nicht verständlich war, nachfragen.
“I am Dyslexic of Borg, Your Ass will be Laminated” — unknown
Antworten