How to start an animation loop in Tkinter for an event? - python

I wrote some code using Tkinter in Python 3 that plots a graph in a canvas. I also made it such that when I move the mouse over the canvas the graph scrolls to the left.
The problem is that I want the graph to scroll when I press the space bar for example. But I don't want it to scroll 1 step each time I press the space bar but I want it to start scrolling indefinitely when I press it once and stop the scroll when I press it again. I want the space bar to be a play/pause key.
How can I accomplish this? I don't want to use matplotlib anywhere.
MY CODE AS IT IS NOW:
from tkinter import *
import numpy as np
# The function of the graph
def f(x):
return np.sin(x)+np.sin(3*x-1)+np.sin(0.5*(x+np.pi))+0.3*np.sin(10*x)
class GraphPlot():
def __init__(self, master):
self.master = master
# Data for the graph and steps to move to the right
self.data_x = np.linspace(0, 4*np.pi, 1000)
self.data_y = f(self.data_x)
self.step = 0.1
# A switch to delete to clear the canvas each iteration before plotting the next frame
self.gate = False
# Setting the Tkinter window and the canvas in place
self.ws = master.winfo_screenwidth()
self.hs = master.winfo_screenheight()
ww = self.ws*0.75
hw = self.hs*0.50
self.canvas = Canvas(self.master, width = ww, height = hw, bg = 'black')
self.canvas.grid()
self.master.update()
self.w = self.canvas.winfo_width()
self.h = self.canvas.winfo_height()
self.canvas.focus_set()
# Plot first frame
self.drawData(self.data_x, self.data_y)
# Plot next frames each time I press the space bar
self.canvas.bind('<KeyPress-space>', self.updateData)
def drawData(self, data_x, data_y):
'''This is my function to plot a grpah in a canvas
canvas without embedding any matplotlib figure'''
# Setting the axis limits
x_min, x_max = min(data_x), max(data_x)
y_min, y_max = min(data_y), max(data_y)
# Translating data to pixel positions inside the canvas
pixel_x = (data_x-x_min)*self.w/(x_max-x_min)
pixel_y = -(data_y-y_max)*self.h/(y_max-y_min)
points = []
for i in range(len(data_x)):
points.append(pixel_x[i])
points.append(pixel_y[i])
points = tuple(points)
# Deleting previous frame before plotting the next frame (except for the first frame)
if self.gate:
self.canvas.delete('curve')
else:
self.gate = True
# Plotting
self.canvas.create_line(points, fill = 'white', tag = 'curve')
def updateData(self, event):
# Changing data for the next frame
self.data_x += self.step
self.data_y = f(self.data_x)
# Plot new frame
self.drawData(self.data_x, self.data_y)
root = Tk()
GraphPlot(root)
root.mainloop()
I've tried some ideas. For example I used a new function, PlayPause(), with a while loop and a new switch, self.go, but this didn't work as expected.
CODE THAT I EXPECTED TO WORK BUT DIDN'T:
from tkinter import *
import numpy as np
def f(x):
return np.sin(x)+np.sin(3*x-1)+np.sin(0.5*(x+np.pi))+0.3*np.sin(10*x)
class GraphPlot():
def __init__(self, master):
self.master = master
self.data_x = np.linspace(0, 4*np.pi, 1000)
self.data_y = f(self.data_x)
self.step = 0.1
self.go = False # The new switch
self.gate = False
self.ws = master.winfo_screenwidth()
self.hs = master.winfo_screenheight()
ww = self.ws*0.75
hw = self.hs*0.50
self.canvas = Canvas(self.master, width = ww, height = hw, bg = 'black')
self.canvas.grid()
self.master.update()
self.w = self.canvas.winfo_width()
self.h = self.canvas.winfo_height()
self.canvas.focus_set()
self.drawData(self.data_x, self.data_y)
self.canvas.bind('<KeyPress-space>', self.PlayPause)
def drawData(self, data_x, data_y):
x_min, x_max = min(data_x), max(data_x)
y_min, y_max = min(data_y), max(data_y)
pixel_x = (data_x-x_min)*self.w/(x_max-x_min)
pixel_y = -(data_y-y_max)*self.h/(y_max-y_min)
points = []
for i in range(len(data_x)):
points.append(pixel_x[i])
points.append(pixel_y[i])
points = tuple(points)
if self.gate:
self.canvas.delete('curve')
else:
self.gate = True
self.canvas.create_line(points, fill = 'white', tag = 'curve')
def updateData(self):
self.data_x += self.step
self.data_y = f(self.data_x)
self.drawData(self.data_x, self.data_y)
def PlayPause(self, event):
if self.go:
self.go = False
else:
self.go = True
while self.go:
self.updateData()
root = Tk()
GraphPlot(root)
root.mainloop()

