dnd.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. """Drag-and-drop support for Tkinter.
  2. This is very preliminary. I currently only support dnd *within* one
  3. application, between different windows (or within the same window).
  4. I am trying to make this as generic as possible -- not dependent on
  5. the use of a particular widget or icon type, etc. I also hope that
  6. this will work with Pmw.
  7. To enable an object to be dragged, you must create an event binding
  8. for it that starts the drag-and-drop process. Typically, you should
  9. bind <ButtonPress> to a callback function that you write. The function
  10. should call Tkdnd.dnd_start(source, event), where 'source' is the
  11. object to be dragged, and 'event' is the event that invoked the call
  12. (the argument to your callback function). Even though this is a class
  13. instantiation, the returned instance should not be stored -- it will
  14. be kept alive automatically for the duration of the drag-and-drop.
  15. When a drag-and-drop is already in process for the Tk interpreter, the
  16. call is *ignored*; this normally averts starting multiple simultaneous
  17. dnd processes, e.g. because different button callbacks all
  18. dnd_start().
  19. The object is *not* necessarily a widget -- it can be any
  20. application-specific object that is meaningful to potential
  21. drag-and-drop targets.
  22. Potential drag-and-drop targets are discovered as follows. Whenever
  23. the mouse moves, and at the start and end of a drag-and-drop move, the
  24. Tk widget directly under the mouse is inspected. This is the target
  25. widget (not to be confused with the target object, yet to be
  26. determined). If there is no target widget, there is no dnd target
  27. object. If there is a target widget, and it has an attribute
  28. dnd_accept, this should be a function (or any callable object). The
  29. function is called as dnd_accept(source, event), where 'source' is the
  30. object being dragged (the object passed to dnd_start() above), and
  31. 'event' is the most recent event object (generally a <Motion> event;
  32. it can also be <ButtonPress> or <ButtonRelease>). If the dnd_accept()
  33. function returns something other than None, this is the new dnd target
  34. object. If dnd_accept() returns None, or if the target widget has no
  35. dnd_accept attribute, the target widget's parent is considered as the
  36. target widget, and the search for a target object is repeated from
  37. there. If necessary, the search is repeated all the way up to the
  38. root widget. If none of the target widgets can produce a target
  39. object, there is no target object (the target object is None).
  40. The target object thus produced, if any, is called the new target
  41. object. It is compared with the old target object (or None, if there
  42. was no old target widget). There are several cases ('source' is the
  43. source object, and 'event' is the most recent event object):
  44. - Both the old and new target objects are None. Nothing happens.
  45. - The old and new target objects are the same object. Its method
  46. dnd_motion(source, event) is called.
  47. - The old target object was None, and the new target object is not
  48. None. The new target object's method dnd_enter(source, event) is
  49. called.
  50. - The new target object is None, and the old target object is not
  51. None. The old target object's method dnd_leave(source, event) is
  52. called.
  53. - The old and new target objects differ and neither is None. The old
  54. target object's method dnd_leave(source, event), and then the new
  55. target object's method dnd_enter(source, event) is called.
  56. Once this is done, the new target object replaces the old one, and the
  57. Tk mainloop proceeds. The return value of the methods mentioned above
  58. is ignored; if they raise an exception, the normal exception handling
  59. mechanisms take over.
  60. The drag-and-drop processes can end in two ways: a final target object
  61. is selected, or no final target object is selected. When a final
  62. target object is selected, it will always have been notified of the
  63. potential drop by a call to its dnd_enter() method, as described
  64. above, and possibly one or more calls to its dnd_motion() method; its
  65. dnd_leave() method has not been called since the last call to
  66. dnd_enter(). The target is notified of the drop by a call to its
  67. method dnd_commit(source, event).
  68. If no final target object is selected, and there was an old target
  69. object, its dnd_leave(source, event) method is called to complete the
  70. dnd sequence.
  71. Finally, the source object is notified that the drag-and-drop process
  72. is over, by a call to source.dnd_end(target, event), specifying either
  73. the selected target object, or None if no target object was selected.
  74. The source object can use this to implement the commit action; this is
  75. sometimes simpler than to do it in the target's dnd_commit(). The
  76. target's dnd_commit() method could then simply be aliased to
  77. dnd_leave().
  78. At any time during a dnd sequence, the application can cancel the
  79. sequence by calling the cancel() method on the object returned by
  80. dnd_start(). This will call dnd_leave() if a target is currently
  81. active; it will never call dnd_commit().
  82. """
  83. import tkinter
  84. __all__ = ["dnd_start", "DndHandler"]
  85. # The factory function
  86. def dnd_start(source, event):
  87. h = DndHandler(source, event)
  88. if h.root:
  89. return h
  90. else:
  91. return None
  92. # The class that does the work
  93. class DndHandler:
  94. root = None
  95. def __init__(self, source, event):
  96. if event.num > 5:
  97. return
  98. root = event.widget._root()
  99. try:
  100. root.__dnd
  101. return # Don't start recursive dnd
  102. except AttributeError:
  103. root.__dnd = self
  104. self.root = root
  105. self.source = source
  106. self.target = None
  107. self.initial_button = button = event.num
  108. self.initial_widget = widget = event.widget
  109. self.release_pattern = "<B%d-ButtonRelease-%d>" % (button, button)
  110. self.save_cursor = widget['cursor'] or ""
  111. widget.bind(self.release_pattern, self.on_release)
  112. widget.bind("<Motion>", self.on_motion)
  113. widget['cursor'] = "hand2"
  114. def __del__(self):
  115. root = self.root
  116. self.root = None
  117. if root:
  118. try:
  119. del root.__dnd
  120. except AttributeError:
  121. pass
  122. def on_motion(self, event):
  123. x, y = event.x_root, event.y_root
  124. target_widget = self.initial_widget.winfo_containing(x, y)
  125. source = self.source
  126. new_target = None
  127. while target_widget:
  128. try:
  129. attr = target_widget.dnd_accept
  130. except AttributeError:
  131. pass
  132. else:
  133. new_target = attr(source, event)
  134. if new_target:
  135. break
  136. target_widget = target_widget.master
  137. old_target = self.target
  138. if old_target is new_target:
  139. if old_target:
  140. old_target.dnd_motion(source, event)
  141. else:
  142. if old_target:
  143. self.target = None
  144. old_target.dnd_leave(source, event)
  145. if new_target:
  146. new_target.dnd_enter(source, event)
  147. self.target = new_target
  148. def on_release(self, event):
  149. self.finish(event, 1)
  150. def cancel(self, event=None):
  151. self.finish(event, 0)
  152. def finish(self, event, commit=0):
  153. target = self.target
  154. source = self.source
  155. widget = self.initial_widget
  156. root = self.root
  157. try:
  158. del root.__dnd
  159. self.initial_widget.unbind(self.release_pattern)
  160. self.initial_widget.unbind("<Motion>")
  161. widget['cursor'] = self.save_cursor
  162. self.target = self.source = self.initial_widget = self.root = None
  163. if target:
  164. if commit:
  165. target.dnd_commit(source, event)
  166. else:
  167. target.dnd_leave(source, event)
  168. finally:
  169. source.dnd_end(target, event)
  170. # ----------------------------------------------------------------------
  171. # The rest is here for testing and demonstration purposes only!
  172. class Icon:
  173. def __init__(self, name):
  174. self.name = name
  175. self.canvas = self.label = self.id = None
  176. def attach(self, canvas, x=10, y=10):
  177. if canvas is self.canvas:
  178. self.canvas.coords(self.id, x, y)
  179. return
  180. if self.canvas:
  181. self.detach()
  182. if not canvas:
  183. return
  184. label = tkinter.Label(canvas, text=self.name,
  185. borderwidth=2, relief="raised")
  186. id = canvas.create_window(x, y, window=label, anchor="nw")
  187. self.canvas = canvas
  188. self.label = label
  189. self.id = id
  190. label.bind("<ButtonPress>", self.press)
  191. def detach(self):
  192. canvas = self.canvas
  193. if not canvas:
  194. return
  195. id = self.id
  196. label = self.label
  197. self.canvas = self.label = self.id = None
  198. canvas.delete(id)
  199. label.destroy()
  200. def press(self, event):
  201. if dnd_start(self, event):
  202. # where the pointer is relative to the label widget:
  203. self.x_off = event.x
  204. self.y_off = event.y
  205. # where the widget is relative to the canvas:
  206. self.x_orig, self.y_orig = self.canvas.coords(self.id)
  207. def move(self, event):
  208. x, y = self.where(self.canvas, event)
  209. self.canvas.coords(self.id, x, y)
  210. def putback(self):
  211. self.canvas.coords(self.id, self.x_orig, self.y_orig)
  212. def where(self, canvas, event):
  213. # where the corner of the canvas is relative to the screen:
  214. x_org = canvas.winfo_rootx()
  215. y_org = canvas.winfo_rooty()
  216. # where the pointer is relative to the canvas widget:
  217. x = event.x_root - x_org
  218. y = event.y_root - y_org
  219. # compensate for initial pointer offset
  220. return x - self.x_off, y - self.y_off
  221. def dnd_end(self, target, event):
  222. pass
  223. class Tester:
  224. def __init__(self, root):
  225. self.top = tkinter.Toplevel(root)
  226. self.canvas = tkinter.Canvas(self.top, width=100, height=100)
  227. self.canvas.pack(fill="both", expand=1)
  228. self.canvas.dnd_accept = self.dnd_accept
  229. def dnd_accept(self, source, event):
  230. return self
  231. def dnd_enter(self, source, event):
  232. self.canvas.focus_set() # Show highlight border
  233. x, y = source.where(self.canvas, event)
  234. x1, y1, x2, y2 = source.canvas.bbox(source.id)
  235. dx, dy = x2-x1, y2-y1
  236. self.dndid = self.canvas.create_rectangle(x, y, x+dx, y+dy)
  237. self.dnd_motion(source, event)
  238. def dnd_motion(self, source, event):
  239. x, y = source.where(self.canvas, event)
  240. x1, y1, x2, y2 = self.canvas.bbox(self.dndid)
  241. self.canvas.move(self.dndid, x-x1, y-y1)
  242. def dnd_leave(self, source, event):
  243. self.top.focus_set() # Hide highlight border
  244. self.canvas.delete(self.dndid)
  245. self.dndid = None
  246. def dnd_commit(self, source, event):
  247. self.dnd_leave(source, event)
  248. x, y = source.where(self.canvas, event)
  249. source.attach(self.canvas, x, y)
  250. def test():
  251. root = tkinter.Tk()
  252. root.geometry("+1+1")
  253. tkinter.Button(command=root.quit, text="Quit").pack()
  254. t1 = Tester(root)
  255. t1.top.geometry("+1+60")
  256. t2 = Tester(root)
  257. t2.top.geometry("+120+60")
  258. t3 = Tester(root)
  259. t3.top.geometry("+240+60")
  260. i1 = Icon("ICON1")
  261. i2 = Icon("ICON2")
  262. i3 = Icon("ICON3")
  263. i1.attach(t1.canvas)
  264. i2.attach(t2.canvas)
  265. i3.attach(t3.canvas)
  266. root.mainloop()
  267. if __name__ == '__main__':
  268. test()