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

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.

Related

Dynamically updating polygon shape in tkinter window

I'm writing a program for my raspberry pi4 for my Master's project which sounded so simple at first but for some reason Python is causing me grief.
In short - I have created a tkinter canvas with a polygon centred on the canvas. The shape of this polygon will depend upon the value of a counter.
The count is controlled by a blink event from a neurosky mindwave headset - this count is working (mostly).
What I want to then do is update the canvas to put the new points for the polygon into the pack but nothing I have tried seems to work. The closest I got was trying a .redraw() command which drew an infinite number of windows before I pulled the plug.
I am not a complete novice to coding having taught many languages in my time but have never used python before and am clearly missing a very simple step which will cause everything to fall out.
I will try to modify the code to use a keyboard press rather than a headset and add it below later if folk think it will help.
import keyboard
import time
from tkinter import *
count = 0
points = [250,250,350,250,350,350,250,350]
root = Tk()
while True:
# set window to middle of screen
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
xcoord = screen_width/2-300
ycoord = screen_height/2 - 300
root.geometry("%dx%d+%d+%d" % (600,600,xcoord,ycoord))
#set up canvas size and background colour
canvas1 = Canvas(root, relief = FLAT,width = 600, height = 600, background = "blue")
#set up buttons shape and colour
button = canvas1.create_polygon(points, fill="darkgreen", outline="yellow")
canvas1.pack()
if keyboard.is_pressed("f"):
if count < 4:
count += 1
elif count == 4:
count = 0
time.sleep(0.1)
if count == 0:
points = [250,250,350,250,350,350,250,350]
elif count == 1:
points = [300,100,500,500,100,500]
elif count == 2:
points = [200,100,400,100,300,500]
elif count == 3:
points = [100,300,500,100,500,500]
elif count == 4:
points = [100,100,100,500,500,300]
print(count)
root.update()
You need to delete the old polygon and create new one. Also don't use while loop in tkinter application. For your case, you can bind a callback on <Key> event and update the polygon in the callback:
import tkinter as tk
count = 0
points = [
[250,250,350,250,350,350,250,350],
[300,100,500,500,100,500],
[200,100,400,100,300,500],
[100,300,500,100,500,500],
[100,100,100,500,500,300],
]
root = tk.Tk()
# set window to middle of screen
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
xcoord = screen_width//2 - 300
ycoord = screen_height//2 - 300
root.geometry("%dx%d+%d+%d" % (600,600,xcoord,ycoord))
#set up canvas
canvas1 = tk.Canvas(root, relief=tk.FLAT, background="blue")
canvas1.pack(fill=tk.BOTH, expand=1)
# create the polygon with tag "button"
canvas1.create_polygon(points[count], fill="darkgreen", outline="yellow", tag="button")
def on_key(event):
global count
if event.char == 'f':
count = (count + 1) % len(points)
print(count)
canvas1.delete("button") # delete the old polygon
canvas1.create_polygon(points[count], fill="darkgreen", outline="yellow", tag="button")
root.bind("<Key>", on_key)
root.mainloop()
You can update any parameters of a shape.
canvas.itemconfigure(shape1_id_or_tag, fill="green")
canvas.itemconfigure(shape2_id_or_tag, fill="#900", outline="red", width=3)
But for your situation try '.coords()':
canvas1.coords("Button", points[count])
Source: https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/canvas-methods.html

Clunky/slow dragging functionality on my analog clock project

