Updatable tkinter canvas with mainloop - python

I want to be able to make my self a graphical api in Tkinter for a project
using Python 2.7
import Tkinter as tk
# import tkinter as tk # if you are using python 3
class Graphics(tk.Tk):
def __init__(self, width=60, height=60, pixel_width=10):
#super().__init__()
tk.Tk.__init__(self)
self.width, self.height = width, height
self.pixel_width = pixel_width
# self.geometry(f'{self.width*self.pixel_width}x{self.height*self.pixel_width}')
self.geometry('600x600')
self.my_canvas = tk.Canvas(
self,
width = self.width * self.pixel_width,
height = self.height * self.pixel_width,
)
self.pixels = [[None for x in range(self.width)] for y in range(self.height)]
self.fill_area((0, 0), (self.width, self.height), 'white')
self.fill_point((30, 30),'red')
self.fill_area((10, 10), (15, 20), 'yellow')
self.my_canvas.pack()
self.run()
def fill_point(self, point, color):
pixel = self.pixels[point[0]][point[1]]
if pixel is None:
cx0 = self.pixel_width * point[0]
cy0 = self.pixel_width * point[1]
cx1 = self.pixel_width * (point[0] + 1)
cy1 = self.pixel_width * (point[1] + 1)
self.pixels[point[0]][point[1]] = self.my_canvas.create_rectangle(cx0, cy0, cx1, cy1, fill=color) # outline=''
else:
self.my_canvas.itemconfig(pixel, fill=color)
def fill_area(self, pointA, pointB, color):
for x in range(pointA[0], pointB[0]):
for y in range(pointA[1], pointB[1]):
self.fill_point((x, y), color)
def run(self):
self.mainloop()
g = Graphics()
g.fill_point((9,9),'blue')
As I've understood the mainloop method is blocking any further actions on the canvas and I need a solution for an updatable screen.
I tried using threads with mainloop and update methods but it would just immediately exit the window.
Thank you for helping and have a nice day

You probably need to read a little bit about the tkinter canvas, and its immense capabilities: unlike pygame, or other graphics canvasses, it does not need to refresh at a given frequency; canvas items can be addressed individually, and their attributes set precisely.
I reworked your code to display a canvas made of 'scaled pixels': here 60 x 60 pixels wide, with each pixels scaled up by a factor 10.
The 2D list self.pixels contains canvas items; the updates directly change the attributes of these items.
You can remove the black lines around each pixel by setting their attribute outline to the empty string (see comment in the code).
import Tkinter as tk
# import tkinter as tk # if you are using python 3
class Graphics(tk.Tk):
def __init__(self, width=60, height=60, pixel_width=10):
super().__init__(self)
self.width, self.height = width, height
self.pixel_width = pixel_width
# self.geometry(f'{self.width*self.pixel_width}x{self.height*self.pixel_width}')
self.geometry('600x600')
self.my_canvas = tk.Canvas(
self,
width = self.width * self.pixel_width,
height = self.height * self.pixel_width,
)
self.pixels = [[None for x in range(self.width)] for y in range(self.height)]
self.fill_area((0, 0), (self.width, self.height), 'white')
self.fill_point((30, 30),'red')
self.fill_area((10, 10), (15, 20), 'yellow')
self.my_canvas.pack()
self.run()
def fill_point(self, point, color):
pixel = self.pixels[point[0]][point[1]]
if pixel is None:
cx0 = self.pixel_width * point[0]
cy0 = self.pixel_width * point[1]
cx1 = self.pixel_width * (point[0] + 1)
cy1 = self.pixel_width * (point[1] + 1)
self.pixels[point[0]][point[1]] = self.my_canvas.create_rectangle(cx0, cy0, cx1, cy1, fill=color) # outline=''
else:
self.my_canvas.itemconfig(pixel, fill=color)
def fill_area(self, pointA, pointB, color):
for x in range(pointA[0], pointB[0]):
for y in range(pointA[1], pointB[1]):
self.fill_point((x, y), color)
def run(self):
self.mainloop()
g = Graphics()

Related

How do I create 4 squares in tkinter so that they start in the middle of the screen and each one of them goes in its own corner and disappears?