You could add a method to toggle_play_pause, and bind the space key to it. Upon space key press, this method toggles a boolean flag pause that when turned off allows the update to be called.
update will keep calling itself every 10/1000 of a second, until the space key is pressed again, and the pause flag set to True.
import tkinter as tk
import numpy as np
def f(x):
return np.sin(x)+np.sin(3*x-1)+np.sin(0.5*(x+np.pi))+0.3*np.sin(10*x)
class GraphPlot():
def __init__(self, master):
self.master = master
# Data for the graph and steps to move to the right
self.data_x = np.linspace(0, 4*np.pi, 1000)
self.data_y = f(self.data_x)
self.step = 0.1
# A switch to delete to clear the canvas each iteration before plotting the next frame
self.gate = False
# Setting the Tkinter window and the canvas in place
self.ws = master.winfo_screenwidth()
self.hs = master.winfo_screenheight()
ww = self.ws * 0.75
hw = self.hs * 0.50
self.canvas = tk.Canvas(self.master, width=ww, height=hw, bg='black')
self.canvas.grid()
self.master.update()
self.w = self.canvas.winfo_width()
self.h = self.canvas.winfo_height()
self.canvas.focus_set()
# Plot first frame
self.drawData(self.data_x, self.data_y)
# Plot next frames each time I press the space bar
self.canvas.bind('<KeyPress-space>', self.toggle_play_pause)
self.pause = True
self._update_call_handle = None
def drawData(self, data_x, data_y):
'''This is my function to plot a grpah in a canvas
canvas without embedding any matplotlib figure'''
# Setting the axis limits
x_min, x_max = min(data_x), max(data_x)
y_min, y_max = min(data_y), max(data_y)
# Translating data to pixel positions inside the canvas
pixel_x = (data_x-x_min)*self.w/(x_max-x_min)
pixel_y = -(data_y-y_max)*self.h/(y_max-y_min)
points = []
for i in range(len(data_x)):
points.append(pixel_x[i])
points.append(pixel_y[i])
points = tuple(points)
# Deleting previous frame before plotting the next frame (except for the first frame)
if self.gate:
self.canvas.delete('curve')
else:
self.gate = True
# Plotting
self.canvas.create_line(points, fill = 'white', tag = 'curve')
def toggle_play_pause(self, dummy_event):
self.pause = not self.pause
if not self.pause:
self.updateData()
def updateData(self):
# Changing data for the next frame
self.data_x += self.step
self.data_y = f(self.data_x)
# Plot new frame
self.drawData(self.data_x, self.data_y)
if not self.pause:
self._update_call_handle = root.after(10, self.updateData)
else:
root.after_cancel(self._update_call_handle)
self._update_call_handle = None
root = tk.Tk()
GraphPlot(root)
root.mainloop()

Related

How can an interactive subplot in Tkinter be improved in terms of speed?

I am currently working on a program that I am creating with Tkinter. Thereby large matrices (signals) are read in, which I represent as an image. In addition, I would like to display the signal at the point X (red vertical line) in the adjacent plot (interactive).
# Imports
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
import numpy as np
from tkinter import *
# Global: Selected points with cursor
points = []
# Cursor
class Cursor:
def __init__(self, ax):
self.ax = ax
self.background = None
self.horizontal_line = ax.axhline(color='r', lw=0.8, ls='--')
self.vertical_line = ax.axvline(color='r', lw=0.8, ls='--')
self._creating_background = False
ax.figure.canvas.mpl_connect('draw_event', self.on_draw)
def on_draw(self, event):
self.create_new_background()
def set_cross_hair_visible(self, visible):
need_redraw = self.horizontal_line.get_visible() != visible
self.horizontal_line.set_visible(visible)
self.vertical_line.set_visible(visible)
return need_redraw
def create_new_background(self):
if self._creating_background:
return
self._creating_background = True
self.set_cross_hair_visible(False)
self.ax.figure.canvas.draw_idle()
self.background = self.ax.figure.canvas.copy_from_bbox(self.ax.bbox)
self.set_cross_hair_visible(True)
self._creating_background = False
def on_mouse_move(self, event, mode: str, matrix=None):
if self.background is None:
self.create_new_background()
if not event.inaxes:
need_redraw = self.set_cross_hair_visible(False)
if need_redraw:
self.ax.figure.canvas.restore_region(self.background)
self.ax.figure.canvas.blit(self.ax.bbox)
else:
self.set_cross_hair_visible(True)
x, y = event.xdata, event.ydata
if mode == "both":
self.horizontal_line.set_ydata(y)
self.vertical_line.set_xdata(x)
self.ax.figure.canvas.restore_region(self.background)
self.ax.draw_artist(self.horizontal_line)
self.ax.draw_artist(self.vertical_line)
elif mode == "horizontal":
self.ax.cla()
self.ax.plot(matrix[:, int(x)], range(0, matrix.shape[0], 1))
self.ax.figure.canvas.draw_idle()
self.horizontal_line.set_ydata(y)
self.ax.figure.canvas.restore_region(self.background)
self.ax.draw_artist(self.horizontal_line)
self.ax.figure.canvas.blit(self.ax.bbox)
# Graphical User Interface
class ToolGUI:
def __init__(self, master):
self.master = master
# Matrix (Example)
self.matrix = np.random.rand(3000, 5000)
# Subplots
self.fig = plt.figure(constrained_layout=True)
self.spec = self.fig.add_gridspec(5, 6)
self.ax_main = self.fig.add_subplot(self.spec[:, :-1])
self.ax_main.imshow(self.matrix, cmap='gray', aspect='auto')
self.ax_right = self.fig.add_subplot(self.spec[:, -1:], sharey=self.ax_main)
self.ax_right.get_yaxis().set_visible(False)
# Canvas - Drawing Area
self.canvas = FigureCanvasTkAgg(self.fig, master=master)
self.canvas.get_tk_widget().grid(column=0, row=0, sticky=NSEW)
# Cursor with crosshair
self.cursor_main = Cursor(self.ax_main)
self.fig.canvas.mpl_connect('motion_notify_event', lambda event: self.cursor_main.on_mouse_move(event, mode="both"))
self.cursor_right = Cursor(self.ax_right)
self.fig.canvas.mpl_connect('motion_notify_event', lambda event: self.cursor_right.on_mouse_move(event, mode="horizontal", matrix=self.matrix))
# Update Canvas
self.canvas.draw() # Update canvas
# Create root window
root = Tk()
# Root window title
root.title("Tool")
# Create GUI
my_gui = ToolGUI(root)
# Execute Tkinter
root.mainloop()
This example is only a small part of my program. In my full program, for example, certain points are picked out manually. With the help of the interactive plot a more exact selection of such points is possible.
Unfortunately, the program runs very slowly due to the use of this interactive plot. Since I haven't been working with Python for too long, I would appreciate any suggestions for improvement!
Thanks in advance! - Stefan

