Tkinter button stuck / frozen upon pressing until all the commands are executed - python

Just started with Tkinter and posting only the snippet of the code.
label and button are Label and Button class objects respectively.
When the button is pressed for the first time on_button_click_organisation is called which changes the button configuration and now when we click it on_button_click_compute is called, which is expected to perform a time taking action via the main method.
But the GUI gets stuck until the main() ends. How to stop this and make the button active once the excecution returns after computing main() and inactive till then?
def on_button_click_organisation(self):
self.organisation_file = askopenfilename(filetypes=[("*.csv", "CSV")])
print("Organisation File : " + self.organisation_file)
self.label.config(text = "Start Code")
self.button.config(text = "START", command = self.on_button_click_compute)
def on_button_click_compute(self):
self.label.config(text = "Wait..\nResult being computed\n(Might take up to 15 minutes)")
self.button.config(state = DISABLED)
#Time taking task
main(self.filename, self.organisation_file, self.result_folder)
sleep(5) #To mimic time taking main() if main() returns in less time
self.button.config(state = NORMAL)
self.label.config(text = "Done..\nCheck the Result Folder")
self.button.configure(text = "Finish", command=root.quit())

Related

How can i pause this code in Jupyter Notebook?

When i click the Submit answer button, additional codes can only be run after that.
import ipywidgets as widgets
Here are a few lines of code that are responsible for the look of the button etc.
selector = widgets.RadioButtons(
options=['Valid', 'Invalid', 'Skip'],
value=None,
description='',
disabled=False
)
button = widgets.Button(
description='Submit answer',
disabled=False,
button_style='',
)
def evaluate(button):
selection = selector.get_interact_value()
if (selection == 'Valid'):
f = open(r"C:\Users\asd\Desktop\asd.txt", "a", encoding='utf-8')
f.write('asd')
f.close()
elif (selection == 'Invalid'):
pass
elif (selection == 'Skip'):
pass
button.on_click(evaluate)
left_box = widgets.VBox([selector, button])
widgets.HBox([left_box])
print('nvm') **#If I click Submit answer button, then run this code**
How can i do that?
a simple trick to do that (without doing an empty while loop that will abuse your cpu) is to put your code in a sleep loop untill the button is pressed.
answer_pressed = False
def evaluate(button):
global answer_pressed
answer_pressed = True
# rest of function here
button.on_click(evaluate)
import time
while answer_pressed == False: # put the interpreter to sleep until the button is pressed.
time.sleep(0.01) # or 0.1 depending on the required resposivity.
Edit: you might want to move the answer_pressed = True to the end of the function instead of the beginning, so you will be sure the function has executed fully before breaking the while loop.

How do I prevent focus change if an entry is invalid?

