1
"""Drag-and-drop support for Tkinter.
3
This is very preliminary. I currently only support dnd *within* one
4
application, between different windows (or within the same window).
6
I an trying to make this as generic as possible -- not dependent on
7
the use of a particular widget or icon type, etc. I also hope that
8
this will work with Pmw.
10
To enable an object to be dragged, you must create an event binding
11
for it that starts the drag-and-drop process. Typically, you should
12
bind <ButtonPress> to a callback function that you write. The function
13
should call Tkdnd.dnd_start(source, event), where 'source' is the
14
object to be dragged, and 'event' is the event that invoked the call
15
(the argument to your callback function). Even though this is a class
16
instantiation, the returned instance should not be stored -- it will
17
be kept alive automatically for the duration of the drag-and-drop.
19
When a drag-and-drop is already in process for the Tk interpreter, the
20
call is *ignored*; this normally averts starting multiple simultaneous
21
dnd processes, e.g. because different button callbacks all
24
The object is *not* necessarily a widget -- it can be any
25
application-specific object that is meaningful to potential
26
drag-and-drop targets.
28
Potential drag-and-drop targets are discovered as follows. Whenever
29
the mouse moves, and at the start and end of a drag-and-drop move, the
30
Tk widget directly under the mouse is inspected. This is the target
31
widget (not to be confused with the target object, yet to be
32
determined). If there is no target widget, there is no dnd target
33
object. If there is a target widget, and it has an attribute
34
dnd_accept, this should be a function (or any callable object). The
35
function is called as dnd_accept(source, event), where 'source' is the
36
object being dragged (the object passed to dnd_start() above), and
37
'event' is the most recent event object (generally a <Motion> event;
38
it can also be <ButtonPress> or <ButtonRelease>). If the dnd_accept()
39
function returns something other than None, this is the new dnd target
40
object. If dnd_accept() returns None, or if the target widget has no
41
dnd_accept attribute, the target widget's parent is considered as the
42
target widget, and the search for a target object is repeated from
43
there. If necessary, the search is repeated all the way up to the
44
root widget. If none of the target widgets can produce a target
45
object, there is no target object (the target object is None).
47
The target object thus produced, if any, is called the new target
48
object. It is compared with the old target object (or None, if there
49
was no old target widget). There are several cases ('source' is the
50
source object, and 'event' is the most recent event object):
52
- Both the old and new target objects are None. Nothing happens.
54
- The old and new target objects are the same object. Its method
55
dnd_motion(source, event) is called.
57
- The old target object was None, and the new target object is not
58
None. The new target object's method dnd_enter(source, event) is
61
- The new target object is None, and the old target object is not
62
None. The old target object's method dnd_leave(source, event) is
65
- The old and new target objects differ and neither is None. The old
66
target object's method dnd_leave(source, event), and then the new
67
target object's method dnd_enter(source, event) is called.
69
Once this is done, the new target object replaces the old one, and the
70
Tk mainloop proceeds. The return value of the methods mentioned above
71
is ignored; if they raise an exception, the normal exception handling
74
The drag-and-drop processes can end in two ways: a final target object
75
is selected, or no final target object is selected. When a final
76
target object is selected, it will always have been notified of the
77
potential drop by a call to its dnd_enter() method, as described
78
above, and possibly one or more calls to its dnd_motion() method; its
79
dnd_leave() method has not been called since the last call to
80
dnd_enter(). The target is notified of the drop by a call to its
81
method dnd_commit(source, event).
83
If no final target object is selected, and there was an old target
84
object, its dnd_leave(source, event) method is called to complete the
87
Finally, the source object is notified that the drag-and-drop process
88
is over, by a call to source.dnd_end(target, event), specifying either
89
the selected target object, or None if no target object was selected.
90
The source object can use this to implement the commit action; this is
91
sometimes simpler than to do it in the target's dnd_commit(). The
92
target's dnd_commit() method could then simply be aliased to
95
At any time during a dnd sequence, the application can cancel the
96
sequence by calling the cancel() method on the object returned by
97
dnd_start(). This will call dnd_leave() if a target is currently
98
active; it will never call dnd_commit().
106
# The factory function
108
def dnd_start(source, event):
109
h = DndHandler(source, event)
116
# The class that does the work
122
def __init__(self, source, event):
125
root = event.widget._root()
128
return # Don't start recursive dnd
129
except AttributeError:
134
self.initial_button = button = event.num
135
self.initial_widget = widget = event.widget
136
self.release_pattern = "<B%d-ButtonRelease-%d>" % (button, button)
137
self.save_cursor = widget['cursor'] or ""
138
widget.bind(self.release_pattern, self.on_release)
139
widget.bind("<Motion>", self.on_motion)
140
widget['cursor'] = "hand2"
148
except AttributeError:
151
def on_motion(self, event):
152
x, y = event.x_root, event.y_root
153
target_widget = self.initial_widget.winfo_containing(x, y)
158
attr = target_widget.dnd_accept
159
except AttributeError:
162
new_target = attr(source, event)
165
target_widget = target_widget.master
166
old_target = self.target
167
if old_target is new_target:
169
old_target.dnd_motion(source, event)
173
old_target.dnd_leave(source, event)
175
new_target.dnd_enter(source, event)
176
self.target = new_target
178
def on_release(self, event):
179
self.finish(event, 1)
181
def cancel(self, event=None):
182
self.finish(event, 0)
184
def finish(self, event, commit=0):
187
widget = self.initial_widget
191
self.initial_widget.unbind(self.release_pattern)
192
self.initial_widget.unbind("<Motion>")
193
widget['cursor'] = self.save_cursor
194
self.target = self.source = self.initial_widget = self.root = None
197
target.dnd_commit(source, event)
199
target.dnd_leave(source, event)
201
source.dnd_end(target, event)
205
# ----------------------------------------------------------------------
206
# The rest is here for testing and demonstration purposes only!
210
def __init__(self, name):
212
self.canvas = self.label = self.id = None
214
def attach(self, canvas, x=10, y=10):
215
if canvas is self.canvas:
216
self.canvas.coords(self.id, x, y)
222
label = tkinter.Label(canvas, text=self.name,
223
borderwidth=2, relief="raised")
224
id = canvas.create_window(x, y, window=label, anchor="nw")
228
label.bind("<ButtonPress>", self.press)
236
self.canvas = self.label = self.id = None
240
def press(self, event):
241
if dnd_start(self, event):
242
# where the pointer is relative to the label widget:
245
# where the widget is relative to the canvas:
246
self.x_orig, self.y_orig = self.canvas.coords(self.id)
248
def move(self, event):
249
x, y = self.where(self.canvas, event)
250
self.canvas.coords(self.id, x, y)
253
self.canvas.coords(self.id, self.x_orig, self.y_orig)
255
def where(self, canvas, event):
256
# where the corner of the canvas is relative to the screen:
257
x_org = canvas.winfo_rootx()
258
y_org = canvas.winfo_rooty()
259
# where the pointer is relative to the canvas widget:
260
x = event.x_root - x_org
261
y = event.y_root - y_org
262
# compensate for initial pointer offset
263
return x - self.x_off, y - self.y_off
265
def dnd_end(self, target, event):
270
def __init__(self, root):
271
self.top = tkinter.Toplevel(root)
272
self.canvas = tkinter.Canvas(self.top, width=100, height=100)
273
self.canvas.pack(fill="both", expand=1)
274
self.canvas.dnd_accept = self.dnd_accept
276
def dnd_accept(self, source, event):
279
def dnd_enter(self, source, event):
280
self.canvas.focus_set() # Show highlight border
281
x, y = source.where(self.canvas, event)
282
x1, y1, x2, y2 = source.canvas.bbox(source.id)
283
dx, dy = x2-x1, y2-y1
284
self.dndid = self.canvas.create_rectangle(x, y, x+dx, y+dy)
285
self.dnd_motion(source, event)
287
def dnd_motion(self, source, event):
288
x, y = source.where(self.canvas, event)
289
x1, y1, x2, y2 = self.canvas.bbox(self.dndid)
290
self.canvas.move(self.dndid, x-x1, y-y1)
292
def dnd_leave(self, source, event):
293
self.top.focus_set() # Hide highlight border
294
self.canvas.delete(self.dndid)
297
def dnd_commit(self, source, event):
298
self.dnd_leave(source, event)
299
x, y = source.where(self.canvas, event)
300
source.attach(self.canvas, x, y)
304
root.geometry("+1+1")
305
tkinter.Button(command=root.quit, text="Quit").pack()
307
t1.top.geometry("+1+60")
309
t2.top.geometry("+120+60")
311
t3.top.geometry("+240+60")
320
if __name__ == '__main__':