Tkinter delete all moving objects at defined coords - python

I have got some shapes and a bunch of( thousands of ) moving lines. Lines are created every time and they move through downward. I want to delete them if their top coords is 200. But when I have tried this the all canvas stops the moving ( other shapes and all lines stop and all lines deleted). I need help here. I want that: The lines will be deleted at certain X coords, meanwhile the creation and move of the the lines will keep going on dynamically without any process stops or closed.
class Flow:
def __init__(self, tph, color, canvas):
self.flows = []
self.tph = tph
self.color = color
self.Xspeed = 10
self.Yspeed = 0
self.canvas = canvas
def create_flow(self):
for i in range(self.tph):
self.flow = self.canvas.create_line(550, 50, 500, 52, fill=self.color, width=2)
self.flows.append(self.flow)
return self.flows
def move_flow(self):
for self.flow in self.flows:
self.canvas.move(self.flow, self.Xspeed - randint(10, 30), self.Yspeed + randint(10, 20))
flow = Flow(10, "#976C3D", canvas)
def animate():
flow.create_flow()
flow.move_flow()
canvas.after(100, animate)
animate()
tk.mainloop()

If you want to remove lines with y-coordinate > 200, you can modify move_flow() as below:
def move_flow(self):
for flow in self.flows:
coords = self.canvas.coords(flow)
if coords[1] > 200: # top y > 200
self.canvas.delete(flow) # delete the canvas line item
self.flows.remove(flow) # delete the item in self.flows as well
else:
self.canvas.move(flow, self.Xspeed-randint(10, 30), self.Yspeed+randint(10, 20))

Related

How to make an object move constantly in tkinter [duplicate]

This is a very basic program with which I want to make two moving balls, but only one of them actually moves.
I have tried some variations as well but can't get the second ball moving; another related question - some people use the move(object) method to achieve this, while others do a delete(object) and then redraw it. Which one should I use and why?
This is my code that is only animating/moving one ball:
from Tkinter import *
class Ball:
def __init__(self, canvas, x1, y1, x2, y2):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.canvas = canvas
self.ball = canvas.create_oval(self.x1, self.y1, self.x2, self.y2, fill="red")
def move_ball(self):
while True:
self.canvas.move(self.ball, 2, 1)
self.canvas.after(20)
self.canvas.update()
# initialize root Window and canvas
root = Tk()
root.title("Balls")
root.resizable(False,False)
canvas = Canvas(root, width = 300, height = 300)
canvas.pack()
# create two ball objects and animate them
ball1 = Ball(canvas, 10, 10, 30, 30)
ball2 = Ball(canvas, 60, 60, 80, 80)
ball1.move_ball()
ball2.move_ball()
root.mainloop()
You should never put an infinite loop inside a GUI program -- there's already an infinite loop running. If you want your balls to move independently, simply take out the loop and have the move_ball method put a new call to itself on the event loop. With that, your balls will continue to move forever (which means you should put some sort of check in there to prevent that from happening)
I've modified your program slightly by removing the infinite loop, slowing down the animation a bit, and also using random values for the direction they move. All of that changes are inside the move_ball method.
from Tkinter import *
from random import randint
class Ball:
def __init__(self, canvas, x1, y1, x2, y2):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.canvas = canvas
self.ball = canvas.create_oval(self.x1, self.y1, self.x2, self.y2, fill="red")
def move_ball(self):
deltax = randint(0,5)
deltay = randint(0,5)
self.canvas.move(self.ball, deltax, deltay)
self.canvas.after(50, self.move_ball)
# initialize root Window and canvas
root = Tk()
root.title("Balls")
root.resizable(False,False)
canvas = Canvas(root, width = 300, height = 300)
canvas.pack()
# create two ball objects and animate them
ball1 = Ball(canvas, 10, 10, 30, 30)
ball2 = Ball(canvas, 60, 60, 80, 80)
ball1.move_ball()
ball2.move_ball()
root.mainloop()
This function seems to be the culprit
def move_ball(self):
while True:
self.canvas.move(self.ball, 2, 1)
self.canvas.after(20)
self.canvas.update()
You deliberately put yourself in an infinite loop when you call it.
ball1.move_ball() # gets called, enters infinite loop
ball2.move_ball() # never gets called, because code is stuck one line above
Its only moving one because the program reads only one variable at a time. If you set the program to read when the ball gets to a certain spot, say the end of the canvas, you could then code the program to read the next line and trigger the second ball to move. But, this will only move one at a time.
Your program is literally stuck on the line:
ball1.move_ball()
And it will never get to line:
ball2.move_ball()
Because there isn't a limit to where the loop should end.
Otherwise, the answer by "sundar nataraj" will do it.
try
instead of self.canvas.move(self.ball, 2, 1) use
self.canvas.move(ALL, 2, 1)
All this used to move all the objects in canvas