I want if one field has invalid entry, message to be displayed on moving out of that field and focus should remain on that field. But in following code validation of next field is also triggered when moving out of first field. I have commented set focus, otherwise, it gets into an infinite loop.
from tkinter import *
from tkinter import ttk, messagebox
root = Tk()
def callback1():
if (len(e1.get()) <4):
messagebox.showinfo("error", "Field 1 length < 4")
#e1.focus_set()
return False
else:
return True
def callback2():
if (len(e2.get()) <4):
messagebox.showinfo("error", "Field 2 length < 4")
#e2.focus_set()
return False
else:
return True
e1 = Entry(root, validate="focusout", validatecommand=callback1)
e1.grid()
e2 = Entry(root, validate="focusout", validatecommand=callback2)
e2.grid()
root.mainloop()
When you place your cursor in e1, type something which does not satisfy the validatecommand condition and then try to place your cursor in e2, the following sequence of events takes place:
e1 loses focus and calls callback1
Meanwhile, the cursor is placed in e2 and e2 gets focus
The msgbox window for e1 takes focus and is ready to pop up just when...
e2 loses focus and calls callback2
The msgbox window for e2 pops up
This is followed by the pop up for e1 which was waiting in the queue
The root cause for the problem is that the messagebox window takes focus, which in turn triggers the other entry box. This approach seems to be highly fragile due to the strong interplay of events.
Notice that if you just print the information to the terminal, everything works perfectly as the terminal doesn't take focus.
So, I would recommend you display the information using an alternate method where the widget showing the information won't steal focus. One option is to display the information using a label.
Now coming to your second problem, if you want the entry to retain focus if the entered text is not valid, you could use a global variable (hanging) to keep track of whether the user is in the process of filling an entry successfully.
If the user is in the process of filling an entry, he/she will not be able to place the cursor in the other entry because FocusIn1 and FocusIn2 return "break" when hanging equals True.
You can replace the print statements in the below working code using a label.
Working Code:
from tkinter import *
from tkinter import ttk, messagebox
root = Tk()
hanging = False #True implies that the user is in the process of filling an entry successfully
def onFocusOut1():
global hanging
hanging = True
if (len(e1.get()) <4):
print("error", "Field 1 length < 4")
e1.focus_set()
return False
else:
hanging = False
return True
def onFocusOut2():
global hanging
hanging = True
if (len(e2.get()) <4):
print("error", "Field 2 length < 4")
e2.focus_set()
return False
else:
hanging = False
return True
def onFocusIn1():
if hanging:
return "break"
e1.configure(validate="focusout", validatecommand=onFocusOut1)
def onFocusIn2():
if hanging:
return "break"
e2.configure(validate="focusout", validatecommand=onFocusOut2)
e1 = Entry(root)
e1.grid()
e2 = Entry(root)
e2.grid()
#Binding both the entries to FocusIn
e1.bind("<FocusIn>", lambda e: onFocusIn1())
e2.bind("<FocusIn>", lambda e: onFocusIn2())
root.mainloop()
PS: It turns out that you can actually use messagebox.showinfo itself in place of print in the working code. The first problem got solved automatically along with the second one. So, this gives the complete solution to your problem.

How handle unexpected Key press

I have designed an application which have two buttons i.e CAL and SAV.
Accordingly that I have two functions but the issue is sometimes production line Operator by mistake presses SAV button. So that attribute error arises and program stuck.
How to overcome this issue? Please guide me.
Here is my code:
class ADS1x15:
"""Base functionality for ADS1x15 analog to digital converters."""
class ADS1115(ADS1x15):
"""Class for the ADS1115 16 bit ADC."""
class AnalogIn:
"""AnalogIn Mock Implementation for ADC Reads."""
import RPi.GPIO as GPIO
import tkinter as tk
GPIO.setmode(GPIO.BCM)
GPIO.setup(12,GPIO.IN) #Save Button
GPIO.setup(5,GPIO.IN) #Cal Button
root=tk.Tk()
root.geometry("1000x600")
file = open("/home/pi/data_log.txt", "r")
f = file.read().split(',')
rangeh = int(f[3])
offset = int(f[4])
fullScale = int(f[5])
chan=AnalogIn(ads,P0,P1)
def cal(channel):
global Dsel,cal_c,rangeh,offset,fullScale,chan
cal_c = cal_c + 1
if cal_c == 1:
root.L1 = tk.Label(root,text="Put Zero Weight and Press CAL btn",fg="brown",font="bold")
root.L1.pack()
root.L1.place(x=1,y=1)
elif cal_c == 2:
root.L1.destroy()
offset = chan.value
file = open("/home/pi/data_log.txt", "w")
if os.stat("/home/pi/data_log.txt").st_size == 0:
file.write("rangeh,offset,Full_Scale,\n")
file.write(str(rangeh)+","+str(offset)+","+str(fullScale))
file.flush()
root.L2 = tk.Label(root,text="Put Full Weight and Press SAV btn",fg="brown",font="bold")
root.L2.pack()
root.L2.place(x=1,y=1)
def sav(channel):
global rangeh,offset,fullScale
file = open("/home/pi/data_log.txt", "w")
if os.stat("/home/pi/data_log.txt").st_size == 0:
file.write("rangeh,offset,Full_Scale,\n")
file.write(str(rangeh)+","+str(offset)+","+str(fullScale))
file.flush()
root.L2.destroy()
def update():
""" function for continuous show value in every 500ms in tkinter window"""
GPIO.add_event_detect(5,GPIO.RISING,callback=cal,bouncetime=1000)
GPIO.add_event_detect(12,GPIO.RISING,callback=sav,bouncetime=1000)
root.after(500,update)
root.mainloop()
This error generated due to root.L2.destroy() this line.
Can I block or disable this sav function, so that without call of cal function, it shouldn't execute?
A brute force solution would be to check whether root has an L2 attribute or not
from tkinter import messagebox
def sav(channel):
if hasattr(root, 'L2'):
global rangeh, offset, fullScale
file = open("/home/pi/data_log.txt", "w")
if os.stat("/home/pi/data_log.txt").st_size == 0:
file.write("rangeh,offset,Full_Scale,\n")
file.write(str(rangeh) + "," + str(offset) + "," + str(fullScale))
file.flush()
root.L2.destroy()
else:
messagebox.showinfo('Unable to save', 'No data was generated yet')
A more elegant approach would be to disable the save button on startup and only enable it after the cal function has been executed.
I am not very familiar with Raspberry Pi implementations, so this is only a rough sketch on how to achieve the button disabling:
By the looks of it, the buttons are "wired in" via the GPIO.add_event_detect functions.
So i would remove the sav-callback from the main script and dynamically add it after the cal script, something like that:
# [...] beginning of your script [...]
def cal(channel):
# [...] original body of cal function [...]
activate_save_button()
def activate_save_button():
GPIO.add_event_detect(12, GPIO.RISING, callback=sav, bouncetime=1000)
def deactivate_save_button():
GPIO.remove_event_detect(12)
def sav(channel):
# [...] original body of sav function [...]
# remove save button functionality after saving
deactivate_save_button()
def update():
""" function for continuous show value in every 500ms in tkinter window"""
GPIO.add_event_detect(5, GPIO.RISING, callback=cal, bouncetime=1000)
# line with callback=sav is deleted here
root.after(500, update)
root.mainloop()