Here is the code, I have managed to position the square a little bit but the whole thing is just messy and fast when executed, it doesn't really resemble anything, let alone what I wanted to make.
What do I need to do in order to get these 4 squares to start in the middle and move to each corner, disappearing from the screen? I made an example here:
from tkinter import *
W, H = 500, 500
tk = Tk()
canvas = Canvas(tk,width=W,height=H)
canvas.pack()
class Square:
def __init__(self,size,speedx, speedy, color):
self.square = canvas.create_rectangle(50,50,100,100,fill=color)
self.speedx = speedx
self.speedy = speedy
self.movement()
def movement(self):
canvas.move(self.square,self.speedx,self.speedy)
pos = canvas.coords(self.square)
if pos[2]>=W or pos[0]<=0:
self.speedx *= -1
if pos[3]>=H or pos[1]<=0:
self.speedy *= -1
tk.after(40,self.movement)
square1 = Square(200,150,200,'brown')
square2 = Square(200,200,150,'yellow')
square3 = Square(200,200,200,'green')
square4 = Square(200,150,150,'blue')
tk.mainloop()
You need to define direction for each rectangle separately, so that each of them moves its own way.
import tkinter as tk
W, H = 500, 500
SPEED = 20
window = tk.Tk()
canvas = tk.Canvas(window, width=W, height=H)
canvas.pack()
class Square:
def __init__(self, x, y, color, speed_x, speed_y):
self.speed_x = speed_x
self.speed_y = speed_y
self.square = canvas.create_rectangle(x, y, x+50, y+50, fill=color)
self.movement()
def movement(self):
canvas.move(self.square, self.speed_x, self.speed_y)
window.after(200, self.movement)
Square(200, 200, 'brown', -SPEED, -SPEED)
Square(250, 200, 'yellow', SPEED, -SPEED)
Square(200, 250, 'green', -SPEED, SPEED)
Square(250, 250, 'blue', SPEED, SPEED)
window.mainloop()
Output:

Exclude results in find_closest (Tkinter, Python)

I am writing a script to store movements over a hexgrid using Tkinter. As part of this I want to use a mouse-click on a Tkinter canvas to first identify the click location, and then draw a line between this point and the location previously clicked.
Generally this works, except that after I've drawn a line, it become an object that qualifies for future calls off the find_closest method. This means I can still draw lines between points, but selecting the underlying Hex in the Hexgrid over times becomes nearly impossible. I was wondering if someone could help me find a solution to exclude particular objects (lines) from the find_closest method.
edit: I hope this code example is minimal enough.
import tkinter
from tkinter import *
from math import radians, cos, sin, sqrt
class App:
def __init__(self, parent):
self.parent = parent
self.c1 = Canvas(self.parent, width=int(1.5*340), height=int(1.5*270), bg='white')
self.c1.grid(column=0, row=0, sticky='nsew')
self.clickcount = 0
self.clicks = [(0,0)]
self.startx = int(20*1.5)
self.starty = int(20*1.5)
self.radius = int(20*1.5) # length of a side
self.hexagons = []
self.columns = 10
self.initGrid(self.startx, self.starty, self.radius, self.columns)
self.c1.bind("<Button-1>", self.click)
def initGrid(self, x, y, radius, cols):
"""
2d grid of hexagons
"""
radius = radius
column = 0
for j in range(cols):
startx = x
starty = y
for i in range(6):
breadth = column * (1.5 * radius)
if column % 2 == 0:
offset = 0
else:
offset = radius * sqrt(3) / 2
self.draw(startx + breadth, starty + offset, radius)
starty = starty + 2 * (radius * sqrt(3) / 2)
column = column + 1
def draw(self, x, y, radius):
start_x = x
start_y = y
angle = 60
coords = []
for i in range(6):
end_x = start_x + radius * cos(radians(angle * i))
end_y = start_y + radius * sin(radians(angle * i))
coords.append([start_x, start_y])
start_x = end_x
start_y = end_y
hex = self.c1.create_polygon(coords[0][0], coords[0][1], coords[1][0], coords[1][1], coords[2][0],
coords[2][1], coords[3][0], coords[3][1], coords[4][0], coords[4][1],
coords[5][0], coords[5][1], fill='black')
self.hexagons.append(hex)
def click(self, evt):
self.clickcount = self.clickcount + 1
x, y = evt.x, evt.y
tuple_alfa = (evt.x, evt.y)
self.clicks.append(tuple_alfa)
if self.clickcount >= 2:
start = self.clicks[self.clickcount - 1]
startx = start[0]
starty = start[1]
self.c1.create_line(evt.x, evt.y, startx, starty, fill='white')
clicked = self.c1.find_closest(x, y)[0]
print(clicked)
root = tkinter.Tk()
App(root)
root.mainloop()

