Programm hängt ohne Fehlermeldung - tkinter-Problem?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
killerkaninchen
User
Beiträge: 6
Registriert: Dienstag 11. Juni 2019, 14:13

Hallo zusammen

Ich weiß nicht genau, ob das Problem mit tkinter zusammenhängt oder mit meinem falschen Umgang mit Klassen, daher poste ich mal hier...

Ich habe mich vor kurzem in Python eingearbeitet und jetzt mein erstes großes Projekt angefangen: ein Programm, das, nachdem ich Schlagworte eingegeben habe, eine interne "Datenbank" (Textdatei) nach dem passenden Titel durchsucht und diesen abspielt. Dabei ist wichtig, dass ich jederzeit die Möglichkeit habe, den Titel zu wechseln, indem ich andere Schlagworte eingebe oder das Programm sauber zu beenden. Das Programm funktioniert auch einwandfrei.

Dann habe ich einen tkinter-Button zugefügt und jetzt hängt das Programm, wenn ich "Anderer Titel" anklicke. Das heißt, das Fenster bleibt bestehen (es kommt sogar noch ein weiteres hinzu) und ich komme nicht wieder an den Punkt zurück, an dem ich neue Schlagworte für ein neu abzuspielendes Musikstück eingeben kann.

Es gibt ein Hauptprogramm, das die "Datenbank" erstellt und meine eingegebenen Schlagworte entgegennimmt und den Player startet:

Code: Alles auswählen

...
while True:
   ...
    # Schlagworte werden eingegeben, ausgewertet und in "auswahl" gespeichert
    ...
    if len(auswahl) == 1:
        musicplayer(auswahl[0])
Musicplayer und Button sehen so aus:

Code: Alles auswählen

import pygame
import sys
from tkinter import *


class MusicplayerButton(object):
    def __init__(self, master):
        frame = Frame(master)
        frame.pack()
        self.button = Button(frame, 
                             text="Programm beenden", fg="red",
                             command=self.player_beenden)
        self.button.pack(side=LEFT)
        self.button2 = Button(frame,
                             text="Anderer Titel",
                             command=self.anderer_titel)
        self.button2.pack(side=LEFT)
    def player_beenden(self):
        pygame.mixer.music.fadeout(3000)
        sys.exit(0)

    def anderer_titel(self):
        pygame.mixer.music.fadeout(3000)
        root = Tk()
        button = MusicplayerButton(root)
        root.mainloop()
        button.frame.destroy()
        pass

def musicplayer(titel):
    pygame.mixer.init()
    pygame.mixer.music.load(titel)
    pygame.mixer.music.play(-1)
    print("Im Moment läuft:", titel)
    root = Tk()
    musicplayer_button = MusicplayerButton(root)
    root.mainloop()
Der"Programm beenden"-Button funktioniert. Wie komme ich mit dem anderen Button wieder in die while-Schleife zurück und wie kann ich das Fenster schließen?

Danke im Voraus
Killerkaninchen
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@killerkaninchen: da fehlen ja wesentliche Teile, z.b. die ›auswahl‹.

So funktioniert ein GUI-Programm nicht. Es darf nur eine Tk-Instanz geben. Und nur einen Mixer. Und es darf keine while-Schleife geben.
*-Importe sind böse, wie Du hier im Forum an vielen Stellen nachlesen kannst.

Korrekt wäre eine ›main‹-Funktion, in der ein Tk-Instanz und ein Mixer erstellt wird, ein GUI-Fenster in dem man den Titel auswählen kann, wo dann auch load und play aufgerufen wird.
killerkaninchen
User
Beiträge: 6
Registriert: Dienstag 11. Juni 2019, 14:13

Ich hatte den Button aus https://www.python-kurs.eu/tkinter_buttons.php und um die Funktionen ergänzt. Die zweite tkinter-Instanz hatte ich erstellt, damit quit bzw. destroy funktioniert. Ansonsten heißt es MusicplayerButton hat keine Methode quit.

Ich hatte gehofft, nach und nach verschiedene tkinter-Elemente einfügen zu können - zu Übungszwecken.
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

So funktioniert Programmieren halt nicht. Man kann nicht irgendwelche Stücke, die man aus irgendwelchen Quellen hat, einfach zusammenkopieren und hoffen, dass das Programm läuft.

Wenn Du eine GUI haben willst, dann kannst Du Dich einfach an das Grundgerüst halten, das Du ja im wesentlichen schon richtig kopiert hast:

Code: Alles auswählen

import tkinter as tk

