Last active
August 26, 2021 07:16
-
-
Save PaulleDemon/0d3db6ac0bcb10be68c0ed5d6bc34317 to your computer and use it in GitHub Desktop.
This is an example on how to resize canvas items by dragging the corner in tkinter.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# MIT License | |
# | |
# Copyright (c) 2021 Paul | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in all | |
# copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
# SOFTWARE. | |
import math | |
import tkinter as tk | |
from tkinter import TclError | |
from tkinter.font import Font | |
class Canvas(tk.Canvas): | |
TOP_LEFT = 0 | |
TOP_RIGHT = 1 | |
BOTTOM_LEFT = 3 | |
BOTTOM_RIGHT = 4 | |
cursors = {TOP_LEFT: 'size_nw_se', TOP_RIGHT: 'size_ne_sw', | |
BOTTOM_LEFT: 'size_ne_sw', BOTTOM_RIGHT: 'size_nw_se'} # WINDOWS SPECIFIC CURSORS IN MAC IT MIGHT BE resizetopright, resizetopright etc | |
def __init__(self, *args, **kwargs): | |
super(Canvas, self).__init__(*args, **kwargs) | |
self.bind('<1>', self.checkCanvasItem) | |
self.bind('<ButtonRelease-1>', self.release) | |
self._current_item = None | |
self._previous_item = None | |
self._current_resize_rect = None | |
self._tag = 'resize' | |
self.resizePoints = {} # stores the resize points | |
self._current_point = None | |
self.previous = (0, 0) # previous mouse coordinates | |
def release(self, event): | |
self.unbind('<B1-Motion>') | |
self._current_point = None | |
def findInBBox(self, items, event): # when an items overlaps find_closest does't work well so check which item is best fit | |
for x in items: | |
if self.check_in_bbox(x, event.x, event.y): | |
return x | |
def checkCanvasItem(self, event): | |
self._previous_item = self._current_item | |
self._current_item = self.find_closest(event.x, event.y) | |
overlapping = self.find_overlapping(*self.bbox(self._current_item)) # find closest does't work well on overlapping item so use this | |
if overlapping: | |
self._current_item = self.findInBBox(overlapping, event) | |
if self._current_item and self.checkInPoints(event.x, event.y) is not None: | |
self._current_point = self.checkInPoints(event.x, event.y) | |
self.previous = (self.canvasx(event.x), self.canvasy(event.y)) | |
self._current_item = self._previous_item | |
self.bind('<B1-Motion>', self.resize) | |
self.bind('<Motion>', self.updateCursor) | |
return | |
elif self._current_item and self._current_item == self._previous_item: | |
self.previous = (self.canvasx(event.x), self.canvasy(event.y)) | |
self.moveItem(event) | |
self.bind('<B1-Motion>', self.moveItem) | |
return | |
self.unbind('<Motion>') | |
if self._current_item and self._current_item == self._current_resize_rect: | |
self._current_item = self.find_enclosed(*self.bbox(self._current_resize_rect))[0] | |
if self._current_item and not self.check_in_bbox(self._current_item, event.x, event.y): | |
self._current_item = None | |
if self._current_item is None: | |
self.removeRect() | |
return | |
if self._current_item != self._previous_item: | |
self.removeRect() | |
self.addRect() | |
self.bind('<Motion>', self.updateCursor) | |
def updateCursor(self, event): # method that updates cursor when hovering over resize points | |
point = self.checkInPoints(event.x, event.y) | |
if point: | |
key = list(self.resizePoints.keys())[list(self.resizePoints.values()).index(point)] | |
self.config(cursor=self.cursors[key]) | |
else: | |
self.config(cursor='') | |
def checkInPoints(self, x, y): # checks if the mouse is over the resizePoints | |
for item in self.resizePoints.values(): | |
if self.check_in_bbox(item, x, y): | |
return item | |
return None | |
def addRect(self): # adds a rect around the canvas item | |
bbox = self.bbox(self._current_item) | |
if bbox: | |
self._current_resize_rect = self.create_rectangle(bbox, tags=(self._tag, )) # draws rectangle | |
# the below are the points at 4 corners of resize rect | |
self.resizePoints[self.TOP_LEFT] = self.create_oval(bbox[0]-5, bbox[1]-5, bbox[0]+5, bbox[1]+5, tags=(self._tag, ), fill='#000000') | |
self.resizePoints[self.TOP_RIGHT] = self.create_oval(bbox[2]-5, bbox[1]-5, bbox[2]+5, bbox[1]+5, tags=(self._tag, ), fill='#000000') | |
self.resizePoints[self.BOTTOM_RIGHT] = self.create_oval(bbox[2]-5, bbox[3]-5, bbox[2]+5, bbox[3]+5, tags=(self._tag, ), fill='#000000') | |
self.resizePoints[self.BOTTOM_LEFT] = self.create_oval(bbox[0]-5, bbox[3]-5, bbox[0]+5, bbox[3]+5, tags=(self._tag, ), fill='#000000') | |
def removeRect(self): # removes thre resize rectangle | |
for x in self.find_withtag(self._tag): | |
self.delete(x) | |
self.resizePoints = {} | |
self._current_resize_rect = None | |
def check_in_bbox(self, item, x, y): # checks if (x, y) points are inside the bounding box | |
box = self.bbox(item) | |
return box[0] < x < box[2] and box[1] < y < box[3] | |
def moveItem(self, event): # moves the canvas item | |
xc, yc = self.canvasx(event.x), self.canvasy(event.y) | |
self.move(self._current_item, xc-self.previous[0], yc-self.previous[1]) | |
self.updateResizeRect() | |
self.previous = (xc, yc) | |
def updateResizeRect(self): # updates the position of the resize rectangle | |
self.coords(self._current_resize_rect, *self.bbox(self._current_item)) | |
new_coord = self.coords(self._current_resize_rect) | |
# note: depending on your tkinter version moveto might not be available. So use the .coords method | |
# eg: coords(self.resizePoints[self.TOP_LEFT], new_coords[0]-5, new_coords[1]-5, new_coords[0]+5,new_coords[1]+5) | |
# check how the coords are assigned in the addRect method and adjust accordingly if your tkinter version does't have `moveto` | |
self.moveto(self.resizePoints[self.TOP_LEFT], new_coord[0]-5, new_coord[1]-5) | |
self.moveto(self.resizePoints[self.TOP_RIGHT], new_coord[2]-5, new_coord[1]-5) | |
self.moveto(self.resizePoints[self.BOTTOM_RIGHT], new_coord[2]-5, new_coord[3]-5) | |
self.moveto(self.resizePoints[self.BOTTOM_LEFT], new_coord[0]-5, new_coord[3]-5) | |
def resize(self, event): # resizes the canvas item | |
item_coords = self.coords(self._current_item) | |
item_bbox = self.bbox(self._current_item) | |
try: | |
if self.resizePoints[self.TOP_LEFT] == self._current_point: | |
self.coords(self._current_item, event.x, event.y, item_coords[2], item_coords[3]) | |
#self.scale(self._current_item, item_coords[0], item_coords[1], 2, 2) | |
elif self.resizePoints[self.TOP_RIGHT] == self._current_point: | |
self.coords(self._current_item, item_coords[0], event.y, event.x, item_coords[3]) | |
elif self.resizePoints[self.BOTTOM_RIGHT] == self._current_point: | |
self.coords(self._current_item, item_coords[0], item_coords[1], event.x, event.y) | |
elif self.resizePoints[self.BOTTOM_LEFT] == self._current_point: | |
self.coords(self._current_item, event.x, item_coords[1], item_coords[2], event.y) | |
except (TclError, IndexError): | |
font = self.itemcget(self._current_item, 'font') | |
new_font = Font(font=font) | |
increase = math.sqrt((event.y-self.previous[1])**2 + (event.x-self.previous[0])**2) #+ new_font.actual()['size'] | |
if not (self.previous[0] < event.x and self.previous[1] < event.y): | |
increase = - increase | |
new_font.configure(size=int(increase)) | |
self.itemconfigure(self._current_item, font=new_font) | |
self.updateResizeRect() | |
root = tk.Tk() | |
canvas = Canvas(root) | |
canvas.pack(fill='both', expand=True) | |
canvas.create_line(10, 50, 100, 200) | |
canvas.create_rectangle(120, 50, 180, 200) | |
canvas.create_oval(200, 50, 400, 200) | |
#canvas.create_line(55, 85, 155, 85, 105, 180, 55, 85) | |
canvas.create_text(100, 150, text='HELLO', font = ("Comic Sans MS", 20, "italic")) | |
root.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
note: another possible method to detect the current clicked item is to use
canvas.find_withtag("current")