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 higlight 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__':