Tkinter window not displaying until after program is run - python

global window
window = Tk()
window.geometry('300x200')
window.minsize(300,200)
window.maxsize(300,200)
window.configure(bg='black')
window.title("testupdate")
global outputtext
outputtext = tk.StringVar()
outputtext.set("starting window...")
my_label = tk.Label(window, textvariable = outputtext, bg = 'black', fg = 'white', font = 'terminal')
my_label.pack()
class ChangeLabel():
def __init__(self, text):
outputtext = tk.StringVar()
outputtext.set(text)
my_label.config(textvariable = outputtext)
Here's the main code: https://replit.com/#YourPetFinch/textadventure#main.py
I've been trying to make a text adventure game with Tkinter so I can package it all nicely as an app. I created a function that I can call from another file to update a label as the game output so I can keep the GUI setup simple, but I've been having a problem where the window won't show up until the code has finished running. I'm new to tkinter (and honestly not very good at Python in general) so this is probably a stupid question.

That's not how global is used. The global statement is only used inside a function, to state that you're going to modify a global. You can't actually make a global that crosses files, but your from gui import * will handle that.
The issue here is understanding event-driven programming. When you create a window, nothing gets drawn. All that does is send a number of messages to signal the core. The messages will not get fetched and dispatched until it gets into the .mainloop(). The main loop processes all the messages and does the drawing, which might queue up more messages.
So, you cannot use time.sleep(2) like that. As you saw, that will interrupt the process and prevent any drawing from being done. Instead, you will have to use window.after to request a callback after some number of seconds. The .mainloop monitors the timers, and will give you a call when time runs out. You cannot use time.sleep inside an event handler either, for the same reason; your GUI will freeze until your event handler returns.

Related

Alternative To Time.Sleep() for pausing a function

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

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.

Delete all from TkInter Canvas and put new items while in mainloop

The goal is to achieve different "screens" in TkInter and change between them. The easiest to imagine this is to think of a mobile app, where one clicks on the icon, for example "Add new", and new screen opens. The application has total 7 screens and it should be able to change screens according to user actions.
Setup is on Raspberry Pi with LCD+touchscreen attached. I am using tkinter in Python3. Canvas is used to show elements on the screen.
Since I am coming from embedded hardware world and have very little experience in Python, and generally high-level languages, I approached this with switch-case logic. In Python this is if-elif-elif...
I have tried various things:
Making global canvas object. Having a variable programState which determines which screen is currently shown. This obviously didn't work because it would just run once and get stuck at the mainloop below.
from tkinter import *
import time
root = Tk()
programState = 0
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
time.sleep(2)
canvas.delete(ALL) #delete all objects from canvas
programState = 1
elif(programState == 1):
....
....
....
root.mainloop()
Using root.after function but this failed and wouldn't show anything on the screen, it would only create canvas. I probably didn't use it at the right place.
Trying making another thread for changing screens, just to test threading option. It gets stuck at first image and never moves to second one.
from tkinter import *
from threading import Thread
from time import sleep
def threadFun():
while True:
backgroundImage = PhotoImage(file="image1.gif")
backgroundImage2 = PhotoImage(file="image2.gif")
canvas.create_image(0,0,image=backgroundImage, anchor=NW)
sleep(2)
canvas.delete(ALL)
canvas.create_image(0,0,image=backgroundImage2, anchor=NW)
root = Tk()
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
# daemon=True kills the thread when you close the GUI, otherwise it would continue to run and raise an error.
Thread(target=threadFun, daemon=True).start()
root.mainloop()
I expect this app could change screens using a special thread which would call a function which redraws elements on the canvas, but this has been failing so far. As much as I understand now, threads might be the best option. They are closest to my way of thinking with infinite loop (while True) and closest to my logic.
What are options here? How deleting whole screen and redrawing it (what I call making a new "screen") can be achieved?
Tkinter, like most GUI toolkits, is event driven. You simply need to create a function that deletes the old screen and creates the new, and then does this in response to an event (button click, timer, whatever).
Using your first canvas example
In your first example you want to automatically switch pages after two seconds. That can be done by using after to schedule a function to run after the timeout. Then it's just a matter of moving your redraw logic into a function.
For example:
def set_programState(new_state):
global programState
programState = new_state
refresh()
def refresh():
canvas.delete("all")
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
canvas.after(2000, set_programState, 1)
elif(programState == 1):
...
Using python objects
Arguably a better solution is to make each page be a class based off of a widget. Doing so makes it easy to add or remove everything at once by adding or removing that one widget (because destroying a widget also destroys all of its children)
Then it's just a matter of deleting the old object and instantiating the new. You can create a mapping of state number to class name if you like the state-driven concept, and use that mapping to determine which class to instantiate.
For example:
class ThisPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
class ThatPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
page_map = {0: ThisPage, 1: ThatPage}
current_page = None
...
def refresh():
global current_page
if current_page:
current_page.destroy()
new_page_class = page_map[programstate]
current_page = new_page_class()
current_page.pack(fill="both", expand=True)
The above code is somewhat ham-fisted, but hopefully it illustrates the basic technique.
Just like with the first example, you can call update() from any sort of event: a button click, a timer, or any other sort of event supported by tkinter. For example, to bind the escape key to always take you to the initial state you could do something like this:
def reset_state(event):
global programState
programState = 0
refresh()
root.bind("<Escape>", reset_state)

