I am creating a tkinter canvas, and I need to check for when the user changes the size of the window. The problem is, initially the window apparently isn't the size it's supposed to be. I have the following:
def print_size(self):
print self.root.winfo_width()
def init_simulation(self, size=300):
self.root = Tk()
canvas = Canvas(self.root, width=size, height=size)
self.print_size()
self.root.after(1000, self.print_size)
When running this I get:
1
and a second later:
306
Ignoring the fact that tkinter will add 6 pixels, why is the size first 1 and then 306? Am I setting it up wrong?
When you instantiate a root widget with Tk(), Tkinter starts a process in a separate thread to actually create the window - it doesn't happen in the main loop.
The reason you get a 1 for the size initially is that the root window doesn't exist yet when you call self.print_size the first time, so it gives you a default value of 1. The next time you call it a second later, the window has finished spawning, so it gives the the actual size. It's essentially a race condition - the main event loop gets to the print self.root.winfo_width() before self.root is done being created.
If you'd like to change this behavior, add this line right after self.root = Tk():
self.root.wait_visibility(self.root)
That command forces the main event loop to pause until the given widget (in this case the root window) has been spawned and is visible.
Also, note that you've set the size of the canvas to 300 pixels, so naturally the container window will have some extra width.
Related
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)
RE: verifying the existence of a Toplevel()
I'm setting a 15-second time limit for the user as they choose whether they want a random vowel or a random consonant, 9 times, to form a random list of letters. (I'm using a Toplevel containing a GUI timer which I got from someone else's answer).
If they manage it on time, a similar timer appears on the ORIGINAL window, replacing the vowel/consonant buttons (which I have destroyed at this point), and the old SEPARATE-window Toplevel timer is destroyed. However, I want to make the second countdown, since it HAS to start at 30 seconds, start at the right time, rather than ticking away in the background (behind the VOWEL/CONSONANT buttons) during which time the user is still selecting letters.
If none of that made any sense, then this is a basic outline of the code that doesn't work.
# I tried to test if the the Toplevel timer had been destroyed (which happens as soon as the
# user has finished with the 9 letters). If so, I could then start the NEW 30-second timer.
import tkinter as tk
root = tk.Tk()
test = tk.Label()
# above: later, will test if a Label() widget counts as a 'child'. If it was the case
# that only Toplevels counted as 'children', then I could have used the 'root.winfo_children'
# command to test if the Toplevel() timer had been destroyed, in which case I can start a new
# 30-second timer on the original window.
test.pack()
extraWindow = tk.Toplevel(root)
extraWindow.destroy() # for below, to TRY and test whether the Toplevel object is destroyed
if not root.winfo_children():
print("N0") # doesn't happen, because test label is also a 'child'
# IMAGINE that this is where I set off the NEW 30-second timer
root.mainloop()
Unfortunately, I have a label on the original window, displaying the list of letters as it develops in the fist 15 seconds, and while the user is coming up with a real word from as many of those letters as possible in the new, 30 seconds. I cannot use winfo_children. Is there something I can do to the effect of winfo_Toplevel?
(EDIT: yes there is; I finally did my homework and found something really obvious that I had missed, so, unfortunately, I answered my own question)
Whoops I just found an answer to my own question.
You can check whether a top level exists using 'tkinter.Toplevel.winfo_exists(my_toplevel_name)'.
If you put this in a print statement, it returns 1 if it does exist, and 0 if it does not.
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()
How do I keep updating a tkinter canvas, like in a while ( True ): loop?
I know that you can do after( 1000 , refresh_function );, but how do I make the loop repeat forever?
Practical example: a program that draws a line with fixed length under an angle, and the angle is constantly increasing (so the line is rotating / spinning).
I think I have taken a look at all relevant questions here, but this may still be a duplicate, and if it is, I am sorry.
A while True: loop in incompatible with using .mainloop(). You make a function repeat by having it re-schedule itself before it exits. There are several examples in other answers, such as making something glide across a canvas. Here is another that illustrates the idea.
import tkinter as tk
root = tk.Tk()
text = tk.StringVar(root)
label = tk.Label(root, textvariable=text)
label.pack()
def add_a():
text.set(text.get()+'a')
root.after(500, add_a) # <== re-schedule add_a
root.after(500, add_a) # <== start the repeating process
root.mainloop()
Perhaps class threading.Timer could help you
def f():
# write our code for repainting canvas
# call f() again in 60 seconds
threading.Timer(60, f).start()
# start calling f now and then every 60 sec
f()
To update a TKinter window (with canvas etc.), you'll need root.mainloop(), which equals in:
while 1:
root.update()
i'm hoping anyone can help me out here. i'm having an issue with a tkinter gui i built. the issue only happens in windows. My GUI creates a results frame with some labels in it, when it's time to calculate something else, the user clicks on the "newPort" button and that button is supposed to remove the results frame and set to False some instance attributes internal to the calculation. The issue i'm having, which is apparent only in windows is that sometimes the results frame, and its descendant labels don't disappear every time. Sometimes they do, sometimes they don't. The instance variable is correctly set to False but the widgets are still visible on the main GUI. The GUI also contains a couple checkboxes and radiobuttons but they don't impact the creation of the results frame nor its expected destruction. I have not been able to pin point a pattern of actions the user takes before clicking on the newPort button which causes the frame and labels to not get destroyed. This happens when i freeze my app with py2exe, as well as running the app from the python interpreter within the eclipse IDE. I have not tried running the app from the python interpreter directly (i.e. without the IDE) and this problem does not happen on my Mac when i run the app using the eclipse python interpreter. Thanks very much all! My code looks like this:
import Tkinter as TK
class widget(object):
def __init__(self,parent=None):
self.parent = TK.Frame(parent)
self.parent.grid()
self.frame = TK.Frame(self.parent)
self.frame.grid()
newLedger = TK.Button(self.parent,command=self.newPort).grid()
self.calcButton = TK.Button(self.frame,command=self.showResults)
self.calcButton.grid()
self.calcVariable = True
def newPort(self):
self.calcVariable = False
try:
self.second.grid_forget()
self.first.grid_forget()
self.resultsFrame.grid_forget()
self.second.destroy()
self.first.destroy()
self.resultsFrame.destroy()
except:
raise
self.frame.update_idletasks()
def showResults(self):
self.resultsFrame = TK.Frame(self.frame)
self.resultsFrame.grid()
self.first = TK.Label(self.resultsFrame,text='first')
self.first.grid()
self.second = TK.Label(self.resultsFrame,text='second')
self.second.grid()
if __name__ == '__main__':
root = TK.Tk()
obj = widget(root)
root.mainloop()
You don't need to destroy or call grid_forget on the labels, and you don't need to call grid_forget on the resultsFrame; when you destroy the resultsFrame it will cause all off its children to be destroyed, and when these widgets are destroyed they will no longer be managed by grid.
The only way I can get widgets to not be destroyed is if I click on the "calc" button twice in a row without clicking on the "new" button in-between. I'm doing this by running your program from the command line.