tkinter – How to drag and drop widgets?
Question:
I am trying to make a Python program in which you can move around widgets.
This is my code:
import tkinter as tk
main = tk.Tk()
notesFrame = tk.Frame(main, bd = 4, bg = "a6a6a6")
notesFrame.place(x=10,y=10)
notes = tk.Text(notesFrame)
notes.pack()
notesFrame.bind("<B1-Motion>", lambda event: notesFrame.place(x = event.x, y = event.y)
But, this gets super glitchy and the widget jumps back and forth.
Answers:
Tkinter has a module for this, documented in the module docstring. It was expected that it would be replaced by a tk dnd module, but this has not happened. I have never tried it. Searching SO for [tkinter] dnd
returns this page. Below is the beginning of the docstring.
>>> from tkinter import dnd
>>> help(dnd)
Help on module tkinter.dnd in tkinter:
NAME
tkinter.dnd - Drag-and-drop support for Tkinter.
DESCRIPTION
This is very preliminary. I currently only support dnd *within* one
application, between different windows (or within the same window).
[snip]
The behavior you’re observing is caused by the fact that the event’s coordinates are relative to the dragged widget. Updating the widget’s position (in absolute coordinates) with relative coordinates obviously results in chaos.
To fix this, I’ve used the .winfo_x()
and .winfo_y()
functions (which allow to turn the relative coordinates into absolute ones), and the Button-1
event to determine the cursor’s location on the widget when the drag starts.
Here’s a function that makes a widget draggable:
def make_draggable(widget):
widget.bind("<Button-1>", on_drag_start)
widget.bind("<B1-Motion>", on_drag_motion)
def on_drag_start(event):
widget = event.widget
widget._drag_start_x = event.x
widget._drag_start_y = event.y
def on_drag_motion(event):
widget = event.widget
x = widget.winfo_x() - widget._drag_start_x + event.x
y = widget.winfo_y() - widget._drag_start_y + event.y
widget.place(x=x, y=y)
It can be used like so:
main = tk.Tk()
frame = tk.Frame(main, bd=4, bg="grey")
frame.place(x=10, y=10)
make_draggable(frame)
notes = tk.Text(frame)
notes.pack()
If you want to take a more object-oriented approach, you can write a mixin that makes all instances of a class draggable:
class DragDropMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
make_draggable(self)
Usage:
# As always when it comes to mixins, make sure to
# inherit from DragDropMixin FIRST!
class DnDFrame(DragDropMixin, tk.Frame):
pass
# This wouldn't work:
# class DnDFrame(tk.Frame, DragDropMixin):
# pass
main = tk.Tk()
frame = DnDFrame(main, bd=4, bg="grey")
frame.place(x=10, y=10)
notes = tk.Text(frame)
notes.pack()
I came up with a different approach and it is useful when you want ALL of your widgets drag-able in the SAME window. I do also like the math used in this approach more than in the accepted answer.
The code is explained line by line as commented lines below:
import tkinter as tk
def drag_widget(event):
if (w:=root.dragged_widget): #walrus assignment
cx,cy = w.winfo_x(), w.winfo_y() #current x and y
#deltaX and deltaY to mouse position stored
dx = root.marked_pointx - root.winfo_pointerx()
dy = root.marked_pointy - root.winfo_pointery()
#adjust widget by deltaX and deltaY
w.place(x=cx-dx, y=cy-dy)
#update the marked for next iteration
root.marked_pointx = root.winfo_pointerx()
root.marked_pointy = root.winfo_pointery()
def drag_init(event):
if event.widget is not root:
#store the widget that is clicked
root.dragged_widget = event.widget
#ensure dragged widget is ontop
event.widget.lift()
#store the currently mouse position
root.marked_pointx = root.winfo_pointerx()
root.marked_pointy = root.winfo_pointery()
def finalize_dragging(event):
#default setup
root.dragged_widget = None
root = tk.Tk()
#name and register some events to some sequences
root.event_add('<<Drag>>', '<B1-Motion>')
root.event_add('<<DragInit>>', '<ButtonPress-1>')
root.event_add('<<DragFinal>>', '<ButtonRelease-1>')
#bind named events to the functions that shall be executed
root.bind('<<DragInit>>', drag_init)
root.bind('<<Drag>>', drag_widget)
root.bind('<<DragFinal>>', finalize_dragging)
#fire the finalizer of dragging for setup
root.event_generate('<<DragFinal>>')
#populate the window
for color in ['yellow','red','green','orange']:
tk.Label(root, text="test",bg=color).pack()
root.mainloop()
I am trying to make a Python program in which you can move around widgets.
This is my code:
import tkinter as tk
main = tk.Tk()
notesFrame = tk.Frame(main, bd = 4, bg = "a6a6a6")
notesFrame.place(x=10,y=10)
notes = tk.Text(notesFrame)
notes.pack()
notesFrame.bind("<B1-Motion>", lambda event: notesFrame.place(x = event.x, y = event.y)
But, this gets super glitchy and the widget jumps back and forth.
Tkinter has a module for this, documented in the module docstring. It was expected that it would be replaced by a tk dnd module, but this has not happened. I have never tried it. Searching SO for [tkinter] dnd
returns this page. Below is the beginning of the docstring.
>>> from tkinter import dnd
>>> help(dnd)
Help on module tkinter.dnd in tkinter:
NAME
tkinter.dnd - Drag-and-drop support for Tkinter.
DESCRIPTION
This is very preliminary. I currently only support dnd *within* one
application, between different windows (or within the same window).
[snip]
The behavior you’re observing is caused by the fact that the event’s coordinates are relative to the dragged widget. Updating the widget’s position (in absolute coordinates) with relative coordinates obviously results in chaos.
To fix this, I’ve used the .winfo_x()
and .winfo_y()
functions (which allow to turn the relative coordinates into absolute ones), and the Button-1
event to determine the cursor’s location on the widget when the drag starts.
Here’s a function that makes a widget draggable:
def make_draggable(widget):
widget.bind("<Button-1>", on_drag_start)
widget.bind("<B1-Motion>", on_drag_motion)
def on_drag_start(event):
widget = event.widget
widget._drag_start_x = event.x
widget._drag_start_y = event.y
def on_drag_motion(event):
widget = event.widget
x = widget.winfo_x() - widget._drag_start_x + event.x
y = widget.winfo_y() - widget._drag_start_y + event.y
widget.place(x=x, y=y)
It can be used like so:
main = tk.Tk()
frame = tk.Frame(main, bd=4, bg="grey")
frame.place(x=10, y=10)
make_draggable(frame)
notes = tk.Text(frame)
notes.pack()
If you want to take a more object-oriented approach, you can write a mixin that makes all instances of a class draggable:
class DragDropMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
make_draggable(self)
Usage:
# As always when it comes to mixins, make sure to
# inherit from DragDropMixin FIRST!
class DnDFrame(DragDropMixin, tk.Frame):
pass
# This wouldn't work:
# class DnDFrame(tk.Frame, DragDropMixin):
# pass
main = tk.Tk()
frame = DnDFrame(main, bd=4, bg="grey")
frame.place(x=10, y=10)
notes = tk.Text(frame)
notes.pack()
I came up with a different approach and it is useful when you want ALL of your widgets drag-able in the SAME window. I do also like the math used in this approach more than in the accepted answer.
The code is explained line by line as commented lines below:
import tkinter as tk
def drag_widget(event):
if (w:=root.dragged_widget): #walrus assignment
cx,cy = w.winfo_x(), w.winfo_y() #current x and y
#deltaX and deltaY to mouse position stored
dx = root.marked_pointx - root.winfo_pointerx()
dy = root.marked_pointy - root.winfo_pointery()
#adjust widget by deltaX and deltaY
w.place(x=cx-dx, y=cy-dy)
#update the marked for next iteration
root.marked_pointx = root.winfo_pointerx()
root.marked_pointy = root.winfo_pointery()
def drag_init(event):
if event.widget is not root:
#store the widget that is clicked
root.dragged_widget = event.widget
#ensure dragged widget is ontop
event.widget.lift()
#store the currently mouse position
root.marked_pointx = root.winfo_pointerx()
root.marked_pointy = root.winfo_pointery()
def finalize_dragging(event):
#default setup
root.dragged_widget = None
root = tk.Tk()
#name and register some events to some sequences
root.event_add('<<Drag>>', '<B1-Motion>')
root.event_add('<<DragInit>>', '<ButtonPress-1>')
root.event_add('<<DragFinal>>', '<ButtonRelease-1>')
#bind named events to the functions that shall be executed
root.bind('<<DragInit>>', drag_init)
root.bind('<<Drag>>', drag_widget)
root.bind('<<DragFinal>>', finalize_dragging)
#fire the finalizer of dragging for setup
root.event_generate('<<DragFinal>>')
#populate the window
for color in ['yellow','red','green','orange']:
tk.Label(root, text="test",bg=color).pack()
root.mainloop()