Let user force stop a loop - python-3.x

I am making an app that uses tkinter for interface
the interface has two buttons, one that says 'calculate' and one that says 'stop'.
the Calculate button fires a calculate() which has a recursive call to itself, which makes it either an infinite loop, or a very deep loop. I want the user to be able to be able to make the calculation stop throw the button 'stop'.
def init():
btnCalculate = Button(myframe, text="Caculate", command= Calculate, width=10)
btnStop = Button(myframe, text="Stop", command= Stop, width=10)
btnCalculate.place(x=0, y=0)
btnStop.place(x=100, y=0)
def Calculate():
Calculate(para)
def Calculate(para):
# do some stuff
# check condition
if condition:
Calculate(para)
def Stop():
return

Recursion blocks event loops that a GUI must use to do its work. So here is a modification of what you have that uses event scheduling to do what you want. To simulate recursion, I repeatedly put a call to the Calculate method on the event stack for processing. You can change the frequency from 1000 (milliseconds) to whatever you need.
from tkinter import *
stop = False
def init():
btnCalculate = Button(myframe, text="Calculate", command=Calculate, width=10)
btnStop = Button(myframe, text="Stop", command= Stop, width=10)
btnCalculate.pack()
btnStop.pack()
#def Calculate():
# Calculate(para)
def Calculate(*args):
global stop
# do some stuff
# check condition
if not stop:
print("Calculating...")
root.after(1000, lambda a=args: Calculate(a))
def Stop():
global stop
print('Stopping')
stop = True
root = Tk()
myframe = Frame(root)
myframe.pack()
init()
root.mainloop()

Related

Python tkinter, how to display the message one by one

I'm using tkinter (Python version 3.9) to build an application. In the application, I want the message can be shown one by one according to the progress of the program. However, my application now can only print all the messages together.
The example code is listed as below:
import time
import tkinter as tk
def start():
txt_edit.delete(1.0, tk.END)
for _ in range(10):
txt_edit.insert(tk.END, f"\nmessage should be printed out one by one")
# do something here
time.sleep(0.5)
# job done
window = tk.Tk()
window.title("User Interface")
window.rowconfigure(0, minsize=40, weight=1)
window.rowconfigure(1, minsize=200, weight=1)
window.columnconfigure(1, minsize=200, weight=1)
lbl_1 = tk.Label(master=window, text="Question: ")
lbl_2 = tk.Label(
master=window, text="How to print the text out one by one?", anchor="w"
)
lbl_1.grid(row=0, column=0, sticky="ns")
lbl_2.grid(row=0, column=1, sticky="nsew")
txt_edit = tk.Text(window, relief=tk.SUNKEN, bd=2)
fr_buttons = tk.Frame(window)
btn_open = tk.Button(master=fr_buttons, text="Start", command=start)
btn_open.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
fr_buttons.grid(row=1, column=0, sticky="ns")
txt_edit.grid(row=1, column=1, sticky="nsew")
window.mainloop()
What is the solution to this problem? Thanks!
Calling time.sleep() from tkinter applications isn't a good idea. Your mainloop and time.sleep() are conflicting. Your program works fine, but the changes are not displayed.
The easiest solution is updating the screen. But as I said, you should avoid using time.sleep(). The following solution satisfies your question, though it freezes your program and will not work on a larger application.
def start():
global window
txt_edit.delete(1.0, tk.END)
for _ in range(10):
txt_edit.insert(tk.END, f"\nmessage should be printed out one by one")
# do something here
time.sleep(0.5)
window.update() # show changes
# job done
I would recommend dropping the time module. You can use a timer instead. Also, take into account the while loop. You might want to use multi-threading inside your applications. Here's a simple Timer object and implementation:
class Timer:
# --- Timer object ---
def __init__(self):
self.start_time = None
self.on = False
def start(self):
# start counting
self.start_time = time.time()
self.on = True
def value(self):
# --- return current value ---
if self.on:
return time.time() - self.start_time
else:
return 0
def stop(self):
# --- stop counting ---
self.__init__()
def start():
txt_edit.delete(1.0, tk.END)
msg_count = 0
timer = Timer()
timer.start()
while msg_count != 10:
window.update()
if timer.value() > 0.5:
txt_edit.insert(tk.END, f"\nmessage should be printed out one by one")
timer.start()
msg_count += 1
timer.stop()

tkinter update() and update_idletasks()

