Tkinter canvas move leaves a pixel trail - python

I'm working on a game in a Tkinter canvas where points move around the screen. I place each point at a location with tkinter.Canvas.create_oval(...) and subsequently move the points with tkinter.Canvas.move(pointID,delta_x,delta_y).
My problem is that the points seem to leave a trail behind when they are moved. I made a simplified example that demonstrates my problem.
from tkinter import Canvas,mainloop,Tk
import numpy as np
import random
import traceback
import threading
import time
from queue import Queue
class Point:
def __init__(self,the_canvas,uID):
self.uID = uID
self.location = np.ones((2)) * 200
self.color = "#"+"".join([random.choice('0123456789ABCDEF') for j in range(6)])
self.the_canvas = the_canvas
self.the_canvas.create_oval(200,200,200,200,
fill=self.color,outline=self.color,width=6,
tags=('runner'+str(self.uID),'runner'))
def move(self):
delta = (np.random.random((2))-.5)*20
self.the_canvas.move('runner'+str(self.uID),delta[0],delta[1])
def queue_func():
while True:
time.sleep(.25)
try:
next_action = the_queue.get(False)
next_action()
except Exception as e:
if str(e) != "":
print(traceback.format_exc())
the_queue = Queue()
the_thread = threading.Thread(target=queue_func)
the_thread.daemon = True
the_thread.start()
window = Tk()
window.geometry('400x400')
the_canvas = Canvas(window,width=400,height=400,background='black')
the_canvas.grid(row=0,column=0)
points = {}
for i in range(100):
points[i] = Point(the_canvas,i)
def random_movement():
while True:
for point in points.values():
point.move()
the_queue.put(random_movement)
mainloop()
And the result is something like this:
I need to be able to move Points around cleanly, without leaving anything behind.
I tried changing the move() function so that each point is deleted according to its tag and redrawn at the new location, but that results in the same problem.
I have tried fill='' and also outline='' in the Canvas.oval config, but this does not help.
The behavior of these pixel trials seem erratic, like they will disappear over time, only leaving a limited amount of footprints behind.
I have tried removing the time.sleep(.2) from the movement loop, and that seems to make the problem a lot more pronounced.
I've found that the only way to cleanup these rogue colored pixels is to run canvas.delete("all"), so as of now, my only solution is to delete everything and redraw everything constantly. That doesn't seem like a great solution to me.
What's a good way to avoid these "pixel trails"? It really just seems like a bug to me, but maybe I'm making a blunder somewhere.

After some digging I found this post here: Python3 tkinter.Canvas.move() method makes artifacts on screen
The problem there was the borders of the oval. So what I did was remove the borders and make the oval slightly larger to compensate and it looks like that did the trick.
If you change this line:
self.the_canvas.create_oval(200, 200, 200, 200,
fill=self.color, outline=self.color, width=6,
tags=('runner' + str(self.uID), 'runner'))
To this:
self.the_canvas.create_oval(200,200,206,206,
fill=self.color,outline='', width=0,
tags=('runner'+str(self.uID),'runner'))
the problem should go away with or without threading.
If you would like to see what your code would look like without threading here is an example:
import tkinter as tk
import numpy as np
import random
class Point:
def __init__(self, the_canvas, uID):
self.uID = uID
self.location = np.ones((2)) * 200
self.color = "#"+"".join([random.choice('0123456789ABCDEF') for j in range(6)])
self.the_canvas = the_canvas
self.the_canvas.create_oval(200, 200, 206, 206,
fill=self.color, outline='', width=0,
tags=('runner'+str(self.uID), 'runner'))
def move(self):
delta = (np.random.random((2))-.5)*20
self.the_canvas.move('runner'+str(self.uID), delta[0], delta[1])
window = tk.Tk()
window.geometry('400x400')
the_canvas = tk.Canvas(window, width=400, height=400, background='black')
the_canvas.grid(row=0, column=0)
points = {}
for i in range(100):
points[i] = Point(the_canvas, i)
def random_movement():
for point in points.values():
point.move()
window.after(50, random_movement)
random_movement()
window.mainloop()
Results:

