Search code examples
pythontkintertkinter-canvas

Is there any method in python to link two blocks dynamically


I want to draw some blocks and connect them through arrow.

I did it, but if I move the coordinates of blocks I also need to change arrow coordinates. Is there any method to bind them, like if I move the block position connecting arrow will also adjust automatically?

Can you suggest any other method in python to which will work?

from tkinter import * 

top = Tk()  
top.geometry("800x600")  
 
#creating a simple canvas  
a = Canvas(top,bg = "White",height = "515", width = "1000")
a.create_rectangle(430, 255, 550, 275,fill="White")
a.create_text(490,265,text="School",fill="black",font=('Helvetica 8 bold'))    
a.create_rectangle(430, 205, 480, 225,fill="White")
a.create_text(455,215,text="Boys",fill="black",font=('Helvetica 8 bold'))    
a.create_rectangle(480, 160, 540, 180,fill="White")
a.create_text(510,170,text="Girls",fill="black",font=('Helvetica 8 bold'))    


#School to Boys
a.create_line(450, 225, 450, 255, arrow= BOTH)
#School to COM
a.create_line(510, 180, 510, 255, arrow= BOTH)   
a.pack(expand=True) 
top.mainloop()

Solution

  • No, there isn't any method given by tkinter to link blocks. But, you can create them yourself. First, create a rectangle and text and assign them the same tag (note: each block must have a unique tag).

    Now, if you want to move the blocks using the mouse, you will have to use the canvas.find_withtag("current") to get the currently selected block and canvas.move(tagorid, x, y) to move the block. Once it is moved use widget.generate_event() to generate a custom virtual event.

    You will now have to use canvas.tag_bind(unqiue_tagn, "<<customevent>>", update_pos) to call the update_pos function when the event is generated, the update_pos should update the connection position.

    Here is a sample code.

    import tkinter as tk
    
    
    class NodeScene(tk.Canvas):
    
        def __init__(self, *args, **kwargs):
            super(NodeScene, self).__init__(*args, **kwargs)
    
            self.bind("<Button-1>", self.click_pos)
            self.bind("<B1-Motion>", self.selected_item)
    
            self.prev_x, self.prev_y = 0, 0
    
        def click_pos(self, event):
            self.prev_x, self.prev_y = event.x, event.y
    
        def selected_item(self, event):
    
            _current = self.find_withtag("current")
    
            assoc_tags = self.itemcget(_current, "tags").split(" ")[0]
       
            if _current and "node" in assoc_tags:
                new_pos_x, new_pos_y = event.x - self.prev_x, event.y - self.prev_y
                self.move(assoc_tags, new_pos_x, new_pos_y)
                
                self.event_generate("<<moved>>", when="tail")
                self.prev_x, self.prev_y = event.x, event.y
    
    
    class Node:
    
        def __init__(self, canvas: tk.Canvas, text: str, pos=(50, 50)):
            
            self.tag_id = f"node{id(self)}"
            self.canvas = canvas
            
            self.c_rect = self.canvas.create_rectangle(0, 0, 0, 0, fill="white", tags=(self.tag_id))
            c_text = self.canvas.create_text(*pos, text=text, fill="black", font=("Helvetica 8 bold"), tags=(self.tag_id))
           
            padding = 10
            text_rect = list(self.canvas.bbox(c_text))
            
            text_rect[0] -= padding
            text_rect[1] -= padding
            text_rect[2] += padding
            text_rect[3] += padding
    
            self.canvas.coords(self.c_rect, *text_rect)
    
        def bbox(self):
            return self.canvas.bbox(self.c_rect)
    
        def top(self):
            bbox = self.bbox()
            return (bbox[2] + bbox[0]) / 2, bbox[1]
        
        def bottom(self):
            bbox = self.bbox()
            return (bbox[2] + bbox[0]) / 2, bbox[3]
    
    
    class Connection:
    
        def __init__(self, canvas, node1: Node, node2: Node, pos_x):  # pos_x is the offset from center
    
            self.node1 = node1
            self.node2 = node2
            self.pos_x_offset = pos_x
            self.canvas = canvas
    
            self.connection_id = self.canvas.create_line(0, 0, 0, 0, arrow= tk.BOTH)
    
            self.canvas.tag_bind(self.node1.tag_id, "<<moved>>", self.update_conn_pos, add="+")
            self.canvas.tag_bind(self.node2.tag_id, "<<moved>>", self.update_conn_pos, add="+")
            
            self.update_conn_pos()
    
        def update_conn_pos(self, event=None):
            """ updates the connection position """
        
            node1_btm = self.node1.bottom()
            node2_top = list(self.node2.top()) 
            node2_top[0] += self.pos_x_offset
    
            self.canvas.coords(self.connection_id, *node1_btm, *node2_top)
    
    
    root = tk.Tk()
    
    canvas = NodeScene(root)
    canvas.pack(expand=True, fill="both")
    
    node = Node(canvas, "School", pos=(100, 150))
    node1 = Node(canvas, "boys", pos=(80, 80))
    node2 = Node(canvas, "girls", pos=(120, 30))
    
    connection = Connection(canvas, node1, node, -20)
    connection2 = Connection(canvas, node2, node, 20)
    
    root.mainloop()