14

So I have a Tkinter GUI with two simple options, a start and stop button. I have defined the GUI layout:

from Tkinter import *

def scanning():
    while True:
        print "hello"

root = Tk()
root.title("Title")
root.geometry("500x500")

app = Frame(root)
app.grid()

Here the Start button runs the infinite loop scanning, and the Stop button should break on press:

start = Button(app, text="Start Scan",command=scanning)
stop = Button(app, text="Stop",command="break")

start.grid()
stop.grid()

However, when I hit the Start button, it is always pushed down (assuming because of the infinite loop). But, I cannot click on the Stop button to break out of the while loop.

Jonathan Davies
  • 882
  • 3
  • 12
  • 27

4 Answers4

19

You cannot start a while True: loop in the same thread that the Tkinter event loop is operating in. Doing so will block Tkinter's loop and cause the program to freeze.

For a simple solution, you could use Tk.after to run a process in the background every second or so. Below is a script to demonstrate:

from Tkinter import *

running = True  # Global flag

def scanning():
    if running:  # Only do this if the Stop button has not been clicked
        print "hello"

    # After 1 second, call scanning again (create a recursive loop)
    root.after(1000, scanning)

def start():
    """Enable scanning by setting the global flag to True."""
    global running
    running = True

def stop():
    """Stop scanning by setting the global flag to False."""
    global running
    running = False

root = Tk()
root.title("Title")
root.geometry("500x500")

app = Frame(root)
app.grid()

start = Button(app, text="Start Scan", command=start)
stop = Button(app, text="Stop", command=stop)

start.grid()
stop.grid()

root.after(1000, scanning)  # After 1 second, call scanning
root.mainloop()

Of course, you may want to refactor this code into a class and have running be an attribute of it. Also, if your program becomes anything complex, it would be beneficial to look into Python's threading module so that your scanning function can be executed in a separate thread.

  • I should have added that I NEED the while loop, as it is scans for bluetooth RSSI signals continuously. Therefore, this program does not work for me. Is there any other way around this with a while loop? @iCodez – Jonathan Davies Nov 21 '14 at 18:49
  • @JonathanDavies - Well, you could always change `root.after(1000, scanning)` to `root.after(1, scanning)` to have the code execute `scanning` every millisecond. This will have about the same effect as a continuous while loop. Otherwise, you will need to put your loop in a separate thread. The link I gave above has more information, but basically you would put the loop in a function and then give the function to `threading.Thread`. –  Nov 21 '14 at 18:52
  • So from what I gather I simply do is this: start = Button(app, text="Start Scan",command=threading.Thread(name="bluetooth",target=scanning)). This does not work, and if I add .start() after the Thread(), it starts the function straight away without me clicking. @iCodez – Jonathan Davies Nov 21 '14 at 19:06
  • 1
    No, you would put your loop in `scanning` and then do `thread = threading.Thread(name="bluetooth",target=scanning); thread.start()`. This will have the loop execute in a different thread. Hooking it up to the buttons is going to be a little more tricky. You will need to use a thread-safe container such as [`Queue.Queue`](https://docs.python.org/2/library/queue.html) to send messages to the worker thread. –  Nov 21 '14 at 19:15
  • OK I am totally lost, the start button launches scanning with the thread command inside it? I've tried that and it does not work. I've tried `command=thread.start()` but this just starts straight away, even without a button click. @iCodez – Jonathan Davies Nov 21 '14 at 19:49
  • 1
    That's because you are calling `thread.start` when you create the button. You need to do `command=thread.start`. But I think a better approach would be to create the worker thread when you start the program and then have it continually check the queue. The start and stop buttons would just deposit messages into the queue to tell the worker to start or stop scanning. –  Nov 21 '14 at 19:51
  • With help from you I have managed to get the Start and Stop buttons working. I also used help from this [link.](https://stackoverflow.com/questions/15268882/loop-while-button-down-in-tkinter-python) Thanks for all of your help @iCodez ! – Jonathan Davies Nov 21 '14 at 21:00
6

Here is a different solution, with the following advantages:

  1. Does not require manually creating separate threads

  2. Does not use Tk.after calls. Instead, the original style of code with a continuous loop is preserved. The main advantage of this is that you do not have to manually specify a number of milliseconds that determines how often your code inside the loop runs, it simply gets to run as often as your hardware allows.

Note: I've only tried this with python 3, not with python 2. I suppose the same should work in python 2 too, I just don't know 100% for sure.

For the UI code and start/stopping logic, I'll use mostly the same code as in iCodez' answer. An important difference is that I assume we'll always have a loop running, but decide within that loop what to do based on which buttons have been pressed recently:

from tkinter import *

running = True  # Global flag
idx = 0  # loop index

def start():
    """Enable scanning by setting the global flag to True."""
    global running
    running = True

def stop():
    """Stop scanning by setting the global flag to False."""
    global running
    running = False

root = Tk()
root.title("Title")
root.geometry("500x500")

app = Frame(root)
app.grid()

start = Button(app, text="Start Scan", command=start)
stop = Button(app, text="Stop", command=stop)

start.grid()
stop.grid()

while True:
    if idx % 500 == 0:
        root.update()

    if running:
        print("hello")
        idx += 1

In this code, we do not call root.mainloop() to have the tkinter GUI continually updating. Instead, we manually update it every so often (in this case, every 500 loop iterations).

Theoretically, this means we may not instantly stop the loop as soon as we hit the Stop button. For example, if at the exact moment where we hit the Stop button, we're at iteration 501, this code will continue looping until iteration 1000 has been hit. So, the disadvantage of this code is that we have a slighlty less responsive GUI in theory (but it will be unnoticeable if the code within your loop is fast). In return, we get the code inside the loop to run almost as fast as possible (only with sometimes overhead from a GUI update() call), and have it running inside the main thread.

Dennis Soemers
  • 8,090
  • 2
  • 32
  • 55
  • This got me to a solution where when I have a loop going in matplotlib I was unable to access the tkinter events. Including root.update() in the loop made the program responsive again for these events. – Bruno Vermeulen Jul 01 '19 at 14:43
4

The best way is to use Thread and global variable. Your code was modified to include those. Hope it helps.

    from tkinter import *
    from threading import Thread

    def scanning():
        while True:
            print ("hello")
            if stop == 1:   
                break   #Break while loop when stop = 1

    def start_thread():
        # Assign global variable and initialize value
        global stop
        stop = 0

        # Create and launch a thread 
        t = Thread (target = scanning)
        t.start()

    def stop():
        # Assign global variable and set value to stop
        global stop
        stop = 1

    root = Tk()
    root.title("Title")
    root.geometry("500x500")

    app = Frame(root)
    app.grid()

    start = Button(app, text="Start Scan",command=start_thread)
    stop = Button(app, text="Stop",command=stop)

    start.grid()
    stop.grid()
Khairil
  • 41
  • 1
0

Another solution is to create an executable that performs the function, and the while is NOT a while-true, but a condition that reads from outside (e.g. a binary file using pickle)

condition = True
while condition:
    condition = pickle.load(open(condition.p,'rb'))
    print('hello from executable')
# endwhile condition

So, from the GUI you have a button that calls method 'pause'. It modifies the content of the file 'condition.p', and therefore the desired loop

def pause(self):
    self.condition = not self.condition
    pickle.dump(self.condition, open('condition.p','wb'))
    if self.condition == True: # reset infinite loop again! :)
        os.system('executable.exe')
# enddef pause