Weirdly enough the issue seems to be coming from
width=6 in create_oval()
getting rid of it seems to resolve the issue.
The way you've originally setup your create_oval() is making an oval of zero area (i.e. non-existent) and border width of 6... which is obviously a troubelsom combination.
So here's the new code, with some extra modfications:
from tkinter import Canvas,mainloop,Tk
import numpy as np
import random
import traceback
import time
class Point:
def __init__(self,uID):
self.uID = uID
self.location = np.ones((2)) * 200
self.color = "#" + "".join([random.choice('0123456789ABCDEF') for j in range(6)])
def move(self):
delta = (np.random.random((2)) - .5) * 20
self.location += delta
def draw(self,canv):
x0, y0 = self.location
canv.create_oval(
x0, y0, x0 + 2, y0 + 2,
fill=self.color, outline=self.color,
tags=('runner'+str(self.uID),'runner')
)
def repeater(window):
the_canvas.delete('all')
for i in points:
i.move()
i.draw(the_canvas)
window.after(25, repeater, window)
window = Tk()
window.geometry('400x400')
the_canvas = Canvas(window,width=400,height=400,background='black')
the_canvas.grid(row=0,column=0)
points = []
for i in range(100):
points.append(Point(i))
repeater(window)
window.mainloop()
Also, btw, deleting all elements on the canvas is the way to clear it. it's not wasteful since you're updating all elements on it anyway.

Related

Tkinter widgets created in an Update function run by tk.after function do not create untill the aforementioned Update ends

