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