Python main script not waiting for user response in Tkinter GUI

I am brand new to using the Tkinter GUI and stuck while trying to make an interactive program that 1) solicits feedback from the user in the GUI, 2) waits for the user to respond and hit enter and 3) then uses the input to inform the next steps in the main script.
However, I am unable to make step 2 execute properly. At the function call waitforinput(), I would expect the main script to wait before moving on to the next lines which are test printouts. Instead, it prints the main script test lines with '' for result and then places an entry box that works. Why is this program moving to the next line before the waitforinput function is completed? Thanks!
import tkinter as tk
from tkinter import *
import threading, time
# assignments for input thread
WAIT_DELAY = 250 #milliseconds
lock = threading.Lock() # Lock for shared resources.
finished = False
result = ''
# Set up the graphical interface
root = tk.Tk(className='My Flashcards')
def main():
# request and wait for input from user
waitforinput()
Test = tk.Label(root, text = "result = " + result)
Test.pack()
Test.config(font = ('verdana', 24), bg ='#BE9CCA')
# sets background thread for getinput from user
def waitforinput():
global finished
with lock:
finished = False
t = threading.Thread(target=getinput)
t.daemon = True
root.after(WAIT_DELAY, check_status) # start waiting
t.start()
# checks to see if user has inputted
def check_status():
with lock:
if not finished:
root.after(WAIT_DELAY, check_status) # keep waiting
# solicits and returns a string from the user
def getinput():
# declaring string variable for storing name and password
answer_var = tk.StringVar()
# define a function that will get the answer and return it
def user_response(event = None):
answer = answer_entry.get()
global result
result = answer
global finished
finished = True # to break out of loop
# creating an entry for inputting answer using widget Entry
answer_entry = tk.Entry(root, width = 1, borderwidth = 5, bg ='#BE9CCA', textvariable = answer_var) ## could be global with args
# making it so that enter calls function
answer_entry.bind('<Return>', user_response)
# placing the entry
answer_entry.pack()
answer_entry.focus()
main()
root.mainloop()
'''
You don't need to use threads to wait for a response. Tkinter has methods specifically for waiting: wait_window and wait_variable.
wait_window waits for a window to be destroyed. This is typically used when creating a modal dialog. wait_variable can be used to wait until one of the special tkinter variable objects has been modified.
If you want your code to wait until a user has pressed the enter key in an entry, you can set a binding on that key to set a variable, and then wait for that variable to be set. Note: you normally don't want to use wait_variable for a variable used as the value of textvariable since the wait will stop as soon as a single character has been entered.
Your getinput function could look something like this, which can be called directly without the use of threads:
def getinput():
answer_var = tk.StringVar()
def user_response(event):
answer_var.set(answer_entry.get())
return
answer_entry = tk.Entry(root, width = 1, borderwidth = 5, bg ='#BE9CCA')
answer_entry.bind('<Return>', user_response)
answer_entry.pack()
answer_entry.focus()
answer_entry.wait_variable(answer_var)
return answer_var.get()
Once the statement answer_entry.wait_variable(answer_var) executes, tkinter will enter an event loop and won't return until answer_var has been set. You can then call get on the variable and return the value.
Here is how you can modify your main function to call this function:
def main():
result = getinput()
Test = tk.Label(root, text = "result = " + result)
Test.pack()
Test.config(font = ('verdana', 24), bg ='#BE9CCA')

Tkinter Button event handler delays the return of the Button to RAISED state

My GUI holds 8 Buttons. Each Button click calls the event handler which then forwards to another function. All of these actions take about 4 seconds. My problem is that this causes the Button to stay in SUNKEN state while the event is handled and also causes the other Buttons to be non-responsive.
What I would like is to release the Button from the SUNKEN state immediately after the click and continue the event handling in the background.
How can this be solved ? Is there a way to make the button released before the event handler finished its job ?
After Editing:
Here is my code:
from tkinter import Tk, Menu, Button
import telnetlib
from time import sleep
On_Color = '#53d411'
Off_Color = '#e78191'
def Power_On_Off (PowerSwitchPort, Action):
'''
Control the Power On Off Switch
'''
on_off = telnetlib.Telnet("10.0.5.9", 2016)
if Action == 'On' or Action =='ON' or Action == 'on':
StringDict = {'1': b"S00D1DDDDDDDE", '2': b"S00DD1DDDDDDE", '3': b"S00DDD1DDDDDE", '4': b"S00DDDD1DDDDE",
'5': b"S00DDDDD1DDDE", '6': b"S00DDDDDD1DDE", '7': b"S00DDDDDDD1DE", '8': b"S00DDDDDDDD1E"}
elif Action == 'Off' or Action =='OFF' or Action == 'off':
StringDict = {'1': b"S00D0DDDDDDDE", '2': b"S00DD0DDDDDDE", '3': b"S00DDD0DDDDDE", '4': b"S00DDDD0DDDDE",
'5': b"S00DDDDD0DDDE", '6': b"S00DDDDDD0DDE", '7': b"S00DDDDDDD0DE", '8': b"S00DDDDDDDD0E"}
PowerSwitchPort = str(PowerSwitchPort)
on_off.read_eager()
on_off.write(b"S00QLE\n")
sleep(4)
on_off.write(StringDict[PowerSwitchPort])
on_off.close()
def OnButtonClick(button_id):
if button_id == 1:
# What to do if power_socket1 was clicked
Power_On_Off('1', 'Off')
elif button_id == 2:
# What to do if power_socket2 was clicked
Power_On_Off('1', 'On')
def main ():
root = Tk()
root.title("Power Supply Control") #handling the application's Window title
root.iconbitmap(r'c:\Users\alpha_2.PL\Desktop\Power.ico') # Handling the application icon
power_socket1 = Button(root, text = 'Socket 1 Off', command=lambda: OnButtonClick(1), bg = On_Color)
power_socket1.pack()
power_socket2 = Button(root, text = 'Socket 1 On', command=lambda: OnButtonClick(2), bg = On_Color)
power_socket2.pack()
'''
Menu Bar
'''
menubar = Menu(root)
file = Menu(menubar, tearoff = 0) # tearoff = 0 is required in order to cancel the dashed line in the menu
file.add_command(label='Settings')
menubar.add_cascade(label='Options', menu = file)
root.config(menu=menubar)
root.mainloop()
if __name__ == '__main__':
main()
As can be seen, I create 2 Buttons one Turns a switch On and the other turns it Off. On/Off actions are delayed in about 4 seconds. This is only small part of my Application that I took for example. In my original code I use a Class in order to create the GUI and control it
Trivia
Your problem stems from these lines of code:
on_off.write(b"S00QLE\n")
sleep(4)
especially from sleep. It's very weak pattern, because you're expecting a completion of S00QLE in four seconds. No more and no less!
And while telnet actually works for these four seconds, the GUI sleeps.
Therefore your GUI is in unresponsive state and can't redraw the relief of the button.
The good alternative to sleep is the after - so you can schedule an execution:
# crude and naive fix via closure-function and root.after
def Power_On_Off (PowerSwitchPort, Action):
...
def write_switch_port_and_close():
on_off.write(StringDict[PowerSwitchPort])
on_off.close()
on_off.write(b"S00QLE\n")
# sleep(4)
root.after(4000, write_switch_port_and_close)
...
Solution
To overcome this problem, you can use generic after self-sheduling loop.
In my example I connect to public telnet server, which broadcasts Star Wars Episode IV (not an add!), to simulate a long-running process.
Of course, your execute (write) two commands in telnet, to represent this behaviour we recive a telnet broadcast until the famous "Far away" line is found (first long-run operation (write)). After that, we update the label-counter and then we connect to the broadcast again (second long-run operation (write)).
Try this code:
import tkinter as tk
import telnetlib
class App(tk.Tk):
def __init__(self):
super().__init__()
# store telnet client without opening any host
self.tn_client = telnetlib.Telnet()
# label to count "far aways"
self.far_aways_encountered = tk.Label(text='Far aways counter: 0')
self.far_aways_encountered.pack()
# start button - just an open telnet command ("o towel.blinkenlights.nl 23")
self.button_start = tk.Button(self, text='Start Telnet Star Wars', command=self.start_wars)
self.button_start.pack()
# start button - just an close telnet command ("c")
self.button_stop = tk.Button(self, text='Stop Telnet Star Wars', command=self.close_wars, state='disabled')
self.button_stop.pack()
# simple counter
self.count = 0
def start_wars(self):
# "o towel.blinkenlights.nl 23"
self.tn_client.open('towel.blinkenlights.nl', 23)
# enabling/disabling buttons to prevent mass start/stop
self.button_start.config(state='disabled')
self.button_stop.config(state='normal')
# scheduling
self.after(100, self.check_wars_continiously)
def close_wars(self):
# "c"
self.tn_client.close()
# enabling/disabling buttons to prevent mass start/stop
self.button_start.config(state='normal')
self.button_stop.config(state='disabled')
def check_wars_continiously(self):
try:
# we're expect an end of a long-run proccess with a "A long time ago in a galaxy far far away...." line
response = self.tn_client.expect([b'(?s)A long time ago in a galaxy far,.*?far away'], .01)
except EOFError:
# end of file is found and no text was read
self.close_wars()
except ValueError:
# telnet session was closed by the user
pass
else:
if response[1]:
# The "A long time ago in a galaxy far far away...." line is reached!
self.count += 1
self.far_aways_encountered.config(text='Far aways counter: %d' % self.count)
# restart via close/open commands (overhead)
self.close_wars()
self.start_wars()
else:
if response[2] != b'':
# just debug-print line
print(response[2])
# self-schedule again
self.after(100, self.check_wars_continiously)
app = App()
app.mainloop()
So the answer is: the simplest alternative to your specific sleep commands is combination of two functions: after and expect (or only expect if it's a console-application)!
Links
How do you run your own code alongside Tkinter's event loop?
after
Telnet.expect
Regular expression operations
If you have to handle a computation heavy process in your Gui while allowing the rest to run you can just multi-process:
import multiprocess as mp
def OnButtonClick(bid):
mp.Process(target=Power_On_Off,args=('1',('Off','On')[bid-1])).start()
Answers to comments
mp.Process defines a Process object. Once I .start() it the process will call the function in target, with arguments args. You can see the use here for example.
You can of course separate, and pass each id on with different on off choices. I implemented what you wrote.
No need for a queue here if I just want it to run and do something while I continue clicking my buttons.
Your problem was that the button is not released until action ends. By delegating the action to a different process you can return immediately, thus allowing the button to raise.
Your problem isn't really "button isn't rising fast enough". Rather it's the Gui is stuck until Power_On_Off completes. If I run that function in a different process, then my Gui won't be stuck for any noticeable time.
If you want something in the main process to happen when the other process ends, you may need to keep the Process in some sort of list for future pinging. Otherwise, this is fine.

Categories

Resources