Python Forum

Full Version: How to terminate a loop in python with button in GUI
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Hi, in my GUI I have a function with a while loop inside which is ruining when START button is pressed. The function drive a microcontroller continuously. I am wondering is it possible to terminate the running function when the STOP button is pressed ?
A while loop will block a GUI's mainloop, the while loop won't block if it running in a separate thread.
A flag variable that can be set outside of the while loop, can be checked in the while loop to decide if the while loop should continue looping or stop.
Yes exactly, this I want to implement.

A flag variable can be set outside of the while loop when the STOP button is pressed while the while loop is running in a separate thread. In side the while loop the flag variable decide the while loop should continue looping or stop.

Question how to run a while loop in a separate thread when START button in GI is pressed and how to assign the main GUI in another thread ?
You don't mention which GUI framework you are using, the following links give an idea of a way to use threads with tkinter and wxpython:
[Tkinter] How to deal with code that blocks the mainloop, freezing the gui
[WxPython] How to deal with code that blocks the mainloop, freezing the gui
I am using tkinter. Here is my code. There are two buttons on the GUI. STRAT and STOP. If I press START then a counter up to 30 should be printed on the shell but if I press STOP anytime before the counter reach 30 then the loop should terminate and back to the man loop of GUI. Where should I add thread ?

import time

from tkinter import * 
root = Tk()
root.title("Loop Terminate")

time.sleep(0.5)

# Function button_stop 
def button_stop():
# If the STOP button is pressed then terminate the loop
  i = 1

# Function button_start 
def button_start():
  j = 1
  while j <= int(30):
    print("Loop Index = " + str(j))
    time.sleep(0.5)
    j = j+1

# Button START
button_start =  Button(root, text = "START", padx=53, pady=20, command=button_start)
button_start.grid(columnspan=1, row=1,column=0)

# Button STOP
button_stop =  Button(root, text = "STOP", padx=44, pady=20, command=button_stop)
button_stop.grid(row=2,column=0)
Please see Namespace flooding with * imports
Here is an example of what you want to do
import threading
import time
import tkinter as tk
from dataclasses import dataclass, field

btn_state = {True: "normal", False: "disabled"}


@dataclass
class Task:
    sleep_duration: float = field(default=0.5)
    _run_loop: bool = field(default=False, init=False)

    def start(self) -> None:
        self._run_loop = True
        for number in range(1, 31):
            if not self._run_loop:
                return
            print(f"Loop index = {number}")
            time.sleep(self.sleep_duration)
        self._run_loop = False

    def stop(self) -> None:
        self._run_loop = False

    def threading_start(self) -> None:
        thread = threading.Thread(target=self.start)
        thread.start()

    @property
    def running(self) -> bool:
        return self._run_loop


class TaskFrame(tk.Frame):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, *kwargs)
        self.task = Task()
        self._create_contrls()
        self._create_layout()
        self._create_binds()
        self._update_btn_states()

    def _create_contrls(self) -> None:
        self.btn_start = tk.Button(master=self, text="START", padx=53, pady=20)
        self.btn_stop = tk.Button(master=self, text="STOP", padx=44, pady=20)

    def _create_layout(self) -> None:
        self.btn_start.grid(columnspan=1, row=1, column=0, padx=2, pady=2)
        self.btn_stop.grid(row=2, column=0, padx=2, pady=2)

    def _create_binds(self) -> None:
        self.btn_start.bind("<Button-1>", self._on_btn_start)
        self.btn_stop.bind("<Button-1>", self._on_btn_stop)

    def _on_btn_start(self, event: tk.Event) -> None:
        if self.task.running:
            return
        self.task.threading_start()

    def _on_btn_stop(self, event: tk.Event) -> None:
        if not self.task.running:
            return
        self.task.stop()

    def _update_btn_states(self) -> None:
        self.btn_start.configure(state=btn_state[not self.task.running])
        self.btn_stop.configure(state=btn_state[self.task.running])
        self.after(500, self._update_btn_states)


def main():
    app = tk.Tk()
    app.title("Loop Terminate")
    main_frame = TaskFrame(app)
    main_frame.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
Your program doesn't work because tkinter processes things like button presses while your program is not running. If you enter a loop, tkinter stops running and waits for the loop to complete. To know that a button was pressed you cannot be waiting for it to happen.

You can get around this by having a thread for the GUI and another for the background task, but is it required? Why are you waiting? Could you implement the logic in some other way. I would implement your example using tkinter.after().
import tkinter as tk

class Counter():
    '''Periodically print counter value to stdout'''
    def __init__(self, parent, end, start=0, increment=1, interval=1000):
        self.parent = parent
        self.start_value = start
        self.end_value = end
        self.increment = -increment if (end - start) * increment < 0 else increment
        self.interval = interval
        self.value = start
        self.running = False

    def start(self):
        '''Start counter'''
        self.value = self.start_value
        self.running = True
        self.doit()

    def stop(self):
        '''Stop counter'''
        self.running = False

    def doit(self):
        '''Called periodically to incrementer counter and print value'''
        if self.running:
            print('Counter value =', self.value)
            self.value += self.increment
            self.running = (self.end_value - self.value) * self.increment > 0
            if self.running:
                self.parent.after(self.interval, self.doit)

root = tk.Tk()
root.title("Loop Terminate")
counter = Counter(root, 0, 10)

# Function button_stop
def button_stop():
    counter.stop()

# Function button_start
def button_start():
    counter.start()

# Button START
button_start =  tk.Button(root, text = "START", padx=53, pady=20, command=button_start)
button_start.grid(columnspan=1, row=1,column=0)

# Button STOP
button_stop =  tk.Button(root, text = "STOP", padx=44, pady=20, command=button_stop)
button_stop.grid(row=2,column=0)

root.mainloop()