Python - pack only takes effect after the method is complete - python

I am working on a small game where you enter some amount n, and you get n color frames with random colors. After 3 seconds, the user is prompted to attempt to recall the order of the color frames.
Note that the events after the countdown are not yet implemented (there should be no issue with that).
I have a method that hides the Entry field and confirmation button, displays the colors, and does a countdown:
def startgame(colornumber): #colornumber is the amount of frames
...
colors=[] #array of "Frame" elements
#loop that creates the colored frames
for i in range (0, colornumber):
colors.append(Frame(window, width=60, height=60, background=randomcolor()))
colors[i].pack(side=LEFT)
#randomcolor() is a method that returns a random color string
#like "white" or "red"
#countdown loop
for i in range(0, 3):
time.sleep(1) #do nothing for 1 second
label.configure(text="ASD") #Count down
print(str(3-i))
label.configure(text="What was the order? Click on the colors to choose")
The button calls the function like so:
colorNumberInput = Entry(window)
colorNumberInput.pack()
buttonConfirmNumber = Button(window, ..., command=lambda: startgame(int(colornumberInput.get()))
buttonConfirmNumber.pack()
For some reason, the effects of the statement color[i].pack(side=LEFT) only take place after the method has finished executing; that is, after the countdown loop finishes. The result is that the colors appear after the countdown, and not before it. I tried putting a print statement into the loop that creates the color frames, and it does execute anyway.
Why do the does pack(element) show the widgets only after the method it is called in has ended? Is there any way that I can work around this issue?

Tkinter is only able to refresh the display by responding to events. It can't respond to events while your code is running or the program is sleeping.
A quick fix is to call window.update_idletasks() whenever you want the screen refreshed. That's not usually the best solution, but it often is Good Enough.

Related

changing label color 2 times within a funtion in tkinter python

I have a funtion, that parses the log. I am updating the label color and text at different lines within the same funtion. I see that only the last changed color and text only reflecting in the UI. i do not see the intermediate color and text changes.
Below is my code:
def OnclickLogParser(self):
if LogFileName == '':
messagebox.showinfo("Error", "Please select a valid Log")
if LogPathName == '':
messagebox.showinfo("Error", "Please select a valid Log Path")
self.lb_log_status.configure(bg="#08DFE8", fg="#010101", text='Parsing inProgress...')
m_logParser = CAdpBrrLogParser()
m_logReader = CAdpBrrLogReader('mrr', m_logParser)
status = m_logReader.readFile(LogPathName)
if status == True:
self.lb_log_status.configure(bg="#F6F50B", fg="#010101", text='Log Ready')
self.btn_log_start["state"] = "normal"
global m_injector
m_injector = CAdpUdpDataInjector()
you can see that i am changing the color and text of lb_log_status at two different places. The time gap between those two lines would be around 3 -5 secs. But i can not witness the first color change. '
Tkinter must process events regularly to perform any UI operations. If you want to see the color changes here you have to give Tk a chance to process the configuration and paint events that get generated when you re-configure a widget. Your code just immediately starts processing the log and will not process any events until this function exits and you return to the Tk mainloop() which is the event processing method. You would see the same problem if you used a progress bar widget and tried to update the progress during processing.
One way to resolve this is to use the Tk after() method and schedule your processing in chunks that take a limited amount of time per chunk. After reading and processing a chunk call after() again to schedule the next chunk.
Another method is to put the processing on a worker thread and use event_generate() to post events back to the Tk thread to announce progress and completion.

Problems with update and update_idletasks

I've been learning python for a month now and run into my first brick wall. I have a large art viewer GUI program and at one point want to put an image on screen with a countdown counter-approx every 5 secs. I thought of a code such as the one below The problem is that this uses update and all my reading says that update is bad (starts a new event loop (?)) and that I should use update_idletasks. when I replace update with update_idletasks in the code below the countdown button is not visible until it reaches single figures, update superficially works fine. But also the q bound key calls the subroutine but has no effect
from tkinter import *
import sys
import time
root = Tk()
def q_key(event):
sys.exit()
frame=Frame(root, padx=100, pady=100, bd=10, relief=FLAT)
frame.pack()
button=Button(frame,relief="flat",bg="grey",fg="white",font="-size 18",text="60")
button.pack()
root.bind("q",q_key)
for x in range(30, -1, -5) :
button.configure(text=str(x))
button.update()
print(x)
button.after(5000)
root.mainloop()
In this case you don't need update nor update_idletasks. You also don't need the loop, because tkinter is already running in a loop: mainloop.
Instead, move the body of the loop to a function, and call the function via after. What happens is that you do whatever work you want to do, and then schedule your function to run again after a delay. Since your function exits, tkinter returns to the event loop and is able to process events as normal. When the delay is up, tkinter calls your function and the whole process starts over again.
It looks something like this:
def show(x):
button.configure(text=x)
if x > 0:
button.after(5000, show, x-5)
show(30)

Why does my progress bar work with "print" command and not with tkinter?

I would like to understand why this code:
import time
for i in range(1,11):
print(i)
time.sleep(1)
shows (as it should!) numbers from 1 to 10, each every 1 second, while this code:
from tkinter import *
import time
root = Tk()
for i in range(1,11):
Label(root, text = i).grid(row=0, column=i-1, padx=5, pady =5)
time.sleep(1)
root.mainloop()
waits for 10 seconds, and then displays a window with the 10 numbers (instead of adding them one by one).
I am aware this is a silly question, but I really can't understand! Many Thanks! Alessandro
Most GUI's work differently to what you expect.
They work in an asynchronous way, which means, that you setup your windows and start an event loop.
This event loop will display all widgets, labels, etc, that you set up before calling the event loop and wait for any events (GUI events like mouse or keyboard events, timer events and perhaps network events).
When any event is encountered code associated to that event will be called and this code can request to change the GUI (show or hide elements, change labels or attributes of graphical widgets) However the change to the GUI will only be performed when you give control back to the event loop (when the code handling an event finished)
In your given code you change a label in a for loop with sleep statements, but only after the for loop is finished your main loop is being called and this is the moment, where the final state of your GUI will be displayed.
So what you encounter is a know issue for almost all GUI / asynhronous kind of applications.
You have to rewrite your code such, that you start a timer event, and when the timer event fires a function will set a label and increase the counter by 1. And if the counter is not 11 it will restart another timer
This is because the time.sleep function is before the root.mainloop function.
root.mainloop is what causes the window to appear on-screen and start doing things. Instead, I'd recommend using window.after, as that tells the window to run a function after some time when it's on-screen.
Here's an example of a modification you could make (it's not that good but it works):
from tkinter import *
import time
root = Tk()
progress = 0
end = 10
def update_progress():
global progress
progress += 1
Label(root, text = progress).grid(row=0, column=progress-1, padx=5, pady =5)
if progress < end: root.after(1000,update_progress) # Tell the window to call this function in 1000ms (1 second)
root.after(0,update_progress) # Tell the window to run the update_progress function 0ms after now.
root.mainloop()
I'd recommend looking at gelonida's answer for an explanation of why your original code didn't work, and what you need to keep in mind when programming with GUIs in the future.