I intend to make a Py code which creates a tkinter dot that turns on a key press and deletes on a key press of couple keys.
The dot already is functional but i need it switch on and off on certain keypresses/mouse clicks which means i need an outside tkinter.mainloop() Update function.
The Update function with a while in it to constantly check if conditions to turn it off/on are present. But the Tkinter widget Somehow gets applied to the screen Only when the function nds. Like widget could be created but it will only take effect when function ends. And i need to turn it off/on dynamically.
I have tried to use a tkinter.after() with additional one at the end of called function only to find out an error of Recursion depth. What i expected to happen was that the function would be called over and over again, instead it runs that function like a while loop. I also have tried Asyncio.run() but it would result not making it visible till the function ends at least once. And I need to change it dynamically.
from tkinter import *
from tkinter import Canvas
from winsound import Beep
from time import sleep
import asyncio
import keyboard
import mouse
root = Tk()
width = root.winfo_screenwidth()
height = root.winfo_screenheight()
class tk_Dot():
def __init__(self,x=-1,y=-1,radius=4,color="red"):
self.x = x
if x == -1:
self.x = width/2-radius//2
print(self.x)
self.y = y
if y == -1:
self.y = height/2+radius//2
print(self.y)
self.radius=radius
self.color = color
self.lines = []
self.count = 1
def line(self,i):
return canvas.create_line(self.x, self.y-i, self.x+self.radius, self.y-i, fill=self.color)
def create(self):
self.lines = []
for i in range(0,self.radius):
self.lines.append(self.line(i))
def delete(self):
for i in range(0,self.radius):
canvas.delete(self.lines[i])
canvas.dtag(self.lines[i])
opacity_of_tk_window = 1 # From 0 to 1 0 meaning completely transparent 1 meaning everything created in canvas will give the color it was given
root.attributes('-alpha',opacity_of_tk_window)
# Invisible Tkinter window label
root.overrideredirect(True)
# Makes Transparent background
transparent_color = '#f0f0f0'
root.wm_attributes('-transparent', transparent_color)
canvas = Canvas()
# Rectangle filled with color that is specified above as the transparent color so practically making transparent background
canvas.create_rectangle(0, 0, width, height, fill=transparent_color)
canvas.pack(fill=BOTH, expand=1)
radius = 2
radius = 1+radius\*2
# Create a dot class
game_dot = tk_Dot(width/2-radius//2+1,height/2+1+radius//2,radius,"Red")
# Create a Dot at the middle of the calorant crosshair
# game_dot.create()
# Delete the dot
# game_dot.delete()
def Update():
game_dot.create()
print("Dot should be visible by now")
print("Is it?")
sleep(5) #sec
print("Oh yeah after the function ends.") # the problem
def Delete():
game_dot.delete()
root.geometry('%dx%d+%d+%d' % (width, height, -2,-2))
# Tkinter window always on top
root.attributes('-topmost',True)
root.after(1000,Update())
root.mainloop()

Passing a tkinter canvas between classes without calling the child from within the parent

I am somewhat of a beginner when it comes to Python, but i decided i want to write a basic 2-d physics playground. Unfortionetly i ran straigt into trouble when trying to setup the basic structure.
My plan is to create a GUI with a canvas in a parent function named mainWindow, then i figured i would create a child class (Hero) which creates a circle the user can manipulate on the canvas. This seems to work fairly well.
The problem occurs when i try to do anything with the Hero class, like call a function to delete the circle so i can redraw it in some direction. I can't seem to pass the canvas from the mainWindow to the Hero class. Any help would be greatly appreciated, including telling me that this is the wrong way to do things.
Im adding the two documents im working with since my rambling is probably hard to follow.
I run the program from the phesics.py document, resulting in the GUI poping up with my canvas and a red circle. When i close the window i get the following error:
classes.py", line 29, in moveHeroBody
canvas.delete(heroBody)
NameError: name 'canvas' is not defined
Unfortionetly i dont know how to get the "world" into the child
classes.py
from tkinter import *
class mainWindow():
def __init__(self):
#Setup the GUI
root = Tk()
root.geometry('800x600')
# Setup the canvas within the GUI (master)
world = Canvas(root, height = 600, width = 800, bg = "#FFFFFF")
world.place(relx = 0.5, rely = 0.5, anchor = CENTER)
Hero(world)
root.mainloop()
class Hero(mainWindow):
def __init__(self,world):
#Initial creation of hero at coordinates
x1 = 10
y1 = 10
x2 = 70
y2 = 70
heroBody = world.create_oval(x1,y1,x2,y2, fill = "#FF0000", outline = "#FF0000")
#Move the hero
def moveHeroBody():
print("moveHeroBody")
world.delete(heroBody)
phesics.py
from tkinter import *
from classes import *
mainWindow1 = mainWindow()
moveHero = Hero.moveHeroBody()
You're passing it ok, but you're throwing the value away. Also, Hero shouldn’t inherit from mainWindow.
You need to save world as an attribute so that you can reference it later.
class Hero():
def __init__(self,world):
self.world = world
...
Then, you can use self.world to reference the canvas:
def moveHeroBody():
print("moveHeroBody")
self.world.delete(heroBody)
Though, the above code will fail because heroBody is a variable local to the __init__ - you need to do the same with it:
class Hero():
def __init__(self,world):
self.world = world
...
self.heroBody = world.create_oval(...)
#Move the hero
def moveHeroBody():
print("moveHeroBody")
self.world.delete(self.heroBody)
I think you need to initialize the class Hero in your mainWindow class. The modifications needed to do in the code are:
classes.py
from tkinter import *
from time import sleep
class mainWindow():
def __init__(self):
#Setup the GUI
self.jump_gap = 25
root = Tk()
root.geometry('800x600')
# Setup the canvas within the GUI (master)
self.world = Canvas(root, height = 600, width = 800, bg = "#FFFFFF")
self.world.place(relx = 0.5, rely = 0.5, anchor = CENTER)
self.hero = Hero(self.world)
self.world.pack()
root.bind("<space>",self.jump) # -> [1] Binds the SPACE BAR Key to the function jump
root.mainloop()
def jump(self,event):
gaps = list(range(self.jump_gap))
for i in gaps:
self.world.after(1,self.hero.moveHeroJump(h=i)) # [2] -> Binds the moveHeroJump method with the window action to a queue of updates
self.world.update() #[2] updates the canvas
sleep(0.01*i) # Added some linear wait time to add some look to it
gaps.reverse()
for i in gaps:
self.world.after(1,self.hero.moveHeroJump(h=-i))
self.world.update()
sleep(0.01*i)
class Hero():
def __init__(self,world):
#Initial creation of hero at coordinates
self.world = world
self.x1 = 10
self.y1 = 410
self.x2 = 70
self.y2 = 470
self.heroBody = self.world.create_oval(self.x1,self.y1,self.x2,self.y2, fill = "#FF0000", outline = "#FF0000")
#Move the hero
def moveHeroJump(self,h):
print("moveHeroBody")
self.y1 -= h
self.y2 -= h
self.world.delete(self.heroBody)
self.heroBody = self.world.create_oval(self.x1,self.y1,self.x2,self.y2, fill = "#FF0000", outline = "#FF0000")
physics.py
from tkinter import *
from classes import *
mainWindow1 = mainWindow()
Edit
So this got me playing some minutes ago, and I researched some sources from stack in order to complete this question. Here are the sources (referenced in the code as well):
How to bind spacebar key to a certain method in tkinter python
Moving Tkinter Canvas
The solution edited above is capable to perform a simple animation of a ball jumping. self.jump_gap is a fixed quantity that tells the ball how much does it needs to jump. The jump parses a certain height h to the moveHeroJump method to make the ball change its position, after the change of position is queued into the Canvas an update is called to see the changes on the ball.

Trouble calling a function from a class to draw an oval in Python

Beginner here trying to make sense of classes. Below is the code for my class Cell:
import tkinter
import random
top = tkinter.Tk()
canvas = tkinter.Canvas(top, bg="grey", height=400, width=400)
canvas.pack()
class Cell:
def __init__(self, x, y, r):
self.x = random(x)
self.y = random(y)
self.r = 200
def show(self):
canvas.create_oval(self.x, self.y, self.r, self.r, fill = "blue")
top.mainloop()
I'm attempting to draw the cell in my main program by calling the function show from the class. Here is the code for my main window:
import tkinter
top = tkinter.Tk()
canvas = tkinter.Canvas(top, bg="grey", height=400, width=400)
canvas.pack()
from Cell import Cell
cell = Cell()
cell.show()
top.mainloop()
This is resulting in the canvas being drawn correctly, but the oval is nowhere to be found. I am not getting any errors either.
Any help would be appreciated. Thank you!
====================
Turns out, I misunderstood the arguments for create_oval. I found some code that converts the clunky create_oval function into a function which receives a set of coordinates for the center of the oval and a radius.
In addition to this, the help I received in understanding classes and other Python functionality helped significantly as well. Thanks to those who helped!
This is my revised code which works as intended.
import tkinter as tk
import random
top = tk.Tk()
canvas = tk.Canvas(top, width=400, height=400, bg="grey")
canvas.grid()
def _create_circle(self, x, y, r, **kwargs):
return self.create_oval(x-r, y-r, x+r, y+r, **kwargs)
tk.Canvas.create_circle = _create_circle
class Cell:
def __init__(self, canvas, x, y, r):
self.canvas = canvas
self.x = x
self.y = y
self.r = r
def show(self):
self.canvas.create_circle(self.x, self. y, self.r, fill = "blue")
cell = Cell(canvas, random.randrange(50, 350), random.randrange(50, 350), 25)
cell.show()
top.mainloop()
The problem here is that your main script and your Cell module are both creating a new Tk instance, adding a Canvas to it, and then calling its mainloop method.
If you trace through the order in which statements get executed, you'll find that the cell = Cell() and cell.show() don't happen until after the first top.mainloop() returns, and mainloop() doesn't return until you quit the program. (In fact, if your code did get that far, it would fail with a TypeError, which I'll get to below.)
But, more generally, you only want one Tk in your program, and everyone else should refer to that.
And, in this case, you want the same for the Canvas: just one of them, packed onto the one Tk main window.
So, how can Cell.show refer to the canvas global from another module?
The best solution is to not refer to it as a global at all, and instead pass it in to the initializer, the same way you do with x, y, and r:
class Cell:
def __init__(self, canvas, x, y, r):
self.canvas = canvas
self.x = random(x)
self.y = random(y)
self.r = 200
def show(self):
self.canvas.create_oval(self.x, self.y, self.r, self.r, fill = "blue")
And then in the main script:
cell = Cell(canvas, ?, ?. ?)
cell.show()
But notice those ?s I put there. Your Cell class definition demands x, y, and r values in its initializer, but your Cell() constructor call doesn't pass any. That will raise a TypeError complaining that you're missing required arguments.
What do you want to pass here? Since the canvas is 400x400, maybe you want to pass something like 400, 400, 200? If so:
cell = Cell(canvas, 400, 400, 200)
cell.show()
Going back to that initializer, you've got some other problems there:
self.x = random(x)
self.y = random(y)
That random is a module. You can't call a module. You probably wanted something like this:
self.x = random.randrange(x)
That calls a function from the random module, one which is defined to return a random number in range(0, x), which seems like what you want.
Also:
self.r = 200
Why take an r parameter, just to ignore it? You probably wanted this:
self.r = r
Or, maybe you didn't actually want x, y, and r as parameters? Maybe you want to hardcode randrange(400), randrange(400), and 200, or maybe you want to compute them from the width and height of the canvas parameter, or… you can do almost anything you want, you just have to think through what you want, and make sure the interface you declare in the def matches the way you call it in the Cell(…) later.
I think your execution path is never getting to the cell.show() line.
When you import Cell, you have code at the top level top.mainloop(). This enters the main loop and never exits, so you never get to the lines below it.
It's a good rule of thumb to avoid putting code at the base level. Leave that for defining classes and functions. If you want code to run when the file is called like a script, put it in a if __name__ == __main__: condition.
You also had some syntax issues using random and calling the Cell constructor.
The example below works as expected.
import tkinter
import random
top = tkinter.Tk()
canvas = tkinter.Canvas(top, bg="grey", height=400, width=400)
canvas.pack()
class Cell:
def __init__(self, x, y, r):
self.x = x
self.y = y
self.r = r
def show(self):
canvas.create_oval(self.x, self.y, self.r, self.r, fill = "blue")
if __name__ == "__main__":
cell = Cell(100, 50, 5)
cell.show()
top.mainloop()

