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)
Related
I have build environment in Tkinter, how to create observation space using this environment. I could not understand how to use grid coordinates in array to make observation space, self.observation_space = spaces.Box(np.array([]), np.array([]), dtype=np.int), code is given. I will be thankful if anyone help.
enter code here
# Setting the sizes for the environment
pixels = 40 # pixels
env_height =9 # grid height
env_width = 9 # grid width
# Global variable for dictionary with coordinates for the final route
a = {}
# Creating class for the environment
class Environment(tk.Tk, object):
def __init__(self):
super(Environment, self).__init__()
self.action_space = ['up', 'down', 'left', 'right']
self.n_actions = len(self.action_space)
self.title('RL Q-learning. Sichkar Valentyn')
self.geometry('{0}x{1}'.format(env_height * pixels, env_height * pixels))
self.build_environment()
print(self.geometry('{0}x{1}'.format(env_height * pixels, env_height * pixels)))
# Dictionaries to draw the final route
self.d = {}
self.f = {}
# Key for the dictionaries
self.i = 0
# Writing the final dictionary first time
self.c = True
# Showing the steps for longest found route
self.longest = 0
# Showing the steps for the shortest route
self.shortest = 0
# Function to build the environment
def build_environment(self):
self.canvas_widget = tk.Canvas(self, bg='black',
height=env_height * pixels,
width=env_width * pixels)
# Uploading an image for background
# img_background = Image.open("images/bg.png")
# self.background = ImageTk.PhotoImage(img_background)
# # Creating background on the widget
# self.bg = self.canvas_widget.create_image(0, 0, anchor='nw', image=self.background)
# Creating grid lines
for column in range(0, env_width * pixels, pixels):
x0, y0, x1, y1 = column, 0, column, env_height * pixels
self.canvas_widget.create_line(x0, y0, x1, y1, fill='grey')
for row in range(0, env_height * pixels, pixels):
x0, y0, x1, y1 = 0, row, env_height * pixels, row
self.canvas_widget.create_line(x0, y0, x1, y1, fill='grey')
self.canvas_widget.pack()
if __name__ == '__main__':
env = Environment()
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()
I am currently using the following code to create a grid the size of the window within python using the tkinter module
import tkinter as tk
class Frame():
def __init__(self, *args, **kwargs):
# Setup Canvas
self.c = tk.Canvas(root, height=500, width=500, bg='white')
self.c.pack(fill=tk.BOTH, expand=True)
self.c.bind('<Configure>', self.createGrid)
self.pixel_width = 20
self.pixel_height = 20
# Setup binds
self.c.bind("<ButtonPress-1>", self.leftClick)
def leftClick(self, event):
items = self.c.find_closest(event.x, event.y)
if items:
rect_id = items[0]
self.c.itemconfigure(rect_id, fill="red")
def createGrid(self, event=None):
for x in range(0, self.c.winfo_width()):
for y in range(0, self.c.winfo_height()):
x1 = (x * self.pixel_width)
x2 = (x1 + self.pixel_width)
y1 = (y * self.pixel_height)
y2 = (y1 + self.pixel_height)
self.c.create_rectangle(x1,y1,x2,y2)
self.c.update()
root = tk.Tk()
gui = Frame(root)
root.mainloop()
If I set the canvas height and width to something like 50 this loads quite quickly, although when the size is increased to 500 x 500 like is set here it takes about 5 seconds to create the grid. I have tried creating the grid with lines but the problem with that is I need squares as I am then planning to change the colour of a square that is selected. Is there any way I can make this more efficient?
I think you created way more rectangles than you need. The below two lines:
for x in range(0, self.c.winfo_width()):
for y in range(0, self.c.winfo_height()):
Will create 504x504 rectangles = 254016. It will work fine if you reduce it to just fill your current screen:
def createGrid(self, event=None):
for x in range(0, int(self.c.winfo_width()/20+1)):
for y in range(0, int(self.c.winfo_height()/20+1)):
x1 = (x * self.pixel_width)
x2 = (x1 + self.pixel_width)
y1 = (y * self.pixel_height)
y2 = (y1 + self.pixel_height)
self.c.create_rectangle(x1,y1,x2,y2)
self.c.update()
I have a question how can you recursively draw a rectangle with the use of fractional coordinates? I have been trying to make the squares/rectangles get smaller by 10% each time until it either takes up the entire canvas(meaning it cannot go beyond the width and height of the board or it just get's to 0 meaning it won't draw anything or it's so small you won't see the final square). Or maybe is there another way that I can go about drawing squares in a recursive way that keeps decreasing by a certain percent?
I have this so far but I can't seem to think of who to get it to continue to recurse until it can no longer do it.
import tkinter
class draw_squares:
def __init__(self, squares: [(float, float, float, float)]):
self.squares = squares
self.root_window = tkinter.Tk()
self.canvas = tkinter.Canvas(master = self.root_window,
width = 500, height = 500,
background = 'blue')
self.canvas.grid(row=0, column = 0, padx=0, pady = 0,
sticky = tkinter.E + tkinter.W + tkinter.S + tkinter.N)
self.canvas.bind('<Configure>', self._resized)
self.root_window.rowconfigure(0, weight = 1)
self.root_window.columnconfigure(0, weight = 1)
def start(self):
self.root_window.mainloop()
def _resized(self, event: tkinter.Event):
self.draw_squares_recursively()
def draw_squares_recursively(self):
self.canvas.delete(tkinter.ALL)
for x, y, x2, y2 in self.squares:
if x != 0.5 and y != 0.5 and x2 != 0.5 and y2 != 0.5:
self._draw_square(self._draw_square(x-.1,y-.1,x2+.1,y2+.1))
def _draw_square(self, x: float, y:float, x2:float, y2:float):
width = self.canvas.winfo_width()
height = self.canvas.winfo_height()
self.canvas.create_rectangle(
x * width, y * height,
x2 * width, y2 *height, outline = 'grey')
if __name__ == '__main__':
squares = [(0.9, 0.9, 0.1, 0.1), (0.8,0.8,0.2,0.2), (0.7,0.7,0.3,0.3),
(0.6,0.6,0.4,0.4), (0.5,0.5,0.5,0.5)]
app = draw_squares(squares)
app.start()
Thank you very much in advance for the help!
First, you are using floating point numbers, so (not) equal is not a good idea http://www.lahey.com/float.htm draw_squares_recursively() can call itself until the limit is reached. You can work out the arithmetic to get the sizes and locations you want.
class DrawSquares:
def __init__(self, square):
## self.squares = squares
self.root_window = tkinter.Tk()
self.canvas = tkinter.Canvas(master = self.root_window,
width = 500, height = 500,
background = 'blue')
self.canvas.grid(row=0, column = 0, padx=0, pady = 0,
sticky = "nsew")
## self.canvas.bind('<Configure>', self._resized)
self.root_window.rowconfigure(0, weight = 1)
self.root_window.columnconfigure(0, weight = 1)
self.draw_squares_recursively(square)
self.root_window.mainloop()
def draw_squares_recursively(self, square):
##self.canvas.delete(tkinter.ALL)
for value in square:
if value < 0.05 or value > 100:
return
self.canvas.create_rectangle(square[0], square[1],
square[2], square[3],
outline = 'grey', width=2)
square[2] += 20
square[3] += 20
self.draw_squares_recursively(square)
if __name__ == '__main__':
squares = [0.9, 0.9, 10.1, 10.1]
app = DrawSquares(squares)
I tried to write my own simple function to draw gradient (only grey colours) by using very naive method (drawing sequence of lines), but actaully the colour of the rectangle which represents the gradient is always an uniform colour (I think it is the latest colour from the loop). Could you explain me why, please? Here is the code:
import Tkinter
class testGUI:
def __init__( self, root ):
C = Tkinter.Canvas( root, bg = "blue", height = 250, width = 300 )
self.drawGradient( C, 10, 10, 100, 50 )
C.pack()
def drawGradient( self, canvas, x, y, w, h ):
for offset in range( 0, w ):
gradColor = '#%02x%02x%02x' % ( x * 10, x * 10, x * 10 )
canvas.create_line( x + offset, y, x + offset, y + h, fill = gradColor )
root = Tkinter.Tk()
app = testGUI( root )
root.mainloop()
The color is always the same because the color you are using does not depend on the iteration of the loop:
for offset in ...
gradColor = '#%02x%02x%02x' % ( x * 10, x * 10, x * 10 )
To make it change, the value of gradColor must depend on the value of offset, for instance:
def drawGradient(self, canvas, x, y, w, h):
factor = 255./w
for offset in range(0, w):
gradColor = '#%02x%02x%02x' % (offset*factor, offset*factor, offset*factor)
canvas.create_line(x + offset, y, x + offset, y + h, fill=gradColor)