Python Forum
How to break out of a for loop on button press? - Printable Version

+- Python Forum (https://python-forum.io)
+-- Forum: Python Coding (https://python-forum.io/forum-7.html)
+--- Forum: General Coding Help (https://python-forum.io/forum-8.html)
+--- Thread: How to break out of a for loop on button press? (/thread-38368.html)



How to break out of a for loop on button press? - philipbergwerf - Oct-04-2022

from tkinter import *

# root
root = Tk()
root.title('SeqTank')
scrwidth = root.winfo_screenwidth()
scrheight = root.winfo_screenheight()
root.geometry("%sx%s+%s+%s" % (int(scrwidth / 1.5), 
                                int(scrheight / 1.25), 
                                int(scrwidth / 6), 
                                int(scrheight / 12)))
root.update()

# paned master with two panels
panedmaster = PanedWindow(root, orient='v', sashwidth=1, relief='flat', sashcursor='arrow', bg='#666666')
panedmaster.place(relwidth=1, relheight=1)
uppanel = PanedWindow(panedmaster, height=35, relief='flat')
panedmaster.add(uppanel)
downpanel = PanedWindow(panedmaster, relief='flat')
panedmaster.add(downpanel)

text_editor = Text(downpanel, 
                    bg='black', 
                    fg='#32a852',
                    insertbackground='white',
                    font=('Courier', 24))
text_editor.place(relwidth=1, 
                    relheight=1)
text_editor.focus_set()


generate_button = Button(uppanel, text='Play!')
generate_button.place(relwidth=1)

def process():
	for i in range(100):
		...
		# playing a midi-file here


generate_button.configure(command=process)

root.mainloop()
This is my simple tkinter interface. I created a little tool that generates a midi-file based on the text input(it uses a special lilypond-like syntax) and plays the midi file using mido in a for loop. My question is how can I break the for loop if I press the play button again before it finished the for loop? I hope someone can tell me in (regarding the above example) how I should write it different...


RE: How to break out of a for loop on button press? - deanhystad - Oct-04-2022

According to your application you are not pressing a button.

In tkinter, mainloop() is the function that sees that you pressed a button and calls your function. Your process() function contains a loop that prevents mainloop() from running. According to your program you are not pressing a button because mainloop() does not get a chance check if you pressed a button. After pressing the play button your application is frozen.

The first thing you need to do is find a way to play the midi file without blocking mainloop(). You should post more information about what is involved in playing the file. Is there a way to start the playback and return immediately, or do you have to wait for completion of the file? If you can start playout and return immediately, is there a continuous play option? If no to all of those, you might need to create a separate thread for playing the midi file.


RE: How to break out of a for loop on button press? - woooee - Oct-04-2022

You would replace the for with a call to after(). This does not do exactly what you want but shows how to start and stop using a variable.

    import tkinter as tk

    class TimerTest():
        def __init__(self, root):
            self.root=root

            self.is_running=False
            self.count=tk.IntVar()
            self.max_seconds=60
            tk.Label(root, textvariable=self.count, font=('DejaVuSansMono', 12, "bold"),
                  bg="lightyellow").grid(row=1, column=0, columnspan=2, sticky="ew")

            tk.Button(root, text="Start", fg="blue", width=15,
                                command=self.startit).grid(row=10, column=0, sticky="nsew")
            tk.Button(root, text="Stop", fg="red", width=15,
                                command=self.stopit).grid(row=10, column=1, sticky="nsew")
            tk.Button(self.root, text="Quit", bg="orange",
                                command=self.root.quit).grid(row=11, column=0,
                                columnspan=2, sticky="nsew")

        def startit(self):
            if not self.is_running:  ## avoid 2 button pushes
                self.is_running=True
                self.increment_counter()

        def increment_counter(self):
            if self.is_running:
                 c=self.count.get() +1
                 self.count.set(c)
                 if c < self.max_seconds:
                     self.root.after(1000, self.increment_counter)  ## every second
                 else:
                     self.is_running=False
                     tk.Label(root, text="Time Is Up", font=('DejaVuSansMono', 14, "bold"),
                     bg="red").grid(row=5, column=0, columnspan=2, sticky="ew")

        def stopit(self):
            self.is_running = False
 
    root = tk.Tk()
    TT=TimerTest(root)
    root.mainloop() 



RE: How to break out of a for loop on button press? - deanhystad - Oct-04-2022

after() is only useful if you want to periodically do something that takes a small amount of time to complete. Like updating your label. It does not help when you need to perform a task that takes a long time.

Here I try to use after() to prevent my window from freezing when the start button is pressed. It is unsuccessful because the play() method takes 30 seconds to complete. Notice that pressing the start and stop buttons does nothing during this time, and you can't even close the window using the frame decorations.
import tkinter as tk
import time
 
class TimerTest(tk.Tk):
    def __init__(self):
        super().__init__()
 
        self.countdown = tk.IntVar(self, 0)
        tk.Label(self, textvariable=self.countdown).pack(padx=10, pady=5)
        tk.Button(self, text="Start", command=self.start).pack(padx=10, pady=5)
        tk.Button(self, text="Stop", command=self.stop).pack(padx=10, pady=5)
        self.counting = False
 
    def start(self):
        if not self.counting:
            print("Starting")
            self.counting = True
            self.after(1, self.play)

    def stop(self):
        print("Stopping")
        self.counting = False

    def play(self):
        for i in range(30, 0, -1):
            if not self.counting:
                break
            self.countdown.set(i)
            time.sleep(1)
        self.counting = False
  
TimerTest().mainloop()
Here's the same program, but the lengthy play function is run in a different thread so it doesn't block mainloop(). This program responds to the run button
import tkinter as tk
import time
import threading
 
class TimerTest(tk.Tk):
    def __init__(self):
        super().__init__()
 
        self.countdown = tk.IntVar(self, 0)
        tk.Label(self, textvariable=self.countdown).pack(padx=10, pady=5)
        tk.Button(self, text="Start", command=self.start).pack(padx=10, pady=5)
        tk.Button(self, text="Stop", command=self.stop).pack(padx=10, pady=5)
        self.counting = False
 
    def start(self):
        if not self.counting:
            print("Starting")
            self.counting = True
            threading.Thread(target=self.play).start()

    def stop(self):
        print("Stopping")
        self.counting = False

    def play(self):
        for i in range(30, 0, -1):
            if not self.counting:
                break
            self.countdown.set(i)
            time.sleep(1)
        self.counting = False
  
TimerTest().mainloop()
Knowing if after() or threading or something else is the best approach to this problem depends on how your midi player works.


RE: How to break out of a for loop on button press? - woooee - Oct-04-2022

If you want to use after() in the way the OP was going, it has to call itself.
def process():
    ## replace for with after
    ##for i in range(100):
    if some_condition:        ...
        root.after(milliseconds, process)
        # playing a midi-file here 
Or look at one of the tkinter video players like https://pypi.org/project/tkvideoplayer/ I don't use them so don't know, but this one says it has pause and stop commands.


RE: How to break out of a for loop on button press? - deanhystad - Oct-05-2022

This attempt assumes that playing a midi file consists of many fairly short events. I simulate these short events using sleep().
import tkinter as tk
import time

class MidiPlayer(tk.Tk):
    """Investigate using .after() to replace for loop in midi playout.
    Periodically plays a bit of a midi file (ok, it sleeps).  See how
    responsive the GUI is when it spends most of its time waiting for
    something to complete.
    """
    def __init__(self):
        super().__init__()

        self.countdown = tk.IntVar(self, 0)
        self.after_event = None
        tk.Label(self, textvariable=self.countdown).pack(padx=10, pady=5)
        tk.Button(self, text="Start", command=self.start).pack(padx=10, pady=5)
        tk.Button(self, text="Stop", command=self.stop).pack(padx=10, pady=5)

    def start(self):
        "Start playing"
        if self.after_event:
            self.after_cancel(self.after_event)
        self.countdown.set(30)
        self.play()

    def stop(self):
        "Stop playing"
        if self.after_event:
            self.after_cancel(self.after_event)
            self.after_event = None
        self.countdown.set(0)

    def play(self):
        # Using sleep in place of play
        time.sleep(1)

        # Update display so we can see someing happen
        countdown = self.countdown.get()

        # Implement for loop using .after()
        if countdown > 0:
            self.countdown.set(countdown-1)
            self.after_event = self.after(1, self.play)
        else:
            self.after_event = None

MidiPlayer().mainloop()
The GUI's responsiveness depends on how long the event is. While sleeping, which is meant to simulate calling a function to play some part of the midi file, the GUI is sleeping. if you press a button, the button press is queued, but not acted upon. When the event is done, the play function returns and mainloop() processes queued events.

If playing part of your midi file takes a very short period of time (less than 0.1 seconds) the GUI will feel fairly responsive. If playing part of the midi file takes longer, the response will be sluggish.


RE: How to break out of a for loop on button press? - philipbergwerf - Oct-06-2022

Thank you all for these examples! I managed to get it working using a thread :)