generating animation in tkinter

I wrote a small program using tkinter to generate a figure based on parameters I'm giving the program (through gui interface). I am drawing the figure directly with tkinter using Canvas. Now I want to generate a series of figures and put them in animated loop. I can generate a gif animation and load it into the figure but the question is if I can do this directly in tkinter. I can put everything in loop, but then I'll lose the event handler. Any other option?
Edit
Here is a simple script:
#!/usr/bin/python
from Tkinter import *
import ttk
import numpy as np
colors = {1:'#F00',
2:'#0F0',
3:'#00F',
4:'#FF0',
5:'#0FF'}
root = Tk()
canvas = Canvas(root,width=400,height=400)
label = Label(root,text='seed')
values = StringVar()
combo = ttk.Combobox(root)
def painter(event):
seed = int(combo.get())
np.random.seed(seed)
nrows = 10
ncols = 10
Y = 0
dx = 400/nrows
dy = 400/ncols
for i in range(nrows):
X = 0
for j in range(ncols):
n = np.random.randint(1,5)
col = colors[n]
canvas.create_rectangle(X,Y,X+dx,Y+dy,width=1,fill=col)
X += dx
Y += dy
canvas.grid(column=0,row=0,rowspan=2)
combo.grid(row=1,column=1)
label.grid(row=0,column=1)
combo['values'] = [1,2,3,4]
combo.bind('<<ComboboxSelected>>',painter)
combo.current(0)
painter(None)
root.mainloop()
I could change the painter function to include an infinite loop but this way I never reach the mainloop(), so I need to call this only after the main loop is created, but how?
a simple example of some animation using tkinter canvas:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.title("Canvas animation example")
self.c = tk.Canvas(self, width=400, height=400)
self.c.pack()
self.f_index = 0 # index so we know which frame to draw next
# array to hold our frame data,
# you'll probably need this to hold more than
# just a set of coordinates to draw a line...
self.f_data = []
for num in range(0, 400, 5): # make up a set of fake data
self.f_data.append([num, num, num+10, num+10])
def next_frame(self):
data = self.f_data[self.f_index] # fetch frame data
self.c.delete('all') # clear canvas
self.c.create_line(*data) # draw new frame data
self.f_index += 1 # increment frame index
if (self.f_index >= len(self.f_data)): # check and wrap if at end of sequence
self.f_index = 0
self.c.after(50, self.next_frame) # call again after 50ms
if __name__ == "__main__":
app = App()
app.next_frame() # called manually once to start animation
# could be started with 'after' instead if desired
app.mainloop()
note that the more items you want to delete and redraw the slower this code will perform. you need to have realistic expectations, drawing a few 10's of items will be fine, a few 100's might be ok on a decent pc, but 1000's of items would be very poorly performing.

