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.
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()