I 'm reading a lot of update() and update_idletasks().
I did a GUI with some widgets. One button is called Run and it runs a motor endless. Another button is called Stop, and as you can image, it is to stop the movement of the motor. All works properly but the print("update") is called more and more often. At the first cycle of the motor 2 time, cycle by cycle leak source from the OS.
How can I solve the issue? I tried with update_idletasks() as well. instead to self.update() i could insert a function just to read the Stop button. any idea?
class GUI(object):
def __init__(self, master):
self.globalvar()
self.bus = smbus.SMBus(1)
self.master = master
#----------Load the GUI
self.loadGUI()
self.up()
def Running(self):
while self.s:
movethemotor()
self.up()
def Stop(self):
self.s = False
print("Stop")
def up(self):
...
self.master.update()
self.master.after(100,self.up)
if __name__ == "__main__":
root = tk.Tk()
root.title("MLCT GUI")
rpi = GUI(root)
root.geometry('1050x530')
root.resizable(False, False)
root.mainloop()
Another things that get me cracy: if I replace the "up" function:
def up(self):
...
root.update()
root.after(100,self.up)
I can read the print update, one time the first cycle, 2 time the second, 3 times the third etc.

Controlling another application from within tkinter

I don't know if what I'm seeking to achieve is even possible. I have written a tkinter app which imports a method from an external class. This method runs a hill-climbing algorithm which will run perpetually and try to improve upon the "score" that it has calculated. After each pass, it presents the current output and score to the user and asks (on the command line) if they wish to continue.
The first challenge in getting this working was to implement threading. I have this working, but I don't don't know if I have done it correctly.
The algorithm will continue until the user signals that they have got the answer they were looking for, or loses the will to live and presses CTRL-C.
In my tkinter main app, this presents me with two problems:
How to display the output from this external method. I tried to write a while loop that polled the output field periodically, (see commented out section of the "start_proc" method) but that was clearly never going to work, and besides, I would ideally like to see real-time output; and
How to interact with the algorithm to either continue or stop (see commented out section of the "my_long_procedure" method). As you can see, I can inject a "stopped" attribute, and that does indeed halt the algorithm, but I can't take my eyes off the output because the desired answer may have gone past before I can press stop.
Below is, I hope, a simplified bare-bones example of what I am trying to do.
This is a learning exercise for me and I would be grateful of any help.
import tkinter as tk
from threading import Thread
from random import randint
import time
class MyTestClass: # This would actually be imported from another module
def __init__(self):
self.stopped = False
def my_long_procedure(self):
# Fake method to simulate actual algorithm
count = 0
maxscore = 0
i = 0
while count < 1000 and not self.stopped:
i += 1
score = randint(1,10000)
if score > maxscore:
maxscore = score
self.message = f'This is iteration {i} and the best score is {maxscore}'
print(self.message)
# self.carry_on = input("Do you want to continue? ")
# if self.carry_on.upper() != "Y":
# return maxscore
time.sleep(2)
print('OK - You stopped me...')
class MyMainApp(tk.Tk):
def __init__(self, title="Sample App", *args, **kwargs):
super().__init__()
self.title(title)
self.test_run = MyTestClass()
self.frame1 = tk.LabelFrame(self, text="My Frame")
self.frame1.grid(row=0, column=0, columnspan=2, padx=5, pady=5, sticky=tk.NSEW)
self.frame1.columnconfigure(0, weight=1)
self.frame1.rowconfigure(0, weight=1)
start_button = tk.Button(self.frame1, text="Start!",
command=self.start_proc).grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
stop_button = tk.Button(self.frame1, text="Stop!",
command=self.stop_proc).grid(row=0, column=2, padx=5, pady=5, sticky=tk.E)
self.output_box = tk.Text(self.frame1, width=60, height=8, wrap=tk.WORD)
self.output_box.grid(row=1, column=0, columnspan=3, sticky=tk.NSEW)
def start_proc(self):
self.test_run.stopped = False
self.control_thread = Thread(target=self.test_run.my_long_procedure, daemon=True)
self.control_thread.start()
time.sleep(1)
self.output_box.delete(0.0, tk.END)
self.output_box.insert(0.0, self.test_run.message)
# self.control_thread.join()
# while not self.test_run.stopped:
# self.output_box.delete(0.0, tk.END)
# self.output_box.insert(0.0, self.test_run.message)
# time.sleep(0.5)
def stop_proc(self):
self.test_run.stopped = True
if __name__ == "__main__":
MyMainApp("My Test App").mainloop()
If you own the implementation of the algorithm you could pass a callback (a method of MyMainApp) so that the algorithm signals on his own whenever he has done some "work" worth notification to the user. This would look like:
def my_long_procedure(self,progress):
The callback prototype could be:
def progress(self,iteration,result):
and instead of print(self.message) you could do progress(i,maxscore)
starting the thread:
self.control_thread = Thread(target=self.test_run.my_long_procedure, daemon=True,args=(self.progress,))
Unfortunatly you need to be aware that you cannot refresh the GUI from another thread than the main thread. This is a much discussed tkinter limitation. So in a nutshell you cannot directly call any of your GUI widgets from progress function. The workaround to this issue is to store the progress in the progress function and register a function to be executed whenever the tkinter main loop will be idle. you can do somethink like self.after_idle(self.update_ui) from progress method. update_ui() would be a new methode updating eg a progress bar or your graph using data transmitted by the progress callback and saved as MyMainApp properties.
More on this "pattern" (using message queues instead of callback) here:
https://www.oreilly.com/library/view/python-cookbook/0596001673/ch09s07.html