how to create rectangles by getting scale values at the time in tkinter?

I want to get the value of a scale and create rectangles as many as the value is. For example, if I adjust the scale to number 7, 7 rectangles would be created next to each other, and after that if I adjust the scale value to 3, the rectangles shown in the canvas decreases to three at that moment. I had used the code below:
from tkinter import *
from tkinter import ttk
class rect:
def __init__(self, root):
self.root = root
self.size = IntVar()
self.canvas = Canvas(self.root, width=800, height=300)
self.scale = Scale(self.root, orient=HORIZONTAL, from_=3, to=20, tickinterval=1, variable=self.size)
self.show()
def show(self):
x = 50
y = 50
for i in range(self.scale.get()):
self.canvas.create_rectangle(x, y, x + 50, y + 50, fill='red')
x += 50
self.canvas.pack()
self.scale.pack()
root = Tk()
a = rect(root)
root.mainloop()
I guess I have to use trace method, But I don't know how to.
Can anyone fix the code I used in the way which I explained.
Thank you.
One solution is to bind to <ButtonRelease>, and call your show method there. Since event bindings pass an object representing the event, you'll need to make that an optional argument if you also want to call show without any arguments.
For example:
class rect:
def __init__(self, root):
...
self.scale = Scale(...)
self.scale.bind("<ButtonRelease>", self.show)
I'm guessing you'll want to remove any previously drawn rectangles, so you'll need to call delete before creating the rectangles:
def show(...):
self.canvas.delete("all")
...

Categories