tkinter python - Can I create window collision?

New to programming. Working on a simple pong clone. Started the ball but want to make sure all sides of the window (500x500) will have the ball bounce off of it. How could I do this? Thanks!
P.S. This is my current code if needed.
import threading
import random
import time
import string
import os.path
from random import randint
from tkinter import *
class Pong:
Title = 'Pong'
Size = '500x500'
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="black")
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)
def PongGame():
print("Moved to PongGame.")
ball1 = Ball(canvas,10,10,30,30)
ball1.move_ball()
def titleButtonClicked(event):
print("Title screen button clicked.")
btn.pack_forget()
btn.place(x=600,y=600)
msg.pack_forget()
PongGame()
root = Tk()
root.geometry(Pong.Size)
root.title(Pong.Title)
root.resizable(False,False)
msg = Label(root, text = Pong.Title, font = ("", 50))
msg.pack()
canvas = Canvas(root, width = 500, height = 500)
canvas.pack()
btn=Button(root, text = "Start")
btn.bind('<Button-1>', titleButtonClicked)
btn.place(x=220,y=300)
root.mainloop()
Collisions are not trivial; the simplest is to reverse the x or the y velocity after checking which edge of the bounding box of the ball intersects with the boundaries of the canvas.
Maybe something like this:
import random
import tkinter as tk
WIDTH, HEIGHT = 500, 500
class Ball:
radius = 10
spawn_center = (250, 100)
def __init__(self, canvas):
self.canvas = canvas
self.id = None
self.create_ball()
self.velocity = None
self.assign_random_velocity()
self.keep_moving = True
self.move()
def create_ball(self):
xc, yc = self.spawn_center
x0, y0, = xc - self.radius, yc + self.radius
x1, y1, = xc + self.radius, yc - self.radius
self.id = self.canvas.create_oval(x0, y0, x1, y1)
def assign_random_velocity(self):
dx = random.randrange(1, 5) * random.choice((1, -1))
dy = random.randrange(1, 5) * random.choice((1, -1))
self.velocity = (dx, dy)
def move(self):
if self.keep_moving is None:
return
self.check_collision()
self.canvas.move(self.id, *self.velocity)
self.keep_moving = self.canvas.after(10, self.move)
def cancel_move(self):
if self.keep_moving is not None:
self.canvas.after_cancel(self.keep_moving)
self.keep_moving = None
def check_collision(self):
x0, y0, x1, y1 = self.canvas.coords(self.id)
dx, dy = self.velocity
if x0 < 0:
x0 = 0
dx = -dx
elif x1 > WIDTH:
x1 = WIDTH
dx = -dx
if y0 < 0:
y0 = 0
dy = -dy
elif y1 > HEIGHT:
y1 = HEIGHT
dy = -dy
self.velocity = dx, dy
class PongBoard(tk.Canvas):
def __init__(self, master):
self.master = master
super().__init__(self.master)
self.ball = None
self.spawn_new_ball()
def spawn_new_ball(self):
if self.ball is not None:
self.ball.cancel_move()
self.delete(self.ball.id)
self.ball = Ball(self)
root = Tk()
root.geometry(f'{WIDTH}x{HEIGHT+20}')
board = PongBoard(root)
new_ball_btn = tk.Button(root, text='spawn new ball', command=board.spawn_new_ball)
board.pack(expand=True, fill=tk.BOTH)
new_ball_btn.pack()
root.mainloop()
This will get you started, but you will have to implement the paddles, the paddles movement, the collision checking of the ball with the paddles, and keep the score by yourself.

Tkinter canvas not updating in an infinite loop

