Search code examples
pythontkinterdrag-and-drop

Python: Dragging a source widget onto a target widget to change its colour


I am looking at Python drag and drop within Tkinter. I am doing this as a proof of concept for something which I am working on. However, it doesn't quite work as expected. I create two Buttons (one red and one blue), and the idea is that when I drag and drop either of the buttons, over the other, the colour should change to cyan. This however isn't working. My code is here:

from tkinter import *

# import tkinterdnd2 as tkd

widget_x_restore = 0
widget_y_restore = 0


def drag_start(event):
    widget = event.widget
    global widget_x_restore
    global widget_y_restore

    widget_x_restore = widget.winfo_x()
    widget_y_restore = widget.winfo_y()
    widget.startX = event.x
    widget.startY = event.y
    widget.lift()


def drag_motion(event):
    widget = event.widget
    x = widget.winfo_x() - widget.startX + event.x
    y = widget.winfo_y() - widget.startY + event.y
    widget.place(x=x, y=y)


def restore_position(event):
    widget = event.widget
    print(widget)
    widget.place(x=widget_x_restore, y=widget_y_restore)


def drop(event):
    target_button = event.widget  # Access the target button
    if isinstance(target_button, Button):
        target_button.config(bg="cyan", fg="cyan")  # Change target button color


root = Tk()
root.geometry("600x400+200+100")

button1 = Button(root, text="button1", bg="red", fg="red", width=10, height=5)
button1.place(x=10, y=100)

button2 = Button(root, text="button2", bg="blue", fg="blue", width=10, height=5)
button2.place(x=200, y=200)

button1.bind("<Button-1>", drag_start)
button1.bind("<B1-Motion>", drag_motion)
button1.bind("<ButtonRelease-1>", restore_position)
button1.bind('<<Drop>>', drop)

button2.bind("<Button-1>", drag_start)
button2.bind("<B1-Motion>", drag_motion)
button2.bind("<ButtonRelease-1>", restore_position)
button2.bind('<<Drop>>', drop)

root.mainloop()

There is no colour change. So the first question is, what am I doing wrong?

UPDATE: Having changed the drop() function to:

def drop(event):
    target_button = event.widget  # Access the target button
    print(f'>>>> {target_button}')
    if isinstance(target_button, Button):
        target_button.config(bg="cyan", fg="cyan")  # Change target button color

It appears that the callback is not being executed? I get zero output from the print statement.

The second question is, is it possible to detect the source widget, from the "drop" bind? Ultimately I want to be able to grab the colour of the source button, and use it to set the colour of the target button.

UPDATE2:

Thanks to the help and comments from @furas and @acw1668, I have produced this class. I'll leave it here in case it's useful for someone else, going through the same learning curve:

    import tkinter as tk
    
    class DnD():
        """Drag and Drop management class, used to drag one widget over another, target widget. 
        The target widget then takes on the colours of the dragged
        widget."""
        payload = ['', '']
    
        def __init__(self, widget, enable_drag=True, enable_drop=True):
            self.widget = widget
            root.update_idletasks()
    
            self.widget_x_restore = widget.winfo_x()
            self.widget_y_restore = widget.winfo_y()
    
            if enable_drag:
                self.widget.bind("<Button-1>", self.drag_start)
                self.widget.bind("<B1-Motion>", self.drag_motion)
                self.widget.bind("<ButtonRelease-1>", self.restore_position)
                DnD.payload = [self.widget.cget('background'), self.widget.cget('foreground')]
    
            if enable_drop:
                self.widget.bind("<<Drop>>", self.drop)
    
        def drag_start(self, event):
            """The drag_start method is a callback function bound to a mouse action (Button-1 click)."""
            widget = event.widget
            widget.startX = event.x
            widget.startY = event.y
            widget.lift()
    
        def drag_motion(self, event):
            """Callback method, used in binding to mouse pointer motion, causing the dragged widget motion."""
            widget = event.widget
    
            x = widget.winfo_x() - widget.startX + event.x
            y = widget.winfo_y() - widget.startY + event.y
            widget.place(x=x, y=y)
    
        def drop(self, event):
            """Register a widget as a drop target."""
            target_button = event.widget  # Access the target button
            fg_colour, bg_colour = DnD.payload
            target_button.config(bg=bg_colour, fg=fg_colour)  # Change target button color
    
        def restore_position(self, event):
    
            self.widget.place(x=self.widget_x_restore, y=self.widget_y_restore)
            root.update_idletasks()  # move top widget (source widget), to access widget below (target widget)
    
            x, y = event.widget.winfo_pointerxy()
            target = event.widget.winfo_containing(x, y)
    
            target.event_generate("<<Drop>>")
    
    
    # --- main ---
    
    root = tk.Tk()
    
    root.geometry("600x400+200+100")
    
    button1 = tk.Button(root, text="button1", bg="red", fg="red", width=10, height=5)
    button1.place(x=10, y=100)
    
    button2 = tk.Button(root, text="button2", bg="blue", fg="blue", width=10, height=5)
    button2.place(x=200, y=200)
    

    # Register the buttons for drag and drop.
    button1_dnd = DnD(button1)
    button2_dnd = DnD(button2)
    
    root.mainloop()

