2

I want to come out of the loop immediately after pressing the stop button. But with this code, i could able to come out only after executing current iteration and next iteration.

It is very important for my application since iam going to use this for automating instruments, where operations have to be stopped immediately after pressing stop button.

# -*- coding: utf-8 -*-
"""
Created on Sun Jun 17 17:01:12 2018

@author: Lachu
"""

import time
from tkinter import *
from tkinter import ttk

root=Tk()

def start():

    global stop_button_state

    for i in range(1,20):
        if (stop_button_state==True):
            break
        else:
            print('Iteration started')
            print('Iteration number: ', i)

            root.update()
            time.sleep(10)
            print('Iteration completed \n')          

def stop_fun():
    global stop_button_state
    stop_button_state=True


start=ttk.Button(root, text="Start", command=start).grid(row=0,column=0,padx=10,pady=10)

p=ttk.Button(root, text="Stop", command=stop_fun).grid(row=1,column=0)

stop_button_state=False

root.mainloop()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • You have to use thread – Smart Manoj Jun 17 '18 at 12:55
  • 1
    The `.grid` method returns `None`, so with `start=ttk.Button(root, text="Start", command=start).grid(row=0,column=0,padx=10,pady=10)` you set `start` to `None`. And you shouldn't try to use `start` for the name of the button and the name of a function. It doesn't really hurt here, since you never need the function name after you pass it into the button constructor. OTOH, why even bother naming the button, you don't use that name anywhere? – PM 2Ring Jun 17 '18 at 12:57
  • In this code you can press the Start button again when the `start` function is running, so that multiple iterations can happen at the same time. Is that intentional? – PM 2Ring Jun 17 '18 at 13:26
  • 2
    research the tkinter `after` method which lets you schedule functions to run in the future or by an interval. You should never use `sleep` in a GUI program because it does exactly what it says: it puts the _whole_ program to sleep. – Bryan Oakley Jun 17 '18 at 14:33
  • @BryanOakley I guess I should have used `.after`, but I thought it might be fun to do this with `threading`. ;) – PM 2Ring Jun 17 '18 at 14:36

3 Answers3

2

It's generally not a good idea to use time.sleep with GUI programs because it puts everything to sleep, so the GUI can't update itself, or respond to events. Also, it gets messy when you want to interrupt sleep.

I've adapted your code to use a Timer from the threading module. We can easily interrupt this Timer instantly, and it doesn't block the GUI.

To make this work, I moved your counting for loop into a generator.

If you press the Start button while a count is in progress it will tell you that it's already counting. When a count cycle is finished, either by pressing Stop, or by getting to the end of the numbers, you can press Start again to start a new count.

import tkinter as tk
from tkinter import ttk
from threading import Timer

root = tk.Tk()

delay = 2.0    
my_timer = None

# Count up to `hi`, one number at a time
def counter_gen(hi):
    for i in range(1, hi):
        print('Iteration started')
        print('Iteration number: ', i)
        yield
        print('Iteration completed\n')

# Sleep loop using a threading Timer
# The next `counter` step is performed, then we sleep for `delay`
# When we wake up, we call `sleeper` to repeat the cycle
def sleeper(counter):
    global my_timer
    try:
        next(counter)
    except StopIteration:
        print('Finished\n')
        my_timer = None
        return
    my_timer = Timer(delay, sleeper, (counter,))
    my_timer.start()

def start_fun():
    if my_timer is None:
        counter = counter_gen(10)
        sleeper(counter)
    else:
        print('Already counting')

def stop_fun():
    global my_timer
    if my_timer is not None:
        my_timer.cancel()
        print('Stopped\n')
        my_timer = None

ttk.Button(root, text="Start", command=start_fun).grid(row=0, column=0, padx=10, pady=10)
ttk.Button(root, text="Stop", command=stop_fun).grid(row=1,column=0)