class Musicplayer:
    def __init__(self, master):
        self.master = master
        frame = tk.Frame(master)
        frame.pack()
        tk.Button(frame, 
            text="Programm beenden", fg="red",
            command=self.player_beenden).pack(side=tk.LEFT)
        tk.Button(frame,
            text="Anderer Titel",
            command=self.anderer_titel).pack(side=tk.LEFT)

    def player_beenden(self):
        # pygame.mixer.music.fadeout(3000) # Blockierender Aufruf ist in einer GUI keine gute Idee
        self.master.destroy()

    def anderer_titel(self):
        # hier Musik starten
        # TODO: titel aus einem noch zu programmierenden GUI-Widget lesen
        # pygame.mixer.music.fadeout(3000) # Blockierender Aufruf ist in einer GUI keine gute Idee
        # pygame.mixer.music.load(titel)
        # pygame.mixer.music.play(-1)
        pass

def main():
    # pygame.mixer.init()
    root = Tk()
    _ = Musicplayer(root)
    root.mainloop()

if __name__ == '__main__':
    main()
Die pygame-Ansteuerung habe ich mal auskommentiert, und die Musikauswahl fehlt natürlich noch.
killerkaninchen
User
Beiträge: 6
Registriert: Dienstag 11. Juni 2019, 14:13

Das heißt nicht der Button ist Teil der musicplayer-Funktion sondern umgekehrt. Ich kann das erst heute abend testen. Danke erstmal dafür!

(Das mit import * kannte ich, wollte ich als nächstes überarbeiten.)
Die Musikauswahl wird von meinem Hauptprogramm getroffen:

Code: Alles auswählen

import sys
from time import sleep
from musicplayer_pygame import musicplayer


numeneramusikdb = {}

with open("Numeneramusik.txt", "r") as numeneramusik:
    for line in numeneramusik:
        titel_schlagworte = line.split(";")
        titel = titel_schlagworte[0]
        line2 = line.lower()
        schlagworte_newline = (line2.split(";")).pop(-1)
        schlagworte = schlagworte_newline[:-1]
        schlagworte_einzeln = schlagworte.split(",")
        anzahl_schlagworte = len(schlagworte_einzeln)

        for i in range(0, anzahl_schlagworte):
            schlagwort = (schlagworte_einzeln[-1]).strip()
            del schlagworte_einzeln[-1]
            numeneramusikdb.setdefault(schlagwort, []).append(titel)

print("Dies sind Schlagworte, denen Musiktitel zugeordnet sind:\n",
       numeneramusikdb.keys())

while True:
    schlagwort1 = input("\nWelche Stimmung soll der Titel haben\n"
                        "oder welche Geräusche soll man hören? \n").lower()
    schlagwort2 = input("\nWas ist noch wichtig? \n").lower()
    schlagwort3 = input("\nGib ein letztes Schlagwort ein! \n").lower()
    ergebnis1 = numeneramusikdb.get(schlagwort1, "")
    ergebnis2 = numeneramusikdb.get(schlagwort2, "")
    ergebnis3 = numeneramusikdb.get(schlagwort3, "")

    vorschlag = []
    auswahl = []

    for i in ergebnis1:
        if i in ergebnis2 and i in ergebnis3:
            auswahl.append(i)
        elif i in ergebnis2 or i in ergebnis3:
            vorschlag.append(i)
        elif i not in ergebnis2 and i not in ergebnis3:
            vorschlag.append(i)
        else:
            print("Dazu gibt es nichts passendes!")

    if len(auswahl) == 1:
        musicplayer(auswahl[0])
    elif len(auswahl) == 0 and len(vorschlag) == 0:
        print("Da gibt es keine Übereinstimmung!\n"
              "Bitte wähle etwas anderes!\n")
    elif len(auswahl) > 1:
        print("Du musst etwas von der Auswahlliste auswählen:\n", auswahl)
        titelwahl = input("Welcher Titel soll es sein?\n"
                          "1., 2., 3. etc...? \n")
        index_titelwahl = int(titelwahl.strip(".")) - 1
        musicplayer(auswahl[index_titelwahl])
    elif len(auswahl) == 0 and len(vorschlag) == 1:
        musicplayer(vorschlag[0])
    elif len(vorschlag) > 1:
        print("Du musst etwas von den Vorschlägen auswählen:\n", vorschlag)
        titelwahl = input("Welcher Titel soll es sein?\n"
                          "1., 2., 3. etc...? \n")
        index_titelwahl = int(titelwahl.strip(".")) - 1
        musicplayer(vorschlag[index_titelwahl])
    else:
        print("Da ist wohl was schief gelaufen!")
