ablmmcu
ablmmcu

Reputation: 159

Tkinter Custom Rectangle widget

I want to create a custom rectangle widget(as shown below) that can resize just by clicking and motioning the mouse. I created a class for widget:

from tkinter import *

class Rect(Canvas):
    def __init__(self, parent,x1,y1,x2,y2,color = 'yellow',transparentcolor = 'grey', default="", **kwargs):
        Canvas.__init__(self, parent)
        self.parent=parent
        self.canvas = Canvas(parent, width = x2+10,height = y2+10,bg='grey',cursor='hand2')
        self.current= self.canvas 
        self.rect = self.canvas.create_rectangle(x1,y1,x2,y2, width=5,outline=color)

        self.corner1 = self.canvas.create_oval(x1-10,y1-10,x1+10,y1+10,fill=color) # Top-left
        self.corner2 = self.canvas.create_oval(x2-10,y1-10,x2+10,y1+10,fill=color) # Top-right
        self.corner3 = self.canvas.create_oval(x1-10,y2-10,x1+10,y2+10,fill=color) # Below-left
        self.corner4 = self.canvas.create_oval(x2-10,y2-10,x2+10,y2+10,fill=color) # Below-right
        self.canvas.grid()

Output:

enter image description here

Here is my full code:

from tkinter import *
from custom_rect import Rect
from tkinter import Canvas
x1 = 12
y1 = 12
x2 = 400
y2 = 400

class DrawCircles(Frame):
    def __init__(self, master=None, **kwargs):
        super().__init__(master, **kwargs)
        self.image = Canvas(self, width=800,height=800)
        self.rect = Rect(self.image,x1,y1,x2,y2,color='green')
        self.image.tag_bind(self.rect, '<Button-1>', self.on_click_rectangle)
        self.image.tag_bind(self.rect, '<Button1-Motion>', self.on_motion)

    def on_click_rectangle(self, tag, event):
        self.current = tag
        global x1,x2,y1,y2
        if abs(event.x-x1) < abs(event.x-x2):
            x1, x2 = x2, x1
        if abs(event.y-y1) < abs(event.y-y2):
            y1, y2 = y2, y1
        self.start = x1, y1
        print(x1,y1,x2,y2)

    def on_motion(self, event):
        self.coords(self.rect, *self.start, event.x, event.y)

def main():
    main = DrawCircles()
    main.pack()
    main.mainloop()

if __name__ == '__main__':
    main()

But when I run this code, I got an error.

_tkinter.TclError: invalid boolean operator in tag search expression

I'm not sure but is the error occurs from the moving part?

Upvotes: 0

Views: 787

Answers (1)

Art
Art

Reputation: 3079

For what you are trying to achieve you shouldn't be making your window transparent. You could just create a normal rectangle and points(ovals) on each corner of the rectangles and add tags.

You also, need to bind button press, motion, and keep checking if the mouse is inside the rectangle or inside the 4 points.

In the below code I'll be showing you how to create a resizable rectangle, most of the code is taken from my previous post :

import tkinter as tk
from PIL import Image, ImageTk

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.config(bg='#1e1e1e')
        
        self._tag = 'resize'  # not necessary you can remove all the tags
        self.resizePoints = {}  # stores the resize points
        self.previous = (0, 0)  # previous mouse coordinates

        self.bind('<Motion>', self.updateCursor)
        self.bind('<1>', self.setResizePoint)
        self.bind('<ButtonRelease-1>', self.release)
        self.createResizeRect()


    def createResizeRect(self):  # adds a rect around the canvas item

        color = '#008000'
        self._current_resize_rect = self.create_rectangle(80, 50, 100, 100, tags=(self._tag), outline=color, width=3)  # draws rectangle

        bbox = self.bbox(self._current_resize_rect)

        # 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, fill=color, tags=(self._tag))
        self.resizePoints[self.TOP_RIGHT] = self.create_oval(bbox[2]-5, bbox[1]-5, bbox[2]+5, bbox[1]+5, fill=color, tags=(self._tag))
        self.resizePoints[self.BOTTOM_RIGHT] = self.create_oval(bbox[2]-5, bbox[3]-5, bbox[2]+5, bbox[3]+5, fill=color, tags=(self._tag))
        self.resizePoints[self.BOTTOM_LEFT] = self.create_oval(bbox[0]-5, bbox[3]-5, bbox[0]+5, bbox[3]+5, fill=color, tags=(self._tag))

        
        
    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 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 setResizePoint(self, event):
        self._current_point = self.checkInPoints(event.x, event.y)

        if self._current_point is not None:
            self.bind('<B1-Motion>', self.resize)

        else:
      
            self.previous = (event.x, event.y)
            self.bind('<B1-Motion>', self.moveItem)


    def release(self, event):
        self.tag_unbind(self._tag, '<B1-Motion>')
        self.unbind('<B1-Motion>')


    def moveItem(self, event):  # moves the canvas item
        xc, yc = self.canvasx(event.x), self.canvasy(event.y)

        self.move(self._current_resize_rect, xc-self.previous[0], yc-self.previous[1])
        self.updateResizeRect()

        self.previous = (xc, yc)

    def updateResizeRect(self):  # updates the position of the resize rectangle

        new_coord = self.bbox(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_resize_rect)


        if self.resizePoints[self.TOP_LEFT] == self._current_point:
            self.coords(self._current_resize_rect, event.x, event.y, item_coords[2], item_coords[3])

        elif self.resizePoints[self.TOP_RIGHT] == self._current_point:
            self.coords(self._current_resize_rect, item_coords[0], event.y, event.x, item_coords[3])

        elif self.resizePoints[self.BOTTOM_RIGHT] == self._current_point:
            self.coords(self._current_resize_rect, item_coords[0], item_coords[1], event.x, event.y)

        elif self.resizePoints[self.BOTTOM_LEFT] == self._current_point:
            self.coords(self._current_resize_rect, event.x, item_coords[1], item_coords[2], event.y)


        self.updateResizeRect()


root = tk.Tk()

canvas = Canvas(root)
canvas.pack(fill='both', expand=True)

ph_image = tk.PhotoImage(file=r"image.png")

canvas.create_image(50, 50, image=ph_image)
canvas.tag_raise(canvas._tag)
root.mainloop()
  • Don't forget to tag_raise() else the rectangle will be behind your image.

output:

enter image description here

Upvotes: 1

Related Questions