I'm trying to make tkinter program to wait in the middle of the function execution, wait until variable is changed in separate thread and then proceed. The code provided is not actual task, it's my attempts to understand wait_variable and make it work at least in some way.
The task itself is actually this: I'm trying to make my code wait until windows service status is changed and then proceed. So I used this code and I need to wait until status changes.
Here is the code I wrote while trying to make it work:
import time
import tkinter as tk
from threading import Thread
from tkinter import messagebox
def test():
my_var = tk.IntVar()
my_var.set(1)
label_2 = tk.Label(textvariable=my_var)
label_2.pack()
def wait_for_var():
nonlocal my_var
for i in range(5):
label.config(text='iteration {}'.format(i))
time.sleep(1)
my_var.set(1)
my_var.set(2)
Thread(target=wait_for_var).start()
while my_var.get() != 2:
root.wait_variable('my_var')
messagebox.showinfo('aha!', 'my_var changed!')
messagebox.showinfo('done!', 'done!')
root = tk.Tk()
root.geometry('800x600')
text = 'init'
label = tk.Label(text='init')
label.pack()
btn = tk.Button(text='click me', command=test)
btn.pack()
root.mainloop()
I expected program to exit while loop when my_var changes it's value to 2. But for some reason it's stuck there forever and it seems to not exit wait_variable even after main window closed. I stumbled upon this and this question about it, but I think that's still not the case. The problem at those questions was in variable not changing. In my code my_var is actually changing and it can be seen in second label. But further execution never happens. As wait_variable description claims: "setting it to it’s current value also counts".
So it means, that after each iteration inside second thread, when it's reassigned to the same value - first messagebox should show up. And after my_var value changes to 2, - second messagebox should show up. But they never do.
What am I missing here?
In order to have my_var work as intended, you must change your call to root.wait_variable from:
root.wait_variable('my_var')
to
root.wait_variable(my_var)
Other than that, you probably need to put the logic of terminating the thread inside the tread itself, or it will continue its iteration until it is done.
Related
I created an example code because my original is too big and has private information(My own) in it.
While running a program from a Tkinter GUI, it runs the program but makes the GUI unresponsive because of time.sleep() blocking the GUI from updating.
I am trying to avoid using timers because it fires a different function after a duration instead of simply pausing the function and then continuing the same function.
Is there an alternative that does not block the GUI but still adds a delay inside of the function?
Example Code:
from tkinter import *
import time
wn = Tk()
wn.geometry("400x300")
MyLabel = Label(wn, text="This is a Status Bar")
MyLabel.pack()
def MyFunction():
Value = 1
while Value < 10:
print("Do something")
time.sleep(1) **# - here blocks everything outside of the function**
MyLabel.config(text=Value)
# A lot more code is under here so I cannot use a timer that fires a new function
Value = 1
MyButton = Button(wn, text="Run Program", command=MyFunction)
MyButton.pack()
wn.mainloop()
Edit: Thanks so much, you're answers were fast and helpful, I changed the code and added "wn.mainloop()" after the delay and replaced "time.sleep(1)" with wn.after(100, wn.after(10, MyLabel.config(text=Value))
here is the final code:
from tkinter import *
import time
wn = Tk()
wn.geometry("400x300")
MyLabel = Label(wn, text="This is a Status Bar")
MyLabel.pack()
def MyFunction():
Value = 0
while Value < 10:
print("Do something")
wn.after(10, MyLabel.config(text=Value))
Value += 1
wn.mainloop()
MyButton = Button(wn, text="Run Program", command=MyFunction)
MyButton.pack()
wn.mainloop()
The short answer is that you can use wn.after() to request a callback after a certain amount of time. That's how you handle it. You get a timer tick at a one-per-second rate, and you have enough state information to let you proceed to the next state, then you go back to the main loop.
Put another way, timers are exactly how you have to solve this problem.
Fundamentally, any callback function in Tkinter runs in the main GUI thread, and so the GUI thread will block until the function exits. Thus you cannot add a delay inside the function without causing the GUI thread to be delayed.
There are two ways to solve this. One would be to refactor your function into multiple pieces so that it can schedule the remaining work (in a separate function) via .after. This has the advantage of ensuring that all of your functions are running in the main thread, so you can perform GUI operations directly.
The other way is to run your function in a separate thread that is kicked off whenever your main callback is executed. This lets you keep all the logic inside the one function, but it can no longer perform GUI operations directly - instead, any GUI operations would have to go through an event queue that you manage from the main thread.
You can combine after() and wait_variable() to simulate time.sleep() without blocking tkinter from handling pending events and updates:
def tk_sleep(delay):
v = wn.IntVar()
# update variable "delay" ms later
wn.after(delay, v.set, 0)
# wait for update of variable
wn.wait_variable(v)
Using tk_sleep() in your while loop:
def MyFunction():
Value = 1
while Value < 10:
print("Do something")
tk_sleep(1000) # waits for one second
MyLabel.config(text=Value)
# A lot more code is under here so I cannot use a timer that fires a new function
Value += 1
I'm trying to create a button in Tkinter that only appears (becoming visible) when it finds the word "hello" in a textbox.
I could imagine using Threads, and Global variables but i do not know how to code it,
I've imagined something like :
import Tkinter as *
from threading import Thread
window = Tk()
active = True
def check_for_word():
global active
textbox1 = textbox.get("1,0", "end")
while active == True:
if "hello" in textbox1:
button.pack()
else:
button.pack_forget()
save_button = Button(window)
textbox = scrolledtext.ScrolledText(window)
textbox.pack()
threading = Thread (target=check_for_word)
threading.start()
window.mainloop()
this is something I would suspect to work but ends up not, the button either doesn't show at all like the code isn't even running, or the thread doesn't work properly. So am I doing something wrong, if so, can you help me, please? Thank you!
You don't need to make use of threads to do this, you can use tkinter event bindings instead.
def check_for_word():
if "hello" in textbox.get("1.0", "end"):
save_button.pack()
else:
save_button.pack_forget()
save_button = Button(window)
textbox = scrolledtext.ScrolledText(window)
textbox.bind("<KeyRelease>", lambda event:check_for_word())
textbox.pack()
To make a binding, you use widget.bind. In this case, the widget is textbox and it's binding to <KeyRelease>, which is when the user releases a key. It then calls check_for_word when a key is released. (The lambda part is to ignore the event parameter). check_for_word then does what it did before.
You have to put the textbox1 assignment inside the while loop and before the if condition, otherwise it will check the value one time before entering the loop and will keep checking always the same value.
I also want to point out that the in operator is case sensitive and return True if it find even just a substring inside the variable you are checking and not just the precise single word (but I'm not shure if this is intensional).
For the while loop you don't necessarily need a global variable, you could just use while True: if you want it to continuously check the condition (if you want the button to disappear after the user cancel the word).
Currently I'm working on a project of mine involving sensors, and showing that sensory data on a display via TKinter. Everythings written in Python 3.7.3.
The issue im currently handling, is to update the label in the window, while the mainloop is running.
What i mean by this, is that if i execute the script, first the window options get defined, then the update function gets defined with a while true loop. Then its supposed to start the window. Now because of the while true loop it does not reach the window.mainloop() point (obviously, the while loop doesn't break...). My interest was peaked and i tried to put the window.mainloop() function inside the while loop of the update (please don't blame me, i know my script is a spaghetti mess.) I figured out that i could run the whole thing in threads, and so i decided to thread the whole window process, and add queues for the sensor data. Now the while loop was still in the way and didnt work properly, and after a bit of googling i found a code snippet that might help me. After trying to implement it in my script, i got an exception "function init expects 3 arguments, but 4 were given.." (code below) and I'm kinda running out of ideas on this.
Bear in mind that im not raelly a developer, i just need a script that can handle sensor data, dispaly it in a window, and export the current data to a database. So go easy on the blame please.
Current Script:
import time
import board
import adafruit_dht
import threading
import queue
from tkinter import *
dhtDevice = adafruit_dht.DHT22(board.D4, use_pulseio=False)
tempQ = queue.Queue(maxsize=0)
humQ = queue.Queue(maxsize=0)
class windowMain:
def __init__(self):
self.tempC_label = Label(fenster, text="Placeholder TempC")
self.humidity_label = Label(fenster, text="Placeholder Humidity")
self.tempC_label.pack()
self.humidity_label.pack()
self.tempC_label.after(2000, self.labelUpdate)
self.humidity_label.after(2000, self.labelUpdate)
def labelUpdate(self, tempQ, humQ):
self.tempC_label.configure(text= tempQ.get() + "°C")
#this is just to confirm if the function called or not, to see if the label updated or not.
#if the label didnt update, and the function called, there is something wrong with the function
#if the label didnt update, and the function didnt call, there is a problem somwhere else
print("Current Temp: " +tempQ.get() + "°C")
self.label.after(2000, self.labelUpdate)
if __name__ == "__main__":
windowName = Tk()
windowName.title = ("Climatemonitor")
windowMain(windowName)
windowName.mainloop()
try:
windowThread = threading.Thread(target=windowMain, args=(tempQ, humQ, ))
windowThread.start()
except:
print("Unable to start thread")
while True:
try:
temperature_c= dhtDevice.temperature
tempText= temperature_c
tempText= str(tempText)
tempQ.put(tempText)
humidity = dhtDevice.humidity
humidityP = str(humidity)
#this one is just to check if the sensor reads data
print(
"Temp: {:.1f} C Humidity: {}% ".format(
temperature_c, humidity
)
)
time.sleep(2.0)
except RuntimeError as error:
print(error.args[0])
time.sleep(2.0)
continue
except Exception as error:
dhtDevice.exit()
raise error
time.sleep(2.0)
The ultimate goal is to display my sensor data, with a 2 second refresh (the HZ rate of the Sensor), while the sensor continues to read every 2 seconds.
I'd also like to add that this is my first time using Python, since im, again, not really a developer yet.
Thanks a bunch in advance for every critique and help
most simple way of doing this would be using a button to execute a function and then including your while loop in that function,
Using an button gives you an point where you can start running while instead of directly starting it as soon as you run your program
Sample code should be something like this,
import tkinter as t
def execute():
print('hello')
window = t.Tk()
window.title("system")
window.geometry("550x250")
b1 = t.Button(window, text="Start", width=15, command=execute)
b1.grid(row=1, sticky="W", padx=4)
window.mainloop()
As there will be no user interaction, a button can invoked using button.invoke method such as following,
import tkinter as t
def execute():
print('hello')
window = t.Tk()
window.title("system")
window.geometry("550x250")
b1 = t.Button(window, text="Start", width=0, command=execute)
#b1.grid(row=1, sticky="W", padx=4)
b1.invoke()
window.mainloop()
here removing .grid() will cause the button to disapper but can affect your GUI while updating the label value later , also have a look at this ->
Is there a way to press a button without touching it on tkinter / python?
Python tkinter button.invoke method trouble
So, my plan is to create one of those games where a sequence is shown on the screen and you have to click the correct buttons, following the sequence. I'm going to use Tkinter for this project. Here's some explanation: When any buttons is clicked, a function will be executed with a parameter that will tell which button has been clicked, and then the function will check whether it's the correct button or not.
THE PROBLEM: if the user clicked the right button, this function will call a second function to show the next buttons of the sequence. The thing is that it should take some time to display this, and meanwhile the tkinter button remains 'clicked'. I don't want it to happen, also because you cannot execute other tasks, such as leaving the game or restarting the round, while this is happening.
Here I have a simple example to show you the problem, if you couldn't see it yet:
import tkinter as tk
from time import sleep
def func():
print('hi')
func2()
def func2():
print('hi2')
for num in range(3, 11):
sleep(1)
print('hi' + str(num))
wn = tk.Tk()
b1 = tk.Button(wn, command=func)
b1.pack()
wn.mainloop()
In this case, the code should print 'hi' + the numbers from 1 to 10 when the button is pressed. However, the things I've said previously happen.
How can I make this example above work correctly and also my project?
You can either use threads or you can define func3 which runs func1 and func2.
When trying to interrupt a tkinter application, it seems that time.sleep() puts some previous command on the hold. According to my understanding and previous experiences, label1's text should be set to "before" for one second and then changed to "after". However, the "before" value never shows up and "after" gets printed normally one second after execution.
from tkinter import *
import time
class Display(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
label1 = Label(text = "before")
label1.grid()
self.after(1000, label1.config(text = "after"))
def main():
root = Tk()
Display(root)
root.mainloop()
if __name__ == '__main__':
main()
Note that using time.sleep(...) yeilds the same result as the tkinter after(...)
from tkinter import *
import time
class Display(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
label1 = Label(text = "before")
label1.grid()
time.sleep(1)
label1.config(text = "after")
def main():
root = Tk()
Display(root)
root.mainloop()
if __name__ == '__main__':
main()
I suppose tkinter is waiting for something to execute the graphical work (console don't have the problem) but I don't see what and the tkinter doc dosen't tackle that issue.
Is there a simple way to get the obvious expected result?
time.sleep(...) does not give the same result as the after(...) call.
The time.sleep method runs, but does not show anything before the second has passed. You can see this by putting print('before') before your sleep call. You'll see the print statement is executed one second before the window is created. This is due to Tkinter not updating anything during a sleep. So nothing happens until the sleep is over, after which the label is immediately updated. Moreover, the mainloop isn't called before the sleep is over, so Tkinter isn't in its mainloop yet. You can force Tkinter to update by using self.parent.update() right before your sleep call. You will see that the window shows, having the label 'before', waits one second and the label changes to 'after'. However, during this second the window is unresponsive, which is why using sleep together with Tkinter is a bad idea.
Instead of sleep, after is almost always the better option since it returns immediately, so it doesn't block further execution, and schedules the specified function to be executed after the specified amount of time. However, after expects a function name to be passed, but you pass a function call. This means that at the time that the after call is evaluated, the function is run and the return value (which is None) is passed as function name. Therefore, you see that the label is changed at the time the window opens, because the change is made at the time the after call is evaluated. What you should do is pass a function name to after:
def update_label:
label1.config(text = "after")
self.after(1000, update_label)
Or shorter, by creating an anonymous function:
self.after(1000, lambda: label1.config(text = "after"))
This will give you the expected result of showing a label with 'before', which changes to 'after' after a second, without blocking Tkinter's mainloop.
It's not surprising that doesn't work. Think about what the Tk framework is doing: it's creating your windows, display = Display(), and at that moment the thread stops. The object is not fully created. Do you expect Tk to render an object that's half way through its constructor?
First, try moving any thread-related code out of the constructor. The constructor should be allowed to finish normally, and thread-code should be in another function.
Second, it's bad practice to use thread.sleep in the middle of your normal logic. Normally, you start a separate thread, and that thread can wait and sleep if it wants to. (Though I don't know whether Tk has a special way of doing things.)