How to stop execution of a function on button click in Python 3?

I'm making a small GUI aplication with tkinter buttons, when button1 is clicked, a function which has infinite loop runs.
I would like to have the possibility to stop the process with another button and reset the aplication to the initial state. I don't know how to do because when the button1 starts the script , button2 is blocked. I want to stop execution of function1 on button2 click. This is my code .
from tkinter import *
root = Tk()
def function1():
x =0
while True : # infinite loop
print(x)
x = x + 1
def function2():
sys.exit()
btn1 = Button(root, text ="Button 1", command = function1)
btn1.place(x=200, y=200)
btn2 = Button(root, text ="Button 2", command = function2)
btn2.place(x= 300,y=300)
root.geometry("400x400")
mainloop()
Thank you.
you can not run the infinite loop in buttons instead you can use recursion. You can call a function recursively and fuction1, function2 can be the functions used to control any variable. Please go thru the following code:
from tkinter import *
running = False
x=0
def scanning():
global x
if running:
x = x+1
print (x)
# After 10 milli second, call scanning again (create a recursive loop)
root.after(10, scanning)
def start():
global running
running = True
def stop():
global running
running = False
root = Tk()
root.title("Continuous Loop")
root.geometry("100x100")
app = Frame(root)
app.grid()
start = Button(app, text="Start counting", command=start)
stop = Button(app, text="Stop counting", command=stop)
start.grid()
stop.grid()
root.after(1000, scanning) # After 1 second, call scanning
root.mainloop()
Hope this will help.

Passing OptionMenu into a callback (or retrieving a reference to the used widget)

