"""Drag-and-drop support for Tkinter. This is very preliminary. I currently only support dnd *within* oneapplication, between different windows (or within the same window). I an trying to make this as generic as possible -- not dependent onthe use of a particular widget or icon type, etc. I also hope thatthis will work with Pmw. To enable an object to be dragged, you must create an event bindingfor it that starts the drag-and-drop process. Typically, you shouldbind <ButtonPress> to a callback function that you write. The functionshould call Tkdnd.dnd_start(source, event), where 'source' is theobject to be dragged, and 'event' is the event that invoked the call(the argument to your callback function). Even though this is a classinstantiation, the returned instance should not be stored -- it willbe kept alive automatically for the duration of the drag-and-drop. When a drag-and-drop is already in process for the Tk interpreter, thecall is *ignored*; this normally averts starting multiple simultaneousdnd processes, e.g. because different button callbacks alldnd_start(). The object is *not* necessarily a widget -- it can be anyapplication-specific object that is meaningful to potentialdrag-and-drop targets. Potential drag-and-drop targets are discovered as follows. Wheneverthe mouse moves, and at the start and end of a drag-and-drop move, theTk widget directly under the mouse is inspected. This is the targetwidget (not to be confused with the target object, yet to bedetermined). If there is no target widget, there is no dnd targetobject. If there is a target widget, and it has an attributednd_accept, this should be a function (or any callable object). Thefunction is called as dnd_accept(source, event), where 'source' is theobject being dragged (the object passed to dnd_start() above), and'event' is the most recent event object (generally a <Motion> event;it can also be <ButtonPress> or <ButtonRelease>). If the dnd_accept()function returns something other than None, this is the new dnd targetobject. If dnd_accept() returns None, or if the target widget has nodnd_accept attribute, the target widget's parent is considered as thetarget widget, and the search for a target object is repeated fromthere. If necessary, the search is repeated all the way up to theroot widget. If none of the target widgets can produce a targetobject, there is no target object (the target object is None). The target object thus produced, if any, is called the new targetobject. It is compared with the old target object (or None, if therewas no old target widget). There are several cases ('source' is thesource object, and 'event' is the most recent event object): - Both the old and new target objects are None. Nothing happens. - The old and new target objects are the same object. Its methoddnd_motion(source, event) is called. - The old target object was None, and the new target object is notNone. The new target object's method dnd_enter(source, event) iscalled. - The new target object is None, and the old target object is notNone. The old target object's method dnd_leave(source, event) iscalled. - The old and new target objects differ and neither is None. The oldtarget object's method dnd_leave(source, event), and then the newtarget object's method dnd_enter(source, event) is called. Once this is done, the new target object replaces the old one, and theTk mainloop proceeds. The return value of the methods mentioned aboveis ignored; if they raise an exception, the normal exception handlingmechanisms take over. The drag-and-drop processes can end in two ways: a final target objectis selected, or no final target object is selected. When a finaltarget object is selected, it will always have been notified of thepotential drop by a call to its dnd_enter() method, as describedabove, and possibly one or more calls to its dnd_motion() method; itsdnd_leave() method has not been called since the last call todnd_enter(). The target is notified of the drop by a call to itsmethod dnd_commit(source, event). If no final target object is selected, and there was an old targetobject, its dnd_leave(source, event) method is called to complete thednd sequence. Finally, the source object is notified that the drag-and-drop processis over, by a call to source.dnd_end(target, event), specifying eitherthe selected target object, or None if no target object was selected.The source object can use this to implement the commit action; this issometimes simpler than to do it in the target's dnd_commit(). Thetarget's dnd_commit() method could then simply be aliased todnd_leave(). At any time during a dnd sequence, the application can cancel thesequence by calling the cancel() method on the object returned bydnd_start(). This will call dnd_leave() if a target is currentlyactive; it will never call dnd_commit(). """ import Tkinter # The factory function def dnd_start(source, event): h = DndHandler(source, event) if h.root: return h else: return None # The class that does the work class DndHandler: root = None def __init__(self, source, event): if event.num > 5: return root = event.widget._root() try: root.__dnd return # Don't start recursive dnd except AttributeError: root.__dnd = self self.root = root self.source = source self.target = None self.initial_button = button = event.num self.initial_widget = widget = event.widget self.release_pattern = "<B%d-ButtonRelease-%d>" % (button, button) self.save_cursor = widget['cursor'] or "" widget.bind(self.release_pattern, self.on_release) widget.bind("<Motion>", self.on_motion) widget['cursor'] = "hand2" def __del__(self): root = self.root self.root = None if root: try: del root.__dnd except AttributeError: pass def on_motion(self, event): x, y = event.x_root, event.y_root target_widget = self.initial_widget.winfo_containing(x, y) source = self.source new_target = None while target_widget: try: attr = target_widget.dnd_accept except AttributeError: pass else: new_target = attr(source, event) if new_target: break target_widget = target_widget.master old_target = self.target if old_target is new_target: if old_target: old_target.dnd_motion(source, event) else: if old_target: self.target = None old_target.dnd_leave(source, event) if new_target: new_target.dnd_enter(source, event) self.target = new_target def on_release(self, event): self.finish(event, 1) def cancel(self, event=None): self.finish(event, 0) def finish(self, event, commit=0): target = self.target source = self.source widget = self.initial_widget root = self.root try: del root.__dnd self.initial_widget.unbind(self.release_pattern) self.initial_widget.unbind("<Motion>") widget['cursor'] = self.save_cursor self.target = self.source = self.initial_widget = self.root = None if target: if commit: target.dnd_commit(source, event) else: target.dnd_leave(source, event) finally: source.dnd_end(target, event) # ----------------------------------------------------------------------# The rest is here for testing and demonstration purposes only! class Icon: def __init__(self, name): self.name = name self.canvas = self.label = self.id = None def attach(self, canvas, x=10, y=10): if canvas is self.canvas: self.canvas.coords(self.id, x, y) return if self.canvas: self.detach() if not canvas: return label = Tkinter.Label(canvas, text=self.name, borderwidth=2, relief="raised") id = canvas.create_window(x, y, window=label, anchor="nw") self.canvas = canvas self.label = label self.id = id label.bind("<ButtonPress>", self.press) def detach(self): canvas = self.canvas if not canvas: return id = self.id label = self.label self.canvas = self.label = self.id = None canvas.delete(id) label.destroy() def press(self, event): if dnd_start(self, event): # where the pointer is relative to the label widget: self.x_off = event.x self.y_off = event.y # where the widget is relative to the canvas: self.x_orig, self.y_orig = self.canvas.coords(self.id) def move(self, event): x, y = self.where(self.canvas, event) self.canvas.coords(self.id, x, y) def putback(self): self.canvas.coords(self.id, self.x_orig, self.y_orig) def where(self, canvas, event): # where the corner of the canvas is relative to the screen: x_org = canvas.winfo_rootx() y_org = canvas.winfo_rooty() # where the pointer is relative to the canvas widget: x = event.x_root - x_org y = event.y_root - y_org # compensate for initial pointer offset return x - self.x_off, y - self.y_off def dnd_end(self, target, event): pass class Tester: def __init__(self, root): self.top = Tkinter.Toplevel(root) self.canvas = Tkinter.Canvas(self.top, width=100, height=100) self.canvas.pack(fill="both", expand=1) self.canvas.dnd_accept = self.dnd_accept def dnd_accept(self, source, event): return self def dnd_enter(self, source, event): self.canvas.focus_set() # Show highlight border x, y = source.where(self.canvas, event) x1, y1, x2, y2 = source.canvas.bbox(source.id) dx, dy = x2-x1, y2-y1 self.dndid = self.canvas.create_rectangle(x, y, x+dx, y+dy) self.dnd_motion(source, event) def dnd_motion(self, source, event): x, y = source.where(self.canvas, event) x1, y1, x2, y2 = self.canvas.bbox(self.dndid) self.canvas.move(self.dndid, x-x1, y-y1) def dnd_leave(self, source, event): self.top.focus_set() # Hide highlight border self.canvas.delete(self.dndid) self.dndid = None def dnd_commit(self, source, event): self.dnd_leave(source, event) x, y = source.where(self.canvas, event) source.attach(self.canvas, x, y) def test(): root = Tkinter.Tk() root.geometry("+1+1") Tkinter.Button(command=root.quit, text="Quit").pack() t1 = Tester(root) t1.top.geometry("+1+60") t2 = Tester(root) t2.top.geometry("+120+60") t3 = Tester(root) t3.top.geometry("+240+60") i1 = Icon("ICON1") i2 = Icon("ICON2") i3 = Icon("ICON3") i1.attach(t1.canvas) i2.attach(t2.canvas) i3.attach(t3.canvas) root.mainloop() if __name__ == '__main__': test()
"""Drag-and-drop support for Tkinter.
This is very preliminary. I currently only support dnd *within* one
application, between different windows (or within the same window).
I an trying to make this as generic as possible -- not dependent on
the use of a particular widget or icon type, etc. I also hope that
this will work with Pmw.
To enable an object to be dragged, you must create an event binding
for it that starts the drag-and-drop process. Typically, you should
bind <ButtonPress> to a callback function that you write. The function
should call Tkdnd.dnd_start(source, event), where 'source' is the
object to be dragged, and 'event' is the event that invoked the call
(the argument to your callback function). Even though this is a class
instantiation, the returned instance should not be stored -- it will
be kept alive automatically for the duration of the drag-and-drop.
When a drag-and-drop is already in process for the Tk interpreter, the
call is *ignored*; this normally averts starting multiple simultaneous
dnd processes, e.g. because different button callbacks all
dnd_start().
The object is *not* necessarily a widget -- it can be any
application-specific object that is meaningful to potential
drag-and-drop targets.
Potential drag-and-drop targets are discovered as follows. Whenever
the mouse moves, and at the start and end of a drag-and-drop move, the
Tk widget directly under the mouse is inspected. This is the target
widget (not to be confused with the target object, yet to be
determined). If there is no target widget, there is no dnd target
object. If there is a target widget, and it has an attribute
dnd_accept, this should be a function (or any callable object). The
function is called as dnd_accept(source, event), where 'source' is the
object being dragged (the object passed to dnd_start() above), and
'event' is the most recent event object (generally a <Motion> event;
it can also be <ButtonPress> or <ButtonRelease>). If the dnd_accept()
function returns something other than None, this is the new dnd target
object. If dnd_accept() returns None, or if the target widget has no
dnd_accept attribute, the target widget's parent is considered as the
target widget, and the search for a target object is repeated from
there. If necessary, the search is repeated all the way up to the
root widget. If none of the target widgets can produce a target
object, there is no target object (the target object is None).
The target object thus produced, if any, is called the new target
object. It is compared with the old target object (or None, if there
was no old target widget). There are several cases ('source' is the
source object, and 'event' is the most recent event object):
- Both the old and new target objects are None. Nothing happens.
- The old and new target objects are the same object. Its method
dnd_motion(source, event) is called.
- The old target object was None, and the new target object is not
None. The new target object's method dnd_enter(source, event) is
called.
- The new target object is None, and the old target object is not
None. The old target object's method dnd_leave(source, event) is
called.
- The old and new target objects differ and neither is None. The old
target object's method dnd_leave(source, event), and then the new
target object's method dnd_enter(source, event) is called.
Once this is done, the new target object replaces the old one, and the
Tk mainloop proceeds. The return value of the methods mentioned above
is ignored; if they raise an exception, the normal exception handling
mechanisms take over.
The drag-and-drop processes can end in two ways: a final target object
is selected, or no final target object is selected. When a final
target object is selected, it will always have been notified of the
potential drop by a call to its dnd_enter() method, as described
above, and possibly one or more calls to its dnd_motion() method; its
dnd_leave() method has not been called since the last call to
dnd_enter(). The target is notified of the drop by a call to its
method dnd_commit(source, event).
If no final target object is selected, and there was an old target
object, its dnd_leave(source, event) method is called to complete the
dnd sequence.
Finally, the source object is notified that the drag-and-drop process
is over, by a call to source.dnd_end(target, event), specifying either
the selected target object, or None if no target object was selected.
The source object can use this to implement the commit action; this is
sometimes simpler than to do it in the target's dnd_commit(). The
target's dnd_commit() method could then simply be aliased to
dnd_leave().
At any time during a dnd sequence, the application can cancel the
sequence by calling the cancel() method on the object returned by
dnd_start(). This will call dnd_leave() if a target is currently
active; it will never call dnd_commit().
"""
import Tkinter
# The factory function
def dnd_start(source, event):
h = DndHandler(source, event)
if h.root:
return h
else:
return None
# The class that does the work
class DndHandler:
root = None
def __init__(self, source, event):
if event.num > 5:
return
root = event.widget._root()
try:
root.__dnd
return # Don't start recursive dnd
except AttributeError:
root.__dnd = self
self.root = root
self.source = source
self.target = None
self.initial_button = button = event.num
self.initial_widget = widget = event.widget
self.release_pattern = "<B%d-ButtonRelease-%d>" % (button, button)
self.save_cursor = widget['cursor'] or ""
widget.bind(self.release_pattern, self.on_release)
widget.bind("<Motion>", self.on_motion)
widget['cursor'] = "hand2"
def __del__(self):
root = self.root
self.root = None
if root:
try:
del root.__dnd
except AttributeError:
pass
def on_motion(self, event):
x, y = event.x_root, event.y_root
target_widget = self.initial_widget.winfo_containing(x, y)
source = self.source
new_target = None
while target_widget:
try:
attr = target_widget.dnd_accept
except AttributeError:
pass
else:
new_target = attr(source, event)
if new_target:
break
target_widget = target_widget.master
old_target = self.target
if old_target is new_target:
if old_target:
old_target.dnd_motion(source, event)
else:
if old_target:
self.target = None
old_target.dnd_leave(source, event)
if new_target:
new_target.dnd_enter(source, event)
self.target = new_target
def on_release(self, event):
self.finish(event, 1)
def cancel(self, event=None):
self.finish(event, 0)
def finish(self, event, commit=0):
target = self.target
source = self.source
widget = self.initial_widget
root = self.root
try:
del root.__dnd
self.initial_widget.unbind(self.release_pattern)
self.initial_widget.unbind("<Motion>")
widget['cursor'] = self.save_cursor
self.target = self.source = self.initial_widget = self.root = None
if target:
if commit:
target.dnd_commit(source, event)
else:
target.dnd_leave(source, event)
finally:
source.dnd_end(target, event)
# ----------------------------------------------------------------------
# The rest is here for testing and demonstration purposes only!
class Icon:
def __init__(self, name):
self.name = name
self.canvas = self.label = self.id = None
def attach(self, canvas, x=10, y=10):
if canvas is self.canvas:
self.canvas.coords(self.id, x, y)
return
if self.canvas:
self.detach()
if not canvas:
return
label = Tkinter.Label(canvas, text=self.name,
borderwidth=2, relief="raised")
id = canvas.create_window(x, y, window=label, anchor="nw")
self.canvas = canvas
self.label = label
self.id = id
label.bind("<ButtonPress>", self.press)
def detach(self):
canvas = self.canvas
if not canvas:
return
id = self.id
label = self.label
self.canvas = self.label = self.id = None
canvas.delete(id)
label.destroy()
def press(self, event):
if dnd_start(self, event):
# where the pointer is relative to the label widget:
self.x_off = event.x
self.y_off = event.y
# where the widget is relative to the canvas:
self.x_orig, self.y_orig = self.canvas.coords(self.id)
def move(self, event):
x, y = self.where(self.canvas, event)
self.canvas.coords(self.id, x, y)
def putback(self):
self.canvas.coords(self.id, self.x_orig, self.y_orig)
def where(self, canvas, event):
# where the corner of the canvas is relative to the screen:
x_org = canvas.winfo_rootx()
y_org = canvas.winfo_rooty()
# where the pointer is relative to the canvas widget:
x = event.x_root - x_org
y = event.y_root - y_org
# compensate for initial pointer offset
return x - self.x_off, y - self.y_off
def dnd_end(self, target, event):
pass
class Tester:
def __init__(self, root):
self.top = Tkinter.Toplevel(root)
self.canvas = Tkinter.Canvas(self.top, width=100, height=100)
self.canvas.pack(fill="both", expand=1)
self.canvas.dnd_accept = self.dnd_accept
def dnd_accept(self, source, event):
return self
def dnd_enter(self, source, event):
self.canvas.focus_set() # Show highlight border
x, y = source.where(self.canvas, event)
x1, y1, x2, y2 = source.canvas.bbox(source.id)
dx, dy = x2-x1, y2-y1
self.dndid = self.canvas.create_rectangle(x, y, x+dx, y+dy)
self.dnd_motion(source, event)
def dnd_motion(self, source, event):
x, y = source.where(self.canvas, event)
x1, y1, x2, y2 = self.canvas.bbox(self.dndid)
self.canvas.move(self.dndid, x-x1, y-y1)
def dnd_leave(self, source, event):
self.top.focus_set() # Hide highlight border
self.canvas.delete(self.dndid)
self.dndid = None
def dnd_commit(self, source, event):
self.dnd_leave(source, event)
x, y = source.where(self.canvas, event)
source.attach(self.canvas, x, y)
def test():
root = Tkinter.Tk()
root.geometry("+1+1")
Tkinter.Button(command=root.quit, text="Quit").pack()
t1 = Tester(root)
t1.top.geometry("+1+60")
t2 = Tester(root)
t2.top.geometry("+120+60")
t3 = Tester(root)
t3.top.geometry("+240+60")
i1 = Icon("ICON1")
i2 = Icon("ICON2")
i3 = Icon("ICON3")
i1.attach(t1.canvas)
i2.attach(t2.canvas)
i3.attach(t3.canvas)
root.mainloop()
if __name__ == '__main__':
test()