I'm trying to code collision detection between objects in tkinter for my coursework (unfortunately we cannot use pygame). I have a red square and a black square and I move the red square, and I want the black square to delete when the red square overalaps with it. However this doesn't happen as when I overlap the red square into the black square, the latter doesn't delete. How do I fix this?
import tkinter as tk
from tkinter import *
window = Tk()
window.geometry("1366x768")
canvas = tk.Canvas(window, width = 1300, height = 700)
rec1 = canvas.create_rectangle(100, 100, 150, 150, fill = "black") #black rectangle
mover = canvas.create_rectangle(400, 400, 450, 450, fill = "red", outline = "red") #red moving rectangle
canvas.pack()
def left(event):
x = -10
y = 0
canvas.move(mover, x, y)
def right(event):
x = 10
y = 0
canvas.move(mover, x, y)
def up (event):
x = 0
y = -10
canvas.move(mover, x, y)
def down(event):
x = 0
y = 10
canvas.move(mover, x, y)
window.bind("<Left>", left)
window.bind("<Right>", right)
window.bind("<Up>", up )
window.bind("<Down>", down)
a = canvas.bbox(1) # coordinates of the black rectangle
b = canvas.bbox(2) # coordinates of the red, moving rectangle
if b[0] in range(a[0],a[2]) or b[2] in range(a[0],a[2]) and b[1] in range(a[1],a[3]) or b[3] in range(a[1],a[3]):
canvas.delete(rec1) #if they overlap, then delete the static black rectangle
window.mainloop()
Related
I want to design a 9x9 board in tkinter canvas. Each rectangle should have a width and height of 30 (pixels?). Do I always have to use the pixel coordinates for drawing shapes onto the canvas or is there a more relative way possible? For example, my board looks like this:
class TkCanvas(tk.Canvas):
RECT_WIDTH = 30
def __init__(self, parent, width=600, height=600, columns=9, rows=9):
super().__init__(parent, width=width, height=height)
self.columns=columns
self.rows=rows
self.board = [[None for col in range(columns)] for row in range(rows)]
def draw_board(self, x1=0, x2=0,y1=RECT_WIDTH,y2=RECT_WIDTH):
for col in range(self.columns):
for row in range(self.rows):
x1 = col * self.RECT_WIDTH
y1 = (self.rows-1-row) * self.RECT_WIDTH
x2 = x1 + self.RECT_WIDTH
y2 = y1 + self.RECT_WIDTH
tag = f"tile{col}{row}"
self.board[row][col] = self.create_rectangle(x1, y1, x2, y2, fill="white", tags=tag, outline="black")
self.tag_bind(tag,"<Button-1>", lambda e, i=col, j=row: self.get_location(e,i,j))
def get_location(self, event, i, j):
print (i, j)
def get_x_coord(self, x):
return x * self.RECT_WIDTH
def get_y_coord(self, y):
return y * self.RECT_WIDTH
Now when I want to draw a shape I get the exact coordinates x0,y0 first with get_x_coord and get_y_coord and then calculate x1 and y1 by adding the RECT_WIDTH.
Is there a cleaner way to draw the shapes onto the canvas? Something where I would only have to pass in the coordinates, eg. (4,5) and it would automatically draw it in the right rectangle or do I always have to make these calculations?
There are many ways to produce a grid board and yours works fine.
Using relative offsets to position squares and pieces is easy in tkinter Canvas,
just use canvas.move(itemID, xoffset, yoffset). You can also move or scale the
entire board by using canvas.addtag_all('somename') then canvas.scale('somename', 0, 0, s, s). Where s is a float s > 0
The following code creates class drawBoard that can be instantiated using
just two values or by using many control values and demonstrates how to use
relative coordinates to build a scalable board.
The board is created by drawing all rectangles at (0, 0, w, h) then moving them
to location via relative values (x, y).
Once all squares have been created the entire board is scaled to size.
Method get_location displays user input.
import tkinter as tk
back, fore, draw, wall, light = "white", "blue", "red", "black", "yellow"
class drawBoard(tk.Tk):
def __init__(
self, w, h, columns = 9, rows = 9, scale = 1, line = 1, border = 1):
super().__init__()
self.withdraw()
self.configure(background = light, borderwidth = border)
# pre calculate sizes and set geometry
self.store = dict()
# small change to w, h and wide, high
x, y, w, h = 0, 0, w + line, h + line
wide = w * columns * scale + (line==1)
high = h * rows * scale + (line==1)
self.geometry(f"{wide + border * 2 }x{high + border * 2}")
# minimal Canvas
self.canvas = tk.Canvas(
self, width = wide, height = high, background = back,
borderwidth = 0, highlightthickness = 0, takefocus = 1)
self.canvas.grid(sticky = tk.NSEW)
# draw the board
for row in range(rows):
for column in range(columns):
item = self.canvas.create_rectangle(
0, 0, # removed line, line
w, h, width = line,
fill = back, outline = fore)
self.canvas.move(item, x, y)
self.store[item] = (row, column)
x = x + w
x, y = 0, y + h
# tag all items and scale them
self.canvas.addtag_all("A")
self.canvas.scale("A", 0, 0, scale, scale)
# bind user interaction
self.bind("<Button-1>", self.get_location)
self.after(500, self.complete)
def complete(self):
self.resizable(0, 0)
self.deiconify()
def get_location(self, ev):
# find user selection
item = self.canvas.find_closest(
self.canvas.canvasx(ev.x), self.canvas.canvasy(ev.y))[0]
# flip color for demo
fill = self.canvas.itemcget(item, "fill")
self.canvas.itemconfig(item, fill = [draw, back][fill == draw])
# extract and display info
row, column = self.store[item]
x, y, w, h = self.canvas.coords(item)
print(f"{row}, {column} >> {x}, {y}, {w-x}, {h-y}")
if True:
# the easiest way
app = drawBoard(30, 30)
else:
# Or with lots of control
app = drawBoard(
30, 30, columns = 40, rows = 20, scale = 1, line = 1, border = 1)
I have a program that opens a window and a circle at the center that is filled green at the start. I want this circle to change from red to green and green to red each time I press a button (r in this code) however I can only change the color once
from tkinter import *
import keyboard
if __name__ == '__main__':
window = Tk()
window.title("On Record")
window.configure(width=500, height=300)
window.configure(bg='lightgray')
myCanvas = Canvas(window)
myCanvas.pack()
def daire(x, y, r, canvasName, color): # center coordinates, radius
x0 = x - r
y0 = y - r
x1 = x + r
y1 = y + r
return canvasName.create_oval(x0, y0, x1, y1, fill=color)
# move window center
winWidth = window.winfo_reqwidth()
winwHeight = window.winfo_reqheight()
posRight = int(window.winfo_screenwidth() / 2 - winWidth / 2)
posDown = int(window.winfo_screenheight() / 2 - winwHeight / 2)
window.geometry("+{}+{}".format(posRight, posDown))
color = "green"
def onKeyPress(event):
print("pressed")
counter = 0
counter = counter + 1
print(counter)
if counter % 2 == 0:
color = "green"
else:
color = "red"
daire(190, 135, 120, myCanvas, color)
window.bind("r", onKeyPress)
daire(190, 135, 120, myCanvas, "green")
window.mainloop()
The problem was the event function is reliant on counter being odd or even, but it auto resets it to 0 every time is is run. One solution to this is to place the counter outside of the function and call it as a global.
from tkinter import *
import keyboard
if __name__ == '__main__':
window = Tk()
window.title("On Record")
window.configure(width=500, height=300)
window.configure(bg='lightgray')
myCanvas = Canvas(window)
myCanvas.pack()
def daire(x, y, r, canvasName, color): # center coordinates, radius
x0 = x - r
y0 = y - r
x1 = x + r
y1 = y + r
return canvasName.create_oval(x0, y0, x1, y1, fill=color)
# move window center
winWidth = window.winfo_reqwidth()
winwHeight = window.winfo_reqheight()
posRight = int(window.winfo_screenwidth() / 2 - winWidth / 2)
posDown = int(window.winfo_screenheight() / 2 - winwHeight / 2)
window.geometry("+{}+{}".format(posRight, posDown))
color = "green"
counter = 0
def onKeyPress(event):
global counter
print("pressed")
counter += 1
print(counter)
if counter % 2 == 0:
color = "green"
else:
color = "red"
daire(190, 135, 120, myCanvas, color)
window.bind("r", onKeyPress)
daire(190, 135, 120, myCanvas, color)
window.mainloop()
Out of interest do you actually need to be able to track how many times r is pressed? Its implied by your script but not explicitly stated. Otherwise theres more streamlined ways of doing this by referring directly to tkinter elements, as is often done with button toggles as displayed in this post.
I am trying to model a simple solar system in Tkinter using circles and moving them around in canvas. However, I am stuck trying to find a way to animate them. I looked around and found the movefunction coupled with after to create an animation loop. I tried fidgeting with the parameters to vary the y offset and create movement in a curved path, but I failed while trying to do this recursively or with a while loop. Here is the code I have so far:
import tkinter
class celestial:
def __init__(self, x0, y0, x1, y1):
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
sol_obj = celestial(200, 250, 250, 200)
sx0 = getattr(sol_obj, 'x0')
sy0 = getattr(sol_obj, 'y0')
sx1 = getattr(sol_obj, 'x1')
sy1 = getattr(sol_obj, 'y1')
coord_sol = sx0, sy0, sx1, sy1
top = tkinter.Tk()
c = tkinter.Canvas(top, bg='black', height=500, width=500)
c.pack()
sol = c.create_oval(coord_sol, fill='black', outline='white')
top.mainloop()
Here's something that shows one way to do what you want using the tkinter after method to update both the position of the object and the associated canvas oval object. It uses a generator function to compute coordinates along a circular path representing the orbit of one of the Celestial instances (named planet_obj1).
import math
try:
import tkinter as tk
except ImportError:
import Tkinter as tk # Python 2
DELAY = 100
CIRCULAR_PATH_INCR = 10
sin = lambda degs: math.sin(math.radians(degs))
cos = lambda degs: math.cos(math.radians(degs))
class Celestial(object):
# Constants
COS_0, COS_180 = cos(0), cos(180)
SIN_90, SIN_270 = sin(90), sin(270)
def __init__(self, x, y, radius):
self.x, self.y = x, y
self.radius = radius
def bounds(self):
""" Return coords of rectangle surrounding circlular object. """
return (self.x + self.radius*self.COS_0, self.y + self.radius*self.SIN_270,
self.x + self.radius*self.COS_180, self.y + self.radius*self.SIN_90)
def circular_path(x, y, radius, delta_ang, start_ang=0):
""" Endlessly generate coords of a circular path every delta angle degrees. """
ang = start_ang % 360
while True:
yield x + radius*cos(ang), y + radius*sin(ang)
ang = (ang+delta_ang) % 360
def update_position(canvas, id, celestial_obj, path_iter):
celestial_obj.x, celestial_obj.y = next(path_iter) # iterate path and set new position
# update the position of the corresponding canvas obj
x0, y0, x1, y1 = canvas.coords(id) # coordinates of canvas oval object
oldx, oldy = (x0+x1) // 2, (y0+y1) // 2 # current center point
dx, dy = celestial_obj.x - oldx, celestial_obj.y - oldy # amount of movement
canvas.move(id, dx, dy) # move canvas oval object that much
# repeat after delay
canvas.after(DELAY, update_position, canvas, id, celestial_obj, path_iter)
top = tk.Tk()
top.title('Circular Path')
canvas = tk.Canvas(top, bg='black', height=500, width=500)
canvas.pack()
sol_obj = Celestial(250, 250, 25)
planet_obj1 = Celestial(250+100, 250, 15)
sol = canvas.create_oval(sol_obj.bounds(), fill='yellow', width=0)
planet1 = canvas.create_oval(planet_obj1.bounds(), fill='blue', width=0)
orbital_radius = math.hypot(sol_obj.x - planet_obj1.x, sol_obj.y - planet_obj1.y)
path_iter = circular_path(sol_obj.x, sol_obj.y, orbital_radius, CIRCULAR_PATH_INCR)
next(path_iter) # prime generator
top.after(DELAY, update_position, canvas, planet1, planet_obj1, path_iter)
top.mainloop()
Here's what it looks like running:
I am trying to find the color of the canvas under a Python turtle. I use canvas.find_overlapping but it is only successful when I negate the ycor, implying that the y-axis is inverted in the canvas object, compared to what is shown. Is there a problem with my code or is the y-axis inverted?
import turtle
wn = turtle.Screen()
maze_drawer = turtle.Turtle()
maze_drawer.color("purple")
maze_drawer.speed("fastest")
path_width = 15
def get_pixel_color(x, y):
c = turtle.Screen().getcanvas()
# -y should not work??
items = c.find_overlapping(x, -y, x, -y)
if len(items) > 0:
return c.itemcget(items[0], "fill") # get 0 object (canvas)
# draw simplified maze
wall_len = 0
for i in range(10):
maze_drawer.left(90)
wall_len += path_width
maze_drawer.forward(wall_len)
# navigate maze from center
maze_runner = turtle.Turtle()
maze_runner.color("green")
maze_runner.penup()
maze_runner.goto(-path_width, -path_width)
# test in y dir: maze_runner.setheading(90)
clear = True
while(clear):
maze_runner.forward(1)
color_at_turtle = get_pixel_color(maze_runner.xcor(), maze_runner.ycor())
if (color_at_turtle == "purple"):
clear = False
wn.exitonclick()
Neat use of tkinter pixel detection within turtle! If the inverted Y coordinate is bothersome, you can flip it from turtle's perspective:
from turtle import Screen, Turtle
screen = Screen()
width, height = screen.window_width() / 2, screen.window_height() / 2
screen.setworldcoordinates(-width, height, width, -height) # flip Y coordinate
Then your code doesn't have to think about negating Y as long as you know you're drawing upside down:
from turtle import Screen, Turtle
PATH_WIDTH = 15
def get_pixel_color(x, y):
canvas = screen.getcanvas()
items = canvas.find_overlapping(x, y, x, y)
if items:
return canvas.itemcget(items[0], "fill") # get 0 object (canvas)
return None
screen = Screen()
width, height = screen.window_width() / 2, screen.window_height() / 2
screen.setworldcoordinates(-width, height, width, -height)
maze_drawer = Turtle(visible=False)
maze_drawer.color("purple")
maze_drawer.speed("fastest")
# draw simplified maze
wall_len = 0
for _ in range(20):
maze_drawer.left(90)
wall_len += PATH_WIDTH
maze_drawer.forward(wall_len)
# navigate maze from center
maze_runner = Turtle()
maze_runner.color("dark green", "green")
maze_runner.penup()
maze_runner.goto(-PATH_WIDTH, -PATH_WIDTH)
def run_maze():
maze_runner.forward(1)
x, y = maze_runner.position()
color_at_turtle = get_pixel_color(x, y)
if color_at_turtle == "purple":
maze_runner.backward(PATH_WIDTH - 1)
maze_runner.left(90)
x, y = maze_runner.position()
if -width < x < width and -height < y < height:
screen.ontimer(run_maze, 10)
run_maze()
screen.exitonclick()
So I've made a "game" that has 2 balls a green ball and a red one you can move the red ball around but when it collides with the green ball I want it to display a success message by printing in the console! Heres my code.
__author__ = 'Zac'
from Tkinter import *
from random import randint
class Application:
def circle(self, r, x, y):
return (x-r, y-r, x+r, y+r)
def square(self, s, x, y):
return (x, y, s, s)
def __init__(self, canvas, r, x, y, **kwargs):
self.canvas = canvas
self.r = r
self.x = x
self.y = y
self.ball = canvas.create_oval(self.circle(r, x, y), **kwargs)
root = Tk()
canvas = Canvas(root, width = 1000, height = 1000)
canvas.pack()
ball1 = Application(canvas, 20, 50, 50, fill='red')
ball2 = Application(canvas, 30, 200, 250, fill='green')
def forward(event):
canvas.delete(ball1.ball)
ball1.y -= 5
ball1.ball = canvas.create_oval(ball1.circle(ball1.r, ball1.x, ball1.y), fill='red')
def backward(event):
canvas.delete(ball1.ball)
ball1.y += 5
ball1.ball = canvas.create_oval(ball1.circle(ball1.r, ball1.x, ball1.y), fill='red')
def left(event):
canvas.delete(ball1.ball)
ball1.x -= 5
ball1.ball = canvas.create_oval(ball1.circle(ball1.r, ball1.x, ball1.y), fill='red')
def right(event):
canvas.delete(ball1.ball)
ball1.x += 5
ball1.ball = canvas.create_oval(ball1.circle(ball1.r, ball1.x, ball1.y), fill='red')
root.bind('<w>', forward)
root.bind('<s>', backward)
root.bind('<a>', left)
root.bind('<d>', right)
root.mainloop()
Ok so heres how I did it!
I added this function
def collide():
x_diff = abs(ball1.x - ball2.x)
y_diff = abs(ball1.y - ball2.y)
if x_diff <= 49:
if y_diff <= 49:
print "COLLIDED"
And everytime you run the function to move the collision function is called and if there touching it prints "Collided"
Thx to #CurlyJoe for the code for checking the distance between them!