stoping infinite loop function using another button - python

I am writing a function which should return two other functions in it until I decide to stop it. Maybe I even want the function to run 5 hours. I write my code, and it runs perfectly except for one problem: when I click on the starting button, the button stays pressed and I cannot close the infinite loop. I want a way to stop my program without doing key-interrupting or something else. I think a button which can stop my started process would be fine.
Here is my button:
self.dugme1 = Button(text="Start ", command=self.start, fg="black", bg="green", font="bold")
self.dugme1.place(relx=0.05, rely=0.65)
Here are my functions:
def greeting(self):
print("hello")
def byee (self):
print("bye")
def start(self):
while True:
self.greeting()
self.byee()
When I click the button these will be run in the terminal infinitely until I stop them using keyboard interrupting. Is there any way to stop it using an elegant way such as a stop button?

You can achieve this with threading. The following code demonstrates a start/stop button for an infinitely running process:
import tkinter as tk
import threading
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.stop_event = threading.Event()
self.thread = threading.Thread(target = self.start, args = (self.stop_event,))
self.btn = tk.Button(self, text = "Start", command = self.start_cmd)
self.btn.pack()
def start_cmd(self):
self.btn.config(text = "Stop", command = self.stop_cmd)
self.thread.start()
def stop_cmd(self):
self.stop_event.set()
self.destroy()
def start(self, stop_event):
while not stop_event.is_set():
self.greeting()
self.byee()
def greeting(self):
print("hello")
def byee(self):
print("bye")
app = App()
app.mainloop()

You can use threading on start function. I am sure you'll find more information about this here on over the internet.
Example below:
import threading
self.dugme1 = Button(text="Start ",
command=lambda: threading.Thread(name='start', target=self.start, daemon=True).start(),
fg="black",
bg="green",
font="bold"
)
then you can have your start function as:
def start(self):
if self.start_running == False:
self.start_running = True
while self.start_running:
self.greeting()
self.byee()
where self.start_running is a variable that is initially set to false at the beginning of your program (or you can set declare it first in start function with a try-except block)
the first if avoids multiple calls to the same button, calling the function more than once.
You can turn "false" the variable self.start_running to stop the execution of start function.
We want the thread to be daemon, so that the function is terminated whenever main terminates.
If you don't want those function to exit abruptly, turn daemon=False, but make sure to signal self.start_running=False, before your program exits.

Related

Python: Cannot join thread after redirected output to tkinter

