I am writing a Python GUI program using tkinter to read data from an instrument based on a list of setpoints and to show the results of each reading in a tkinter scrolled textbox as the program is running, once the user clicks the "Start" button.
However, my program only shows the results at the end of all loops, rather than update the textbox after each loop. In actual usage, the program might run for many tens of minutes due to the large number of test points.
I tried using the .after method, to allow the GUI to update, but the program does not update the textbox during each loop.
How can I modify my code so that the tkinter textbox in the GUI is updated during each loop?
Here is my simplified code using random results as a sample instrument reading:
import tkinter as tk
import tkinter.scrolledtext as tkscrolled
from tkinter import ttk
def start_measurement():
import random
test_points = [tp for tp in range(1,5)]
for setpoint in test_points:
data_reading = random.normalvariate(setpoint,0.05/2)
data_results.insert(tk.INSERT,"Setpoint = {:.3f}, Reading = {:.3f}\n".format(setpoint, data_reading))
data_results.after(1000)
data_results.insert(tk.INSERT,"\nDone\n")
root = tk.Tk()
# Create main frame for entire program: mainframe
mainframe = ttk.Frame(root, padding="10 10 10 10", style='bgstyle.TFrame')
mainframe.grid(row=0, column=0, sticky=(tk.N, tk.W, tk.E, tk.S))
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
# Create Data Output Text Box with Vertical Scrollbar
data_label = ttk.Label(mainframe, text="Results")
data_label.grid(row=1, column=0, padx=(30,30), pady=(20,0), sticky=(tk.S,tk.W,tk.E))
data_results = tkscrolled.ScrolledText(mainframe, width=100, height=20)
data_results.grid(row=2, column=0, padx=(30,30), pady=(0,20), sticky=(tk.N,tk.W,tk.E))
start_button = ttk.Button(mainframe, width=15, text="Start", command=lambda:start_measurement())
start_button.grid(row=0, column=0, padx=(30,30), pady=(20,0), sticky=(tk.N, tk.W))
root.mainloop()
data_results.after(1000) behaves like time.sleep(1). So the updates are not shown until tkinter mainloop() takes back the control after the function exits.
You should use .after() like below:
def start_measurement(setpoint=1):
import random
if setpoint < 5:
start_button.config(state=tk.DISABLED) # prevent multiple executions
data_reading = random.normalvariate(setpoint, 0.05/2)
data_results.insert(tk.INSERT, "Setpoint = {:.3f}, Reading = {:.3f}\n".format(setpoint, data_reading))
# execute this function one second later with updated setpoint
data_results.after(1000, start_measurement, setpoint+1)
else:
data_results.insert(tk.INSERT, "\nDone\n")
start_button.config(state=tk.NORMAL) # enable for next execution
You can use root.after like this -
data_results.after(1000,lambda:data_results.insert(tk.INSERT,"\nDone\n"))
So, after approximately 1 second it inserts Done. And you should keep this out of the for loop -
def start_measurement():
test_points = [tp for tp in range(1,5)]
for setpoint in test_points:
data_reading = random.normalvariate(setpoint,0.05/2)
data_results.insert(tk.INSERT,"Setpoint = {:.3f}, Reading = {:.3f}\n".format(setpoint, data_reading))
data_results.after(1000,lambda:data_results.insert(tk.INSERT,"\nDone\n"))
Change your code as follows.
def start_measurement():
import random
test_points = [tp for tp in range(1,5)]
for setpoint in test_points:
data_reading = random.normalvariate(setpoint,0.05/2)
data_results.insert(tk.INSERT,"Setpoint = {:.3f}, Reading = {:.3f}\n".format(setpoint, data_reading))
data_results.update()
data_results.after(1000)
data_results.insert(tk.INSERT,"\nDone\n")
Add data_results.update() to your code.This updates your textbox every second.After all iteration You get 'Done' displayed.
Related
I am new to python and I have been learning tkinter recently. So I thought with myself that using the grid_forget() function I can remove a widget and redefine it. I thought of this animation that changes the padding of a label so it would create space (kind of like moving the label but not exactly). However, the animation does not work at all. The program freezes until the label reaches the last value of the padding. How can I fix this? Or is there a better way to animate a label moving in the screen?
Here is my code:
from tkinter import *
import time
root = Tk()
lbl = Label(root, text='------')
lbl.grid(row=0, column=0)
def animation():
padding = 0
while padding < 31:
lbl.grid_forget()
padding += 1
lbl.grid(row=0, column=0, padx=padding)
time.sleep(0.2)
# alternative: root.after(200, lambda: lbl.grid(row=0, column=0, padx=padding))
btn = Button(root, text='Animate', command=animation)
btn.grid(row=1, column=1)
root.mainloop()
You need to update the screen for changes to be shown.
Here is a working version using the .update() method:
from tkinter import *
import time
root = Tk()
lbl = Label(root, text='------')
lbl.grid(row=0, column=0)
def animation():
padding = 0
while padding < 31:
lbl.grid_forget()
padding += 1
lbl.grid(row=0, column=0, padx=padding)
root.update()
time.sleep(0.2)
# alternative: root.after(200, lambda: lbl.grid(row=0, column=0, padx=padding))
btn = Button(root, text='Animate', command=animation)
btn.grid(row=1, column=1)
root.mainloop()
Here is a way I also use to animate stuff on the screen, I am not able to understand what you were trying to achieve with your code snippet above, I tried making some changes to it but I feel this way is much better and let's you get more control of your window.
This uses the widely used Canvas widget in the tkinter library.
The Canvas is a general purpose widget, You can use it for a lot of things. Visit the hyper link for more clarity
Here is a short example of how you would create text on the screen.
from tkinter import *
root = Tk()
root.title("My animation")
c = Canvas(root)
x = 20
y = 20 #Instead of using row and column, you simply use x and y co-ordinates
#We will use these co-ordinates to show where the text is in the starting
my_text = c.create_text(x,y,text = '-----')
c.pack()
# This is all you need to create this text on your screen!
root.mainloop()
The idea is that you put your canvas up on your window , and then place whatever you want on it.
There are a lot more attributes that you can add to make your text look even better. Here is an in-depth tutorial on it.
Now that we have made your text widget, It is now time to move it around. Let us move it to 90,20 From our initial position which is 20,20
Here is how we will do it. If we simply move to text object to 90,90, We won't see any animations, it will just directly have it there. So what we will do is first create it at 21,20. Then 22,20. And so on...
We do this really fast till we reach 90,20
This looks like we are moving the text
from tkinter import *
import time
root = Tk()
root.title("My animation")
c = Canvas(root)
x = 20
y = 20 #Instead of using row and column, you simply use x and y co-ordinates
#We will use these co-ordinates to show where the text is in the starting
my_text = c.create_text(x,y,text = 'weee')
c.pack()
def animation():
y = 0.1
x = 0
for _ in range(1000):
c.move(my_text,x,y)
root.update()
anlabel = Button(root,text = 'Animate!',command = animation).pack()
root.mainloop()
This is not only applicable to text, but everything (like other images)that is there on the canvas. The canvas also has Events which will let you use mouse-clicks and other keys on the computer too.
I have made some changes from the previous code, But it is executable and you can try it for yourself to see how it works. increasing the value in time.sleep() makes the animation slower, the lesser the value, the faster.
Are you sure you aren't trying to do something more like the below example? Animating the padding on one of your widgets is going to screw up the rest of your display.
from tkinter import *
import time
root = Tk()
lbl = Label(root, text='')
lbl.grid(row=0, column=0)
def animation(step=12):
step = 12 if step < 0 else step
lbl['text'] = ' ------ '[step:step+6]
root.after(200, lambda: animation(step-1))
Button(root, text='Animate', command=animation).grid(row=1, column=0, sticky='w')
root.mainloop()
I've got a program that does some simple webscraping. I'm working on giving it to my coworkers to use, but they are unfamiliar with code so I'd like to make a GUI. I've started using tkinter, and I'm currently working on making a progress bar showing where the program is at (it can take up to a couple hours to run). My problem is that I can't seem to get the progress bar to update, and all the online sources use Tkinter, which is an old version. Here is my code:
I've tried updating progressBar['value'] to whatever number I want, but that hasn't been working.
from tkinter import *
from tkinter import ttk
import time
def clicked(progressBar): # runs a couple functions and updates the progress bar when the button is clicked
num = 0
for item in range(5):
# functions go here
num += 10
progressBar['value'] = num
time.sleep(2)
window = Tk()
window.title("Welcome to my app")
window.geometry('600x400')
progressBar = ttk.Progressbar(window, orient='horizontal', length=300, mode='determinate', maximum=100, value=0)
progressBar.grid(columnspan=3, row=2, sticky=(W, E))
btn = Button(window, text="Click me!", command=clicked(progressBar))
btn.grid(column=1, row=1)
window.mainloop()
The tkinter window doesn't open up until 10 seconds after I run the program, and it has the progress bar already at 50% full. I'd like for the bar to slowly increment up, AFTER the button has been clicked. Any advice would be helpful! Thank you!
There are two problems with the code:
command=clicked(progressBar) indeed calls the function promptly.So simply use command=clicked. There is no need to pass progressBar as the argument since the function gets it from global scope.
while the function clicked() is running, GUI freezes. After 5*2sec, progressBar updates to 5*10 abruptly. To update the widgets in a loop, use update_idletastk method:
import tkinter as tk
from tkinter import ttk
import time
def clicked():
num = 0
for item in range(5):
num += 10
progressBar['value'] = num
window.update_idletasks()
time.sleep(2)
window = tk.Tk()
progressBar = ttk.Progressbar(window, orient='horizontal', length=300, mode='determinate', maximum=100, value=0)
progressBar.grid(columnspan=3, row=2, sticky=(tk.W, tk.E))
btn = tk.Button(window, text="Click me!", command=clicked)
btn.grid(column=1, row=1)
window.mainloop()
Alright so here's my problem. I am currently in the process of making a GUI for a discord.js bot ( I know how to interact between python and node ) but my problem is that I have a "Password" 4 digit pin and what's supposed to happen is:
Whenever you enter the four digit pin it should delete the frame, entry, text, etc (Unpack/destroy()) it but it's not doing that. It is, however, deleting the text and text entry point but not the actual frame its self.
>##Importing Functions (eg, npm modules but in python)
from tkinter import *
from tkinter import ttk
from random import randint
import sys
##Root core
root = Tk()
##Developers pin-codes
JaredP = "9119"
##End
##Hash Check + Main core functions
def check_pin():
if code.get() == JaredP:
##Main Core (After auth is done forget packs at bottom)
#End of main
##End of check
##Frames for the main core function(Auth)
content = ttk.Frame(root, padding=(6,6,12,12))
frame = ttk.Frame(content, borderwidth=5, relief="sunken", width=200, height=100)
checkt = ttk.Label(content, text="Hello, please enter your 4 digit pin.", padding=(12,12,12,12))
namelbl = ttk.Label(content, text="Access Code")
code = ttk.Entry(content)
submit = ttk.Button(content, text="Submit", width=10, command=check_pin)
##End of frames
##Design##
root.title("TEX")
root.wm_iconbitmap('icon.ico')
##End##
##Packing for main body
content.pack()
checkt.pack()
namelbl.pack()
code.pack()
submit.pack()
root.mainloop()
##End of packing
To remove you can use, YOUR_FRAME.pack_forget() or YOUR_GRID.grid_forget() depending on whether the frame was packed or grid.
if you aren't going to use it again (independently if it's a frame or grid) YOUR_ELEMENT.destroy()
I'm trying to create a simple program to monitor data that displays in a Tkinter window and refreshes itself every second. The method I'm currently using creates an ever-increasing lag in the refresh cycle as the program runs.
This is a super simplified representation of the approach in the form of a counter. While the cycle begins at the intended rate of one second per loop, even after just a few minutes the cycle-time becomes noticeably slower. Any suggestions to eliminate this lag accumulation would be greatly appreciated. Thanks so much!
from tkinter import *
i=0
root = Tk()
root.title("Simple Clock")
root.configure(background="black")
def Cycle():
global i
Label(root, text="------------------------", bg="black", fg="black").grid(row=0, column=0, sticky=W)
Label(root, text = i, bg="black", fg="gray").grid(row=0, column=0, sticky=W)
i += 1
root.after(1000,Cycle)
root.after(1000,Cycle)
root.mainloop()
Stop creating new objects with each call. Instead, only update the parts that change. For the code above it would be updating the 2nd Label's text:
from tkinter import *
def Cycle():
global i
labels[1]['text'] = i
i += 1
root.after(1000,Cycle)
i=0
root = Tk()
root.title("Simple Clock")
root.configure(background="black")
labels = list()
labels.append(Label(root, text="------------------------", bg="black", fg="black"))
labels.append(Label(root, bg="black", fg="gray"))
for j in range(len(labels)):
labels[j].grid(row=0, column=0, sticky=W)
root.after(1000,Cycle)
root.mainloop()
I've been trying to develop a small gui to calculate some totals with weighting etc. While trying to get a total to update real-time with a changing entry, I noticed it took two events to update; This is a simplified version of my code that shows the problem:
from Tkinter import *
root=Tk()
frame=Frame(root)
frame.pack()
entry=Entry(frame)
entry.pack()
label=Label(frame,text="entry:")
label.pack()
def updatelabel(event):
label=Label(frame,text="entry:"+entry.get())
label.pack()
print "called"
entry.bind("<Key>", updatelabel)
root.mainloop()
When you input into the field the function calls, but does not update to what was typed until the next character is typed. How would I go about getting the label to update to what is in the field at the time?
You don't really need to explicitly process events and use callback functions to accomplish what you want. In other words it's possible to get Tkinter to do it for you automatically using a StringVar().
from Tkinter import *
root=Tk()
frame=Frame(root)
frame.pack()
entry_var = StringVar()
entry_var.set('')
entry = Entry(frame, textvariable=entry_var)
entry.pack()
label = Label(frame, text='entry: ')
label.pack(side=LEFT)
contents = Label(frame, textvariable=entry_var)
contents.pack(side=LEFT)
entry.focus_set() # force initial keyboard focus to be on entry widget
root.mainloop()
Instead of using entry.bind("<Key>", update label), I used the root window instead: root.bind("<Key>", update label). This did the trick, however it is important to realize that the function updatelabel() will be called every time a key is pressed in your tkinter window. This could cause some problems if you have more than one entry box updating labels.
Here is the code I wrote with a few modifications:
from Tkinter import *
root=Tk()
frame=Frame(root)
frame.pack()
update_label = StringVar() # Made a StringVar so you don't get new labels every time a key is pressed.
update_label.set("entry:")
entry=Entry(frame)
entry.pack()
label=Label(frame,textvariable=update_label) # Used textvariable= instead of text=
label.pack()
def updatelabel(event):
update_label.set("entry:" + entry.get()) # Setting the variable used in textvariable=
print "called"
root.bind("<Key>", updatelabel) # Changed entry.bind to root.bind
root.mainloop()
No it doesn't require two entries to be called, it is called on the first entry. The key bindings are on the Entry widgets to avoid the problems that a binding to the root will create if you have more than one entry widget.
import tkinter as tk
class SmallApp(tk.Frame):
def __init__(self, master = None):
tk.Frame.__init__(self, master)
self.master = master
self.pack()
self.entry = tk.Entry(self)
self.entry.pack()
self.var = "entry:"
self.label = tk.Label(text = self.var)
self.label.pack()
self.entry.bind("<Key>", self.updatelabel)
def updatelabel(self, event):
self.var += event.char
self.label.configure(text=self.var)
root = tk.Tk()
app = SmallApp(root)
app.mainloop()