I created some code to show 2 balls moving, but when I run it, it doesn't show the movement of the balls.Furthermore it stops running and ignores the infinite loop This is my code up to now:
import tkinter as tk
class ObjectHolder:
def __init__(self, pos, velocity, radius, id):
self.id = id # the id of the canvas shape
self.pos = pos # the position of the object
self.r = radius # the radius of incluence of the object
self.velocity = velocity # the velocity of the object
def moveobject(object):
x = object.pos[0] + object.velocity[0] # moves the object where
y = object.pos[1] + object.velocity[1] # 0=x and 1=y
object.pos = (x, y)
canvas.move(object, x, y)
class App():
def __init__(self, canvas):
self.canvas = canvas
self.objects = []
for i in range(0, 2):
position = ((i+1)*100, (i+1)*100)
velocity = (-(i+1)*10, -(i+1)*10)
radius = (i + 1) * 20
x1 = position[0]-radius
y1 = position[1]-radius
x2 = position[0]+radius
y2 = position[1]+radius
id = canvas.create_oval(x1, y1, x2, y2)
self.objects.append(ObjectHolder(position, velocity, radius, id))
self.symulation(self.objects)
def symulation(self, objects):
for object in objects: # this moves each object
ObjectHolder.moveobject(object)
# This part doesn't work. It is supposed to update the canvas
# and repeat forever.
self.canvas.update()
root.update()
self.canvas.after(50, self.symulation, objects)
root = tk.Tk()
canvas = tk.Canvas(root, width=800, height=600, bg="light blue")
canvas.pack()
App(canvas)
There are a number of issues with your code. One big one was the way you were updating the position of the existing canvas objects. The move() method needs to know the amount of movement (change in x and y value), not the new absolute position.
When I fixed that it turned out that the velocities were too big, so I reduced them to only be 10% of the values you had.
Another problem was with the way the ObjectHolder class was implemented. For one thing, the moveobject() method had no self argument, which it should have been using instead of having an object argument. You should probably also rename the method simply move().
The code below runs and does animate the movement.
import tkinter as tk
class ObjectHolder:
def __init__(self, pos, velocity, radius, id):
self.id = id # the id of the canvas shape
self.pos = pos # the position of the object
self.r = radius # the radius of incluence of the object
self.velocity = velocity # the velocity of the object
def moveobject(self):
x, y = self.pos
dx, dy = self.velocity
self.pos = (x + dx, y + dy)
canvas.move(self.id, dx, dy) # Amount of movement, not new position.
class App():
def __init__(self, canvas):
self.canvas = canvas
self.objects = []
for i in range(0, 2):
position = ((i+1)*100, (i+1)*100)
# velocity = (-(i+1)*10, -(i+1)*10)
velocity = (-(i+1), -(i+1)) # Much slower speed...
radius = (i + 1) * 20
x1 = position[0]-radius
y1 = position[1]-radius
x2 = position[0]+radius
y2 = position[1]+radius
id = canvas.create_oval(x1, y1, x2, y2)
self.objects.append(ObjectHolder(position, velocity, radius, id))
self.simulation(self.objects)
def simulation(self, objects):
for object in objects: # this moves each object
object.moveobject()
# This part doesn't work. It is supposed to update the canvas
# and repeat forever.
# self.canvas.update() # Not needed.
# root.update() # Not needed.
self.canvas.after(50, self.simulation, objects)
root = tk.Tk()
canvas = tk.Canvas(root, width=800, height=600, bg="light blue")
canvas.pack()
app = App(canvas)
root.mainloop() # Added.

Mutli-threading python with Tkinter

