Python tkinter Canvas root.after() maximum recursion depth exceeded - python

from tkinter import *
root = Tk()
canvas = Canvas(root, width=400, height=400, bg="white")
canvas.pack()
rect = canvas.create_rectangle(100, 100, 110, 110, fill='blue')
def move_down(event):
canvas.move(rect, 0, 10)
root.after(1, move_down(event))
root.bind('<Down>', move_down)
root.mainloop()
I can't seem to figure out how to make root.after() work. How can I fix this so the rectangle keeps moving down?

Short version: you can't put parentheses on the function you pass to after.
root.after(1,move_down(event))
This line does not register the function move_down as the callback of the after event. Instead, it calls move_down immediately, and would register the return value of move_down as the callback, if you didn't enter an infinite recursion.
To solve this, use just move_down without actually calling it, and make event an optional variable because after isn't going to supply a value. You should probably also use a time larger than 1 ms, or else your rectangle will zip off the screen in the blink of an eye.
from tkinter import *
root = Tk()
canvas = Canvas(root, width=400, height= 400, bg="white")
canvas.pack()
rect = canvas.create_rectangle(100, 100, 110, 110, fill='blue')
def move_down(event=None):
canvas.move(rect, 0, 10)
root.after(100,move_down)
root.bind('<Enter>', move_down) #or whatever you're binding it to
root.mainloop()
Bonus info: If you're about to ask "ok, now how do I get the rectangle to stop moving when I release the key? And how do I make it move in each other direction when I press the other arrow keys?" That requires a more sophisticated design. You need the function registered to root.after to move a variable number of pixels depending the rectangle's velocity, which gets changed based on key events happening independently. Sample implementation:
from tkinter import *
root = Tk()
canvas = Canvas(root, width=400, height= 400, bg="white")
canvas.pack()
rect = canvas.create_rectangle(100, 100, 110, 110, fill='blue')
x_velocity = 0
y_velocity = 0
keys_being_held_down = set()
key_accelerations = {
"Up": (0, -10),
"Down": (0, 10),
"Left": (-10, 0),
"Right": (10, 0)
}
def key_pressed(event):
global x_velocity, y_velocity
#ignore autorepeat events
if event.keysym in keys_being_held_down:
return
keys_being_held_down.add(event.keysym)
acceleration = key_accelerations[event.keysym]
x_velocity += acceleration[0]
y_velocity += acceleration[1]
def key_released(event):
global x_velocity, y_velocity
keys_being_held_down.remove(event.keysym)
acceleration = key_accelerations[event.keysym]
x_velocity -= acceleration[0]
y_velocity -= acceleration[1]
def tick():
canvas.move(rect, x_velocity, y_velocity)
print(x_velocity, y_velocity)
root.after(100,tick)
for key in key_accelerations:
root.bind("<{}>".format(key), key_pressed)
root.bind("<KeyRelease-{}>".format(key), key_released)
root.after(100, tick)
root.mainloop()
(This isn't necessarily the best way to do it, but it demonstrates the basic approach)

I would recommend not using root.after(), so it will move when you click, and not move when you stop clicking

Related

using tkinter.Canvas.after

I'm trying to make 2d game using tkinter, but faced the problem: instead of starting act2() after act() end, act2() and act() executes at the same time.
import tkinter as tk
root = tk.Tk()
can = tk.Canvas(height=500, width=1000)
can.pack()
rect = can.create_rectangle(0, 240, 20, 260, fil='#5F6A6A')
def act():
global rect, can
pos = can.coords(rect)
if pos[2] < 1000:
can.move(rect, 5, 0)
can.update()
can.after(2, act)
def act2():
global rect, can
pos = can.coords(rect)
if pos[3] < 500:
can.move(rect, 0, 5)
can.update()
can.after(2, act2)
def key_down(key):
act()
act2()
can.bind("<Button-1>", key_down)
root.mainloop()
How can I set order of methods executing?
The problem is that can.after(1, act) is non-blocking and doesn't call act() and wait for it to finish before moving onto the next line of code - it instead schedules it to be called later, then immediately moves on to the next line of code. This means that in key_down, when you call act() then act2() it does not move your rectangle all the way to the right then all the way down, but interleaves moving it right and down. In order to wait for the rectangle to move all the way right before starting moving it down, schedule act2() after act() has finished by using the else of the existing if statement:
import tkinter as tk
import datetime
root = tk.Tk()
can = tk.Canvas(height=500, width=1000)
can.pack()
rect = can.create_rectangle(0, 240, 20, 260, fil='#5F6A6A')
def act():
global rect, can
pos = can.coords(rect)
if pos[2] < 1000:
can.move(rect, 5, 0)
can.update()
can.after(1, act)
else:
can.after(1, act2)
def act2():
global rect, can
pos = can.coords(rect)
if pos[3] < 500:
can.move(rect, 0, 5)
can.update()
can.after(1, act2)
def key_down(key):
act()
can.bind("<Button-1>", key_down)
root.mainloop()
If you would prefer to keep your code looking sequential, you could use an approach with generators, and a scheduler, such as this: https://pastebin.com/H6meR3q1

Tkinter making image variable into a class

I was making a game recently using tkinter and it's Canvas widget, but ran into a problem when trying to move an image. I set the canvas.create_image to a variable called character but noticed it turned it into an object. This causes issues as when I try and use the canvas.move(character), it comes back with "character not defined". Here is the full code:
from tkinter import *
from PIL import ImageTk, Image
import pygame
import threading
import time
gamestate= 'title'
waiting = False
#window init
root = Tk()
WIDTH=1280
HEIGHT=720
canvas = Canvas(root, width=WIDTH, height=HEIGHT, bg='black')
canvas.pack(expand=1, fill=BOTH)
logo = PhotoImage(file='Ghost clipart.png')
root.iconphoto(False, logo)
#setting up all the images
title_image = PhotoImage(file='title.png')
board_image = Image.open('board.png')
done_board = board_image.resize((1920,1080), Image.ANTIALIAS)
board_image = ImageTk.PhotoImage(done_board)
character_image = ImageTk.PhotoImage(file='first_run_left.png')
#display the title screen
def title_screen():
title = canvas.create_image(WIDTH/2,HEIGHT/2, anchor=CENTER, image=title_image)
#define the events after clicking play
def play():
canvas.delete("all")
board = canvas.create_image(WIDTH/2 + 100,HEIGHT/2 - 360, anchor=CENTER, image=board_image)
character = canvas.create_image(556,304,anchor=CENTER,image=character_image)
#testing for what should happen the the mouse button is clicked
def click(event):
if gamestate == 'title':
print(event.x, event.y)
if event.x > 475 and event.x < 804 and event.y > 213 and event.y < 337:
play()
elif event.x > 475 and event.x < 804 and event.y > 404 and event.y < 527:
print("skin")
def up(event):
canvas.move(character,0,10)
root.bind('<Button-1>', click)
root.bind('<Up>', up)
#audio module
def test_loop():
while True:
pygame.mixer.init()
pygame.mixer.music.load("nokia-ringtone-arabic.mp3")
pygame.mixer.music.play()
time.sleep(23)
thread = threading.Thread(target=test_loop)
thread.daemon = True
thread.start()
title_screen()
root.mainloop()
Here is a simple script that uses the arrow keys to move an image on the canvas. Note that the variable img (in your case character) must be accessible from the functions that act upon it, therefore its scope must be above these functions.
You can use this as a starting point to solve your problem.
image used:
import tkinter as tk
def up(event):
canvas.move(img, 0, -10)
def down(event):
canvas.move(img, 0, 10)
def left(event):
canvas.move(img, -10, 0)
def right(event):
canvas.move(img, 10, 0)
width = 500
height = 500
window = tk.Tk()
window.title("Moving image")
canvas = tk.Canvas(window, width=500, height=500)
canvas.pack()
my_image = tk.PhotoImage(file="pix.png")
img = canvas.create_image(260, 125, anchor=tk.NW, image=my_image)
window.bind("<Up>", up)
window.bind("<Down>", down)
window.bind("<Left>", left)
window.bind("<Right>", right)
window.mainloop()
Since character is a local variable inside play() function, it cannot be accessed inside up() function.
However you can use tags option of create_image():
def play():
...
canvas.create_image(556,304,anchor=CENTER,image=character_image, tags='character')
Then you can refer to that image item using the same tag:
def up(event):
canvas.move('character',0,-10) # use tag 'character'
create_image doesn't turn anything into an object. It simply returns an integer identifier. The original object is unchanged. character is undefined because it's a local variable and thus only visible in the method that created it.

python 3 tkinter: how to add .after() to graphics

i am trying to make a circle that slowly gets bigger and to do so i need to add a delay. I tried using time.sleep() but found out that won't work and that i need to use .after(). i have tried making a function that makes the circle but that didn't work. i have tried adding a lambda before the oval is created but that didn't work either. is there anyway i can add a delay before the new circle is made?
Thanks.
My code:-
from tkinter import *
root = Tk()
c = Canvas(root, width = 500, height = 500)
c.pack()
oval = c.create_oval(0, 0, 1, 1)
for x in range(2, 200, 5):
c.delete(oval)
root.after(100, oval = c.create_oval(0, 0, x, x))
root.after() takes a ms delay and a function to run after that delay. What you are trying to do is after that delay run a function with a parameter which is x (the radius of the circle). The problem with this is that you cannot ask it to run a function with an input.
To get around this, you could use an update function and then global variables for the radius of the circle etc.
This worked for me:
from tkinter import *
root = Tk()
c = Canvas(root, width = 500, height = 500, highlightthickness = 0)
c.pack()
oval = c.create_oval(0, 0, 1, 1)
r = 2
def update():
global r, oval
if x < 200:
x += 5
c.delete(oval)
oval = c.create_oval(0, 0, r, r)
root.after(100, update)
update()
root.mainloop()
I also added the option highlightthickness = 0 to your canvas because otherwise the coordinate system does not match up and your circles will overflow out of the canvas.

Canvas.Move doesn't work

def moveR(amount):
global x
global y
x = x+amount
can.itemconfig(player, image = playerImageL)
can.move("player", x, y)
root.update()
##SETTING##
can = Canvas(width = 850, height = 550, bg = "black")
can.pack(expand = YES, fill = BOTH)
player = can.create_image(x, y, image = playerImageL, anchor = NW)
root.update()
Hey, i am trying to create a mini game using tkinter and canvas however the move command doesn't work. As you see the SETTING is the setup later in the code i am calling
moveR(100)
however it doesn't work and seems to completly destroy my sprite/image.
Text "player" and variable player are two different things.
Use variable player in move()
can.move(player, x, y)
BTW: you don't need itemconfig()
Please read https://stackoverflow.com/help/mcve. To make the code easily verifiable, use something like a rectangle instead of an image. (Your code fails the same for any item.)
Canvas.move(item, delta_x, delta_y) moves the item a given x and y amount. It does the x + dy calculation itself, so you should not. If you want to move to a given position, use Canvas.coords(item, x0, y0, x1, y1). If the size of the new bounding box is different from what it was, it will also change the shape. The following example uses both methods. As a bonus, it also shows how to use root.after to make repeated changes.
import tkinter as tk
root = tk.Tk()
can = tk.Canvas(root, width=800, height=500)
can.pack()
rec = can.create_rectangle(0, 0, 100, 100, fill='red')
def rmove():
box = can.bbox(rec)
if box[0] < 700:
can.move(rec, 100, 30)
root.after(1000, rmove)
else:
can.coords(rec, 0, 400, 50, 500) # position with new shape
root.after(1000, rmove)
root.mainloop()

Tkinter events outside of the mainloop?

The program I am writing has a tkinter window that is constantly being fed with data manually rather than being part of a mainloop. It also needs to track mouse location. I havn't found a workaround for tracking the mouse outside of mainloop yet, but if you have one please do tell.
from Tkinter import *
import random
import time
def getCoords(event):
xm, ym = event.x, event.y
str1 = "mouse at x=%d y=%d" % (xm, ym)
print str1
class iciclePhysics(object):
def __init__(self, fallrange, speed=5):
self.speed = speed
self.xpos = random.choice(range(0,fallrange))
self.ypos = 0
def draw(self,canvas):
try:
self.id = canvas.create_polygon(self.xpos-10, self.ypos, self.xpos+10, self.ypos, self.xpos, self.ypos+25, fill = 'lightblue')
except:
pass
def fall(self,canvas):
self.ypos+=self.speed
canvas.move(self.id, 0, self.ypos)
root = Tk()
mainFrame = Frame(root, bg= 'yellow', width=300, height=200)
mainFrame.pack()
mainCanvas = Canvas(mainFrame, bg = 'black', height = 500, width = 500, cursor = 'circle')
mainCanvas.bind("<Motion>", getCoords)
mainCanvas.pack()
root.resizable(0, 0)
difficulty = 1500
#root.mainloop()
currentIcicles = [iciclePhysics(difficulty)]
root.update()
currentIcicles[0].draw(mainCanvas)
root.update_idletasks()
time.sleep(0.1)
currentIcicles[0].fall(mainCanvas)
root.update_idletasks()
tracker = 0
sleeptime = 0.04
while True:
tracker+=1
time.sleep(sleeptime)
if tracker % 3 == 0 and difficulty > 500:
difficulty -= 1
elif difficulty <= 500:
sleeptime-=.00002
currentIcicles.append(iciclePhysics(difficulty))
currentIcicles[len(currentIcicles)-1].draw(mainCanvas)
for i in range(len(currentIcicles)):
currentIcicles[i].fall(mainCanvas)
root.update_idletasks()
for i in currentIcicles:
if i.ypos >= 90:
currentIcicles.remove(i)
root.update_idletasks()
There is no way. Mouse movement is presented to the GUI as a series of events. In order to process events, the event loop must be running.
Also, you should pretty much never do a sleep inside a GUI application. All that does is freeze the GUI during the sleep.
Another hint: you only need to create an icicle once; to make it fall you can use the move method of the canvas.
If you are having problems understanding event based programming, the solution isn't to avoid the event loop, the solution is to learn how event loops work. You pretty much can't create a GUI without it.

Categories