How to use Line Collection with pyplot

When you run the code the error should be pretty obvious, just click your mouse in the black space and move it around. I'm not sure how the line segments are being created, I pulled it from the library and its kind of confusing. How can I get the line segments to plot along my scatter plot? When I looks at the segments array being generated I believe it’s creating pairs of x,x and y,y instead of x,y and x,y
import tkinter as tk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.collections import LineCollection
from matplotlib.colors import ListedColormap
import numpy as np
import random
class Mouse_Game:
'''Creates a matplotlib canvas object that plot mouse coordinates when animated.'''
def __init__(self, root, width=400, height=400):
self.frame = tk.Frame(master=root)
self.status = False
self.fig = plt.Figure(dpi=100, facecolor='black')
self.ax = self.fig.add_axes([0,0,1,1], fc='black')
self.width = width
self.height = height
self.ax.axis('off')
#set up the canvas object and bind commands
self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame, resize_callback=self.configure) # A tk.DrawingArea.
self.canvas.draw()
self.canvas.get_tk_widget().pack(side='top', fill='both', expand=True)
self.fig.canvas.mpl_connect('button_press_event', self.animate)
self.fig.canvas.mpl_connect('motion_notify_event', self.motion)
def animate(self, event):
#Check if currently running
if not self.status:
self.status = True
self.capture = np.array([(event.xdata, event.ydata)]*40)
else:
self.status = False
#plot a line that follows the mouse around
while self.status:
self.ax.clear()
self.ax.set_xlim(0, self.width)
self.ax.set_ylim(0, self.height)
x = self.capture[:,0]
y = self.capture[:,1]
############################################################
points = self.capture.T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
###########################################################
lc = LineCollection(segments, cmap='magma')
lc.set_array(x)
lc.set_linewidth(2)
self.ax.add_collection(lc)
self.ax.scatter(x, y, marker='o')
self.fig.canvas.draw()
self.fig.canvas.flush_events()
def motion(self, event):
if self.status:
#Append mouse coordinates to array when the mouse moves
self.capture = self.capture[1:40]
self.capture = np.append(self.capture, [(event.xdata, event.ydata)], axis=0)
def configure(self, event):
#Used to adjust coordinate setting when the screen size changes
self.width, self.height = self.canvas.get_width_height()
def _quit():
#destroy window
root.quit()
root.destroy()
root = tk.Tk()
root.wm_title("Mouse Plot")
MG = Mouse_Game(root=root)
MG.frame.pack(expand=True, fill='both')
button = tk.Button(root, text="Quit", command=_quit)
button.pack(side='bottom', pady=10)
tk.mainloop()
I'm not sure why you transpose the array. If you leave the transposition out, it'll work just fine
points = self.capture.reshape(-1, 1, 2)

Python Tkinter Canvas does not appear

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

Tkinter Canvas : Scale on moving object

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()

Make a Grid with Tkinter Rectangle

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()

Categories

Resources