Change a label's text colour, change back on keypress

I want to change a label's text colour, wait a few seconds, then change it back when I press a key.
My end goal is making a full onscreen keyboard that will highlight the key that you pressed. However I can't get the function to pause between turning text blue, then back to black. I attempted using time.sleep(2), but it appears to do that at the start of the function, as opposed to the order I wrote it in.
from tkinter import *
import time
window = Tk()
window.geometry("1000x700")
LabQ = Label(window,text="Q",font=("Courier", 30))
LabQ.place(x=210,y=260)
def key(event):
LabQ = Label(window,text="Q",fg="ROYALBLUE",font=("Courier", 30))
LabQ.place(x=210,y=260)
time.sleep(2)
LabQ = Label(window,text="Q",font=("Courier", 30))
LabQ.place(x=210,y=260)
window.bind("<key>", key)
window.mainloop()
You have two problems. One is that you're not changing the color, you're creating an entirely new widget. To change the color you need to use the configure method on an existing widget.
Second, when you call sleep that's exactly what the GUI does -- it sleeps. No code is running and the screen can't be refreshed. As a general rule of thumb, a GUI should never call sleep.
The solution is to use use after to schedule the change for some point in the future:
def key(event):
bg = LabQ.cget("background")
LabQ.configure(background="royalblue")
LabQ.after(2000, lambda color=bg: LabQ.configure(background=color))
This example doesn't gracefully handle the case where you type the same key twice in under two seconds, but that's unrelated to the core issue of how to change the value after a period of time has elapsed.

Tkinter box packing in loop is slow

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()

Categories