While-Schleife mit Button beenden

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
BlackDullah
User
Beiträge: 2
Registriert: Dienstag 10. Mai 2022, 12:47

Hallo,

ich bin neu hier und erstelle gerade mein erstes Python Programm.

Ich möchte eine While-Schleife mit einem Button beenden (Tkinter).

Soweit funktioniert das auch, ich beende die Schleife - jedoch hängt sich mein Programm danach auf.

Code: Alles auswählen

import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
import time
import sys
import tkinter.ttk as ttk
from tkinter import filedialog, simpledialog, messagebox, colorchooser
import threading
import pygame


pygame.mixer.init()

root = tk.Tk()
root.title("Metronome")
root.geometry("800x400")
root.minsize(width=600, height=300)
root.configure(bg="#517357")


image = Image.open("logo.png").resize((60, 80))
photo = ImageTk.PhotoImage(image)


active = tk.BooleanVar()
active.set(True)

def stopme():
    active.set(False)
    t1.join()
    #root.update_idletasks()


def play_metronome():

    seconds_per_metronome = 60 / int(entry1.get())
    i = 0

    while active.get():

        pygame.mixer.music.load("C:\\Users\\Marvin Klankert\\Desktop\\Metronome\\metronome_sound.mp3")
        pygame.mixer.music.play(loops=0)
        text_metronome = i
        labelAusgabe.config(text=text_metronome)
        print(i)
        i = i + 1
        #labelAusgabe.delete("1.0", "end")  # Löscht textfeld
        #labelAusgabe.insert(tk.END, i)
        time.sleep(seconds_per_metronome)
        root.update_idletasks()
        #print(active.get())


def start():
    print(t1)
    active.set(True)
    t1.start()

t1 = threading.Thread(target=play_metronome)


labelAusgabe = tk.Label(root, text="", font="Calibri 32 bold", bg="#517357", height=1)
labelAusgabe.place(x=400, y=100, width=150, height=150)

buttonstop = tk.Button(root, text="Stop", width=5, height=1, fg="red", command=stopme)
buttonstop.place(x=100, y=280)

label1 = tk.Label(root, bg="#517357")
label1.pack()

label2 = tk.Label(root, text="Metronome", font="Calibri", bg="white")
label2.pack(fill="x")

label3 = tk.Label(root, bg="#517357")
label3.pack()

label4 = tk.Label(root, image=photo)
label4.place(x=700, y=50)

buttonstart = tk.Button(root, text="Start", width=15, height=10, command=start)
buttonstart.place(x=100, y=100)

button2 = tk.Button(root, text="Quit", width=9, command=root.destroy)
button2.place(x=700, y=350)

entry1 = ttk.Entry(root, width=10)
entry1.place(x=200, y=354)

label5 = tk.Label(root, text="Tempo (BPM): ", font="Calibri", bg="#517357", height=1)
label5.place(x=100, y=350)



#labelAnzeige = tk.Label(root, textvariable="")
#labelAnzeige.place(x=400, y=100, width=150, height=150)

root.mainloop()


Mit dem buttonstart springe ich in "def play_metronome():", wo sich die While Schleife befindet.
Mit buttonstop möchte ich es beenden.


Bild


Vielen Dank,
Black Dullah
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du mischst GUI mit multithreading. Das geht nicht. Die GUI-Elemente duerfen *ausschliesslich* im Main-Thread manipuliert werden. Der Zugriff auf active ist ok, weil die tk.*Vars nach meinem Wissen nach threadsicher sind. Aber danach gehst du an das Label ran, und das ist verboten. Auch das idletasks darf da nicht passieren. Weil es eben zu Instabilitaet fuehren kann, wie du ja selbst erlebst. Wenn du zwischen GUI und threds kommunizieren willst, musst du tk.*Vars benutzen, und in der GUI und ihrem Main-Thread mit Timern via der after-Methode.

Es gibt noch eine ganze Reihe anderer Probleme, zb liefert dein Metronom einen falschen Takt, weil du die Ausfuehrungszeit deines Codes ignorierst, das permanente laden der Samples ist vie zu teuer und sollte vor der Schleife liegen. Es gibt dazu entsprechende Moeglichkeiten in pygame.

Selbst im besten Fall wird dir aber dein Vorhaben, ein Metronom zu bauen, mit pygame nicht gelingen. Dafuer ist das nicht gedacht. Fuer sowas braucht man eher pyaudio, das buffer-basiert arbeitet, und bei dem du entsprechend samplegenau Audio zB eben vom Metronom platzieren kannst. Diese simple Methode von pygame ist nur genug, wenn man ein Schuss-Sample oder etwas aenhliches abfeuern will.
BlackDullah
User
Beiträge: 2
Registriert: Dienstag 10. Mai 2022, 12:47

Okay, danke für die Information. Es ist mein erstes Projekt mit Python. Es dient lediglich zu lernzwecken. Aber ich werde mir das nochmal anschauen bezüglich der Ausführungszeit.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Na das Mindeste ist sowas wie

Code: Alles auswählen

next_beat = time.monotonic() + beat_time
while True:
    time.sleep(next_beat - time.monotonic())
    play()
    next_beat += beat_time
