How to make a rounded button tkinter? - python

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)

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

Get area bounded by Tkinter canvas lines

I have a short code that allows people to freely draw using tkinter, and when they release the mouse button a line is automatically created between the two endpoints of their free draw, thus forming a close loop.
Here is my code:
from tkinter import *
from PIL import Image, ImageTk
class App(Frame):
def __init__(self, master):
Frame.__init__(self, master)
self.columnconfigure(0,weight=1)
self.rowconfigure(0,weight=1)
self.original = Image.open("C:/Users/elver/Pictures/living.jpg")
self.image = ImageTk.PhotoImage(self.original)
self.display = Canvas(self, bd=0, highlightthickness=0)
self.display.create_image(0, 0, image=self.image, anchor=NW, tags="IMG")
self.display.grid(row=0, sticky=W+E+N+S)
self.pack(fill=BOTH, expand=1)
self.bind("<Configure>", self.resize)
self.display.bind('<Button-1>', self.click)
self.display.bind('<B1-Motion>', self.move)
self.display.bind('<ButtonRelease-1>', self.release)
self.linelist = []
def resize(self, event):
size = (event.width, event.height)
resized = self.original.resize(size,Image.ANTIALIAS)
self.image = ImageTk.PhotoImage(resized)
self.display.delete("IMG")
self.display.create_image(0, 0, image=self.image, anchor=NW, tags="IMG")
def click(self, click_event):
global prev
prev = click_event
for x in range(0, len(self.linelist)-1):
self.display.delete(self.linelist[x])
self.linelist.clear()
self.display.create_image(0, 0, image=self.image, anchor=NW, tags="IMG")
def move(self, move_event):
global Canline
global prev
Canline=self.display.create_line(prev.x, prev.y, move_event.x, move_event.y, width=2)
self.linelist.append(Canline)
prev = move_event
#print(len(self.linelist))
def release(self, release_event):
global Canline
Canline=self.display.create_line(self.display.coords(self.linelist[1])[0], self.display.coords(self.linelist[1])[1], \
self.display.coords(self.linelist[len(self.linelist)-1])[0], self.display.coords(self.linelist[len(self.linelist)-1])[1], width=2)
root =Tk()
app = App(root)
app.mainloop()
I am now trying to fill in the area bounded by the close loop, but I can't seem to find a way to do that.
I can't find a way to differentiate the area inside the closed loop and the area outside of it.
Is there an easy way to do so?
You could keep points (event.x, event.y) on list self.points and use this list in release to draw filled polygon:
create_polygon(self.points)
You can even use first and last element from this list to draw closing line - so you don't need to get coordinates from self.display.coords(self.lines[0]) and self.display.coords(self.lines[-1])
first = self.points[0]
last = self.points[-1]
line = self.display.create_line(*first, *last, width=2)
Working code with many other changes
import tkinter as tk
from PIL import Image, ImageTk
# --- classes ---
class App(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.pack(fill='both', expand=True)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.original = Image.open("Obrazy/images/image-800x600.jpg")
self.image = ImageTk.PhotoImage(self.original)
self.display = tk.Canvas(self, bd=0, highlightthickness=0)
self.display.create_image(0, 0, image=self.image, anchor='nw', tags="IMG")
self.display.grid(row=0, sticky='news')
self.display.bind('<Button-1>', self.click)
self.display.bind('<B1-Motion>', self.move)
self.display.bind('<ButtonRelease-1>', self.release)
self.bind("<Configure>", self.resize)
self.lines = []
self.points = []
self.polygon = None
def resize(self, event):
size = (event.width, event.height)
resized = self.original.resize(size, Image.ANTIALIAS)
self.image = ImageTk.PhotoImage(resized)
# replace image in object instead of deleting and creating object again
self.display.itemconfig("IMG", image=self.image)
#self.display.delete("IMG")
#self.display.create_image(0, 0, image=self.image, anchor='nw', tags="IMG")
# TODO: recalculate self.points to resize polygon ???
def click(self, event):
for item in self.lines:
self.display.delete(item)
if self.polygon:
self.display.delete(self.polygon)
self.polygon = None
self.lines.clear()
self.points.clear()
#self.lines = []
#self.points = []
self.points.append((event.x, event.y))
self.prev = event
# ??? I don't know what is this ????
#self.display.create_image(0, 0, image=self.image, anchor='nw', tags="IMG")
def move(self, event):
line = self.display.create_line(self.prev.x, self.prev.y, event.x, event.y, width=2)
self.lines.append(line)
self.points.append((event.x, event.y))
self.prev = event
def release(self, event):
#first = self.display.coords(self.lines[0])
#last = self.display.coords(self.lines[-1])
first = self.points[0]
last = self.points[-1]
line = self.display.create_line(*first, *last, width=2)
self.lines.append(line)
self.polygon = self.display.create_polygon(self.points, fill='red', outline='black', width=2)
# you could delet lines here if you don't need them
# --- main ---
root = tk.Tk()
app = App(root)
app.mainloop()
EDIT 1:
In this version I use create_polygon at start and later I use coords() to update points in this polygon. This way I don't need list with lines and don't use create_line().
It displays closing line all time during drawing.
coords() needs flat list [x1, y1, x2, y1, ...] instead of [(x1, y1), (x2, y2), ...] so I use list.extend([x, y]) (list += [x, y]) instead of list.append([x, y])
I use fill="" to have transparent polygon during drawing and in release() I use configitem() to change it to fill="red"
def click(self, event):
# `coords()` needs flat list [x1, y1, x2, y2, ...] instead of [(x1, y1), (x2, y2), ...]
# so I use `list.extend(other_list)` (`list += other_list`) instead of `list.append(other_list)
self.points = [event.x, event.y]
# at start there is no polygon on screen so there is nothing to delete
if self.polygon:
self.display.delete(self.polygon)
# polygon can be created with one point so I can do it here - I don't have to do it in `move` like with `create_line`
# (BTW: `fill=""` creates transparent polygon)
self.polygon = self.display.create_polygon(self.points, fill='', outline='black', width=2)
def move(self, event):
# `coords()` needs flat list [x1, y1, x2, y2, ...] instead of [(x1, y1), (x2, y2), ...]
# so I use `list.extend(other_list)` (`list += other_list`) instead of `list.append(other_list)
self.points += [event.x, event.y]
# update existing polygon
self.display.coords(self.polygon, self.points)
def release(self, event):
# change fill color at the end
self.display.itemconfig(self.polygon, fill='red')
EDIT 2:
I realized that create_line can get more then two points and create many lines at once so I use it in this version.
I add points to list and use coords() to update points in existing line. This way I have only one line (with many points - polygon without closing line) and I don't need list with lines.
def click(self, event):
# `coords()` needs flat list [x1, y1, x2, y2, ...] instead of [(x1, y1), (x2, y2), ...]
# so I use `list.extend(other_list)` (`list += other_list`) instead of `list.append(other_list)
self.points = [event.x, event.y]
# at start there is no polygon on screen so there is nothing to delete
if self.polygon:
self.display.delete(self.polygon)
self.polygon = None # I need it in `move()`
# `create_line()` needs at least two points so I cann't create it here.
# I have to create it in `move()` when I will have two points
def move(self, event):
# `coords()` needs flat list [x1, y1, x2, y2, ...] instead of [(x1, y1), (x2, y2), ...]
# so I use `list.extend(other_list)` (`list += other_list`) instead of `list.append(other_list)
self.points += [event.x, event.y]
if not self.polygon:
# create line if not exists - now `self.points` have two points
self.polygon = self.display.create_line(self.points, width=2)
else:
# update existing line
self.display.coords(self.polygon, self.points)
def release(self, event):
# replace line with polygon to close it and fill it (BTW: `fill=""`if you want transparent polygon)
self.display.delete(self.polygon)
self.polygon = self.display.create_polygon(self.points, width=2, fill='red', outline='black')
Full code for EDIT 1
import tkinter as tk
from PIL import Image, ImageTk
# --- classes ---
class App(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.pack(fill='both', expand=True)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.original = Image.open("image.jpg")
self.image = ImageTk.PhotoImage(self.original)
self.display = tk.Canvas(self, bd=0, highlightthickness=0)
self.display.create_image(0, 0, image=self.image, anchor='nw', tags="IMG")
self.display.grid(row=0, sticky='news')
self.display.bind('<Button-1>', self.click)
self.display.bind('<B1-Motion>', self.move)
self.display.bind('<ButtonRelease-1>', self.release)
self.bind("<Configure>", self.resize)
self.points = []
self.polygon = None
def resize(self, event):
size = (event.width, event.height)
resized = self.original.resize(size, Image.ANTIALIAS)
self.image = ImageTk.PhotoImage(resized)
# replace image in object instead of deleting and creating object again
self.display.itemconfig("IMG", image=self.image)
# TODO: recalculate self.points to resize polygon ???
def click(self, event):
# `coords()` needs flat list [x1, y1, x2, y2, ...] instead of [(x1, y1), (x2, y2), ...]
# so I use `list.extend(other_list)` (`list += other_list`) instead of `list.append(other_list)
self.points = [event.x, event.y]
# at start there is no polygon on screen so there is nothing to delete
if self.polygon:
self.display.delete(self.polygon)
# polygon can be created with one point so I can do it here - I don't have to do it in `move` like with `create_line`
# (BTW: `fill=""` creates transparent polygon)
self.polygon = self.display.create_polygon(self.points, fill='', outline='black', width=2)
def move(self, event):
# `coords()` needs flat list [x1, y1, x2, y2, ...] instead of [(x1, y1), (x2, y2), ...]
# so I use `list.extend(other_list)` (`list += other_list`) instead of `list.append(other_list)
self.points += [event.x, event.y]
# update existing polygon
self.display.coords(self.polygon, self.points)
def release(self, event):
# change fill color at the end
self.display.itemconfig(self.polygon, fill='red')
# --- main ---
root = tk.Tk()
app = App(root)
app.mainloop()
Full code for EDIT 2
import tkinter as tk
from PIL import Image, ImageTk
# --- classes ---
class App(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.pack(fill='both', expand=True)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.original = Image.open("image.jpg")
self.image = ImageTk.PhotoImage(self.original)
self.display = tk.Canvas(self, bd=0, highlightthickness=0)
self.display.create_image(0, 0, image=self.image, anchor='nw', tags="IMG")
self.display.grid(row=0, sticky='news')
self.display.bind('<Button-1>', self.click)
self.display.bind('<B1-Motion>', self.move)
self.display.bind('<ButtonRelease-1>', self.release)
self.bind("<Configure>", self.resize)
self.points = []
self.polygon = None
def resize(self, event):
size = (event.width, event.height)
resized = self.original.resize(size, Image.ANTIALIAS)
self.image = ImageTk.PhotoImage(resized)
# replace image in object instead of deleting and creating object again
self.display.itemconfig("IMG", image=self.image)
# TODO: recalculate self.points to resize polygon ???
def click(self, event):
# `coords()` needs flat list [x1, y1, x2, y2, ...] instead of [(x1, y1), (x2, y2), ...]
# so I use `list.extend(other_list)` (`list += other_list`) instead of `list.append(other_list)
self.points = [event.x, event.y]
# at start there is no polygon on screen so there is nothing to delete
if self.polygon:
self.display.delete(self.polygon)
self.polygon = None # I need it in `move()`
# `create_line()` needs at least two points so I cann't create it here.
# I have to create it in `move()` when I will have two points
def move(self, event):
# `coords()` needs flat list [x1, y1, x2, y2, ...] instead of [(x1, y1), (x2, y2), ...]
# so I use `list.extend(other_list)` (`list += other_list`) instead of `list.append(other_list)
self.points += [event.x, event.y]
if not self.polygon:
# create line if not exists - now `self.points` have two points
self.polygon = self.display.create_line(self.points, width=2)
else:
# update existing line
self.display.coords(self.polygon, self.points)
def release(self, event):
# replace line with polygon to close it and fill it (BTW: `fill=""`if you want transparent polygon)
self.display.delete(self.polygon)
self.polygon = self.display.create_polygon(self.points, width=2, fill='red', outline='black')
# --- main ---
root = tk.Tk()
app = App(root)
app.mainloop()

Is there a way to have a draggable ruler in tkinter?

Is it possible?
Just to place it anywhere onto the window and drag it anywhere I want it.
Here is an example of draggable item I am looking to achieve like from HTML (I know, it's got nothing to do with html): How TO - Create a Draggable HTML Element.
Here is an example of what I mean by a ruler. A ruler like this:
It's only for display purposes and not calculating anything..
I'll be using Grid manager in this case.
I'll be happy to see any examples!
Standard module tkinter and ttk doesn't have rulers and I don't know any external module for tkinter which has rulers.
Using Canvas I can create widget which draws lines with numbers.
But it is still primitive widget which doesn't resize, doesn't scroll lines and numbers, doesn't rescale, and doesn't show mouse position.
EDIT: Now rules show mouse position using red lines. But if there is no Canvas then they have to know offset - how far they are from left top corner of window.
import tkinter as tk
class VRuler(tk.Canvas):
'''Vertical Ruler'''
def __init__(self, master, width, height, offset=0):
super().__init__(master, width=width, height=height)
self.offset = offset
step = 10
# start at `step` to skip line for `0`
for y in range(step, height, step):
if y % 50 == 0:
# draw longer line with text
self.create_line(0, y, 13, y, width=2)
self.create_text(20, y, text=str(y), angle=90)
else:
self.create_line(2, y, 7, y)
self.position = self.create_line(0, 0, 50, 0, fill='red', width=2)
def set_mouse_position(self, y):
y -= self.offset
self.coords(self.position, 0, y, 50, y)
class HRuler(tk.Canvas):
'''Horizontal Ruler'''
def __init__(self, master, width, height, offset=0):
super().__init__(master, width=width, height=height)
self.offset = offset
step = 10
# start at `step` to skip line for `0`
for x in range(step, width, step):
if x % 50 == 0:
# draw longer line with text
self.create_line(x, 0, x, 13, width=2)
self.create_text(x, 20, text=str(x))
else:
self.create_line((x, 2), (x, 7))
self.position = self.create_line(0, 0, 0, 50, fill='red', width=2)
def set_mouse_position(self, x):
x -= self.offset
self.coords(self.position, x, 0, x, 50)
def motion(event):
x, y = event.x, event.y
hr.set_mouse_position(x)
vr.set_mouse_position(y)
def click(event):
print(event.x, event.y)
root = tk.Tk()
root['bg'] = 'black'
vr = VRuler(root, 25, 250)#, offset=25)
vr.place(x=0, y=28)
hr = HRuler(root, 250, 25)#, offset=25)
hr.place(x=28, y=0)
c = tk.Canvas(root, width=250, height=250)
c.place(x=28, y=28)
#root.bind('<Motion>', motion) # it needs offset=28 if there is no Canvas
#root.bind('<Button-1>', click)
c.bind('<Motion>', motion)
c.bind('<Button-1>', click)
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

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

Categories

Resources