
Drag and drop in Tkinter can be created by combining mouse events with widget positioning. A user presses the mouse button on a widget, moves the pointer while holding the button, and releases it at the required position.
Core Tkinter can handle dragging widgets and Listbox items. Dropping files from the operating system into a Tkinter application requires additional support such as the tkinterdnd2 package.
Basic widget dragging normally uses three Tkinter mouse events.
| Event | Purpose |
|---|---|
<ButtonPress-1> | Runs when the left mouse button is pressed. |
<B1-Motion> | Runs while the pointer moves with the left mouse button held down. |
<ButtonRelease-1> | Runs when the left mouse button is released. |
A widget is connected to an event by using bind().
widget.bind(
'<ButtonPress-1>',
start_drag
)
widget.bind(
'<B1-Motion>',
on_drag
)
widget.bind(
'<ButtonRelease-1>',
on_drop
)
This example allows a Label to move freely inside the main Tkinter window.
The mouse position is converted from screen coordinates to coordinates relative to the widget's parent. This avoids mixing the two coordinate systems while the widget is moving.
import tkinter as tk
def start_drag(event):
widget=event.widget
widget.drag_offset_x=event.x
widget.drag_offset_y=event.y
widget.lift()
def on_drag(event):
widget=event.widget
parent=widget.master
x=(
event.x_root
-
parent.winfo_rootx()
-
widget.drag_offset_x
)
y=(
event.y_root
-
parent.winfo_rooty()
-
widget.drag_offset_y
)
widget.place(
x=x,
y=y
)
root=tk.Tk()
root.geometry('400x300')
root.title('plus2net Drag and Drop')
label=tk.Label(
root,
text='Drag Me',
bg='lightblue',
padx=10,
pady=5
)
label.place(
x=50,
y=50
)
label.bind(
'<ButtonPress-1>',
start_drag
)
label.bind(
'<B1-Motion>',
on_drag
)
root.mainloop()
Tkinter provides several mouse and widget coordinates. The important point is to know which coordinate system each value uses.
| Value | Meaning |
|---|---|
event.x | Mouse X position relative to the widget receiving the event. |
event.y | Mouse Y position relative to the widget receiving the event. |
event.x_root | Mouse X position relative to the screen. |
event.y_root | Mouse Y position relative to the screen. |
winfo_rootx() | Screen X position of the widget or container. |
winfo_rooty() | Screen Y position of the widget or container. |
When dragging begins, this code stores where inside the Label the user clicked:
widget.drag_offset_x=event.x
widget.drag_offset_y=event.y
During movement, the pointer's screen position is converted to coordinates relative to the parent:
x=event.x_root - parent.winfo_rootx() - widget.drag_offset_x
y=event.y_root - parent.winfo_rooty() - widget.drag_offset_y
The Label is then moved using:
widget.place(
x=x,
y=y
)
The basic example allows coordinates outside the visible area. We can restrict the new position to the parent's width and height.
def on_drag(event):
widget=event.widget
parent=widget.master
x=event.x_root - parent.winfo_rootx() - widget.drag_offset_x
y=event.y_root - parent.winfo_rooty() - widget.drag_offset_y
max_x=(
parent.winfo_width()
-
widget.winfo_width()
)
max_y=(
parent.winfo_height()
-
widget.winfo_height()
)
x=max(
0,
min(x, max_x)
)
y=max(
0,
min(y, max_y)
)
widget.place(
x=x,
y=y
)
This keeps the complete widget visible inside its parent container.
A Tkinter widget belongs to the parent supplied when it is created. If a Label is created as a child of frame1, its geometry is managed relative to frame1.
Creating the draggable Label as a child of the main window instead allows one coordinate system to cover both visual frame areas.
import tkinter as tk
def start_drag(event):
widget=event.widget
widget.drag_offset_x=event.x
widget.drag_offset_y=event.y
widget.lift()
def on_drag(event):
widget=event.widget
x=(
event.x_root
-
root.winfo_rootx()
-
widget.drag_offset_x
)
y=(
event.y_root
-
root.winfo_rooty()
-
widget.drag_offset_y
)
widget.place(
x=x,
y=y
)
root=tk.Tk()
root.geometry('500x300')
frame1=tk.Frame(
root,
bg='lightgray',
width=250,
height=300
)
frame1.place(
x=0,
y=0
)
frame2=tk.Frame(
root,
bg='white',
width=250,
height=300
)
frame2.place(
x=250,
y=0
)
# Parent is root, not frame1
label=tk.Label(
root,
text='Drag Me',
bg='lightblue',
padx=10,
pady=5
)
label.place(
x=50,
y=50
)
label.bind(
'<ButtonPress-1>',
start_drag
)
label.bind(
'<B1-Motion>',
on_drag
)
root.mainloop()
Drag and drop can also be used to reorder items inside a Listbox.
The index selected on mouse press is stored. When the mouse is released, the item is removed from its old position and inserted at the new position.
import tkinter as tk
def start_drag(event):
listbox=event.widget
index=listbox.nearest(
event.y
)
box=listbox.bbox(
index
)
if box is None:
listbox.dragged_index=None
return
top=box[1]
height=box[3]
if not (
top <= event.y <= top + height
):
listbox.dragged_index=None
return
listbox.dragged_index=index
def on_drop(event):
listbox=event.widget
source=listbox.dragged_index
if source is None:
return
target=listbox.nearest(
event.y
)
item=listbox.get(
source
)
listbox.delete(
source
)
if source < target:
target -= 1
listbox.insert(
target,
item
)
listbox.selection_clear(
0,
'end'
)
listbox.selection_set(
target
)
listbox.dragged_index=None
root=tk.Tk()
root.geometry('300x250')
listbox=tk.Listbox(
root
)
listbox.pack(
padx=20,
pady=20,
fill='both',
expand=True
)
for item in [
'Apple',
'Banana',
'Cherry',
'Orange',
'Grapes'
]:
listbox.insert(
'end',
item
)
listbox.dragged_index=None
listbox.bind(
'<ButtonPress-1>',
start_drag
)
listbox.bind(
'<ButtonRelease-1>',
on_drop
)
root.mainloop()
When an item is dragged downward, deleting the original item changes the later indexes. Therefore, the target index is reduced by one:
if source < target:
target -= 1
This prevents the item from being inserted one position lower than intended.
For a more visual interface, a temporary floating Label can follow the mouse while an item moves between Listboxes.
The program performs four tasks:
import tkinter as tk
def start_drag(event):
source = event.widget
if source.size() == 0:
return
index = source.nearest(event.y)
box = source.bbox(index)
if box is None:
return
top = box[1]
height = box[3]
if not (top <= event.y <= top + height):
return
drag_data["source"] = source
drag_data["index"] = index
drag_data["item"] = source.get(index)
floating_label.config(
text=drag_data["item"]
)
move_floating_label()
floating_label.lift()
def move_floating_label():
x = root.winfo_pointerx() - root.winfo_rootx()
y = root.winfo_pointery() - root.winfo_rooty()
floating_label.place(
x=x + 10,
y=y + 10
)
def on_drag(event):
if drag_data["source"] is not None:
move_floating_label()
def on_drop(event):
source = drag_data["source"]
if source is None:
return
floating_label.place_forget()
target = root.winfo_containing(
event.x_root,
event.y_root
)
if target in listboxes and target is not source:
source.delete(drag_data["index"])
target.insert("end", drag_data["item"])
drag_data["source"] = None
drag_data["index"] = None
drag_data["item"] = None
root = tk.Tk()
root.geometry("600x300")
root.title("Drag Items Between Listboxes")
listbox1 = tk.Listbox(
root,
selectmode="single",
bg="lightgray"
)
listbox1.pack(
side="left",
padx=10,
pady=20,
fill="both",
expand=True
)
listbox2 = tk.Listbox(
root,
selectmode="single",
bg="white"
)
listbox2.pack(
side="left",
padx=10,
pady=20,
fill="both",
expand=True
)
listbox3 = tk.Listbox(
root,
selectmode="single",
bg="lightblue"
)
listbox3.pack(
side="left",
padx=10,
pady=20,
fill="both",
expand=True
)
for item in [
"Apple",
"Banana",
"Cherry",
"Orange",
"Grapes"
]:
listbox1.insert("end", item)
listboxes = [
listbox1,
listbox2,
listbox3
]
floating_label = tk.Label(
root,
bg="yellow",
relief="solid",
padx=5,
pady=2
)
drag_data = {
"source": None,
"index": None,
"item": None
}
for listbox in listboxes:
listbox.bind("<ButtonPress-1>", start_drag)
listbox.bind("<B1-Motion>", on_drag)
listbox.bind("<ButtonRelease-1>", on_drop)
root.mainloop()
The floating Label is hidden before winfo_containing() is used. Otherwise, the floating Label itself could be detected as the widget below the pointer instead of the destination Listbox.
Core Tkinter does not provide a general native file-drop interface for files dragged from the operating system. One option is the third-party tkinterdnd2 package.
pip install tkinterdnd2
import tkinter as tk
from tkinterdnd2 import DND_FILES, TkinterDnD
def on_drop(event):
files=root.tk.splitlist(
event.data
)
label.config(
text='\n'.join(files)
)
root=TkinterDnD.Tk()
root.geometry(
'500x250'
)
root.title(
'Drop Files Here'
)
label=tk.Label(
root,
text='Drag file(s) here',
bg='lightgray',
width=50,
height=8
)
label.pack(
padx=20,
pady=40,
fill='both',
expand=True
)
label.drop_target_register(
DND_FILES
)
label.dnd_bind(
'<<Drop>>',
on_drop
)
root.mainloop()
event.data contains the dropped path information. Using:
root.tk.splitlist(
event.data
)
is better than simply treating event.data as one filename because multiple dropped files and filenames containing spaces can be returned in Tcl/Tk list format.
<<Drop>> event runs when the files are released over the target.Instead of leaving the widget at any pixel position, the final coordinates can be rounded to fixed grid intervals.
For a 50-pixel grid:
grid=50
x=round(
x / grid
) * grid
y=round(
y / grid
) * grid
Snapping is usually more natural when it is applied on mouse release rather than on every movement.
def on_drop(event):
widget=event.widget
x=widget.winfo_x()
y=widget.winfo_y()
grid=50
x=round(x / grid) * grid
y=round(y / grid) * grid
widget.place(
x=x,
y=y
)
Bind it using:
label.bind(
'<ButtonRelease-1>',
on_drop
)
A Canvas item is different from a normal Tkinter widget. Canvas shapes, images and text are managed by the Canvas itself, so they are normally moved with Canvas methods rather than place().
For example:
canvas.move(
item_id,
dx,
dy
)
or its coordinates can be changed with:
canvas.coords(
item_id,
x,
y
)
Moving Items on a Tkinter Canvas
event.x and event.y are relative to the event widget, while event.x_root and event.y_root are screen coordinates. Convert coordinates before using them with place().
Store the original click offset so the point grabbed by the user remains under the pointer during dragging.
A widget created inside one Frame remains a child of that Frame. Use a common parent for objects that must move across several visual regions, or recreate the widget after the drop.
<ButtonRelease-1> is useful for final actions such as snapping, validating a drop zone or transferring an item between Listboxes.
When an item is deleted before being inserted farther down the same Listbox, later indexes shift upward by one.
With tkinterdnd2, use root.tk.splitlist(event.data) so paths containing spaces and multiple dropped files are handled correctly.
Normal widgets can use place(). Canvas objects should normally use Canvas methods such as move() or coords().
<ButtonPress-1> starts a mouse drag.<B1-Motion> tracks pointer movement while the left button is held.<ButtonRelease-1> can process the final drop.event.x and event.y are relative to the event widget.event.x_root and event.y_root are screen coordinates.place() can reposition normal Tkinter widgets.nearest(), get(), delete() and insert().tkinterdnd2 can accept files dragged from Explorer, Finder or another file manager.splitlist() safely separates multiple dropped file paths.move() and coords() instead of widget place().Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.