Auf die Art und Weise akkumuliert man keine Verzoegerung, sondern hat nur die Latenz zwischen Kommando und tatsaechlichem abspielen.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@BlackDullah: Anmerkungen zu Lernzwecken: Die Importe könnte man aufräumen. Da werden Sachen doppelt importiert (`ttk`) und auch welche die gar nicht verwendet werden `sys`, `simpledialog`, `messagebox`, `filedialog`, und `colorchooser`.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Wenn man das macht, fällt auf, dass das Programm keine Funktionen verwendet. Es werden zwar welche definiert, da werden aber Funktionen einfach nur als Codeabschnitte mit einem Namen ”missbraucht”. Echte Funktionen bekommen alles was sie ausser Konstanten benötigen als Argument(e) übergeben und greifen nicht einfach so auf globale Variablen zu, die es ja auch gar nicht gibt, wenn man das Hauptprogramm in einer Funktion und nicht auf Modulebene definiert.

`update_idletasks()` (und `update()`) verwendet man nicht, jedenfalls nicht um sich eine eigene `mainloop()` zu basteln. Das ist komplizierter als man denkt und birgt Gefahren. Verwende nur die `mainloop()` um die GUI am Laufen zu halten.

`time.sleep()` in GUI-Programmen ist fast immer ein Fehler. Um zeitverzögert etwas auszuführen haben eigentlich alle GUI-Rahmenwerke einen Mechanismus um das in die GUI-Hauptschleife zu integrieren. So auch Tk mit der `after()`-Methode. Das ist auch simpler und mit etwas weniger Stolperfallen versehen als Threading.

Bei jedem nicht-trivialen GUI-Programm kommt man letztlich nicht um objektorientierte Programmierung (OOP) herum. Wenn man sich Zustand über Aufrufe hinweg merken muss, ist eine Klasse in Python das übliche Mittel um den Zustand und die Funktionen als Attribute und Methoden zusammenzufassen.

Absolute Grössen und Positionierungen sind keine gute Idee. Wenn man Glück hat, sieht das Ergebnis einfach nur komisch aus auf anderen Systemen als dem das man zum Entwickeln verwendet hat. Wenn man Pech hat wird die GUI dadurch unbenutzbar. Bei mir ist beispielsweise die "Quit"-Schaltfläche nicht mehr ganz im Fenster und das die Eingabe für das Tempo etwas tiefer ist als die Beschriftung davor ist auch nicht schön, und das bekommt man mit absoluten Positionen auch nicht sinnvoll geregelt, denn man weiss ja gar nicht wie gross die Schrift ist und was für eine Auflösung verwendet wird:
Bild

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

Namen sollten keine kryptischen Abkürzungen enthalten, oder gar nur daraus bestehen, und man nummeriert keine Namen. Wenn man nicht gerade Yoda heisst, sollte man auf eine sinnvolle Reihenfolge von Worten achten. Eine `label_ausgabe` ist was anderes als ein `ausgabe_label`. Und `button_start` bedeutet etwas anderes als `start_button`.

Code: Alles auswählen

#!/usr/bin/env python3
import time
import tkinter as tk
from tkinter import ttk

import pygame
from PIL import Image, ImageTk

BACKGROUND_COLOR = "#517357"


class MainWindow(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.title("Metronome")
        self.configure(bg=BACKGROUND_COLOR)

        self.tick_task_id = None
        self.bpm = 60

        tk.Label(self, bg=BACKGROUND_COLOR).pack()
        tk.Label(self, text="Metronome", font="Calibri", bg="white").pack(
            fill=tk.X
        )
        tk.Label(self, bg=BACKGROUND_COLOR).pack()

        frame = tk.Frame(self, bg=BACKGROUND_COLOR)

        tk.Button(
            frame, text="Start", width=15, height=10, command=self.start
        ).grid(row=0, column=0, sticky=tk.W)
        self.ausgabe_label = tk.Label(
            frame, font="Calibri 32 bold", width=10, bg=BACKGROUND_COLOR
        )
        self.ausgabe_label.grid(row=0, column=1)
        self.photo = ImageTk.PhotoImage(
            Image.open("logo.png").resize((60, 80))
        )
        tk.Label(frame, image=self.photo).grid(row=0, column=2, sticky=tk.NW)

        tk.Button(
            frame, text="Stop", width=5, fg="red", command=self.stop
        ).grid(row=1, column=0, sticky=tk.W)

        bpm_frame = tk.Frame(frame)

        tk.Label(
            bpm_frame,
            text="Tempo (BPM): ",
            font="Calibri",
            bg=BACKGROUND_COLOR,
        ).pack(side=tk.LEFT)

        self.bpm_entry = ttk.Entry(bpm_frame, width=10)
        self.bpm_entry.pack(side=tk.LEFT)
        self.bpm_entry.insert(0, self.bpm)

        bpm_frame.grid(row=2, column=0)

        tk.Button(frame, text="Quit", width=9, command=self.quit).grid(
            row=2, column=2
        )

        frame.pack(padx=10, pady=10)

    def do_tick(self, tick_number=1):
        now = time.monotonic()
        seconds_per_tick = 60 / self.bpm
        pygame.mixer.music.load(
            R"C:\Users\Marvin Klankert\Desktop\Metronome\metronome_sound.mp3"
        )
        pygame.mixer.music.play(loops=0)
        self.ausgabe_label["text"] = tick_number
        self.tick_task_id = self.after(
            int((now + seconds_per_tick - time.monotonic()) * 1000),
            self.do_tick,
            tick_number + 1,
        )

    def start(self):
        if self.tick_task_id is None:
            self.bpm = int(self.bpm_entry.get())
            self.do_tick(1)

    def stop(self):
        if self.tick_task_id is not None:
            self.after_cancel(self.tick_task_id)
            self.tick_task_id = None


def main():
    pygame.mixer.init()
    window = MainWindow()
    window.mainloop()


if __name__ == "__main__":
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten