I would like to make a python tkinter window with custom-moving widgets on a canvas to simulate motion. For now, I have one canvas, and one not-moving oval widget. I am having problems at the base level; mainloop(). I understand that it runs in wait for the user to do something, but I am having a hard time seeing:
How to control/see exactly what code mainloop() is reiterating (where, and only tkinter?);
How to properly interrupt it and return to it from another function, if it doesn't do it itself;
What code should be reiterated? All tkinter objects, or only updating changing ones? Use some kind of update operation instead? Finally;
What is the functionality difference between tkinter.mainloop() and window.mainloop()? Perhaps the previous questions will answer.
I have minor experience with Swift, and started learning the very similar Python yesterday evening. I've tried probably hundred of mutations to my code, which currently is in the test stage. I have moved everything in and out of the apparent range of the mainloop, and even got several hundred tiny Python windows all over the screen. Everything does one of two things: it does nothing, or gives me an error. Since I don't know what is even running, or if it is running, I can't diagnose anything. My goal is simply to move a circle one hundred pixels repeatedly. I've scanned around for sources, but—it may be me—a clear one is scarce. I have my code here all marked up. This page is closest to what I am looking for: Move a ball inside Tkinter Canvas Widget (simple Arkanoid game). Everything appears to be under mainloop. So, everything is redrawn every pass? Here, unfortunately, is my whole script; I can't only show pieces. It, for some reason, only brings up a small window, not a full-screen one. (Edit: I seem to have lost the screen size code)
import tkinter
import time
# Initial values for circle's corners and start idicator ('b'):
x1 = 10
y1 = 10
x2 = 210
y2 = 210
b = 0
# Window ('window')
window = tkinter.Tk()
# Canvas ('area')
area = tkinter.Canvas(window, width=1368, height=650)
area.place(x=0, y=0)
# Ovals to be placed on 'area'
oval1 = area.create_oval(x1,y1,x2,y2,fill='#42befe')
oval2 = area.create_oval(100,10,300,210,fill='#d00000')
# Turns b to 1 to start shifting when 'butt' is pressed:
def startFunc():
b = 1
print('b = 1')
# My button to activate 'startFunc'
butt = tkinter.Button(window, text='Start movement', command=startFunc)
butt.pack()
# Adjusts the x and y coordinates when they are fed in:
def Shift(A, B, C, D):
print('Shift activated.')
window.after(1000)
print('Edit and return:')
A += 100
B += 100
C += 100
D += 100
return(A, B, C, D)
# Problems start about here: my Mainloop section;
# I have little idea how this is supposed to be.
while True:
if b == 1:
# Takes adjusted tuple
n = Shift(x1, y1, x2, y2)
print('Returned edited tuple.')
# Changes coordinates
x1 = n[0]
y1 = n[1]
x2 = n[2]
y2 = n[3]
print(f'{x1}, {y1}, {x2}, and {y2}')
# Reiterate moving oval
oval1 = area.create_oval(x1,y1,x2,y2,fill='#42befe')
#Does this re-run 'window' relations outside here, or only within the 'while'?
window.mainloop()
It ought to show a 1368 by 650 window, not a tiny one. The button does nothing but print, which means the final 'while' is not running, despite the mainloop. It want it to loop inside the 'while' line, which should adjust coordinates and move my blue circle. The iteration may NOT touch the initial values, or else it would reset them.
In effect, calling mainloop is the same as if you added this to your code instead of calling mainloop():
while the_program_is_running():
event = wait_for_event()
process_the_event(event)
As a rule of thumb, mainloop() should be called exactly once after the UI has initialized and you are ready for the user to start interacting with your program. When it exits, you typically won't have any code after it, and your program will exit.
How to control/see exactly what code mainloop() is reiterating (where, and only tkinter?);
I don't know what you mean by "reiterating". It doesn't run any code except it's own internal code. It simply waits for events, and then dispatches them to handlers.
How to properly interrupt it and return to it from another function, if it doesn't do it itself;
It's exceedingly rare to do this in a running program. Typically, calling mainloop is the last thing your program does before the user starts interacting with it, and as soon as it exits your program quits.
However, to answer the specific answer of how to interrupt it, you can call the quit method of the root window. That will cause the most recent call to mainloop() to return.
What code should be reiterated? All tkinter objects, or only updating changing ones? Use some kind of update operation instead?
That question is hard to answer because it doens't make much sense. When you call mainloop(), it will watch for all events on all tkinter objects.
What is the functionality difference between tkinter.mainloop() and window.mainloop()
They have exactly the same effect and behavior. Tkinter oddly chose to make mainloop available from any widget. The most common way to call it is from either the tkinter module itself, or from the root window.
My goal is simply to move a circle one hundred pixels repeatedly.
The normal way to do that is to create a function that moves it one hundred pixels. Then, that function (-- or a function that calls it -- can put itself on an event queue to be run in the future.
For example, the following code will move a canvas object 100 pixels every second until the program exits:
def move_object():
the_canvas.move(item_id, 100, 0)
the_canvas.after(1000, move_object)
When it is called, it will move the item 100 pixels to the right. Then, it will place a new call to itself on the event queue to be picked up and handled in approximately 1000 milliseconds.
There are many working examples of using after on this site, including the question you linked to in your question.
Everything appears to be under mainloop. So, everything is redrawn every pass?
No, not exactly. The only objects that are redrawn are things that need to be redrawn. Moving objects on a canvas, resizing a window, dragging another window over your window, etc, all place an event on the event queue that tells tkinter "this object needs to be redrawn". The processing of that event happens automatically by mainloop. If nothing is happening in your application, nothing gets redrawn by mainloop.
It ought to show a 1368 by 650 window, not a tiny one
That is because you haven't given the main window a size. You've given the canvas a size, but you're using place which won't cause the containing window to grow or shrink to fit. As a beginner, you should completely avoid place and instead use pack or grid, because pack and grid will both automatically size your window to fit everything inside.
While it's tempting to use place for its perceived simplicity, in reality it usually requires you to do a lot more work than if you used one of the other geometry managers, and it results in a GUI that isn't particularly responsive to change.
while True:
You should almost never do this in tkinter. Tkinter -- and almost all event based programs -- rely on a steady flow of events. When you have an infinite loop, it cannot process those events. You can put an explicit call to update the screen inside your loop, but that is inefficient and should be avoided. If you need to do something periodically, create a function that encapsulates the body of your loop, then use after to get mainloop to run it while it is processing events.
window.after(1000)
You should almost never use after this way without a second argument. This usage is functionally no different than calling time.sleep(1) in that it prevents mainloop from processing events. You should structure your code to allow for a steady stream of events to be processed by mainloop.
while True: ... window.mainloop()
You definitely need to avoid calling mainloop inside a loop. A well behaved tkinter program should call mainloop() exactly once.
Related
I am trying to write a function to dynamically resize an image displayed in a tkinter window.
Therefore I bound this function to the Configure event:
connroot.bind( "<Configure>", connresiz)
My problems are:
That the connresiz() function gets called 3 times (why 3?) at program start, and
More troublesome, that dynamically resizing the window calls the function continuously as I drag the mouse! How can avoid this?
I thought about checking at the simultaneous presence of a <Configure> and <ButtonRelease-1> events, but I don't know how to code it.
1) We don't know that, since we can't see your code...
2) Short answer is: you can't, because that's exactly what <Configure> event does! Long answer, you can, with a little trick/hack. Since anytime the window is changing, it will call all the binded functions to <Configure>, and the same happens anytime as the mouse button released (right after the last <Configure> call) we can create a flag/switch which will tell us, if the window was "configured" then we can check that switch anytime the mouse button is released, and switch it back to the default value after we ran some actions.
So if you want the image to resized only, when the mouse was released and the window was changed this is the code you need:
from tkinter import *
class Run:
def __init__(self):
self.root = Tk()
self.clicked = False
self.root.bind('<ButtonRelease-1>', self.image_resize)
self.root.bind('<Configure>', lambda e: self.click(True))
def image_resize(self, event):
if self.clicked:
print("I'm printed after <Configure>.") # the action goes here!
self.click(False)
def click(self, value):
self.clicked = value
app = Run()
app.root.mainloop()
According to the official tk documentation, <Configure> events fire "whenever its size, position, or border width changes, and sometimes when it has changed position in the stacking order." This can happen several times during startup.
It is called continuously while you resize the window because the size of the widget is changing. That's what it's defined to do. You can't prevent it from being called, though you can certainly modify what you do in the callback. For example, you could delay resizing the image until you've not received another <Configure> event for a second or two -- which likely means the user has stopped interactive resizing.
This question is NOT a duplicate of this question: Why doesn't the .bind() method work with a frame widget in Tkinter?
As you can see, I set the focus to the current frame in my game_frame() method.
I'm writing a Chip-8 emulator in Python and using Tkinter for my GUI. The emulator is running, but I can't get Tkinter to recognize keypresses. Here is my code:
def game_frame(self):
self.screen = Frame(self.emulator_frame, width=640, height=320)
self.screen.focus_set()
self.canvas = Canvas(self.screen, width=640, height=320, bg="black")
self._root.bind("<KeyPress-A>", self.hello)
for key in self.CPU.KEY_MAP.keys():
print(key)
self.screen.bind(key, self.hello)
self.screen.pack()
self.canvas.pack()
def hello(self, event):
if event.keysym in self.CPU.KEY_MAP.keys():
self.CPU.keypad[self.CPU.KEY_MAP[event.keysym]] = 1
self.CPU.key_pressed = True
self.CPU.pc += 2
sys.exit()
def run_game(self, event):
self.game_frame()
self.CPU.load_rom("TANK")
while True:
self._root.update()
self.after(0, self.CPU.emulate_cycle)
Could you please help me figure out what's going wrong? I think it might have something to do with my game loop interfering with the key bindings, but I'm not sure. The hello method never gets called when I run the game because the program continues to run in an infinite loop and never exits, regardless of what key is pressed. Thank you!
The problem could be due to two things. Without seeing all your code it's impossible to say for sure.
For one, you are binding to a capital "A" rather than a lowercase "a" -- have you testing that the binding works or not when you press a capital A?
Also, you are using after and update incorrectly. You may be starving the event loop, preventing it from processing key presses. The right way to run a function periodically is to have a function that (re)schedules itself.
class CPU_Class():
...
def run_cycle(self):
self.emulate_cycle()
self._root.after(1, self.run_cycle)
Two things to note:
don't use after(0, ...) -- you need to give tkinter at least a ms or so to process other events.
the run_cycle function is responsible for running one cycle, and then scheduling the next cycle to run in the future.
Once you do that, you no longer need your while loop. You can simply call run_cycle once, and that will start the CPU running.
I've searched for a simple animation code with Tkinter but I've found very different examples and I can't understand the correct way to write an animation.
Here my working code to display a simple moving circle:
import tkinter as tk
import time
root=tk.Tk()
canvas=tk.Canvas(root,width=400,height=400)
canvas.pack()
circle=canvas.create_oval(50,50,80,80,outline="white",fill="blue")
def redraw():
canvas.after(100,redraw)
canvas.move(circle,5,5)
canvas.update()
canvas.after(100,redraw)
root.mainloop()
In this code I can't correctly understand: how the after method works, where correctly put the update and the move method (before after method ?), is there another way to write an animation code? may you post me another example and comment the code please?
Thanks :)
Calling update
You should not call canvas.update(). As a general rule of thumb you should never call update. For a short essay on why, see this essay written by one of the original developers of the underlying tcl interpreter.
If you take out the call to canvas.update(), you have the proper way to do animation in a tkinter program.
Calling after to start the animation
You don't need to call after immediately before calling root.mainloop(). This works just as well:
...
redraw()
root.mainloop()
The choice to use or not use after in this specific case is dependent on if you want the animation to start immediately (possibly even before the widget is visible) or if you want it to happen after a short delay (possibly after the widget is made visible)
How after works
mainloop is nothing more than an infinite loop that checks the event queue for events. When it finds an event, it pops it off of the list and processes it. after is nothing more than making a request that says "in 100 ms, please add a new event to the queue". When the time limit expires, an event is added to the queue that says, in effect, "run this command". The next time the loop checks for events, it sees this event, pulls it off of the queue, and runs the command.
When you call after from within a method that itself was called by after, you're saying in effect "wait 100ms and do it again", creating an infinite loop. If you put the call to after before moving the object, you're saying "every 100ms run this function". If you put it after you're saying "run this function 100 ms after the last time it was run". The difference is very subtle and usually not perceptible unless your function takes a long time to run.
my code is:
from tkinter import *
import time
tk = Tk()
płótno = Canvas(tk, width=500, height=500)
płótno.pack()
płótno.create_polygon(10,10,10,70,70,10,fill="blue",outline="black")
for x in range(0,51):
płótno.move(1,5,0)
płótno.update()
rest(0.05)
płótno means canvas
I've been creating a game using Python, and in one of my functions,
def Enemy_Party(enemies, party):
def Sub_Fight(member, enemy, enemies, Prep):
Prep.destroy()
Fight(member, enemy, enemies)
for x in enemies:
Prep = Tk()
for y in party:
button = Button(Prep, text=y.name, command=Sub_Fight(y, x, enemies, Prep)).pack()
Prep.mainloop()
a single window is created, but with nothing in it. There is no error message, the blank window just sits there.
Yes, I have a function Fight, and it does take four arguments. Both 'enemies' and 'party' are defined as lists, and both have values within them (I've checked).
On that note, I have two questions: "Why does the program halt after creating one blank window?" and "Can you define windows in a 'For Loop'?
Let me know if you need any more code, and thanks ahead of time.
The program halts the first time mainloop is called, because that's what mainloop does -- it doesn't return until you destroy the window.
You have a fundamental misunderstanding of how to create windows. Every tkinter application needs a single instance of Tk. To create additional windows you need to create instances of Toplevel. You then need to call mainloop exactly once.
In your code, replace calls to Tk with calls to Toplevel, then remove the call to mainloop in your function. Presumably you're calling mainloop somewhere else in your code after all of the widgets have been created.
I notice that you're defining Tk() inside a for loop. Depending on how many times this loop iterates, you could be creating many instances of Tk() when actually you should never create more than one. I would advise creating your window outside of the loop and populating it as you need to using the loop (or whatever else you may decide to use).
For my class, I am creating a "Mandelbrot Explorer" program. There is one main issue: I lose control of the GUI (all written in Tkinter/Ttk, in Python 2.7) when actually drawing to the Canvas.
Here is my code:
# There is some code above and below, but only this is relevant
for real, imag in graph.PlaneIteration(self.graph.xMin, self.graph.xMax, resolution, self.graph.yMin, self.graph.yMax, resolution, master = self.graph, buffer_action = self.graph.flush):
# the above line iterates on the complex plane, updating the Canvas for every x value
c = complex(real, imag)
function, draw, z, current_iter = lambda z: z**2 + c, True, 0, 1
while current_iter <= iterations:
z = function(z)
if abs(z) > limit:
draw = False
break
current_iter += 1
self.progressbar.setValue(100 * (real + self.graph.xMax) / total)
color = self.scheme(c, current_iter, iterations, draw)
# returns a hex color value
self.graph.plot(c, color)
# self.graph is an instance of my custom class (ComplexGraph) which is a wrapper
# around the Canvas widget
# self.graph.plot just creates a line on the Canvas:
# self.create_line(xs,ys,xs+1,ys+1, fill=color)
My issue is that when run, the graphing takes a while - about 30 seconds. In this time, I cannot use the GUI. If I try to, the window freezes and only unfreezes once the drawing is done.
I tried using threading (I enclosed the entirety of the upper code in a function, thread_process):
thread.start_new_thread(thread_process, ())
However, the problem remains.
Is there a way to fix this? Thanks!
You can execute your loop "threaded" with Tkinter by implicitly returning to Tkinter's main loop execution after every point your draw. Do this by using widget.after to register the next function call:
plane = graph.PlaneIteration(...)
def plotNextPoint():
try:
real, imag = plane.next()
except StopIteration:
return
c = complex(real, imag)
...
self.graph.plot(c, color)
self.graph.after(0, plotNextPoint)
plotNextPoint()
This way, after each point you draw, the Tkinter mainloop will run again and update the display before calling your plotNextPoint function again. If this is too slow, try wrapping the body of plotNextPoint in a for _ in xrange(n) loop to draw n points between redraws.
You're right about the cause of the problem—the GUI event loop is not running while you're busy running this code.
And you're right about threading being a good solution. (The other major solution is to break the job up into smaller subtasks and have each one schedule the next. For a more detailed overview of the options and all of the wrinkles, see Why your GUI app freezes.)
But it's not quite as simple as putting the whole thing on a thread.
Unfortunately, Tkinter (like many GUI frameworks) is not free-threaded. You cannot call methods on any GUI objects from a background thread. If you do, different things happen on different platforms and versions, ranging from blocking the main thread to crashing the program to raising exceptions.
Also, remember that, even without Tkinter, you can't safely share mutable objects between threads without some kind of synchronization. And you're doing exactly that with the Tkinter objects, right?
The Tkinter wiki explains one way to get around both of these problems at once in Tkinter and Threads: Create a Queue, have the background thread put messages on it, and have the main thread check it every so often (e.g., by using after to schedule a nonblocking get every 100ms until the background thread is done).
If you don't want to come up with a "protocol" for passing data from the background thread to the main thread, remember that in Python, a bound method, or a tuple of a bound method and some arguments, it perfectly good, passable data. So, instead of calling self.graph.plot(c, color), you can just self.q.put((self.graph.plot, c, color)).
The library mtTkinter wraps this all up for you, making it look like Tkinter is free-threaded by using a Queue in the background. It isn't highly tested or frequently maintained, but even if it doesn't work in the future it still makes great sample code.