I'm working on a (toplevel in a) GUI that consists of an array of 8 OptionMenus, each of them containing the same option list. Currently, Im building these widgets using a for-loop, and I save references in a dictionary. All OptionMenus link to the same (lambda) callback function.
To stay practical: the items in the option list represent a sequence of processing steps, and the user can alter the order of processes.
A change in one of the lists will result in one process being executed twice, and one process not at all. However, I want each item to occur only once. Hence, each user input should be accompanied by a second OptionMenu alteration.
For example: initial order 1-2-3 --> user changes the second process: 1-3-3, which autocorrects to: 1-3-2, where each process is again executed only once.
To my understanding, I can only get this to work if I have a reference to the OptionMenu that was just altered (from within the callback function). I was looking into passing the widget into the callback. The sample code is an attempt to implement the second suggested method, but the result is not what I would have expected.
The thing is that the OptionMenu widget seems to behave somewhat differently from other widgets. The OptionMenu does not allow for a re-defintion of the command function. No matter what input I pass along with the command function, the callback only seems to retrieve the OptionMenu selection, which is insufficient information for me to determine my process order.
Suggestions would be much apreciated!
import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
self.create_widgets()
def create_widgets(self):
self.active_procs = ['proc 1','proc 2','proc 3','proc 4',
'proc 5','proc 6','proc 7','proc 8']
itemnr, widgets = dict(), dict()
for index in range(8):
name_construct = 'nr' + str(index)
itemnr[name_construct] = tk.StringVar(root)
itemnr[name_construct].set(self.active_procs[index])
widgets[name_construct] = tk.OptionMenu(self, itemnr[name_construct], *self.active_procs,
command=lambda widget=name_construct:
self.order_change(widget))
widgets[name_construct].grid(row=index+2, column=2, columnspan=2,
sticky="nwse", padx=10, pady=10)
def order_change(self,widget):
print(widget)
root = tk.Tk()
root.title("OptionMenu test")
app = Application(master=root)
root.mainloop()
The OptionMenu will pass the new value to the callback, so you don't have to do anything to get the new value. That's why your widget value isn't the value of name_construct -- the value that is passed in is overwriting the default value that you're supplying in the lambda.
To remedy this you simply need to add another argument so that you can pass the value of name_construct to the callback to go along with the value which is automatically sent.
It would look something like this:
widgets[name_construct] = tk.OptionMenu(..., command=lambda value, widget=name_construct: self.order_change(value, widget))
...
def order_change(self, value, widget):
print(value, widget)
Note: the OptionMenu isn't actually a tkinter widget. It's just a convenience function that creates a standard Menubutton with an associated Menu. It then creates one item on the menu for each option, and ties it all together with a StringVar.
You can get the exact same behavior yourself fairly easily. Doing so would make it possible to change what each item in the menu does when selected.
For those interested, below you can find an example code of how I got the widget behaviour I wanted. I took Bryan's advice to replace the OptionMenu for a Menubutton/Menu combination. I also made use of this post to find duplicate entries in my process order list.
Any thoughts or suggestions on how to implement this in a cleaner or shorter way, or how to get the same functionality with a different interface (e.g. drag and drop), are ofcourse welcome!
import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
self.create_widgets()
def create_widgets(self):
# Assisting text
l1 = tk.Label(self, text = "Data in", font=(None, 15))
l1.grid(row=0, column=2)
l2 = tk.Label(self, text = u'\N{BLACK DOWN-POINTING TRIANGLE}', font=(None, 15))
l2.grid(row=1, column=2)
l3 = tk.Label(self, text = "Data out", font=(None, 15))
l3.grid(row=11, column=2)
l4 = tk.Label(self, text = u'\N{BLACK DOWN-POINTING TRIANGLE}', font=(None, 15))
l4.grid(row=10, column=2)
# Process list
self.active_procs = ['proc a','proc b','proc c','proc d',
'proc e','proc f','proc g','proc h']
self.the_value, self.widgets, self.topmenu = dict(), dict(), dict()
for index in range(8):
name_construct = 'nr' + str(index)
self.the_value[name_construct] = tk.StringVar(root)
self.the_value[name_construct].set(self.active_procs[index])
self.widgets[name_construct] = tk.Menubutton(self, textvariable=
self.the_value[name_construct],
indicatoron=True)
self.topmenu[name_construct] = tk.Menu(self.widgets[name_construct],
tearoff=False)
self.widgets[name_construct].configure(menu=self.topmenu[name_construct])
for proc in self.active_procs:
self.topmenu[name_construct].add_radiobutton(label=proc, variable=
self.the_value[name_construct],
command=lambda proc=proc,
widget=name_construct:
self.order_change(proc,widget))
self.widgets[name_construct].grid(row=index+2, column=2, columnspan=2,
sticky="nwse", padx=10, pady=10)
def order_change(self,proc,widget):
# Get the index of the last changed Menubutton
index_user_change = list(self.widgets.keys()).index(widget)
procs_order = [] # Current order from widgets
for index in range(8):
name_construct = 'nr' + str(index)
procs_order.append(self.widgets[name_construct].cget("text"))
# 1 change may lead to 1 double and 1 missing process
doubles = self.list_duplicates_of(procs_order,proc)
if len(doubles) == 2: # If double processes are present...
doubles.remove(index_user_change) # ...remove user input, change the other
missing_proc = str(set(self.active_procs)^set(procs_order)).strip('{"\'}')
index_change_along = int(doubles[0])
# Update references
self.active_procs[index_user_change] = proc
self.active_procs[index_change_along] = missing_proc
# Update widgets
name_c2 = 'nr'+str(index_change_along)
self.the_value[name_c2].set(self.active_procs[index_change_along])
self.widgets[name_c2].configure(text=missing_proc)
def list_duplicates_of(self,seq,item):
start_at = -1
locs = []
while True:
try:
loc = seq.index(item,start_at+1)
except ValueError:
break
else:
locs.append(loc)
start_at = loc
return locs
root = tk.Tk()
root.title("OptionMenu test")
app = Application(master=root)
root.mainloop()

Resources