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 came across this interesting question (How to make a tkinter canvas rectangle with rounded corners?) related to creating rounded rectangles in Tkinter and specifically, this answer by Francisco Gomes (modified a bit):
def roundPolygon(x, y, sharpness):
# The sharpness here is just how close the sub-points
# are going to be to the vertex. The more the sharpness,
# the more the sub-points will be closer to the vertex.
# (This is not normalized)
if sharpness < 2:
sharpness = 2
ratioMultiplier = sharpness - 1
ratioDividend = sharpness
# Array to store the points
points = []
# Iterate over the x points
for i in range(len(x)):
# Set vertex
points.append(x[i])
points.append(y[i])
# If it's not the last point
if i != (len(x) - 1):
# Insert submultiples points. The more the sharpness, the more these points will be
# closer to the vertex.
points.append((ratioMultiplier*x[i] + x[i + 1])/ratioDividend)
points.append((ratioMultiplier*y[i] + y[i + 1])/ratioDividend)
points.append((ratioMultiplier*x[i + 1] + x[i])/ratioDividend)
points.append((ratioMultiplier*y[i + 1] + y[i])/ratioDividend)
else:
# Insert submultiples points.
points.append((ratioMultiplier*x[i] + x[0])/ratioDividend)
points.append((ratioMultiplier*y[i] + y[0])/ratioDividend)
points.append((ratioMultiplier*x[0] + x[i])/ratioDividend)
points.append((ratioMultiplier*y[0] + y[i])/ratioDividend)
# Close the polygon
points.append(x[0])
points.append(y[0])
When I adapted this code to work with my graphics library, it worked well enough! but when I create a 'stretched-square' (a non-square rectangle), the roundness becomes stretched too:
So how can I change this code to remove the stretched roundness and keep it a constant radius?
Here is one approach that uses the built in tcl tk primitives canvas.create_line, and canvas.create_arc to build rectangles of various sizes, and proportions with round corners (arc of a circle).
The corners radii is expressed as a proportion of the shortest side of the rectangle (0.0 --> 0.5), and can be parametrized.
The function make_round_corners_rect returns a tuple containing all canvas item ids as fragments of the rectangle entity. All fragments are tagged with their companions' ids, so accessing the entire object is possible with only one fragment id.
#! python3
import math
import tkinter as tk
from tkinter import TclError
def make_round_corners_rect(canvas, x0, y0, x1, y1, ratio=0.2, npts=12):
if x0 > x1:
x0, x1 = x1, x0
if y0 > y1:
y0, y1 = y1, y0
r = min(x1 - x0, y1 - y0) * ratio
items = []
topleft = x0, y0
tld = x0, y0 + r
tlr = x0 + r, y0
item = canvas.create_arc(x0, y0, x0+2*r, y0+2*r, start=90, extent=90, fill='', outline='black', style=tk.ARC)
items.append(item)
top_right = x1, y0
trl = x1 - r, y0
trd = x1, y0 + r
item = canvas.create_line(*tlr, *trl, fill='black')
items.append(item)
item = canvas.create_arc(x1-2*r, y0, x1, y0+2*r, start=0, extent=90, fill='', outline='black', style=tk.ARC)
items.append(item)
bot_right = x1, y1
bru = x1, y1 - r
brl = x1 - r, y1
item = canvas.create_line(*trd, *bru, fill='black')
items.append(item)
item = canvas.create_arc(x1-2*r, y1-2*r, x1, y1, start=270, extent=90, fill='', outline='black', style=tk.ARC)
items.append(item)
bot_left = x0, y1
blr = x0 + r, y1
blu = x0, y1 - r
item = canvas.create_line(*brl, *blr, fill='black')
items.append(item)
item = canvas.create_arc(x0, y1-2*r, x0+2*r, y1, start=180, extent=90, fill='', outline='black', style=tk.ARC)
items.append(item)
item = canvas.create_line(*blu, *tld, fill='black')
items.append(item)
items = tuple(items)
print(items)
for item_ in items:
for _item in items:
canvas.addtag_withtag(item_, _item)
return items
if __name__ == '__main__':
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=500)
canvas.pack(expand=True, fill=tk.BOTH)
TL = 100, 100
BR = 400, 200
make_round_corners_rect(canvas, *TL, *BR)
TL = 100, 300
BR = 400, 400
make_round_corners_rect(canvas, *TL, *BR, ratio = .3)
TL = 300, 50
BR = 350, 450
that_rect = make_round_corners_rect(canvas, *TL, *BR, ratio=.4)
for fragment in that_rect:
canvas.itemconfig(fragment, width=4)
try:
canvas.itemconfig(fragment, outline='blue')
except TclError:
canvas.itemconfig(fragment, fill='blue')
TL = 150, 50
BR = 200, 450
make_round_corners_rect(canvas, *TL, *BR, ratio=.07)
TL = 30, 30
BR = 470, 470
that_rect = make_round_corners_rect(canvas, *TL, *BR, ratio=.3)
for fragment in that_rect:
canvas.itemconfig(fragment, dash=(3, 3))
TL = 20, 20
BR = 480, 480
make_round_corners_rect(canvas, *TL, *BR, ratio=.1)
root.mainloop()
The next step, (left to the reader as an exercise), is to encapsulate the round rectangles in a class.
Edit: how to fill a rounded corners rectangle:
It is a bit involved, and in the long run, probably requires an approach where all points are explicitly defined, and the shape is formed as a polygon, instead of the aggregation of tkinter primitives. In this edit, the rounded corners rectangle is filled with two overlapping rectangles, and four disks; it allows to create a filled/unfilled shape, but not to change that property after creation - although it would not require too much work to be able to do this too. (collecting the canvas ids, and turning them on/off on demand, in conjunction with the outline property); however, as mentioned earlier, this would make more sense to encapsulate all this behavior in a class that mimicks the behavior of tk.canvas.items.
def make_round_corners_rect(canvas, x0, y0, x1, y1, ratio=0.2, npts=12, filled=False, fillcolor=''):
...
if filled:
canvas.create_rectangle(x0+r, y0, x1-r, y1, fill=fillcolor, outline='')
canvas.create_rectangle(x0, y0+r, x1, y1-r, fill=fillcolor, outline='')
canvas.create_oval(x0, y0, x0+2*r, y0+2*r, fill=fillcolor, outline='')
canvas.create_oval(x1-2*r, y0, x1, y0+2*r, fill=fillcolor, outline='')
canvas.create_oval(x1-2*r, y1-2*r, x1, y1, fill=fillcolor, outline='')
canvas.create_oval(x0, y1-2*r, x0+2*r, y1, fill=fillcolor, outline='')
...
if __name__ == '__main__':
...
TL = 100, 300
BR = 400, 400
make_round_corners_rect(canvas, *TL, *BR, ratio=.3, filled=True, fillcolor='cyan')
...
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 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:
So I'm spawning circles at random in Python and I decided I wanted to make each circle it's own object. So I created a class for them.
I wanted each circle to have their own tick function so that each one can do their own thing. (change colors, size, de-spawn etc.) However I'm having a lot of issues getting the tick method to work.
Here is my code:
from random import *
from tkinter import *
from time import *
size = 2000
window = Tk()
count = 0
class Shape(object):
def __init__(self,name, canv, size, col, x0, y0,d,outline):
self.name = name
self.canv = canv
self.size = size
self.col = col
self.x0 = x0
self.y0 = y0
self.d = d
self.outline = outline
self.age=0
def death(self):
pass
def spawn(self):
self.canv.create_oval(self.x0, self.y0, self.x0 + self.d, self.y0 + self.d, outline=self.outline, fill = self.col)
def tick(self):
self.age = self.age +time.time()
while True:
tick()
# var canv = A new Canvas Object (The Window Object to be put in, The Canvice width, The canvice Height)
canv = Canvas(window, width=size, height=size)
canv.pack()
d = 0
while True:
col = choice(['#EAEA00'])
x0 = randint(0, size)
y0 = randint(0, size)
#d = randint(0, size/5)
d = (d + 0.001)
count = count+1
outline = 'white'
shapes[count] = Shape("shape" + str(count), canv, size, col, x0, y0, d, outline)
shapes[count].spawn()
#canv.create_oval(x0, y0, x0 + d, y0 + d, outline='white', fill = col)
window.update()
So basically I get two different errors from this part of the code:
while True:
tick()
Ether it's a TypeError: tick() missing one required positional argument: 'self' or if I put self into the parameters I get self is undefined.
So what am I doing wrong?