I am thinking of creating a board game in Python, one which will have a grid of spaces, each with different properties, and which may or may not have pieces resting on them. These pieces should be able to move between spaces, though subject to various rules. (Chess or checkers would be good examples of what I'm thinking of, though my game would have different/more complicated rules, and the grid may not be square, even if the spaces are).
I wrote a Java implementation of something similar for a data structures class, using a modified version of linked lists. But this is Python, so I imagine there's a better way to do it (maybe even a library out there?)
Drawing a chessboard is pretty trivial with Tkinter. Here's a really simple example:
import Tkinter as tk
class GameBoard(tk.Frame):
def __init__(self, parent, rows=8, columns=8, size=32, color1="white", color2="blue"):
'''size is the size of a square, in pixels'''
self.rows = rows
self.columns = columns
self.size = size
self.color1 = color1
self.color2 = color2
self.pieces = {}
canvas_width = columns * size
canvas_height = rows * size
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(self, borderwidth=0, highlightthickness=0,
width=canvas_width, height=canvas_height, background="bisque")
self.canvas.pack(side="top", fill="both", expand=True, padx=2, pady=2)
# this binding will cause a refresh if the user interactively
# changes the window size
self.canvas.bind("<Configure>", self.refresh)
def addpiece(self, name, image, row=0, column=0):
'''Add a piece to the playing board'''
self.canvas.create_image(0,0, image=image, tags=(name, "piece"), anchor="c")
self.placepiece(name, row, column)
def placepiece(self, name, row, column):
'''Place a piece at the given row/column'''
self.pieces[name] = (row, column)
x0 = (column * self.size) + int(self.size/2)
y0 = (row * self.size) + int(self.size/2)
self.canvas.coords(name, x0, y0)
def refresh(self, event):
'''Redraw the board, possibly in response to window being resized'''
xsize = int((event.width-1) / self.columns)
ysize = int((event.height-1) / self.rows)
self.size = min(xsize, ysize)
self.canvas.delete("square")
color = self.color2
for row in range(self.rows):
color = self.color1 if color == self.color2 else self.color2
for col in range(self.columns):
x1 = (col * self.size)
y1 = (row * self.size)
x2 = x1 + self.size
y2 = y1 + self.size
self.canvas.create_rectangle(x1, y1, x2, y2, outline="black", fill=color, tags="square")
color = self.color1 if color == self.color2 else self.color2
for name in self.pieces:
self.placepiece(name, self.pieces[name][0], self.pieces[name][1])
self.canvas.tag_raise("piece")
self.canvas.tag_lower("square")
# image comes from the silk icon set which is under a Creative Commons
# license. For more information see http://www.famfamfam.com/lab/icons/silk/
imagedata = '''
R0lGODlhEAAQAOeSAKx7Fqx8F61/G62CILCJKriIHM+HALKNMNCIANKKANOMALuRK7WOVLWPV9eR
ANiSANuXAN2ZAN6aAN+bAOCcAOKeANCjKOShANKnK+imAOyrAN6qSNaxPfCwAOKyJOKyJvKyANW0
R/S1APW2APW3APa4APe5APm7APm8APq8AO28Ke29LO2/LO2/L+7BM+7BNO6+Re7CMu7BOe7DNPHA
P+/FOO/FO+jGS+/FQO/GO/DHPOjBdfDIPPDJQPDISPDKQPDKRPDIUPHLQ/HLRerMV/HMR/LNSOvH
fvLOS/rNP/LPTvLOVe/LdfPRUfPRU/PSU/LPaPPTVPPUVfTUVvLPe/LScPTWWfTXW/TXXPTXX/XY
Xu/SkvXZYPfVdfXaY/TYcfXaZPXaZvbWfvTYe/XbbvHWl/bdaPbeavvadffea/bebvffbfbdfPvb
e/fgb/Pam/fgcvfgePTbnfbcl/bfivfjdvfjePbemfjelPXeoPjkePbfmvffnvbfofjlgffjkvfh
nvjio/nnhvfjovjmlvzlmvrmpvrrmfzpp/zqq/vqr/zssvvvp/vvqfvvuPvvuvvwvfzzwP//////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////yH+FUNyZWF0ZWQgd2l0aCBU
aGUgR0lNUAAh+QQBCgD/ACwAAAAAEAAQAAAIzAD/CRxIsKDBfydMlBhxcGAKNIkgPTLUpcPBJIUa
+VEThswfPDQKokB0yE4aMFiiOPnCJ8PAE20Y6VnTQMsUBkWAjKFyQaCJRYLcmOFipYmRHzV89Kkg
kESkOme8XHmCREiOGC/2TBAowhGcAyGkKBnCwwKAFnciCAShKA4RAhyK9MAQwIMMOQ8EdhBDKMuN
BQMEFPigAsoRBQM1BGLjRIiOGSxWBCmToCCMOXSW2HCBo8qWDQcvMMkzCNCbHQga/qMgAYIDBQZU
yxYYEAA7
'''
if __name__ == "__main__":
root = tk.Tk()
board = GameBoard(root)
board.pack(side="top", fill="both", expand="true", padx=4, pady=4)
player1 = tk.PhotoImage(data=imagedata)
board.addpiece("player1", player1, 0,0)
root.mainloop()
You can consider your underlying board implementation as different kind of datastructures.
List of lists - l1 = [[1,2,3],[4,5,6],[7,8,9]]
Dict with values as list = d1 = {a:[1,2,3],b:[4,5,6],c:[7,8,9]}
Dict with keys are coordinates and values which you can assign.
As a Graph
Here is a design of an empty chessboard.
>>> chessboard = {}
>>> for row in range(8):
... for col in range(8):
... chessboard[(row,col)] = 0
...
>>>
You can use any of these and design the logic of your game. For higher level elements you can tie these objects or elements within to spites of pygame
Related
I am trying to simulate a stellar system. I aim to manipulate the parameters via slider widgets, see the (reduced) code below. The slider widget in my code accepts a new value for the sun mass, which was set via a StellarSys instance. However the slider.set method fails with TypeError: 'NoneType' object is not subscriptable. Does somebody have a solution or can explain what I'm doing wrong? Many thanks.
import tkinter as tk
import math
class Space(tk.Frame):
def __init__(self, master, size, bg=None):
super().__init__(master)
frame = tk.Frame(self, border=5)
frame.pack()
self.width, self.height = size
self.canvas = tk.Canvas(frame, width=self.width,height=self.height,
borderwidth=0, highlightthickness=0, bg=bg)
self.canvas.pack()
self.bodies = None
def place_bodies(self):
for body in self.bodies:
x1, y1 = int(body.loc[0]-body.size/2.0),int(body.loc[1]-body.size/2.0)
x2, y2 = x1 + body.size, y1 + body.size
body.tk_id = self.canvas.create_oval(x1, y1, x2, y2, fill=body.color)
class SpaceBody:
def __init__(self, **kwargs):
self.name = kwargs['name']
self.size = kwargs['size']
self.mass = kwargs['mass']
self.loc = kwargs['loc']
self.speed = kwargs['speed']
self.color = kwargs['color']
self.dxdy = (0,0)
self.tk_id = None
def __repr__(self):
return f"\n{self.name} is {self.color}"
class Dashboard(tk.Frame):
def __init__(self, master, bg=None):
super().__init__(master)
frame = tk.Frame(self, border=5, bg=bg)
frame.pack()
sun_frame=tk.Frame(frame)
sun_frame.grid(row=1)
w, h = 15, 3
tk.Label(sun_frame, text = '').grid(row=0)
tk.Label(sun_frame, text = 'SUN MASS').grid(row=1)
self.sun_mass = tk.Scale(sun_frame, from_=0, to=1000, orient='horizontal')
self.sun_mass.bind("<ButtonRelease-1>", self.update)
# self.sun_mass.set(500) # this works
self.sun_mass.set(space.bodies[0].mass) # This doesn't work
self.sun_mass.grid(row=2)
def update(self, event):
space.bodies[0].mass = self.sun_mass.get()
print(space.bodies[0].mass)
class StellarSys:
def __init__(self):
sun = SpaceBody(name='Sun', size=30, mass=500, loc=(500,400),speed=(0, 0), color='yellow')
earth = SpaceBody(name='Earth',size=15, mass=1, loc=(500,200),speed=(15,0), color='green')
space.bodies = [sun, earth]
space.place_bodies()
# MAIN
root = tk.Tk()
root.title('UNIVERSE')
size = (1000, 800)
space = Space(root, size, bg='black')
space.grid(row=0, column = 0,sticky="nsew")
dashboard = Dashboard(root)
dashboard.grid(row=0, column = 1,sticky="nsew")
stellarsys = StellarSys()
root.mainloop()
You initialize self.bodies to None, and then try to subscript that value. As the error says, you can't use subscripts on a value of None.
You need to rework your logic so that self.bodies is a non-empty list before trying to reference an item in the list.
I'm trying to draw a grid with tkinter canvas. The logic is fairly simple: for each node in the grid, I draw a rectangle using its top-left and bottom-right corners [code below].
The problem is that tkinter doesn't render the left and top borders of nodes in the first column and row respectively. It's like tkinter is offsetting the canvas by some small # of pixels. Is there a config to correct this? My current workaround is to subtract some small value from cellW and cellH, then offset every node's x1 and y1...this is very hacky.
I am not doing anything weird, just a simple canvas on a root window.
import tkinter as tk
winH = 400
winW = 400
ncols = 10
nrows = 10
cellW = winW / ncols
cellH = winH / nrows
class Node:
def __init__(self, row, col):
self.row = row
self.col = col
return
def generatGrid(nrows, ncols):
grid = []
for r in range(nrows):
row = [ Node(r, c) for c in range(ncols) ]
grid.append(row)
return grid
def drawNode(canvas, node):
x1 = cellW * node.col
y1 = cellH * node.row
x2 = x1 + cellW
y2 = y1 + cellH
canvas.create_rectangle(x1, y1, x2, y2)
return
def drawGrid(canvas, grid):
for row in grid:
for node in row:
drawNode(canvas, node)
return
window = tk.Tk()
canvas = tk.Canvas(window, width=winW, height=winH)
canvas.pack()
grid = generatGrid(nrows, ncols)
drawGrid(canvas, grid)
window.mainloop()
One of the things I find annoying about the canvas is that the border is part of the coordinate space. When you draw a line on the left edge, it gets obscured by the border.
Is there a config to correct this?
Yes.
You can get around this by completely turning off the attributes related to the border:
canvas = tk.Canvas(window, width=winW, height=winH,
borderwidth=0, highlightthickness=0)
If you want to have some sort of border around the canvas, you can place the canvas in a frame, and use the frame to draw the border.
Hello I got a problem with using the Tkinter package on python. I want to create a window containing two major widgets of which one is a canvas that will later show a grid with cells. When initializing the canvas I use "create_rectangle" to fill the canvas with the desired objects. Also when clicking on a cell (for testing reasons) the canvas should change its color in the area of the rectangle. However when initialy displaying the window at first no objects can be seen (the expected result would be only white colored rectangles) and only when a Click on the canvas is performed the area changes its color as desired.
While looking through the internet I tried several variations of the order of the pack()- and create_rectangle()-methods. This is the code:
from tkinter import *
from tkinter.ttk import *
import cells
GRID_WIDTH = 15
GRID_HEIGHT = 15
class Ui(Frame):
""" Class to represent the ui of conways game of life"""
def __init__(self, parent, grid):
self.parent = parent
self.grid = grid
Frame.__init__(self, parent)
self.__setup()
def __setup(self):
""" Method to setup the components of the ui """
self.parent.title("Conway's game of life")
self.pack()
#Setup a frame to hold control components
frame_cntrl = Frame(self)
frame_cntrl.pack(side = RIGHT, anchor="n")
self.__setup_cntrl_components(frame_cntrl)
#Setup a frame to hold the Grid
self.canvas = Canvas(self)
self.canvas.pack(side = LEFT)
self.__draw_grid()
self.canvas.bind("<Button-1>", self.__canvas_clicked)
def __setup_cntrl_components(self, parent):
""" Method to setup the control elements of the ui"""
#Setup a label for the generation
self.lb_generation = Label(parent, text="dummy")
self.lb_generation.pack(side = TOP)
#Setup a button for iteration
self.bt_iteration = Button(parent, text="Iterate")
self.bt_iteration.pack(side = TOP)
def __draw_cell(self, x, y):
""" Draws a cell on the canvas"""
width, height = self.canvas.winfo_width(), self.canvas.winfo_height()
color = "black" if self.grid.cell_alive(x, y) else "white"
x0 = width * x / self.grid.width + 1
x1 = width * (x + 1) / self.grid.width
y0 = height * y / self.grid.height + 1
y1 = height * (y + 1) / self.grid.height
self.canvas.create_rectangle(x0, y0, x1, y1, width=0, fill=color)
def __draw_grid(self):
""" Method to setup the grid itself"""
width, height = self.canvas.winfo_width(), self.canvas.winfo_height()
for i in range(0, self.grid.width):
for j in range(0, self.grid.height):
self.__draw_cell(i, j)
def __canvas_clicked(self, event):
""" Method for when the cell was clicked """
x, y, width, height = event.x, event.y, self.canvas.winfo_width(), self.canvas.winfo_height()
cell_x = int(x / width * self.grid.width)
cell_y = int(y / height * self.grid.height)
self.grid.switch_cell(cell_x, cell_y)
self.__draw_cell(cell_x, cell_y)
if __name__ == "__main__":
Ui(Tk(), cells.Grid(GRID_WIDTH, GRID_HEIGHT)).mainloop()
Problem 1:
Your main problem is that, before the canvas is actually displayed, canvas.winfo_width() and canvas.winfo_height() do not return the canvas width and height, but the value 1.
I suggest you create the canvas as follows:
# Define canvas and save dimensions
self.canvas_width = 300
self.canvas_height = 200
self.canvas = Canvas(self, width = self.canvas_width,
height = self.canvas_height)
Then, in your code, replace each instance of:
width, height = self.canvas.winfo_width(), self.canvas.winfo_height()
with
width, height = self.canvas_width, self.canvas_height
Problem 2:
When creating each cell I don't think you need to add 1 to x0 and y0. Instead, it should be:
x0 = width * x / self.grid.width
x1 = width * (x + 1) / self.grid.width
y0 = height * y / self.grid.height
y1 = height * (y + 1) / self.grid.height
I do not know if my question is a stupid one or a tricky one.
So, using Tkinter and Canvas, I succeed to implement a scrolling/zooming function that work perfectly, thanks to this post Move and zoom a tkinter canvas with mouse. I also add a binding to resize the canvas size when the window size change without trouble.
Using coords and after, I have no trouble to move object around.
The trouble came when I tried to combine everything.
Moving and scrolling : no trouble
Scrolling and Zooming : ok
Zooming, moving and scrolling : do not work
The code bellow reproduce the trouble (python 2.7, work on windows). For what I can see, the trouble come from the scaling, maybe caused by the change of coords of the objects, that induce the canvas resizing, and then disable the scaling? If it is the case, I need help to solve this issue. If it is not the case, I need help to found the issue...
By removing/disable the line self.master.after(50, self.Display), moving do no occur anymore.
import Tkinter as tk
import math
class Example:
def __init__ (self, master):
self.master = master
self.interval = 0
self.SizeX, self.SizeY = master.winfo_width(), master.winfo_height()
#Canvas Frame
self.SystemCanvasFrame = tk.Frame(master, bg='black')
self.SystemCanvasFrame.grid(row=0, column=0)
#Canvas
self.SystemCanvas = tk.Canvas(self.SystemCanvasFrame, width=int(self.SizeX*0.75)-20, height=self.SizeY-20, bg="black")
self.SystemCanvas.focus_set()
self.xsb = tk.Scrollbar(self.SystemCanvasFrame, orient="horizontal", command=self.SystemCanvas.xview)
self.ysb = tk.Scrollbar(self.SystemCanvasFrame, orient="vertical", command=self.SystemCanvas.yview)
self.SystemCanvas.configure(scrollregion=(-500,-500,500,500))
self.SystemCanvas.configure(yscrollcommand=self.ysb.set, xscrollcommand=self.xsb.set)
#add the canvas with scroll bar in grid format
self.xsb.grid(row=1, column=0, sticky="ew")
self.ysb.grid(row=0, column=1, sticky="ns")
self.SystemCanvas.grid(row=0, column=0, sticky="nsew")
# This is what enables using the mouse to slide the window:
self.SystemCanvas.bind("<ButtonPress-1>", self.move_start)
self.SystemCanvas.bind("<B1-Motion>", self.move_move)
#windows scroll
self.SystemCanvas.bind("<MouseWheel>",self.zoomer)
#resize the main window
self.master.bind('<Configure>', self.UpdateCanvasSize)
#Create Objects
self.Size = 5 #object Size
x0 = 0
y0 = 0
x1 = self.Size
y1 = self.Size
self.SystemCanvas.create_oval(x0,y0,x1,y1, fill='green', outline='green', width=3, tags='Green')
self.SystemCanvas.create_oval(x0,y0,x1,y1, fill='red', outline='red', width=3, tags='Red')
self.SystemCanvas.create_oval(x0,y0,x1,y1, fill='yellow', outline='yellow', width=1, tags='Yellow')
self.Display()
def Display(self):
self.interval += 0.5 #speed parameter
GreenPos = self.UpdatePosition(0.1*self.interval, (0,0), 50)
RedPos = self.UpdatePosition(0.02*self.interval+180, (0,0), 200)
YellowPos = self.UpdatePosition(0.3*self.interval, RedPos, 10)
self.MoveObject('Green', GreenPos)
self.MoveObject('Red', RedPos)
self.MoveObject('Yellow', YellowPos)
self.master.after(50, self.Display) #Disable to zoom
def MoveObject (self, Obj, pos): #only move object that are in the field of view
"""Move Obj to the given position (tuple - xy)"""
ID = self.SystemCanvas.find_withtag(Obj)
#Convert the Center of the object to the coo need for tk
x0 = pos[0] - self.Size/2.0 #radius of the circle
y0 = pos[1] - self.Size/2.0
x1 = pos[0] + self.Size/2.0
y1 = pos[1] + self.Size/2.0
self.SystemCanvas.coords(ID, x0,y0,x1,y1)
def UpdatePosition(self, angle, center, distance):
"""Calculate next object position around the Center at the Distance and speed determine by Angle (in Radian) - Center of the object"""
h = center[0]
k = center[1]
radius = distance
Rad = angle
x = h+radius*math.cos(Rad)
y = k+radius*math.sin(Rad)
return (x, y)
def UpdateCanvasSize(self, event):
"""Permit to resize the canvas to the window"""
self.SizeX, self.SizeY = self.master.winfo_width(), self.master.winfo_height()
self.SystemCanvas.config(width=int(self.SizeX*0.75)-20, height=self.SizeY-20)
def move_start(self, event):
"""Detect the beginning of the move"""
self.SystemCanvas.scan_mark(event.x, event.y)
self.SystemCanvas.focus_set() #security, set the focus on the Canvas
def move_move(self, event):
"""Detect the move of the mouse"""
self.SystemCanvas.scan_dragto(event.x, event.y, gain=1)
def zoomer(self,event):
"""Detect the zoom action by the mouse. Zoom on the mouse focus"""
true_x = self.SystemCanvas.canvasx(event.x)
true_y = self.SystemCanvas.canvasy(event.y)
if (event.delta > 0):
self.SystemCanvas.scale("all", true_x, true_y, 1.2, 1.2)
elif (event.delta < 0):
self.SystemCanvas.scale("all", true_x, true_y, 0.8, 0.8)
self.SystemCanvas.configure(scrollregion = self.SystemCanvas.bbox("all"))
if __name__ == '__main__':
root = tk.Tk()
root.geometry('1125x750')
app = Example(root)
root.mainloop()
I'm new to Tkinter so this might not be the most elegant solution but I hope it gives you an idea on how to solve the problem.
The zoomer method scales your coordinates but these coordinates are reset anytime you call MoveObject or UpdatePosition. I added code that keeps track of the scale factor, self.scale, and a method update_coord that scales a given coordinate based on the scale factor. Finally, I called update_coord in the MoveObject and UpdatePosition methods.
Here is the working code;
import Tkinter as tk
import math
class Example:
def __init__ (self, master):
self.scale = 1 #Added
self.master = master
self.interval = 0
self.SizeX, self.SizeY = master.winfo_width(), master.winfo_height()
#Canvas Frame
self.SystemCanvasFrame = tk.Frame(master, bg='black')
self.SystemCanvasFrame.grid(row=0, column=0)
#Canvas
self.SystemCanvas = tk.Canvas(self.SystemCanvasFrame, width=int(self.SizeX*0.75)-20, height=self.SizeY-20, bg="black")
self.SystemCanvas.focus_set()
self.xsb = tk.Scrollbar(self.SystemCanvasFrame, orient="horizontal", command=self.SystemCanvas.xview)
self.ysb = tk.Scrollbar(self.SystemCanvasFrame, orient="vertical", command=self.SystemCanvas.yview)
self.SystemCanvas.configure(scrollregion=(-500,-500,500,500))
self.SystemCanvas.configure(yscrollcommand=self.ysb.set, xscrollcommand=self.xsb.set)
#add the canvas with scroll bar in grid format
self.xsb.grid(row=1, column=0, sticky="ew")
self.ysb.grid(row=0, column=1, sticky="ns")
self.SystemCanvas.grid(row=0, column=0, sticky="nsew")
# This is what enables using the mouse to slide the window:
self.SystemCanvas.bind("<ButtonPress-1>", self.move_start)
self.SystemCanvas.bind("<B1-Motion>", self.move_move)
#windows scroll
self.SystemCanvas.bind("<MouseWheel>",self.zoomer)
#resize the main window
self.master.bind('<Configure>', self.UpdateCanvasSize)
#Create Objects
self.Size = 5 #object Size
x0 = 0
y0 = 0
x1 = self.Size
y1 = self.Size
self.SystemCanvas.create_oval(x0,y0,x1,y1, fill='green', outline='green', width=3, tags='Green')
self.SystemCanvas.create_oval(x0,y0,x1,y1, fill='red', outline='red', width=3, tags='Red')
self.SystemCanvas.create_oval(x0,y0,x1,y1, fill='yellow', outline='yellow', width=1, tags='Yellow')
self.Display()
#**Added Method
def update_coord(self, coord):
"""Calculate the scaled cordinate for a given cordinate based on the zoomer scale factor"""
new_coord = [coord_i * self.scale for coord_i in coord]
return new_coord
def Display(self):
self.interval += 0.5 #speed parameter
GreenPos = self.UpdatePosition(0.1*self.interval, (0,0), 50)
RedPos = self.UpdatePosition(0.02*self.interval+180, (0,0), 200)
YellowPos = self.UpdatePosition(0.3*self.interval, RedPos, 10)
self.MoveObject('Green', GreenPos)
self.MoveObject('Red', RedPos)
self.MoveObject('Yellow', YellowPos)
self.master.after(1, self.Display) #Disable to zoom
def MoveObject (self, Obj, pos): #only move object that are in the field of view
"""Move Obj to the given position (tuple - xy)"""
ID = self.SystemCanvas.find_withtag(Obj)
#Convert the Center of the object to the coo need for tk
x0 = pos[0] - self.Size/2.0 #radius of the circle
y0 = pos[1] - self.Size/2.0
x1 = pos[0] + self.Size/2.0
y1 = pos[1] + self.Size/2.0
c_0 = self.update_coord([x0, y0]) #Added
c_1 = self.update_coord([x1, y1]) #Added
self.SystemCanvas.coords(ID, c_0[0], c_0[1], c_1[0], c_1[1]) #Added/Edited
def UpdatePosition(self, angle, center, distance):
"""Calculate next object position around the Center at the Distance and speed determine by Angle (in Radian) - Center of the object"""
h = center[0]
k = center[1]
radius = distance
Rad = angle
x = h+radius*math.cos(Rad)
y = k+radius*math.sin(Rad)
return self.update_coord([x, y]) #Added/Edited
def UpdateCanvasSize(self, event):
"""Permit to resize the canvas to the window"""
self.SizeX, self.SizeY = self.master.winfo_width(), self.master.winfo_height()
self.SystemCanvas.config(width=int(self.SizeX*0.75)-20, height=self.SizeY-20)
def move_start(self, event):
"""Detect the beginning of the move"""
self.SystemCanvas.scan_mark(event.x, event.y)
self.SystemCanvas.focus_set() #security, set the focus on the Canvas
def move_move(self, event):
"""Detect the move of the mouse"""
self.SystemCanvas.scan_dragto(event.x, event.y, gain=1)
def zoomer(self,event):
"""Detect the zoom action by the mouse. Zoom on the mouse focus"""
true_x = self.SystemCanvas.canvasx(event.x)
true_y = self.SystemCanvas.canvasy(event.y)
if (event.delta > 0):
self.SystemCanvas.scale("all", true_x, true_y, 1.2, 1.2)
self.scale *= 1.2 #**Added
elif (event.delta < 0):
self.SystemCanvas.scale("all", true_x, true_y, 0.8, 0.8)
self.scale *= 0.8 #**Added
#self.SystemCanvas.configure(scrollregion = self.SystemCanvas.bbox("all")) #**Removed (This disables scrollbar after zoom)
if __name__ == '__main__':
root = tk.Tk()
root.geometry('1125x750')
app = Example(root)
root.mainloop()
I am trying to simulate a grid like what would be used for a game board using tkinter rectangles being drawn onto a canvas, however I am having trouble making a loop that does so correctly.
Pretty much I have a variable that contains a certain amount of rows for a grid and a variable for the amount of columns, and I need to create a grid of rectangles based off of those configurations which can change anytime the application is run, which is making it a bit tricky for me.
Anyways, I currently have a function to draw a rectangle to the screen like so:
def _place_empty_tiles(self):
self._canvas.update()
width = self._canvas.winfo_width()
height = self._canvas.winfo_height()
self.x = width / self._game.columns
self.y = height / self._game.rows
for i in range(self._game.columns):
click_point = point.from_pixel(self.x, self.y, width, height)
self._state.handle_click(click_point)
self.x += 60
def _redraw_game_pieces(self)->None:
'''Delete and redraw all the of game pieces'''
self._canvas.delete(tkinter.ALL)
canvas_width = self._canvas.winfo_width()
canvas_height = self._canvas.winfo_height()
for tile in self._state.all_tiles():
center_x, center_y = tile.center().pixel(canvas_width, canvas_height)
radius_x = tile.radius_frac() * canvas_width
radius_y = tile.radius_frac() * canvas_height
self._canvas.create_rectangle(
center_x - radius_x, center_y - radius_y,
center_x + radius_x, center_y + radius_y,
fill = '#006000', outline = '#000000')
You may noticed I have some custom coordinate conversion methods going on to compensate for the screen re sizing. However, my problem is under the function _place_empty_tiles(self) in the for loop. Let's say the amount of columns is 4, it will currently print out on the canvas the following:
However,
it is actually making a row, and
that is only one row.
How can I make it so I can create a grid with a similar style given the amount of rows and columns are stored in self._game.columns and self._game.rows?
UPDATE:
Tried basing it off The tkinter Knight's Tour Demo I rewrote the function like so:
def _place_empty_tiles(self):
self._canvas.update()
width = self._canvas.winfo_width()
height = self._canvas.winfo_height()
for r in range(self._game.rows -1,-1,-1):
for c in range(self._game.columns):
click_point = point.from_pixel(c*30+4, r*30+4, c*30+30, r*30+30)
self._state.handle_click(click_point)
self._redraw_game_pieces()
which is close, but still getting some unexpected results:
The Tk demos include a Knight's Tour demo that draws a chessboard. Someone converted it for tkinter and you can examine the _draw_board function to see an example of how to create such a layout using canvas rectangles:
def draw_board(canvas):
# draw checkerboard
for r in range(7, -1, -1):
for c in range(8):
if c&1 ^ r&1:
fill = 'tan3'
dfill = 'tan4'
else:
fill = 'bisque'
dfill= 'bisque3'
coords = (c*30+4, r*30+4, c*30+30, r*30+30)
canvas.create_rectangle(coords, fill=fill, disabledfill=dfill,
width=2, state='disabled')
Here's an example that draws a grid that fits the window as closely as possible. It is designed to redraw itself whenever the window resizes. If you don't want that behavior, you can hard-code the cell width and height.
The cells are stored in a dictionary indexed by row and column, to make it easy to reference each tile. Clicking on a tile will toggle it between blue and red. Note that when the window resizes it won't remember which cells were previously clicked on. That's easy to fix if you want.
import Tkinter as tk
class App(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.canvas = tk.Canvas(self, width=500, height=500, borderwidth=0, highlightthickness=0)
self.canvas.pack(side="top", fill="both", expand="true")
self.rows = 20
self.columns = 20
self.tiles = {}
self.canvas.bind("<Configure>", self.redraw)
self.status = tk.Label(self, anchor="w")
self.status.pack(side="bottom", fill="x")
def redraw(self, event=None):
self.canvas.delete("rect")
cellwidth = int(self.canvas.winfo_width()/self.columns)
cellheight = int(self.canvas.winfo_height()/self.columns)
for column in range(self.columns):
for row in range(self.rows):
x1 = column*cellwidth
y1 = row * cellheight
x2 = x1 + cellwidth
y2 = y1 + cellheight
tile = self.canvas.create_rectangle(x1,y1,x2,y2, fill="blue", tags="rect")
self.tiles[row,column] = tile
self.canvas.tag_bind(tile, "<1>", lambda event, row=row, column=column: self.clicked(row, column))
def clicked(self, row, column):
tile = self.tiles[row,column]
tile_color = self.canvas.itemcget(tile, "fill")
new_color = "blue" if tile_color == "red" else "red"
self.canvas.itemconfigure(tile, fill=new_color)
self.status.configure(text="you clicked on %s/%s" % (row, column))
if __name__ == "__main__":
app = App()
app.mainloop()