How can I perform an action after mainloop() has been called in Tkinter?

I want to make a GUI command line using the Text widget. For debugging purposes, I am trying to print whatever the user types into the separate GUI window to the system terminal. I know that it is frowned upon to mix GUI and Text Based commands into the same script, but I am just debugging, so forgive me 😉
Here is my code:
from Tkinter import *
main = Tk()
console = Text(main)
console.pack()
main.mainloop()
while True:
text = console.get("1.0", "end-1c")
print(text)
My current issue is that when the mainloop starts, (of course) the while loop doesn't. If I were to move the while loop in front of the mainloop call, it would never call mainloop. I really want it to continuously check for new text.
Is there a way to like "pause" the mainloop, or just carry out the command, maybe on a new thread or something?
I want to avoid using main.after(), but if that is the only way, then so be it. ¯\(°_o)/¯
I recommend using main.after(), as it's the canonical way to do things like this in Tkinter. The following will also ensure that it only tries to print every second, instead of as fast as the console can handle it (as the while loop in your code would do if it worked).
def print_console():
print(console.get("1.0", "end-1c"))
main.after(1000, print_console)
print_console()
main.mainloop()
You can also bind widgets to "Modified"
from Tkinter import *
class TextModified():
def __init__(self):
root = Tk()
self.txt = Text(root)
self.txt.pack()
self.txt.focus_set()
self.txt.bind('<<Modified>>', self.changed)
Button(text='Exit', command=root.quit).pack()
root.mainloop()
def changed(self, value=None):
flag = self.txt.edit_modified()
if flag: # prevent from getting called twice
print "changed called", self.txt.get("1.0", "end-1c")
## reset so this will be called on the next change
self.txt.edit_modified(False)
TM=TextModified()

Destroying a Toplevel in a thread locks up root

I have a program I've been writing that began as a helper function for me to find a certain report on a shared drive based on some information in that report. I decided to give it a GUI so I can distribute it to other employees, and have ran into several errors on my first attempt to implement tkinter and threading.
I'm aware of the old adage "I had one problem, then I used threads, now I have two problems." The thread did, at least, solve the first problem -- so now on to the second....
My watered down code is:
class GetReport(threading.Thread):
def __init__(self,root):
threading.Thread.__init__(self)
# this is just a hack to get the StringVar in the new thread, HELP!
self.date = root.getvar('date')
self.store = root.getvar('store')
self.report = root.getvar('report')
# this is just a hack to get the StringVar in the new thread, HELP!
self.top = Toplevel(root)
ttk.Label(self.top,text="Fooing the Bars into Bazes").pack()
self.top.withdraw()
def run(self):
self.top.deiconify()
# a function call that takes a long time
self.top.destroy() #this crashes the program
def main():
root = Tk()
date,store,report = StringVar(),StringVar(),StringVar()
#####
## labels and Entries go here that define and modify those StringVar
#####
def launchThread(rpt):
report.set(rpt)
# this is just a hack to get the StringVar in the new thread, HELP!
root.setvar('date',date.get())
root.setvar('store',store.get())
root.setvar('report',report.get())
# this is just a hack to get the StringVar in the new thread, HELP!
reportgetter = GetReport(root)
reportgetter.start()
ttk.Button(root,text="Lottery Summary",
command=lambda: launchThread('L')).grid(row=1,column=3)
root.mainloop()
My expected output is for root to open and populate with Labels, Entries, and Buttons (some of which are hidden in this example). Each button will pull data from the Entries and send them to the launchThread function, which will create a new thread to perform the foos and the bars needed to grab the paperwork I need.
That thread will launch a Toplevel basically just informing the user that it's working on it. When it's done, the Toplevel will close and the paperwork I requested will open (I'm using ShellExecute to open a .pdf) while the Thread exits (since it exits its run function)
What's ACTUALLY happening is that the thread will launch its Toplevel, the paperwork will open, then Python will become non-responsive and need to be "end processed".
As far as I know you cannot use Threading to alter any GUI elements. Such as destroying a Toplevel window.
Any Tkinter code needs to be done in the main loop of your program.
Tkinter cannot accept any commands from threads other than the main thread, so launching a TopLevel in a thread will fail by design since it cannot access the Tk in the other thread. To get around this, use the .is_alive method of threads.
def GetReport(threading.Thread):
def __init__(self,text):
self.text = text
super().__init__()
def run(self):
# do some stuff that takes a long time
# to the text you're given as input
def main():
root = Tk()
text = StringVar()
def callbackFunc(text):
top = Toplevel(root)
ttk.Label(top,text="I'm working here!").pack()
thread = GetReport(text)
thread.start()
while thread.is_alive():
root.update() # this keeps the GUI updating
top.destroy() # only when thread dies.
e_text = ttk.Entry(root,textvariable=text).pack()
ttk.Button(root,text="Frobnicate!",
command = lambda: callbackFunc(text.get())).pack()

Categories