how to redraw a line on a line which is already drawn in tkinter canvas smoothly?

I have some lines in tinter canvas, and also have their code. I want to make them red but not at a same time I want to draw another line(red line) go on them but it should take different time. for example fo one specific line it should take 3 seconds that line get red for another one it should take 7 seconds to make that red. it is like drawing another red line on the previous one.
def activator(self, hexagon, duration_time):
if not hexagon.is_end:
self.canvas.itemconfigure(hexagon.drawn, fill="tomato")
self.canvas.itemconfigure(hexagon.hex_aspects.outputs.drawn, fill="tomato")
for example I want my hexagon which created by createpolygon method of tinter get red but not immediately. It should do regarding to duration_time which is the a second variable. I mean it should be done within duration_time second(let say 3 second). is there any way for doing this? I have lots of object in my canvas which should get red during an specific time. line, circle, polygon..
A line on a tk.canvas is defined by a start and an end point; in order to access points on the line, we need to first create an affine line by first generating many points at an interval on the line, then join them with line segments.
This affine line is created upon clicking on an item on the canvas, but is hidden at first, and progressively revealed over a short time interval.
Once the redraw is completed, the affine line is hidden again, and the item being redrawn set to its new color.
This "simple" redraw requires quite a bit of machinery to implement. You can try it by clicking on a line to redraw it, and see the animation of the redraw.
Code:
import random
import tkinter as tk
WIDTH, HEIGHT = 500, 500
class AffinePoint:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return AffinePoint(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return AffinePoint(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return AffinePoint(self.x * scalar, self.y * scalar)
def __iter__(self):
yield self.x
yield self.y
def draw(self, canvas):
offset = AffinePoint(2, 2)
return canvas.create_oval(*(self + offset), *self - offset, fill='', outline='black')
def create_affine_points(canvas, num_points):
"""sanity check"""
for _ in range(num_points):
AffinePoint(random.randrange(0, WIDTH), random.randrange(0, HEIGHT)).draw(canvas)
class AffineLineSegment:
def __init__(self, start, end, num_t=100):
self.start = AffinePoint(*start)
self.end = AffinePoint(*end)
self.num_t = num_t
self.points = []
self._make_points()
self.segments = []
def _make_points(self):
for _t in range(self.num_t):
t = _t / self.num_t
self.points.append(self.start + (self.end - self.start) * t)
def __iter__(self):
for point in self.points:
yield point
def draw(self, canvas):
for p0, p1 in zip(self.points[:-1], self.points[1:]):
self.segments.append(canvas.create_line(*p0, *p1, width=5, state='hidden', fill='red'))
def hide(self, canvas):
for seg in self.segments:
canvas.itemconfigure(seg, state='hidden')
def create_affine_line(canvas, num_lines):
"""sanity check"""
for _ in range(num_lines):
start = random.randrange(0, WIDTH), random.randrange(0, HEIGHT)
end = random.randrange(0, WIDTH), random.randrange(0, HEIGHT)
AffineLineSegment(start, end).draw(canvas)
def select_and_redraw(event):
item = canvas.find_closest(event.x, event.y)[0]
x0, y0, x1, y1 = canvas.coords(item)
canvas.itemconfigure(item, fill='grey25')
canvas.itemconfigure(item, width=1)
a = AffineLineSegment((x0, y0), (x1, y1))
a.draw(canvas)
gen = (segment for segment in a.segments)
redraw(gen, a, item)
def redraw(gen, a, item):
try:
segment = next(gen)
canvas.itemconfigure(segment, state='normal')
root.after(10, redraw, gen, a, item)
except StopIteration:
a.hide(canvas)
canvas.itemconfigure(item, state='normal')
canvas.itemconfigure(item, fill='red')
canvas.itemconfigure(item, width=3)
finally:
root.after_cancel(redraw)
root = tk.Tk()
canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg="cyan")
canvas.pack()
canvas.bind('<ButtonPress-1>', select_and_redraw)
# sanity checks
# create_affine_points(canvas, 500)
# create_affine_line(canvas, 100)
for _ in range(10):
start = random.randrange(0, WIDTH), random.randrange(0, HEIGHT)
end = random.randrange(0, WIDTH), random.randrange(0, HEIGHT)
canvas.create_line(*start, * end, activefill='blue', fill='black', width=3)
root.mainloop()
Screen capture showing a line in the process of being redrawn
Try something like this
from tkinter import *
import numpy as np
root = Tk()
def lighter(color, percent):
color = np.array(color)
white = np.array([255, 255, 255])
vector = white-color
return tuple(color + vector * percent)
def Fade(line, start_rgb, percentage, times, delay):
'''assumes color is rgb between (0, 0, 0) and (255, 255, 255) adn percentage a value between 0.0 and 1.0'''
new_color = lighter(start_rgb, percentage)
red, blue, green = new_color
red = int(red)
blue = int(blue)
green = int(green)
new_hex = '#%02x%02x%02x' % (red, blue, green)
canvas.itemconfigure(line, fill=new_hex)
if times > 0:
root.after(delay, lambda: Fade(line, new_color, percentage, times - 1, delay))
canvas = Canvas(root, bg="black")
canvas.pack()
line = canvas.create_line(0, 0, 100, 100, width=10)
Fade(line, (0, 0, 50), 0.01, 1000, 10)
root.mainloop()

tkinter - problem with a coloring a rectangle in a grid made out of rectangles

I am trying to write a pathfinding algorithm in python. The user is supposed to select a starting point by hovering the mouse of a field and pressing s. The field should now change the color.
However, I can't figure out what is wrong with my code. I am only able to color to color the fields from top left corner to the bottom right corner. In the code, Im printing out the objectID in console, which shows that there is maybe something wrong with the way of how I created the rectangles.
I'm creating the rectangles in the draw_grid method in the Window class and coloring the fields in the select_start_node method.
import tkinter as tk
class Window:
def __init__(self):
self.height = 600
self.width = 600
self.grid_list = {x for x in range(0, 600)}
self.grid = []
self.grid_dict = {}
self.root = tk.Tk()
self.root.geometry("600x600")
self.root.resizable(False, False)
self.canvas = tk.Canvas(self.root, width=self.width,
height=self.height, background="white")
self.canvas.bind("s", self.select_start_node)
self.canvas.bind("<1>", lambda event:
self.canvas.focus_set())
def draw_grid(self):
print(self.grid)
for x in self.grid_list:
if x % 30 == 0:
self.grid.append(x)
else:
pass
print(self.grid)
for x in self.grid:
for y in self.grid:
print(x, y+30)
rec = self.canvas.create_rectangle(x, x, y+30, y+30)
self.canvas.pack()
def select_start_node(self, event):
print(event.x, event.y)
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
item = self.canvas.find_closest(x, y)
p = self.canvas.coords(item)
print(item)
print(p)
self.canvas.create_rectangle(p[0], p[0], p[0]+30, p[0]+30, fill="red")
def main():
node_list = []
cord_list = []
window = Window()
window.draw_grid()
window.root.mainloop()
if __name__ == "__main__":
main()
I don't know the entire design of you game, but suggest that you do things differently with respect to the grid of rectangles. In the code below self.grid is a 2-dimensional list-of-lists and each entry is a Canvas rectangle object. This make selecting and changing one of them relatively each because canvas.find_closest(x, y) will give you the object id of the associated rectangle object directly, which makes changing its fill color trivial.
Because of that, I also changed it so you can just click on one of the rectangles to change it instead of moving the mouse cursor and then pressing a key.
Also note that I also got rid of most those hardcoded numerical constants you were using all over the place, which makes the code more flexible in case you decide to change one of them at a later time.
import tkinter as tk
class Window:
def __init__(self):
self.cell_size = 30
self.height = 600
self.width = 600
self.hz_cells = self.width // self.cell_size # Number of horizontal cells.
self.vt_cells = self.height // self.cell_size # Number of vertical cells.
# Preallocate 2D grid (list-of-lists).
self.grid = [[None for _ in range(self.hz_cells)]
for _ in range(self.vt_cells)]
self.root = tk.Tk()
self.root.geometry("%sx%s" % (self.width, self.height))
self.root.resizable(False, False)
self.canvas = tk.Canvas(self.root, width=self.width,
height=self.height, background="white")
self.canvas.pack()
self.canvas.bind("<1>", self.select_start_node)
# You can still do it this way if you want.
# self.canvas.bind("s", self.select_start_node)
# self.canvas.bind("<1>", lambda event: self.canvas.focus_set())
def draw_grid(self):
""" Fill Canvas with a grid of white rectangles. """
for i in range(self.hz_cells):
x = i * self.cell_size
for j in range(self.vt_cells):
y = j * self.cell_size
self.grid[i][j] = self.canvas.create_rectangle(
x, y, x+self.cell_size, y+self.cell_size, fill="white")
def select_start_node(self, event):
""" Change the color of the rectangle closest to x, y of event. """
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
selected_rect = self.canvas.find_closest(x, y)
if selected_rect:
self.canvas.itemconfigure(selected_rect, fill="red") # Change color.
def main():
node_list = []
cord_list = []
window = Window()
window.draw_grid()
window.root.mainloop()
if __name__ == "__main__":
main()

Tkinter .after() not responding to keypress fast enough

I am trying to animate a ball, that is be moving forward. In addition, if the user presses up or down button, it should move respectively.
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
class Movement:
def __init__(self, _frame, _canvas):
self.frame = _frame
self.canvas = _canvas
self.x = 10
self.y = 150
self.count = 0
self.update()
self.frame.bind_all("<Up>", lambda event: self.move_up())
self.frame.bind_all("<Down>", lambda event: self.move_down())
pass
def move_up(self):
self.y -= 10
pass
def move_down(self):
self.y += 10
pass
def update(self):
canvas.delete("obj")
self.count += 10
x2 = self.x + 50
y2 = self.y + 50
self.canvas.create_oval(self.x + self.count, self.y, x2 + self.count, y2, fill="red", tag="obj")
root.after(1000, self.update)
pass
root = tk.Tk()
width = 400
height = 400
canvas = tk.Canvas(root, width=width, height=height)
frame = tk.Frame(root, width=width, height=height)
if __name__ == '__main__':
Movement(frame, canvas)
canvas.grid()
root.mainloop()
There's an issue, though. Given the fact that the ball is moving every 1 second, it doesn't respond to the key press fast enough. As a result, if you click the up kick, the screen is updating after one second.
I know there's a .move() function inside Tkinter. However, due to many reason I cannot use that.
My main concern is the interval it take for the screen to be updated.
From what I understand you want to be able to keep the timer moving forward consistent but also be able to move up and down at any point even outside of the timer.
to accomplish this I moved the timer to its own function and allowed the up down functions to call the object destroy/create function.
I have updated you code to reflect that.
Take a look at this code and let me know if it helps.
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
class Movement:
def __init__(self, _frame, _canvas):
self.frame = _frame
self.canvas = _canvas
self.x = 10
self.y = 150
self.count = 0
self.object_timer()
self.frame.bind_all("<Up>",lambda event: self.move_up())
self.frame.bind_all("<Down>",lambda event: self.move_down())
def move_up(self):
self.y -= 10
self.update()
def move_down(self):
self.y += 10
self.update()
def update(self):
canvas.delete("obj")
x2 = self.x + 50
y2 = self.y + 50
self.canvas.create_oval(self.x + self.count, self.y, x2 + self.count, y2, fill="red", tag="obj")
def object_timer(self):
self.update()
self.count += 10
root.after(1000, self.object_timer)
root = tk.Tk()
width = 400
height = 400
canvas = tk.Canvas(root, width=width, height=height)
frame = tk.Frame(root, width=width, height=height)
if __name__ == '__main__':
Movement(frame, canvas)
canvas.grid()
root.mainloop()
It's all just math. You can call update twice as much, and move the ball half as much if you want to keep the ball moving at the same speed while reacting to keypresses faster.
For example, instead of moving 10 pixels every second, have it move 1 pixel every tenth of a second. The end result is still 10 pixels every second. The animation will be smoother, and it will react to keypresses much faster.
By the way, there's no need to delete and recreate the oval every iteration. It's possible to move items on the canvas. Deleting and recreating will eventually cause performance problems do to how the canvas is implemented internally. Each object you create gets a new unique id, and the canvas will slow down when it has a large number of ids. The canvas does not re-use an id.
I would also recommend against using lambda. Unless you need it, it adds complexity without adding any value. Just add an extra parameter to move_up and move_down:
self.frame.bind_all("<Up>",self.move_up)
self.frame.bind_all("<Down>", self.move_down)
...
def move_up(self, event=None):
...
def move_down(self, event=None):
...

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