Numeneramusik.txt ist nach folgendem Muster aufgebaut:

Code: Alles auswählen

titel.ogg; Schlagwort1, Schlagwort2, Schlagwort3
weiterer_titel.ogg; Schlagwort4, Schlagwort1 usw...
Hatte ich bisher nur nicht gepostet, da der Teil lief - die entsprechende Musik war zu hören. Mit tkinter bis zum Klicken des Buttons und ohne tkinter lief es so wie ich es wollte:

Code: Alles auswählen

import pygame
import sys
from time import sleep
from tkinter import *


def musicplayer(titel):

    pygame.mixer.init()
    pygame.mixer.music.load(titel)
    pygame.mixer.music.play(-1)
    print("Im Moment läuft:", titel)
    nochmal = input("Soll etwas anderes laufen? \n").lower()

    if "ja" in nochmal:
        pygame.mixer.music.fadeout(3000) # Musik wird leiser Dauer in ms
        pass
    elif "nein" in nochmal:
        print("Bis zum nächsten Mal. :)")
        pygame.mixer.music.fadeout(3000)
        sleep(3)
        sys.exit(0)
    else:
        print("Bitte mit 'Ja' oder 'Nein' antworten!")
        nochmal2 = input("Soll etwas anderes laufen? \n").lower()

        if "ja" in nochmal2:
            pass
        else:
            print("Selber Schuld! ;)")
            pygame.mixer.music.fadeout(3000)
            sleep(3)
            sys.exit(0)
Ich habe Python über http://learnpythonthehardway.org/ gelernt. Gibt es ähnliches zu tkinter? https://www.python-kurs.eu/python_tkinter.php scheint mir ein wenig knapp zu sein, war aber das (vermeintlich) Beste, was ich finden konnte.
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

Das was Du zuerst programmiert hast, war ein Text-Konsolen-Programm. Da die Ein-Ausgabe mit dem Datenzugriff vermixt ist, mußt Du für ein GUI-Programm alles neu schreiben. Diesmal dann am besten richtig, nämlich, dass der Zugriff auf die Daten in Funktionen gekapselt ist, und es dann egal ist, ob Du per Text-Eingabe oder per GUI darauf zugreifst.

Zum Code:
Der with-Block sollte in eine Funktion, die load_irgendwas heißen könnte. Der Dateiname sollte nicht als String direkt im open stehen, sondern als KONSTANTE am Anfang der Datei definiert werden, damit man ihn leicht ändern kann.
Alles ab `print` gehört dann in eine Funktion, die `main` heißt.

numeneramusikdb könnte einen besseren Namen vertragen. Was ist denn in dem Wörterbuch drin?
In Zeile 10 splittest Du die Zeile zum ersten mal, um nur den titel eine Zeile danach herauszuholen, in Zeile 13 dann nochmal, um dann die Schlagworte zu bekommen, wobei Du auch noch pop benutzt, obwohl Du die Liste danach wegschmeißt.
In Zeile 18 hast Du dann ein sogenanntes Anti-Pattern, weil Du über die Anzahl der Schlagworte gehst, statt über die Liste schlagworte_einzeln direkt zu iterieren. Du benutzt aber `i` dann gar nicht, sondern nimmst immer das letzte Element der Liste und leerst sie dabei. Statt setdefault würde man ein defaultdict benutzen.

Das ganze könnte dann so aussehen:

Code: Alles auswählen

def load_numeneramusikdb(filename):
    keyword_to_title = defaultdict(list)
    with open(filename) as lines:
        for line in lines:
            title, keywords = line.split(";")
            for keyword in keywords.split(","):
                keyword_to_title[keyword.strip().lower()].append(title)
    return keyword_to_title
Zeile 31-33: wenn in numeneramusikdb Listen sind, warum nimmst Du dann als Defaultwert einen leeren String?
ergebnis ist ein zu generischer Name, `i` dann totaler Mist.
Zeile 43: das ist genau das Gegenteil der Bedingung vom vorherigen elif-Block, der else-Block wird also nie betreten und es gibt keine Unterscheidung zwischen den beiden Blöcken, also alles was nicht in auswahl landet, landet in vorschlag.
Es sollte nur an einer Stelle `musicplayer` aufgerufen werden.

In `musicplayer` sollte es kein sys.exit geben. Der normale Programmablauf darf nicht irgendwo so abrupt unterbrochen werden. Am besten kehrt die main-Funktion ganz natürlich per `return` zurück. Dazu sollte musicplayer einen Wert zurückliefern, anhand dessen main erkennt, ob es die while-True Schleife verlassen soll, oder nicht.
Antworten