I'm drawing little circles on a canvas with these functions :
This is the function that will draw the circles :
class Fourmis:
def __init__(self, can, posx, posy, name, radius):
self.can = can
self.largeur_can = int(self.can.cget("width"))
self.hauteur_can = int(self.can.cget("height"))
self.posx = posx
self.posy = posy
self.name = name
self.radius = radius
self.ball1 = self.can.create_oval(self.posy, self.posx, self.posy+radius, self.posx+radius, outline=self.name, fill=self.name, width=2)
self.nx = randrange(-10,10,1)
self.nx /= 2.0
self.ny = randrange(-10,10,1)
self.ny /= 2.0
#self.can.bind("<Motion>", self.destruction, add="+")
self.statut = True
self.move()
def move(self):
if self.statut == True :
self.pos_ball = self.can.coords(self.ball1)
self.posx_ball = self.pos_ball[0]
self.posy_ball = self.pos_ball[1]
if self.posx_ball < 0 or (self.posx_ball + self.radius) > self.largeur_can:
self.nx = -self.nx
if self.posy_ball < 0 or (self.posy_ball + self.radius) > self.hauteur_can:
self.ny = -self.ny
self.can.move(self.ball1, self.nx, self.ny)
self.can.after(10, self.move)
this one creates the canvas and the circles :
class App(Frame):
def __init__(self):
self.root=Tk()
self.can=Canvas(self.root,width=800,height=600,bg="black")
self.can.pack()
self.create(50, "green")
self.create(50, "purple")
def mainloop(self):
self.root.mainloop()
def create(self, i, name):
for x in range(i):
self.x=Fourmis(self.can,100,400, name,0)
I call these lines to run the project :
jeu = App()
jeu.mainloop()
What is the correct way to execute self.create(50, "green") and self.create(50, "purple") in different threads?
I have tried the following, but could not get it to work.:
class FuncThread(threading.Thread):
def __init__(self, i, name):
self.i = i
self.name = name
threading.Thread.__init__(self)
def run(self):
App.create(self, self.i, self.name)
Is someone able to tell me how to run these threads?
When this functionality is needed, what you do is schedule the events you wish to perform by putting them in a queue shared by the threads. This way, in a given thread you specify that you want to run "create(50, ...)" by queueing it, and the main thread dequeue the event and perform it.
Here is a basic example for creating moving balls in a second thread:
import threading
import Queue
import random
import math
import time
import Tkinter
random.seed(0)
class App:
def __init__(self, queue, width=400, height=300):
self.width, self.height = width, height
self.canvas = Tkinter.Canvas(width=width, height=height, bg='black')
self.canvas.pack(fill='none', expand=False)
self._oid = []
self.canvas.after(10, self.move)
self.queue = queue
self.canvas.after(50, self.check_queue)
def check_queue(self):
try:
x, y, rad, outline = self.queue.get(block=False)
except Queue.Empty:
pass
else:
self.create_moving_ball(x, y, rad, outline)
self.canvas.after(50, self.check_queue)
def move(self):
width, height = self.width, self.height
for i, (oid, r, angle, speed, (x, y)) in enumerate(self._oid):
sx, sy = speed
dx = sx * math.cos(angle)
dy = sy * math.sin(angle)
if y + dy + r> height or y + dy - r < 0:
sy = -sy
self._oid[i][3] = (sx, sy)
if x + dx + r > width or x + dx - r < 0:
sx = -sx
self._oid[i][3] = (sx, sy)
nx, ny = x + dx, y + dy
self._oid[i][-1] = (nx, ny)
self.canvas.move(oid, dx, dy)
self.canvas.update_idletasks()
self.canvas.after(10, self.move)
def create_moving_ball(self, x=100, y=100, rad=20, outline='white'):
oid = self.canvas.create_oval(x - rad, y - rad, x + rad, y + rad,
outline=outline)
oid_angle = math.radians(random.randint(1, 360))
oid_speed = random.randint(2, 5)
self._oid.append([oid, rad, oid_angle, (oid_speed, oid_speed), (x, y)])
def queue_create(queue, running):
while running:
if random.random() < 1e-6:
print "Create a new moving ball please"
x, y = random.randint(100, 150), random.randint(100, 150)
color = random.choice(['green', 'white', 'yellow', 'blue'])
queue.put((x, y, random.randint(10, 30), color))
time.sleep(0) # Effectively yield this thread.
root = Tkinter.Tk()
running = [True]
queue = Queue.Queue()
app = App(queue)
app.create_moving_ball()
app.canvas.bind('<Destroy>', lambda x: (running.pop(), x.widget.destroy()))
thread = threading.Thread(target=queue_create, args=(queue, running))
thread.start()
root.mainloop()

Categories

Resources