1

Im trying to move some rectangles with text in them around a canvas with mouse dragNdrop. Im using find_overlapping to select rectangles to be moved. This means the text originally created as part of class object Rect is not moved. Is there a way to modify my code to move all objects in a class object or perhaps find the class object ID using find_overlapping?

Text on rectangles can be identical, as shown in example. Tagging all elements in the class object with a random tag to group them together was my first idea, but retrieving such tag info using find_ovelapping has not been succesful.

import tkinter as tk

root=tk.Tk()
PAB=tk.Canvas(width=400, height=400)

#checks if a certain canvas object has a certain tag
def hastag(tag, id):
    if any(tag in i for i in PAB.gettags(id)):return True
    else:return False


class Rect:
    def __init__(self, x1, y1, name):
        rec = PAB.create_rectangle(x1,y1,x1+40,y1+40, fill='#c0c0c0', tag=('movable', name))
        text = PAB.create_text(x1+20,y1+20, text=name)
#mouse click find object to move
def get_it(event):
    delta=5
    global cur_rec
    for i in PAB.find_overlapping(event.x-delta, event.y-delta, event.x+delta, event.y-delta):
        if hastag('movable', i):
            cur_rec = i
    
PAB.bind('<Button-1>', get_it)

#mouse movement moves object
def move_it(event):
    xPos, yPos = event.x, event.y
    xObject, yObject = PAB.coords(cur_rec)[0],PAB.coords(cur_rec)[1]
    PAB.move(cur_rec, xPos-xObject, yPos-yObject)
PAB.bind('<B1-Motion>', move_it)

#test rects
bob = Rect(20,20,'Bob')
rob = Rect(80,80,'Rob')
different_bob = Rect(160,160,'Bob')

PAB.pack()
root.mainloop()

Thanks. If any clarifications are neccesary Id be happy to help.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685

3 Answers3

1

A better way would be to use the same tag for all the items that you want to move together so in your case both rectangle and text must have the same tag.

import tkinter as tk

root=tk.Tk()
PAB=tk.Canvas(width=400, height=400, bg="gray")

class Rect:
    def __init__(self, x1, y1, name):

        tag = f"movable{id(self)}"
        rec = PAB.create_rectangle(x1,y1,x1+40,y1+40, fill='#c0c0c0', tag=(tag, ))
        text = PAB.create_text(x1+20,y1+20, text=name, tag=(tag,))

def in_bbox(event, item):  # checks if the mouse click is inside the item
    bbox = PAB.bbox(item)

    return bbox[0] < event.x < bbox[2] and bbox[1] < event.y < bbox[3]
    
#mouse click find object to move
def get_it(event):
    delta=5
    global cur_rec
    cur_rec = PAB.find_closest(event.x, event.y)  # returns the closest object

    if not in_bbox(event, cur_rec):  # if its not in bbox then sets current_rec as None
        cur_rec = None

#mouse movement moves object
def move_it(event):
    if cur_rec:
        xPos, yPos = event.x, event.y
        xObject, yObject = PAB.coords(cur_rec)[0],PAB.coords(cur_rec)[1]
                
        PAB.move(PAB.gettags(cur_rec)[0], xPos-xObject, yPos-yObject) 

PAB.bind('<Button-1>', get_it)
PAB.bind('<B1-Motion>', move_it)
#test rects
bob = Rect(20,20,'Bob')
rob = Rect(80,80,'Rob')
different_bob = Rect(160,160,'Bob')

PAB.pack()
root.mainloop()

Art
  • 2,836
  • 4
  • 17
  • 34
0

This will work but I'm not sure it's the best way to do it.

Basically it uses the fact that the text is added to the canvas after the rectangle so it can be identified using cur_rec+1.

def move_it(event):
    xPos, yPos = event.x, event.y
    xObject, yObject = PAB.coords(cur_rec)[0],PAB.coords(cur_rec)[1]
    # move rectangle
    PAB.move(cur_rec, xPos-xObject, yPos-yObject)
    # move text associated with rectangle
    PAB.move(cur_rec+1, xPos-xObject, yPos-yObject)
norie
  • 9,609
  • 2
  • 11
  • 18
  • 1
    First, this works great for my problem example! Such a neat and short solution! I'm only afraid that: 1) if I ever need to move a rectangle that has no text, this will also move the next element 2) this means all rectangles must have the same amount of text instances in them – Pēteris Andrušaitis Jun 10 '21 at 15:59
  • 1
    @PēterisAndrušaitis You add new extra requirements here is not fair to answer provider. – acw1668 Jun 10 '21 at 16:10
  • @acw1668 Absolutely fair, as far as the provided code example goes, the answer is 100% sufficient. Should I edit the question to reflect the extra requirements? – Pēteris Andrušaitis Jun 10 '21 at 16:17
  • 1
    @PēterisAndrušaitis If the answer is 100% sufficient, you should accept it as the answer to your question. Then you can raise another question with the extra requirements. – acw1668 Jun 10 '21 at 16:24
  • @acw1668 I understand your logic, but I think accepting an answer for general case by giving canvas objects tag self.id would be more beneficial for future people looking for the answer to the same kind of problem. – Pēteris Andrušaitis Jun 10 '21 at 17:39
0

Apply the same tag to both the text and the rectangle. Then, use the tag when calling move. Here's one way to do it:

class Rect:
    def __init__(self, x1, y1, name):
        identifier = f"id:{id(self)}"
        rec = PAB.create_rectangle(x1,y1,x1+40,y1+40, fill='#c0c0c0', tags=('movable', name, identifier))
        text = PAB.create_text(x1+20,y1+20, text=name, tags=('movable', identifier))

You can then return the identifier rather than then index of the selected item:

def get_it(event):
    delta=5
    global cur_rec
    for i in PAB.find_overlapping(event.x-delta, event.y-delta, event.x+delta, event.y-delta):
        if hastag('movable', i):
            identifier = [tag for tag in PAB.gettags(i) if tag.startswith("id:")][0]
            cur_rec = identifier
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Thank you, this works great! Why does the `[tag for tag in PAB.gettags(i) if tag.startswith("id:")]` has to be a list if I know its only going to be one element? ps your tinkter related answers on here have been a godsend in this project – Pēteris Andrušaitis Jun 10 '21 at 17:46
  • @PēterisAndrušaitis: you may get more than one tag - for example, if you click on the element it will have the tag "current" in addition to any other tags you've added. – Bryan Oakley Jun 10 '21 at 17:59
  • this might be just my lack of knowledge on the python syntax, but its the same as writing `for tag in PAB.gettags(id): if tag.startswith('id:'): identifier = tag` , right? – Pēteris Andrušaitis Jun 10 '21 at 18:12
  • 1
    @PēterisAndrušaitis It is called List Comprehension – Delrius Euphoria Jun 10 '21 at 18:19