I am currently coding a small app in Tkinter and need to resize a Frame any time the window is resized to keep the aspect ratio of the frame.
I have used the Configure event for that. However, when using this event, the widgets of the window do not show up until I hover my mouse above them.
I, of course, don't want this behaviour to happen.
I have tried binding the configure event to the root window before and after the creation of all the widgets. Without success.
Furthermore, I have tried waiting before I call the configure event with the sleep method.
I have also tried adding a thread which executes binds the event a few seconds later. It does work but when I create new widgets, those new widgets are not properly loaded. I was wondering I there was a way around this.
The bind is done in the init function of my app class
self.root = tk.Tk()
self.root.bind("<Configure>", self.changeWindowSize)
The call back is another class function
def changeWindowSize(self, *args):
self.root.update()
# SOME CALCULATIONS
# Replace the UI
self.ui.place_forget()
self.ui.place(relx=0.25+(0.75 - ui_width)/2, rely=0.05+(0.75 - ui_height)/2, relwidth=ui_width, relheight=ui_height)
Edit:
Here is how to reproduce it:
import tkinter as tk
root = tk.Tk()
root.geometry("1100x600")
def callback(event):
root.update()
root.bind("<Configure>", callback)
for i in range(10):
btn = tk.Button(root, text="Button " + str(i) )
btn.place(relx=0.1*i, rely=0, relwidth=0.1, relheight=1)
root.mainloop()
This issue is now obvious. The root.update causes this behaviour.
Related
I am trying to delete text inside a text box after waiting 5 seconds, but instead the program wont run and does sleep over everything else. Also is there a way for me to just make my textbox sleep so i can run other code while the text is frozen?
from time import time, sleep
from Tkinter import *
def empty_textbox():
textbox.insert(END, 'This is a test')
sleep(5)
textbox.delete("1.0", END)
root = Tk()
frame = Frame(root, width=300, height=100)
textbox = Text(frame)
frame.pack_propagate(0)
frame.pack()
textbox.pack()
empty_textbox()
root.mainloop()
You really should be using something like the Tkinter after method rather than time.sleep(...).
There's an example of using the after method at this other stackoverflow question.
Here's a modified version of your script that uses the after method:
from time import time, sleep
from Tkinter import *
def empty_textbox():
textbox.delete("1.0", END)
root = Tk()
frame = Frame(root, width=300, height=100)
textbox = Text(frame)
frame.pack_propagate(0)
frame.pack()
textbox.pack()
textbox.insert(END, 'This is a test')
textbox.after(5000, empty_textbox)
root.mainloop()
You can emulate time.sleep in tkinter. For this we still need to use the .after method to run our code alongside the mainloop, but we could add readability to our code with a sleep function. To add the desired behavior, tkinter provides another underestimated feature, wait_variable. wait_variable stops the codeblock till the variable is set and thus can be scheduled with after.
def tksleep(t):
'emulating time.sleep(seconds)'
ms = int(t*1000)
root = tk._get_default_root('sleep')
var = tk.IntVar(root)
root.after(ms, var.set, 1)
root.wait_variable(var)
Real world examples:
update a Label to display a clock while-loop
animated writing nested for-loops
Limitation:
tkinter does not quit while tksleep is used.
Make sure there is no pending tksleep by exiting the application.
Using tksleep casually can lead to unintended behavior
UPDATE
TheLizzard worked out something superior to the code above here. Instead of tkwait command he uses the mainloop and this overcomes the bug of not quitting the process as described above, but still can lead to unintended output, depending on what you expect:
import tkinter as tk
def tksleep(self, time:float) -> None:
"""
Emulating `time.sleep(seconds)`
Created by TheLizzard, inspired by Thingamabobs
"""
self.after(int(time*1000), self.quit)
self.mainloop()
tk.Misc.tksleep = tksleep
# Example
root = tk.Tk()
root.tksleep(2)
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)
I'm currently creating an adventure game and I want to bind alt+a to my callback. It doesn't do what I want, so I have two questions:
Is it possible to bind a function to a Label, too?
Why does this (simplyfied) code doesn't work?
Here is the code:
import tkinter as tk
dw = tk.Tk()
dw.title('Hearts')
def play(event):
print('This is the test.')
areal = tk.Frame(master=dw, width=1200, height=600, bg='blue')
areal.pack_propagate(0)
areal.pack(fill=tk.BOTH, expand=bool(dw))
areal.bind("<Alt-A>", play)
dw.mainloop()
It doesn't give me an error, but it doesn't do anything when I click the Frame and afterwards press alt+a. What is wrong here?
EDIT:
import tkinter as tk
def go_fwd(event):
areal.focus_set()
print(event.x, event.y)
dw = tk.Tk()
dw.title('Adventure')
areal = tk.Frame(master=dw, width=20000, height=600, bg='blue')
areal.pack_propagate(0)
areal.pack(fill=tk.BOTH, expand=bool(dw)-100)
areal.focus_set()
dw.bind("<Alt-A>", go_fwd)
enter = tk.Frame(master=dw, width=20000, height=100, bg='cyan')
enter.pack(fill=tk.X)
enterentry = tk.Text(master=enter, width=100, height=4, bg='white')
enterentry.pack()
enterbutton = tk.Button(master=enter, text='Senden', bg='red')
enterbutton.pack()
dw.mainloop()
Here is the complete code.
Is it possible to bind a function to a Label, too?
You can bind to any widget you want. However, if you bind key events, the bindings will only work if the widget has focus. By default, most widgets other than Entry and Text don't get focus unless you explicitly set the focus to them.
Note: only one widget can have keyboard focus at a time.
You can also set a binding to the root window, which will cause it to fire no matter what widget has focus.
For a more thorough explanation of how key bindings are processed, see this answer: https://stackoverflow.com/a/11542200/7432
Why does this (simplyfied) code doesn't work?
It doesn't work the way you expect because the binding is on a Frame widget, but that widget doesn't have the keyboard focus. You could give it focus with something like this:
areal.focus_set()
Or, you could only give it focus after you click on the frame, by creating a binding on a mouse click:
areal.bind("<1>", lambda event: areal.focus_set())
Note: you are binding to a capital "A", so make sure when you test that you're pressing control-alt-a
You need to bind to dw instead of your frame.
So, you can do dw.bind("<Alt-A>", play).
A minor note, Alt-A will bind to the uppercase A as expected, so you'd have to click Alt+Shift+A on your keyboard. Doing Alt+A on your keyboard won't work, you'd have to bind to Alt-a for this to work.
The main window has keyboard focus. Or, alternatively you can leave the bind on the frame and just do areal.focus_set() to set the focus to the frame.
I am trying to delete text inside a text box after waiting 5 seconds, but instead the program wont run and does sleep over everything else. Also is there a way for me to just make my textbox sleep so i can run other code while the text is frozen?
from time import time, sleep
from Tkinter import *
def empty_textbox():
textbox.insert(END, 'This is a test')
sleep(5)
textbox.delete("1.0", END)
root = Tk()
frame = Frame(root, width=300, height=100)
textbox = Text(frame)
frame.pack_propagate(0)
frame.pack()
textbox.pack()
empty_textbox()
root.mainloop()
You really should be using something like the Tkinter after method rather than time.sleep(...).
There's an example of using the after method at this other stackoverflow question.
Here's a modified version of your script that uses the after method:
from time import time, sleep
from Tkinter import *
def empty_textbox():
textbox.delete("1.0", END)
root = Tk()
frame = Frame(root, width=300, height=100)
textbox = Text(frame)
frame.pack_propagate(0)
frame.pack()
textbox.pack()
textbox.insert(END, 'This is a test')
textbox.after(5000, empty_textbox)
root.mainloop()
You can emulate time.sleep in tkinter. For this we still need to use the .after method to run our code alongside the mainloop, but we could add readability to our code with a sleep function. To add the desired behavior, tkinter provides another underestimated feature, wait_variable. wait_variable stops the codeblock till the variable is set and thus can be scheduled with after.
def tksleep(t):
'emulating time.sleep(seconds)'
ms = int(t*1000)
root = tk._get_default_root('sleep')
var = tk.IntVar(root)
root.after(ms, var.set, 1)
root.wait_variable(var)
Real world examples:
update a Label to display a clock while-loop
animated writing nested for-loops
Limitation:
tkinter does not quit while tksleep is used.
Make sure there is no pending tksleep by exiting the application.
Using tksleep casually can lead to unintended behavior
UPDATE
TheLizzard worked out something superior to the code above here. Instead of tkwait command he uses the mainloop and this overcomes the bug of not quitting the process as described above, but still can lead to unintended output, depending on what you expect:
import tkinter as tk
def tksleep(self, time:float) -> None:
"""
Emulating `time.sleep(seconds)`
Created by TheLizzard, inspired by Thingamabobs
"""
self.after(int(time*1000), self.quit)
self.mainloop()
tk.Misc.tksleep = tksleep
# Example
root = tk.Tk()
root.tksleep(2)
I am trying to delete text inside a text box after waiting 5 seconds, but instead the program wont run and does sleep over everything else. Also is there a way for me to just make my textbox sleep so i can run other code while the text is frozen?
from time import time, sleep
from Tkinter import *
def empty_textbox():
textbox.insert(END, 'This is a test')
sleep(5)
textbox.delete("1.0", END)
root = Tk()
frame = Frame(root, width=300, height=100)
textbox = Text(frame)
frame.pack_propagate(0)
frame.pack()
textbox.pack()
empty_textbox()
root.mainloop()
You really should be using something like the Tkinter after method rather than time.sleep(...).
There's an example of using the after method at this other stackoverflow question.
Here's a modified version of your script that uses the after method:
from time import time, sleep
from Tkinter import *
def empty_textbox():
textbox.delete("1.0", END)
root = Tk()
frame = Frame(root, width=300, height=100)
textbox = Text(frame)
frame.pack_propagate(0)
frame.pack()
textbox.pack()
textbox.insert(END, 'This is a test')
textbox.after(5000, empty_textbox)
root.mainloop()
You can emulate time.sleep in tkinter. For this we still need to use the .after method to run our code alongside the mainloop, but we could add readability to our code with a sleep function. To add the desired behavior, tkinter provides another underestimated feature, wait_variable. wait_variable stops the codeblock till the variable is set and thus can be scheduled with after.
def tksleep(t):
'emulating time.sleep(seconds)'
ms = int(t*1000)
root = tk._get_default_root('sleep')
var = tk.IntVar(root)
root.after(ms, var.set, 1)
root.wait_variable(var)
Real world examples:
update a Label to display a clock while-loop
animated writing nested for-loops
Limitation:
tkinter does not quit while tksleep is used.
Make sure there is no pending tksleep by exiting the application.
Using tksleep casually can lead to unintended behavior
UPDATE
TheLizzard worked out something superior to the code above here. Instead of tkwait command he uses the mainloop and this overcomes the bug of not quitting the process as described above, but still can lead to unintended output, depending on what you expect:
import tkinter as tk
def tksleep(self, time:float) -> None:
"""
Emulating `time.sleep(seconds)`
Created by TheLizzard, inspired by Thingamabobs
"""
self.after(int(time*1000), self.quit)
self.mainloop()
tk.Misc.tksleep = tksleep
# Example
root = tk.Tk()
root.tksleep(2)