Python Forum
[Tkinter] How to terminate a loop in python with button in GUI
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] How to terminate a loop in python with button in GUI
#1
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 ?
Reply
#2
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.
Reply
#3
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 ?
Reply
#4
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
Reply
#5
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)
Reply
#6
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()
Reply
#7
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()
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [PyQt] Loop triggered by button help Purple0 1 2,317 May-17-2020, 02:57 AM
Last Post: deanhystad
  [Tkinter] Binding Entry box to <Button-3> created in for loop iconit 5 4,962 Apr-22-2020, 05:47 AM
Last Post: iconit
  [PySimpleGui] How to alter mouse click button of a standard submit button? skyerosebud 3 4,997 Jul-21-2019, 06:02 PM
Last Post: FullOfHelp
  [Tkinter] loop function when called from tkinter button click WantedStarling 5 8,968 Jul-13-2018, 06:12 PM
Last Post: nilamo

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020