Tkinter Canvas : Scale on moving object - python
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()
Related
Manual Annotation with Tkinter
I am using Tkinter to import images with Openslide. I would like to integrate a manual annotation module into my program like this: class ResizableCanvas(Canvas): def __init__(self, parent, **kwargs): Canvas.__init__(self, parent, **kwargs) self.bind("<Configure>", self.on_resize) self.height = self.winfo_reqheight() self.width = self.winfo_reqwidth() def on_resize(self, event): wscale = float(event.width) / self.width hscale = float(event.height) / self.height self.width = event.width self.height = event.height self.config(width=self.width, height=self.height) class ViewerTab: def __init__(self, master, model, dim=800): self.sideFrame = ttk.Frame(self.master, width=100) self.coords = {"x":0,"y":0,"x2":0,"y2":0} self.lines = [] def click(self): self.coords["x"] = self.x self.coords["y"] = self.y self.lines.append(self.canvas.create_line(self.coords["x"],self.coords["y"],self.coords["x"],self.coords["y"])) def drag(self): # update the coordinates from the event self.coords["x2"] = self.x self.coords["y2"] = self.y self.canvas.coords(self.lines[-1], self.coords["x"],self.coords["y"],self.coords["x2"],self.coords["y2"]) #self.canvas.bind("<Button-1>", self.dirbutton) #self.canvas.bind("<B1-Motion>", self.move) self.canvas.bind("<ButtonRelease-1>", self.nomove) self.canvas.bind("<Button-2>", self.get_position) self.canvas.bind("<ButtonPress-1>", self.click) self.canvas.bind("<B1-Motion>", self.drag)
So if I got it right from the comments, the issue is to be able to both pan the slide and draw on it using binding to mouse clicks and motion. There are several way to do that, for instance: Use radiobuttons so that the user selects the "mode": either pan or annotate. Here is a small example based on https://stackoverflow.com/a/50129744/6415268 for the drawing part. The click() and drag() functions do different actions depending on the selected mode (stored a the StringVar). import tkinter as tk coords = {"x": 0, "y": 0, "x2": 0, "y2": 0} # keep a reference to all lines by keeping them in a list lines = [] def click(event): if mode.get() == "pan": canvas.scan_mark(event.x, event.y) else: # define start point for line coords["x"] = canvas.canvasx(event.x) coords["y"] = canvas.canvasy(event.y) # create a line on this point and store it in the list lines.append(canvas.create_line(coords["x"], coords["y"], coords["x"], coords["y"])) def drag(event): if mode.get() == "pan": canvas.scan_dragto(event.x, event.y, gain=1) else: # update the coordinates from the event coords["x2"] = canvas.canvasx(event.x) coords["y2"] = canvas.canvasy(event.y) # Change the coordinates of the last created line to the new coordinates canvas.coords(lines[-1], coords["x"], coords["y"], coords["x2"], coords["y2"]) root = tk.Tk() mode = tk.StringVar(root, "pan") toolbar = tk.Frame(root) toolbar.pack(fill='x') tk.Radiobutton(toolbar, text="Pan", variable=mode, value="pan").pack(side='left') tk.Radiobutton(toolbar, text="Annotate", variable=mode, value="annotate").pack(side='left') canvas = tk.Canvas(root, bg="white") canvas.create_rectangle(0, 0, 50, 50, fill='red') canvas.create_rectangle(400, 400, 450, 450, fill='blue') canvas.pack(fill='both') canvas.bind("<ButtonPress-1>", click) canvas.bind("<B1-Motion>", drag) root.mainloop() Another possibility is to use different bindings for the two kinds of actions using event modifiers (e.g. pressing the Ctrl, Shift or Alt key). For instance, the panning can be bound to Ctrl + mouse events while the drawing happens on simple mouse clicks and motion. import tkinter as tk coords = {"x": 0, "y": 0, "x2": 0, "y2": 0} # keep a reference to all lines by keeping them in a list lines = [] def draw_click(event): # define start point for line coords["x"] = canvas.canvasx(event.x) coords["y"] = canvas.canvasy(event.y) # create a line on this point and store it in the list lines.append(canvas.create_line(coords["x"], coords["y"], coords["x"], coords["y"])) def draw_drag(event): # update the coordinates from the event coords["x2"] = canvas.canvasx(event.x) coords["y2"] = canvas.canvasy(event.y) # Change the coordinates of the last created line to the new coordinates canvas.coords(lines[-1], coords["x"], coords["y"], coords["x2"], coords["y2"]) root = tk.Tk() toolbar = tk.Frame(root) toolbar.pack(fill='x') canvas = tk.Canvas(root, bg="white") canvas.create_rectangle(0, 0, 50, 50, fill='red') canvas.create_rectangle(400, 400, 450, 450, fill='blue') canvas.pack(fill='both') canvas.bind("<ButtonPress-1>", draw_click) canvas.bind("<B1-Motion>", draw_drag) canvas.bind('<Control-ButtonPress-1>', lambda event: canvas.scan_mark(event.x, event.y)) canvas.bind("<Control-B1-Motion>", lambda event: canvas.scan_dragto(event.x, event.y, gain=1)) root.mainloop()
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
drag an arc after creating a line tkinter
Here was the code to draw a line. Is it possible to let the user drag a line so that it forms a curve? from tkinter import Canvas, Tk # Image dimensions w,h = 640,480 # Create canvas root = Tk() canvas = Canvas(root, width = w, height = h, bg = 'white') canvas.pack() def on_click(event): """ set starting point of the line """ global x1, y1 x1 = event.x y1 = event.y def on_click_release(event): """ draw the line """ canvas.create_line(x1, y1, event.x, event.y) def clear_canvas(event): canvas.delete('all') canvas.bind("<Button-1>", on_click) canvas.bind("<ButtonRelease-1>", on_click_release) root.bind("<Key-c>", clear_canvas) root.mainloop() Once again thank youuuuu!!! :)))))
For an arc you need to track the mouse as it moves across the screen, rather than just the start and end points. The code below will only create arcs from the bottom left, to the bottom right of a rectangle, but you can add any other arcs you want by changing the start angle and the extent angle of the arc. from tkinter import Canvas, Tk, ARC # Image dimensions w,h = 640,480 # Create canvas root = Tk() canvas = Canvas(root, width = w, height = h, bg = 'white') canvas.pack() # curve points global points global temp_arc points = [] temp_arc = None def arc(): x = [point[0] for point in points] y = [point[1] for point in points] return canvas.create_arc(x[0], y[0], x[-1], y[-1], start = 0, style = ARC, width = 2, extent = 180) def motion(event): global temp_arc points.append([event.x, event.y]) if temp_arc != None: canvas.delete(temp_arc) temp_arc = arc() def on_click_release(event): arc() global points points = [] def clear_canvas(event): canvas.delete('all') canvas.bind("<B1-Motion>", motion) canvas.bind("<ButtonRelease-1>", on_click_release) root.bind("<Key-c>", clear_canvas) root.mainloop()
How to make a rounded button tkinter?
I am trying to get rounded buttons for my script using tkinter. I found the following code in an answer to How to make a Button using the tkinter Canvas widget?: from tkinter import * import tkinter as tk class CustomButton(tk.Canvas): def __init__(self, parent, width, height, color, command=None): tk.Canvas.__init__(self, parent, borderwidth=1, relief="raised", highlightthickness=0) self.command = command padding = 4 id = self.create_oval((padding,padding, width+padding, height+padding), outline=color, fill=color) (x0,y0,x1,y1) = self.bbox("all") width = (x1-x0) + padding height = (y1-y0) + padding self.configure(width=width, height=height) self.bind("<ButtonPress-1>", self._on_press) self.bind("<ButtonRelease-1>", self._on_release) def _on_press(self, event): self.configure(relief="sunken") def _on_release(self, event): self.configure(relief="raised") if self.command is not None: self.command() app = CustomButton() app.mainloop() but I get the following error: TypeError: __init__() missing 4 required positional arguments: 'parent', 'width', 'height', and 'color'
A very easy way to make a rounded button in tkinter is to use an image. First create an image of what you want you button to look like save it as a .png and remove the outside background so it is rounded like the one below: Next insert the image in a button with PhotoImage like this: self.loadimage = tk.PhotoImage(file="rounded_button.png") self.roundedbutton = tk.Button(self, image=self.loadimage) self.roundedbutton["bg"] = "white" self.roundedbutton["border"] = "0" self.roundedbutton.pack(side="top") Ensure to use border="0" and the button border will be removed. I added the self.roundedborder["bg"] = "white" so that the the background the background of the button is the same as the Tkinter window. The great part is that you can use any shape you like not just the normal button shapes.
I made this rounded rectangle button if anyone was looking for more of an apple look or something. For convenience here are the arguments: RoundedButton(parent, width, height, cornerradius, padding, fillcolor, background, command) Note: If the corner radius is greater than half of the width or height an error message will be sent in the terminal. Pill shapes can still be made through if you set the corner radius to exactly half of the height or width. Finally the code: from tkinter import * import tkinter as tk root = Tk() class RoundedButton(tk.Canvas): def __init__(self, parent, width, height, cornerradius, padding, color, bg, command=None): tk.Canvas.__init__(self, parent, borderwidth=0, relief="flat", highlightthickness=0, bg=bg) self.command = command if cornerradius > 0.5*width: print("Error: cornerradius is greater than width.") return None if cornerradius > 0.5*height: print("Error: cornerradius is greater than height.") return None rad = 2*cornerradius def shape(): self.create_polygon((padding,height-cornerradius-padding,padding,cornerradius+padding,padding+cornerradius,padding,width-padding-cornerradius,padding,width-padding,cornerradius+padding,width-padding,height-cornerradius-padding,width-padding-cornerradius,height-padding,padding+cornerradius,height-padding), fill=color, outline=color) self.create_arc((padding,padding+rad,padding+rad,padding), start=90, extent=90, fill=color, outline=color) self.create_arc((width-padding-rad,padding,width-padding,padding+rad), start=0, extent=90, fill=color, outline=color) self.create_arc((width-padding,height-rad-padding,width-padding-rad,height-padding), start=270, extent=90, fill=color, outline=color) self.create_arc((padding,height-padding-rad,padding+rad,height-padding), start=180, extent=90, fill=color, outline=color) id = shape() (x0,y0,x1,y1) = self.bbox("all") width = (x1-x0) height = (y1-y0) self.configure(width=width, height=height) self.bind("<ButtonPress-1>", self._on_press) self.bind("<ButtonRelease-1>", self._on_release) def _on_press(self, event): self.configure(relief="sunken") def _on_release(self, event): self.configure(relief="raised") if self.command is not None: self.command() def test(): print("Hello") canvas = Canvas(root, height=300, width=500) canvas.pack() button = RoundedButton(root, 200, 100, 50, 2, 'red', 'white', command=test) button.place(relx=.1, rely=.1) root.mainloop()
Unfortunately, images don't work well when resized. Below is an example of a rounded button using canvas which works well even if resized. import tkinter as tk class RoundedButton(tk.Canvas): def __init__(self, master=None, text:str="", radius=25, btnforeground="#000000", btnbackground="#ffffff", clicked=None, *args, **kwargs): super(RoundedButton, self).__init__(master, *args, **kwargs) self.config(bg=self.master["bg"]) self.btnbackground = btnbackground self.clicked = clicked self.radius = radius self.rect = self.round_rectangle(0, 0, 0, 0, tags="button", radius=radius, fill=btnbackground) self.text = self.create_text(0, 0, text=text, tags="button", fill=btnforeground, font=("Times", 30), justify="center") self.tag_bind("button", "<ButtonPress>", self.border) self.tag_bind("button", "<ButtonRelease>", self.border) self.bind("<Configure>", self.resize) text_rect = self.bbox(self.text) if int(self["width"]) < text_rect[2]-text_rect[0]: self["width"] = (text_rect[2]-text_rect[0]) + 10 if int(self["height"]) < text_rect[3]-text_rect[1]: self["height"] = (text_rect[3]-text_rect[1]) + 10 def round_rectangle(self, x1, y1, x2, y2, radius=25, update=False, **kwargs): # if update is False a new rounded rectangle's id will be returned else updates existing rounded rect. # source: https://stackoverflow.com/a/44100075/15993687 points = [x1+radius, y1, x1+radius, y1, x2-radius, y1, x2-radius, y1, x2, y1, x2, y1+radius, x2, y1+radius, x2, y2-radius, x2, y2-radius, x2, y2, x2-radius, y2, x2-radius, y2, x1+radius, y2, x1+radius, y2, x1, y2, x1, y2-radius, x1, y2-radius, x1, y1+radius, x1, y1+radius, x1, y1] if not update: return self.create_polygon(points, **kwargs, smooth=True) else: self.coords(self.rect, points) def resize(self, event): text_bbox = self.bbox(self.text) if self.radius > event.width or self.radius > event.height: radius = min((event.width, event.height)) else: radius = self.radius width, height = event.width, event.height if event.width < text_bbox[2]-text_bbox[0]: width = text_bbox[2]-text_bbox[0] + 30 if event.height < text_bbox[3]-text_bbox[1]: height = text_bbox[3]-text_bbox[1] + 30 self.round_rectangle(5, 5, width-5, height-5, radius, update=True) bbox = self.bbox(self.rect) x = ((bbox[2]-bbox[0])/2) - ((text_bbox[2]-text_bbox[0])/2) y = ((bbox[3]-bbox[1])/2) - ((text_bbox[3]-text_bbox[1])/2) self.moveto(self.text, x, y) def border(self, event): if event.type == "4": self.itemconfig(self.rect, fill="#d2d6d3") if self.clicked is not None: self.clicked() else: self.itemconfig(self.rect, fill=self.btnbackground) def func(): print("Button pressed") root = tk.Tk() btn = RoundedButton(text="This is a \n rounded button", radius=100, btnbackground="#0078ff", btnforeground="#ffffff", clicked=func) btn.pack(expand=True, fill="both") root.mainloop() To create this use canvas.create_rectangle() and canvas.create_text() methods and give them both of them same tag, say "button". The tag will be used when using canvas.tag_bind("tag", "<ButtonPress>")(you can also simply pass "current" as tag, which is assigned to the currently selected item by tkinter, in which case you can remove button tag). Use canvas.tag_bind on canvas item instead of bind on canvas, this way the button color will change only if the mouse press happens inside the rounded button and not at edges. You can scale and improve this to generate custom events when clicked inside the button, add configure method to configure button text and background, etc. output:
You need to create root window first (or some other widget) and give it to your CustomButton together with different parameters (see definition of __init__ method). Try instead of app = CustomButton() the following: app = tk.Tk() button = CustomButton(app, 100, 25, 'red') button.pack() app.mainloop()
You are not passing any arguments to the constructor. Specifically, on this line app = CustomButton() you need to pass the arguments that were defined in the constructor definition, namely parent, width, height and color.
I have had a lot of trouble finding the code that works for me. I have tried applying images to buttons and also tried the custom button styles from above. This is the custom button code that worked for me and I am thankful for this issue on Github Here is the code just in case : from tkinter import * import tkinter as tk import tkinter.font as font class RoundedButton(tk.Canvas): def __init__(self, parent, border_radius, padding, color, text='', command=None): tk.Canvas.__init__(self, parent, borderwidth=0, relief="raised", highlightthickness=0, bg=parent["bg"]) self.command = command font_size = 10 self.font = font.Font(size=font_size, family='Helvetica') self.id = None height = font_size + (1 * padding) width = self.font.measure(text)+(1*padding) width = width if width >= 80 else 80 if border_radius > 0.5*width: print("Error: border_radius is greater than width.") return None if border_radius > 0.5*height: print("Error: border_radius is greater than height.") return None rad = 2*border_radius def shape(): self.create_arc((0, rad, rad, 0), start=90, extent=90, fill=color, outline=color) self.create_arc((width-rad, 0, width, rad), start=0, extent=90, fill=color, outline=color) self.create_arc((width, height-rad, width-rad, height), start=270, extent=90, fill=color, outline=color) self.create_arc((0, height-rad, rad, height), start=180, extent=90, fill=color, outline=color) return self.create_polygon((0, height-border_radius, 0, border_radius, border_radius, 0, width-border_radius, 0, width, border_radius, width, height-border_radius, width-border_radius, height, border_radius, height), fill=color, outline=color) id = shape() (x0, y0, x1, y1) = self.bbox("all") width = (x1-x0) height = (y1-y0) self.configure(width=width, height=height) self.create_text(width/2, height/2,text=text, fill='black', font= self.font) self.bind("<ButtonPress-1>", self._on_press) self.bind("<ButtonRelease-1>", self._on_release) def _on_press(self, event): self.configure(relief="sunken") def _on_release(self, event): self.configure(relief="raised") if self.command is not None: self.command() Now save this code in a file, for example, name it custombutton.py. Next import this file in your current python file (like so: from custombutton import RoundedButton) and use it like so: RoundedButton(root, text="Some Text", border_radius=2, padding=4, command=some_function, color="#cda989")
If you use an image such as in #Xantium 's method, you could set the button parameter borderwidth to 0. As in: homebtn = tk.Button(root, image=img, borderwidth=0)
How to find distance traveled Tkinter ButtonPress
I can track where the user clicks and where they release but I want to track distance traveled. from Tkinter import * root = Tk() class DragCursor(): def __init__(self, location): self.label = location location.bind('<ButtonPress-1>', self.StartMove) location.bind('<ButtonRelease-1>', self.StopMove) def StartMove(self, event): startx = event.x starty = event.y print [startx, starty] def StopMove(self, event): self.StartMove stopx = event.x stopy = event.y print [stopx, stopy] location = Canvas(root, width = 300, height = 300) DragCursor(location) location.pack() root.mainloop()
You just need to use the distance formula for determining the distance between two points in an xy-plane, Also, you need to include some kind of instance variable that will save the coordinates for the start and end points so that you can compute it after the mouse release. This is pretty much your code just with a new distancetraveled function that is printed at the end of StopMove using self.positions. from Tkinter import * root = Tk() class DragCursor(): def __init__(self, location): self.label = location location.bind('<ButtonPress-1>', self.StartMove) location.bind('<ButtonRelease-1>', self.StopMove) self.positions = {} def StartMove(self, event): startx = event.x starty = event.y self.positions['start'] = (startx, starty) def StopMove(self, event): stopx = event.x stopy = event.y self.positions['stop'] = (stopx, stopy) print self.distancetraveled() def distancetraveled(self): x1 = self.positions['start'][0] x2 = self.positions['stop'][0] y1 = self.positions['start'][1] y2 = self.positions['stop'][1] return ((x2-x1)**2 + (y2-y1)**2)**0.5 location = Canvas(root, width = 300, height = 300) DragCursor(location) location.pack() root.mainloop()