root.mainloop()
PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
  • It serves the purpose. Please explain how ''my_timer = Timer(delay, sleeper, (counter,))'' is working here. – mahesh chaluvadi Jun 18 '18 at 15:28
  • @maheshchaluvadi It creates a Timer which sleeps for `delay` seconds and then calls the `sleeper` function, passing it the `counter` generator as its argument. The Timer runs in its own thread. The `.start` call starts the Timer, and returns immediately, it doesn't wait like `time.sleep` does, so the GUI doesn't freeze. – PM 2Ring Jun 18 '18 at 15:36
  • I understood the functionality after your explanation and working out on the program for some time with multiple changes. Thanks a lot . It completely fulfills my requirement. – mahesh chaluvadi Jun 19 '18 at 07:09
  • @maheshchaluvadi I'm glad you like it. For this case, Reblochon Masque's solution using `.after` is a bit simpler, but it's useful to know how to use threading with Tkinter for more complex situations. – PM 2Ring Jun 19 '18 at 07:14
  • yes, but the problem in that approach is , it doesn't instantly come out of the loop. It has to complete the current iteration. It will come out only after that. – mahesh chaluvadi Jun 20 '18 at 04:13
  • @maheshchaluvadi Very true, it can take up to a second for his code to respond, although you could reduce the `.after` delay arg to 100, that is 0.1 seconds. But if you prefer my solution you can choose it as the accepted answer. ;) – PM 2Ring Jun 20 '18 at 04:24
  • @maheshchaluvadi To be fair, I should mention it's possible to cancel `.after`. See http://effbot.org/tkinterbook/widget.htm#Tkinter.Widget.after_cancel-method – PM 2Ring Jun 20 '18 at 04:29
  • wow, its great. Thanks for such a transparent and clear suggestions. You have now opened up a new avenue for me to reduce the code. – mahesh chaluvadi Jun 20 '18 at 15:25
1

You are probably better off using root.after than threads:

In any events, as other pointed out, using time.sleep is a bad idea in a GUI.

You should also not name your buttons the same as your functions.

calling root.update, is also not necessary here.

from tkinter import *
from tkinter import ttk


def start_process(n=0, times=10):
    n += 1
    if not stop_button_state and n < times:
        print('Iteration started')
        print(f'Iteration number: {n}')
        print('Iteration completed \n')
        root.after(1000, start_process, n)
    else:
        print('stopping everything')


def stop_fun():
    global stop_button_state
    stop_button_state = True


if __name__ == '__main__':

    root = Tk()

    start = ttk.Button(root, text="Start", command=start_process)
    start.grid(row=0, column=0, padx=10, pady=10)
    p = ttk.Button(root, text="Stop", command=stop_fun)
    p.grid(row=1, column=0)

    stop_button_state = False

    root.mainloop()
Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80
  • Yes, that's a bit simpler than my version. But you "cheated" by using `n += 1` instead of a generator. :D – PM 2Ring Jun 17 '18 at 17:51
  • Sweet. Just one thing, formatted string literals prefixed with an 'f' were only introduced in python 3.6 – Rolf of Saxony Jun 17 '18 at 18:31
  • That's great. It fulfilled half the requirements. It is not coming out of the loop immediately. It comes only after executing the current iteration. Is it possible to come out immediately by slightly changing the code? – mahesh chaluvadi Jun 18 '18 at 08:55
  • yes, you would need to provide a value for the keyword argument `times` – Reblochon Masque Jun 18 '18 at 08:57
-1

Without using a separate thread, you could always iterate over the sleep command, which would make the code more responsive.
e.g. This would reduce your wait between clicking stop and loop exit to 1/10th of a second, whilst retaining a 10 second gap between loops.

# -*- coding: utf-8 -*-
"""
Created on Sun Jun 17 17:01:12 2018

@author: Lachu
"""

import time
from tkinter import *
from tkinter import ttk

root=Tk()
stop_button_state=False

def start():

    global stop_button_state
    for i in range(1,20):
        if (stop_button_state==True):
           break
        print('Iteration started')
        print('Iteration number: ', i)
        for i in range(100):
            root.update()
            time.sleep(0.1)
            if (stop_button_state==True):
                break
        print('Iteration completed \n')

def stop_fun():
    global stop_button_state
    stop_button_state=True

ttk.Button(root, text="Start", command=start).grid(row=0,column=0,padx=10,pady=10)
ttk.Button(root, text="Stop", command=stop_fun).grid(row=1,column=0)
root.mainloop()
Rolf of Saxony
  • 21,661
  • 5
  • 39
  • 60
  • 1
    You shouldn't ever call `sleep` in the GUI thread because tkinter cannot process any events while the program is sleeping. – Bryan Oakley Jun 17 '18 at 14:34
  • @BryanOakley Given that OP has asked a question specifically using sleep, I elected to provide an answer that fits the question. I don't use tkinter but I thought that `root.update()` returns control momentarily to the main loop. – Rolf of Saxony Jun 17 '18 at 14:40
  • 1
    I've probably posted 100 answers showing how to use `after` on this site, there is probably nothing I could write here that would improve on any of my other answers. – Bryan Oakley Jun 17 '18 at 15:24
  • @BryanOakley Then how about posting an appropriate link to one of those? https://stackoverflow.com/questions/2400262/how-to-create-a-timer-using-tkinter/2401181#2401181 – Rolf of Saxony Jun 17 '18 at 15:43