I just finished my first Python project, an analog clock. I managed to reach a state where I implemented everything I had in mind and fixed the issues that followed. The last thing I added in is a dragging capability for my clock and while it works it seems slow or at least just unsmooth. Right now I lack the knowledge and experience to understand why its happening and let alone fix it.. Any help or ideas would be appreciated, Thanks in advance. :)
Here is my code:
# Imported tkinter for its methods
import tkinter as tk
# Imported turtle for drawing the clock's hands
import turtle
# Imported time to handle setting and updating the time
import time
# Declared and set the color used for transparency
transparent_clr = '#FB00FF'
# Setup a borderless window with transparent background
# and always on top flag
root = tk.Tk()
root.overrideredirect(True)
root.wm_attributes('-topmost', 1)
root.deiconify()
root.attributes('-transparentcolor', transparent_clr)
# Setup the clock's face using an image
Clock_bg = tk.PhotoImage(file='Clock_bg.png')
canvas = tk.Canvas(width=300, height=300, highlightthickness=0)
screen = turtle.TurtleScreen(canvas)
canvas.create_image(0, 0, image=Clock_bg)
canvas.pack()
screen.tracer(0)
screen.bgcolor(transparent_clr)
# Configure the pen used for the clock's hands
draw = turtle.RawTurtle(screen)
draw.hideturtle()
draw.speed(0)
draw.pensize(3)
# Retain Windows TaskBar visibility and function such as exiting
# the app
wn = tk.Toplevel(root)
wn.iconify()
wn.iconbitmap('Clock_icon.ico')
wn.attributes('-alpha', 0.0)
def wn_destroy():
wn.protocol('WM_DELETE_WINDOW', exit_func)
def exit_func():
root.destroy()
wn_destroy()
# Make the clock draggable
def draggable():
root._offsetx = 0
root._offsety = 0
root.bind('<Button-1>', winclick)
root.bind('<B1-Motion>', windrag)
def windrag(event):
x = root.winfo_pointerx() - root._offsetx
y = root.winfo_pointery() - root._offsety
root.geometry('+{x}+{y}'.format(x=x, y=y))
def winclick(event):
root._offsetx = event.x
root._offsety = event.y
draggable()
# Draw the clock and its hands
def draw_clock(h, m, s, draw):
# Draw the hours hand
draw.penup()
draw.goto(0, 0)
draw.color('black')
draw.setheading(90)
angle = (h / 12) * 360 + (m / 60) * 30
draw.rt(angle)
draw.pendown()
draw.fd(70)
# Draw the minutes hand
draw.penup()
draw.goto(0, 0)
draw.color('black')
draw.setheading(90)
angle = (m / 60) * 360 + (s / 60) * 6
draw.rt(angle)
draw.pendown()
draw.fd(100)
# Draw the seconds hand
draw.penup()
draw.goto(0, 0)
draw.color('red')
draw.setheading(90)
angle = (s / 60) * 360
draw.rt(angle)
draw.pendown()
draw.fd(60)
# Update the time in real time
while True:
# Declared and set the hour, minutes and seconds
h = int(time.strftime('%I'))
m = int(time.strftime('%M'))
s = int(time.strftime('%S'))
draw_clock(h, m, s, draw)
screen.update()
time.sleep(1)
draw.clear()
"You are using your own mainloop with time.sleep(1), therefore root.geometry( gets only called every second. –stovfl"
Thanks a billion, stovf1!

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

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

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()

Remove canvas widgets in Tkinter

from Tkinter import *
import random
root = Tk()
width = 700
height = 600
canvas = Canvas(root, width = width, height = height, bg = "light blue")
canvas.pack()
pipes = []
class NewPipe:
def __init__(self, pipe_pos, pipe_hole):
self.pipe_pos = list(pipe_pos)
def update(self):
self.pipe_pos[0] -= 3
self.pipe_pos[2] -= 3
def draw(self):
canvas.create_rectangle(self.pipe_pos, fill = "green")
def get_pos(self):
return self.pipe_pos
def generate_pipe():
pipe_hole = random.randrange(0, height)
pipe_pos = [width - 100, 0, width, pipe_hole]
pipes.append(NewPipe(pipe_pos, pipe_hole))
draw_items()
canvas.after(2000, generate_pipe)
def draw_items():
for pipe in pipes:
if pipe.get_pos()[2] <= 0 - 5:
pipes.remove(pipe)
else:
pipe.draw()
pipe.update()
canvas.after(100, draw_items)
def jump(press):
pass
canvas.bind("<Button-1>", jump)
canvas.after(2000, generate_pipe)
draw_items()
mainloop()
Right now I am trying to make a game where you have to dodge rectangles, which are pipes. It is basically Flappy Bird, but on Tkinter. In this code I am trying to generate pipes and move them, but the pipes I have drawn before do not leave and they just stay there. This means that when the pipe moves, the position it was just in doesnt change and that shape stays there. Is there any way to delete past shapes, or another way to move them?
canvas.create_rectangle(self.pipe_pos, fill = "green") returns an ID.
You can use this ID to put it into methods like
canvas.coords
canvas.delete
canvas.itemconfigure
canvas.scale
canvas.type
...
Have a look at help(canvas).
The canvas is not a framebuffer on which you paint stuff for one frame. The painted stuff does not go away and you can move it and change all the parameters you can use when creating.

Categories