I use a class variable to regiister the colours of the dragged button, and when the "<<Drop>>" event is detected by a "drop registered" button, the colours get retrieved from the variable.


Solution

  • I think '<<Drop>>' exists only when you use module tkinterdnd2.
    But it seems this <<Drop>> works only for external files and external text, not for widgets.


    You should do it in restore_position - similar to code in answer

    python - How can I create a Drag and Drop interface? - Stack Overflow

    Problem is that it gives top widget under mouse - so it needs update() to move source widget to old place and then target widget is top widget under mouse.

    def restore_position(event):
        widget = event.widget
        print('source:', widget)
    
        widget.place(x=widget_x_restore, y=widget_y_restore)
        root.update_idletasks()  # move top widget (source widget), to access widget below (target widget)
        
        x,y = event.widget.winfo_pointerxy()
        target = event.widget.winfo_containing(x,y)
        print('target:', target)
        
        if isinstance(target, tk.Button):
            target.config(bg="cyan", fg="cyan")  # Change target button color
    

    Full working code:

    import tkinter as tk
    
    
    def drag_start(event):
        global widget_x_restore
        global widget_y_restore
    
        widget = event.widget
    
        widget_x_restore = widget.winfo_x()
        widget_y_restore = widget.winfo_y()
        widget.startX = event.x
        widget.startY = event.y
        widget.lift()
        
    def drag_motion(event):
        widget = event.widget
    
        x = widget.winfo_x() - widget.startX + event.x
        y = widget.winfo_y() - widget.startY + event.y
        widget.place(x=x, y=y)
    
    
    def restore_position(event):
        widget = event.widget
        print('source:', widget)
        
        widget.place(x=widget_x_restore, y=widget_y_restore)
        root.update_idletasks()  # move top widget (source widget), to access widget below (target widget)
        
        x,y = event.widget.winfo_pointerxy()
        target = event.widget.winfo_containing(x,y)
        print('target:', target)
        
        if isinstance(target, tk.Button):
            target.config(bg="cyan", fg="cyan")  # Change target button color
    
    # --- main ---
    
    widget_x_restore = 0
    widget_y_restore = 0
    
    root = tk.Tk()
    
    root.geometry("600x400+200+100")
    
    button1 = tk.Button(root, text="button1", bg="red", fg="red", width=10, height=5)
    button1.place(x=10, y=100)
    
    button2 = tk.Button(root, text="button2", bg="blue", fg="blue", width=10, height=5)
    button2.place(x=200, y=200)
    
    button1.bind("<Button-1>", drag_start)
    button1.bind("<B1-Motion>", drag_motion)
    button1.bind("<ButtonRelease-1>", restore_position)
    
    button2.bind("<Button-1>", drag_start)
    button2.bind("<B1-Motion>", drag_motion)
    button2.bind("<ButtonRelease-1>", restore_position)
    
    root.mainloop()