I have a problem (Python just hang) if I enable the line "thread.join()" after redirected output to tkinter.
Here is the code:
import tkinter as tk
import sys
import threading
def run():
thread = threading.Thread(target=test)
thread.start()
# thread.join()
print('Want to add some code here')
def test():
print('Process thread...')
class Redirect():
def __init__(self, widget):
self.widget = widget
def write(self, text):
self.widget.insert('end', text)
#def flush(self):
# pass
root = tk.Tk()
text = tk.Text(root)
text.pack()
button = tk.Button(root, text='TEST', command=run)
button.pack()
old_stdout = sys.stdout
sys.stdout = Redirect(text)
root.mainloop()
sys.stdout = old_stdout
Current it prints out:
Want to add some code here
Process thread...
And what I expected (when using thread.join(after finished the thread):
Process thread...
Want to add some code here
If I don't use Redirect function, then the case work well, so I guess I need to add something into the class Redirect above.
Can you please help to take a look and give yur comment?
Thanks
If you join the thread, tkinter will be held up until the thread is finished, but you can't start the thread either because it won't give the desired output. The solution is to use a global variable to keep track of whether the thread has finished or not, then check it periodically with root.after.
def run():
global completed
thread = threading.Thread(target=test)
thread.start()
completed = False
wait_for_finish()
def wait_for_finish():
global completed
if completed:
print('Want to add some code here')
else:
root.after(100, wait_for_finish)
def test():
global completed
print('Process thread...')
completed = True
This calls wait_for_finish every 100ms to check if completed is True. When it is, it runs the next part of the code.

Threading not successful

I'm doing Tkinter. My functions run correctly (playing a bell sound once a minute), but it's not running on a thread. Whenever I click the Start button, the program window grays out and the top says "Not responding" (because my recursive calls to start() are locking it, I'm assuming.)
Why am I not threading correctly? Thanks.
def start():
now = tt1.time()
listOfTimes = []
for t in timeSlotEFs:
listOfTimes.append(t.get())
now = datetime.now()
timee = now.strftime('%H%M')
print('here')
for t in listOfTimes:
if t==timee:
winsound.PlaySound('newhourlychimebeg.wav',winsound.SND_FILENAME)
s.enterabs(now+60,1,start) #I want to call recursively every 60 seconds
s.run()
def start2():
t = threading.Thread(target=start)
t.run()
startBtn = Button(ssFrame, text='Start', command=start2)
startBtn.grid(row=0,column=0,padx=paddX,pady=paddY)
It feels like you mixed between the definitions that threading.Thread imports from.
You should create run function and then start the thread.
This way i see the result and it works:
from tkinter import *
import threading
root = Tk()
def run():
print('hello Thread')
def start2():
t = threading.Thread(target=run)
t.start()
print(t) # Output: <Thread(Thread-1, stopped 10580)>
startBtn = Button(root, text='Start', command=start2)
startBtn.grid(row=0,column=0)
root.mainloop()
In start2(), I changed t.run() to t.start(). That was it, and it now works. Thx Jim.

'Confirm' a user click

I saw that when you press the button on the Television remote it doesn't go to the channel immediately, after some time it lands on the channel, but if you press the button within a certain time period it doesn't land.
Is there a way to do this in tkInter, where if the user clicks the button within a time period (in this case two seconds), the function doesn't get executed unless the user does not click the button.
To clarify, I want to delay command execution by 2 seconds so you can cancel it again if the user presses the same button again.
You can always disconnect buttons and actions, and put a timer in-between that can be cancelled. Tkinter offers the Widget.after() method to invoke a callback with a delay. You can also use a thread for this to give you more control.
The following implementation can be used as callback for any tkInter element; it takes a a delay (in seconds as a float) and callback to call once the delay has passed without another click. Two further optional callbacks are called immediately when the DelayedCancellable() is called, and when cancelled:
import time
from threading import Thread, Event
class DelayedCancellable:
def __init__(self, delay, after_delay, invoked=None, cancelled=None):
super().__init__()
self._after_delay = after_delay
self._on_invoked = invoked
self._on_cancelled = cancelled
self._delay = delay
self._thread = None
self._cancelled = Event()
def __call__(self):
if self._thread is not None and self._thread.is_alive():
self._cancelled.set()
else:
if self._on_invoked is not None:
self._on_invoked()
self._thread = Thread(target=self._delayed_execution)
self._thread.start()
def _delayed_execution(self):
try:
if self._cancelled.wait(self._delay):
# we got cancelled, exit
if self._on_cancelled is not None:
self._on_cancelled()
return
self._after_delay()
finally:
self._thread = None
self._cancelled.clear()
When using it, pass in a delay in seconds as a floating point number, and the callback:
from tkinter import Button
def callback():
print("Hello, world!")
b = Button(
master,
text="Click me!",
command=DelayedCancellable(2.0, callback)
)
The above button will cause Hello, world! to be printed, unless you click the button again within 2 seconds.
The extra invoked and cancelled actions could be used to update the UI so there is a bit more feedback for the user:
from tkinter import Button, RAISED, SUNKEN
def button_invoked():
b.config(relief=SUNKEN)
def button_cancelled():
b.config(relief=RAISED)
def callback():
button_cancelled() # clear state
print("Hello, world!")
b = Button(
master,
text="Click me!",
command=DelayedCancellable(2.0, callback, button_invoked, button_cancelled)
)
A different approach from Martijn Pieters. I've used .after to schedule an event to happen after 2 seconds rather than a seperate thread.
Clicking the button once will cause the ConfirmedAction function to be called after 2 seconds. Clicking the button a second time, within the 2 second window, will cancel the timer.
import tkinter as tk
def ConfirmedAction():
print("Doing something")
class ConfirmButton(tk.Button):
def __init__(self,master,**kw):
self.confirmcommand = kw.pop('confirm',None)
super(ConfirmButton, self).__init__(master, **kw)
self.timer = None
self['command'] = self._clicked
def _clicked(self):
if not self.timer:
self.timer = self.after(2000,self._doAction)
else:
self.after_cancel(self.timer)
self.timer = None
print("Action Cancelled")
def _doAction(self):
self.confirmcommand()
self.timer = None
root = tk.Tk()
btn1 = ConfirmButton(root,text="Hello",confirm=ConfirmedAction)
btn1.grid()
root.mainloop()

Python multiprocessing asynchronous callback

I'm writing a program with a GUI using TKinter, in which the user can click a button and a new process is started to perform work using multiprocess.Process. This is necessary so the GUI can still be used while the work is being done, which can take several seconds.
The GUI also has a text box where the status of the program is displayed when things happen. This is often straight forward, with each function calling an add_text() function which just prints text in the text box. However, when add_text() is called in the separate process, the text does not end up in the text box.
I've thought about using a Pipe or Queue, but that would require using some sort of loop to check if anything has been returned from the process and that would also cause the main (GUI) process to be unusable. Is there some way to call a function in one process that will do work in another?
Here's an simple example of what I'm trying to do
import time
import multiprocessing as mp
import tkinter as tk
textbox = tk.Text()
def add_text(text):
# Insert text into textbox
textbox.insert(tk.END, text)
def worker():
x = 0
while x < 10:
add_text('Sleeping for {0} seconds'.format(x)
x += 1
time.sleep(1)
proc = mp.Process(target=worker)
# Usually happens on a button click
proc.start()
# GUI should still be usable here
The asyncronous things actually require loop.
You could attach function to the TkInter's loop by using Tk.after() method.
import Tkinter as tk
class App():
def __init__(self):
self.root = tk.Tk()
self.check_processes()
self.root.mainloop()
def check_processes(self):
if process_finished:
do_something()
else:
do_something_else()
self.after(1000, check_processes)
app=App()
I ended up using a multiprocessing.Pipe by using TKinter's after() method to perform the looping. It loops on an interval and checks the pipe to see if there's any messages from the thread, and if so it inserts them into the text box.
import tkinter
import multiprocessing
def do_something(child_conn):
while True:
child_conn.send('Status text\n')
class Window:
def __init__(self):
self.root = tkinter.Tk()
self.textbox = tkinter.Text()
self.parent_conn, child_conn = multiprocessing.Pipe()
self.process = multiprocessing.Process(target=do_something, args=(child_conn,))
def start(self):
self.get_status_updates()
self.process.start()
self.root.mainloop()
def get_status_updates()
status = self.check_pipe()
if status:
self.textbox.add_text(status)
self.root.after(500, self.get_status_updates) # loop every 500ms
def check_pipe():
if self.parent_conn.poll():
status = self.parent_conn.recv()
return status
return None

Python Tkinter: How do I make my GUI responsive as long as a thread runs?

For example:
import threading
import time
import Tkinter
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
print "Step Two"
time.sleep(20)
class MyApp(Tkinter.Tk):
def __init__(self):
Tkinter.Tk.__init__(self)
self.my_widgets()
def my_widgets(self):
self.grid()
self.my_button = Tkinter.Button(self, text="Start my function",
command=self.my_function)
self.my_button.grid(row=0, column=0)
def my_function(self):
print "Step One"
mt = MyThread()
mt.start()
while mt.isAlive():
self.update()
print "Step Three"
print "end"
def main():
my_app = MyApp()
my_app.mainloop()
if __name__ == "__main__":
main()
Well, if I start my example it works as expected. I click on a button, my_function starts and the GUI is responsive. But I have read that I should avoid the use of update(). So, it would be nice if some one could explain why and how I have to wait for a thread correctly? Step Two is in a thread because it takes a much longer time than Step One and Step Three, otherwise it would block the GUI.
I'm new to Python and I trying to write my first "program". Maybe I'm thinking in a wrong way since I'm not very experienced...
Regards,
David.
You need to remember that you have an event loop running, so all you need to do is check on the thread each time the event loop makes an iteration. Well, not every time, but periodically.
For example:
def check_thread(self):
# Still alive? Check again in half a second
if self.mt.isAlive():
self.after(500, self.check_thread)
else:
print "Step Three"
def my_function(self):
self.mt = MyThread()
self.mt.start()
self.check_thread()

Categories

Resources