What I am trying to do with the following code is read from arduino serial and update a label with that data every few seconds.
When I run the code it only gets/updates the label once. So I know its something to do with a loop. My understanding was that all code between Tk() and mainloop() was in a loop. Any help would be appreciated.
from Tkinter import *
import serial
import time
def show_values():
arduinoSerialData.write("55")#Write some data to test Arduino read serial and turn on LED if it does
arduinoSerialData = serial.Serial('/dev/cu.usbmodem1461', 9600, timeout=None)
time.sleep(5) #Arduino Serial Reset Timeout
Joes = Tk()
Joes.wm_title("Read Serial")
myData= arduinoSerialData.readline()
temp = float(myData) #convert string to float store in var
templabel = Label(Joes, text=(temp))
templabel.pack()
c = Button(Joes, text="Send Data", command=show_values)
c.pack()
time.sleep(2)
Joes.mainloop()
It appears that you misunderstand how the TK mainloop works. It is not, as you described, a loop between calling Tk() and mainloop(), but rather within Tkinter, external of your programs code.
In order to have a loop, updating a label, you would have to specifically write a loop, using Tk's after method, calling an iterable function over and over.
You could make a function like this to do what you want:
def update_label():
data= float(arduinoSerialData.readline())
templabel.config(text=str(data)) #Update label with next text.
Joes.after(1000, update_label)
#calls update_label function again after 1 second. (1000 milliseconds.)
I am unsure of how the arduino data is retrieved, so you may need to modify that slightly to have the correct data.
This is a general premise though for creating a loop in the manner you described.
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
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
I was racking my brain a bit trying to figure out why a tkinter window would only appear after I had stopped my script. Turns out, it won't appear if the delay time in my root.after (that is within my infinite fruity loop) was set to 0. Setting it to 1 or higher caused it to work correctly. Is this a bug or am I missing something important about how .after works? I'm running this with Python 2.7 in Anaconda on mac OS.
import time
import Tkinter as tk
import random
root = tk.Tk()
root.title("random numbers")
root.geometry("220x220+5+5")
frame = tk.Frame(root, width=210, height=210)
frame.pack()
luckynumber = tk.IntVar()
label1 = tk.Label(frame, text="random number").pack(side=tk.LEFT)
display1 = tk.Label(frame, textvariable=luckynumber)
display1.pack( side=tk.LEFT )
def askrandy():
randy = random.randrange(0, 100, 1)
luckynumber.set(randy)
def fruityloop():
time.sleep(.5)
askrandy()
root.after(1, fruityloop)
root.after(0, fruityloop)
root.mainloop()
Second question: this code doesn't run very smoothly. Seeing as it's quite simple, I assumed it would be pretty solid. But I find that it takes a couple seconds to get started and moving the window around causes it to stutter as well. Would this work better with my main loop run as a class?
This is normal behavior.
Tkinter maintains a queue of work to be done when it goes idle. This is the "idle" queue.
When you call after, the function you supply is added to this queue. When the main event loop (or a call to after_idle) processes the queue, it looks for items on the queue that should be run based on the current time and the time that the item should be run. All items that are due to be run are run before processing of the queue stops.
If one of those adds an item to the queue with a value of zero it will be run since its time is due. If that item itself adds an item to the queue, then you take one item off of the queue and immediately put one one so the queue will never become empty. If the queue never becomes empty, tkinter isn't able to process other types of events.
The reason that the program seems slow and jerky is because of the call to sleep. When you call sleep, tkinter does exactly that: it sleeps. It cannot process any events, even events that simply refresh the window. If you want askrandy to be called once every half second, you should simply call after with a value of 500, rather than call it with a value of zero and then sleep for half a second.
Whether the main window is a class or not will not affect your program all all. You simply need to stop using sleep, and provide sane values to after. If you are trying to show a simple animation, a value of 30 is about as small as you need to go.
This is how it should looks without sleep(). But I don't know if it can help. It works fast on Linux.
If you run code in IDLE then you may have problem because it uses Tkinter to display windows and runs own mainloop() but Tkinter should run only one mainloop(). You can try directly in console python script.py.
import Tkinter as tk
import random
# --- functions ---
def fruityloop():
randy = random.randrange(0, 100, 1)
luckynumber.set(randy)
# run again after 500ms = 0.5s
root.after(500, fruityloop)
# --- main ---
root = tk.Tk()
root.title("random numbers")
root.geometry("220x220+5+5")
luckynumber = tk.IntVar()
frame = tk.Frame(root, width=210, height=210)
frame.pack()
label = tk.Label(frame, text="random number")
label.pack(side=tk.LEFT)
display = tk.Label(frame, textvariable=luckynumber)
display.pack(side=tk.LEFT)
# run first time
fruityloop()
root.mainloop()
i have a much more advanced code, but it all comes to this simple example:
from Tkinter import *
import time
def destroyPrint():
global printOut
try:
printOut.destroy()
except:
pass
def sendData():
global new
global printOut
for i in range(6):
destroyPrint()
time.sleep(1)
printOut=Label(new,text=i,font=('arial',15,'bold'))
printOut.place(x=300,y=500)
def newWindow():
global new
print("ok")
new=Toplevel()
new.minsize(800,600)
functionButton=Button(new,text="Send me",width=20,height=20, command=sendData)
functionButton.place(x=300,y=150)
main = Tk()
main.minsize(800, 600)
menu=Button(main,text="Send data",width=20,height=20, command=newWindow)
menu.place(x=300,y=150)
mainloop()
In this simple example, i want to start sendData function, which will update printOut Label accordingly on every loop iteration. We all know that it doesn't, and that it hangs, until function is done, and prints last number (5).
I tried countless examples with threading and queueing, and i am failing badly.
Please, just simple clarification on this example, how to do threading correctly when you have Tkinter elements in a function that needs to be performed in another thread.
I am really getting frustrated here and i spent last 2 days on this step...
You have to add update_idletasks() to update the label. Instead of destroying and creating, just update the text , and use after() in Tkinter instead of sleep as it spawns a new process whereas time.sleep() hangs the program while sleeping.
from Tkinter import *
import time
def sendData():
global new
##global printOut
printOut=Label(new,text="0",font=('arial',15,'bold'))
printOut.place(x=300,y=500)
for x in range(6):
##destroyPrint()
printOut.config(text=str(x))
new.update_idletasks()
time.sleep(1)
def newWindow():
global new
print("ok")
new=Toplevel()
new.minsize(800,600)
functionButton=Button(new,text="Send me",width=20,
height=20, command=sendData)
functionButton.place(x=300,y=150)
main = Tk()
main.minsize(800, 600)
menu=Button(main,text="Send data",width=20,height=20, command=newWindow)
menu.place(x=300,y=150)
main.mainloop()
I'm running a slowish process of building a set of PDFs using LaTeX that's put together by my script.
The PDFs are built in a for loop. I wanted to show a status window that would add a line for each student that the loop goes through, so that you could see the progress. I have been doing this with print, but I wanted something that integrated well with the Tkinter interface that I have moved to.
I have this:
ReStatuswin = Toplevel(takefocus=True)
ReStatuswin.geometry('800x300')
ReStatuswin.title("Creating Reassessments...")
Rebox2 = MultiListbox(ReStatuswin, (("Student", 15), ("Standard", 25), ("Problems", 25) ))
Rebox2.pack(side = TOP)
OKR = Button(ReStatuswin, text='OK', command=lambda:ReStatuswin.destroy())
OKR.pack(side = BOTTOM)
and then the loop:
for row in todaylist:
and then, inside the loop, after the PDF has been made,
Rebox2.insert(END, listy)
It inserts the row fine, but they all show up (along with the ReBox2 window itself) only after the entire loop is finished.
Any idea about what's causing the delay in display?
Thanks!
Yes, from what I can tell, there are two problems. First, you are not updating the display with each new entry. Second, you are not triggering the for loop with a button but instead having it run on startup (which means that the display won't be created until after the loop exits). Unfortunately however, I can't really work with the code you gave because it is a snippet of a much larger thing. However, I made a little script that should demonstrate how to do what you want:
from Tkinter import Button, END, Listbox, Tk
from time import sleep
root = Tk()
# My version of Tkinter doesn't have a MultiListbox
# So, I use its closest alternative, a regular Listbox
listbox = Listbox(root)
listbox.pack()
def start():
"""This is where your loop would go"""
for i in xrange(100):
# The sleeping here represents a time consuming process
# such as making a PDF
sleep(2)
listbox.insert(END, i)
# You must update the listbox after each entry
listbox.update()
# You must create a button to call a function that will start the loop
# Otherwise, the display won't appear until after the loop exits
Button(root, text="Start", command=start).pack()
root.mainloop()