0001 """Drag-and-drop support for Tkinter. 0002 0003 This is very preliminary. I currently only support dnd *within* one 0004 application, between different windows (or within the same window). 0005 0006 I an trying to make this as generic as possible -- not dependent on 0007 the use of a particular widget or icon type, etc. I also hope that 0008 this will work with Pmw. 0009 0010 To enable an object to be dragged, you must create an event binding 0011 for it that starts the drag-and-drop process. Typically, you should 0012 bind <ButtonPress> to a callback function that you write. The function 0013 should call Tkdnd.dnd_start(source, event), where 'source' is the 0014 object to be dragged, and 'event' is the event that invoked the call 0015 (the argument to your callback function). Even though this is a class 0016 instantiation, the returned instance should not be stored -- it will 0017 be kept alive automatically for the duration of the drag-and-drop. 0018 0019 When a drag-and-drop is already in process for the Tk interpreter, the 0020 call is *ignored*; this normally averts starting multiple simultaneous 0021 dnd processes, e.g. because different button callbacks all 0022 dnd_start(). 0023 0024 The object is *not* necessarily a widget -- it can be any 0025 application-specific object that is meaningful to potential 0026 drag-and-drop targets. 0027 0028 Potential drag-and-drop targets are discovered as follows. Whenever 0029 the mouse moves, and at the start and end of a drag-and-drop move, the 0030 Tk widget directly under the mouse is inspected. This is the target 0031 widget (not to be confused with the target object, yet to be 0032 determined). If there is no target widget, there is no dnd target 0033 object. If there is a target widget, and it has an attribute 0034 dnd_accept, this should be a function (or any callable object). The 0035 function is called as dnd_accept(source, event), where 'source' is the 0036 object being dragged (the object passed to dnd_start() above), and 0037 'event' is the most recent event object (generally a <Motion> event; 0038 it can also be <ButtonPress> or <ButtonRelease>). If the dnd_accept() 0039 function returns something other than None, this is the new dnd target 0040 object. If dnd_accept() returns None, or if the target widget has no 0041 dnd_accept attribute, the target widget's parent is considered as the 0042 target widget, and the search for a target object is repeated from 0043 there. If necessary, the search is repeated all the way up to the 0044 root widget. If none of the target widgets can produce a target 0045 object, there is no target object (the target object is None). 0046 0047 The target object thus produced, if any, is called the new target 0048 object. It is compared with the old target object (or None, if there 0049 was no old target widget). There are several cases ('source' is the 0050 source object, and 'event' is the most recent event object): 0051 0052 - Both the old and new target objects are None. Nothing happens. 0053 0054 - The old and new target objects are the same object. Its method 0055 dnd_motion(source, event) is called. 0056 0057 - The old target object was None, and the new target object is not 0058 None. The new target object's method dnd_enter(source, event) is 0059 called. 0060 0061 - The new target object is None, and the old target object is not 0062 None. The old target object's method dnd_leave(source, event) is 0063 called. 0064 0065 - The old and new target objects differ and neither is None. The old 0066 target object's method dnd_leave(source, event), and then the new 0067 target object's method dnd_enter(source, event) is called. 0068 0069 Once this is done, the new target object replaces the old one, and the 0070 Tk mainloop proceeds. The return value of the methods mentioned above 0071 is ignored; if they raise an exception, the normal exception handling 0072 mechanisms take over. 0073 0074 The drag-and-drop processes can end in two ways: a final target object 0075 is selected, or no final target object is selected. When a final 0076 target object is selected, it will always have been notified of the 0077 potential drop by a call to its dnd_enter() method, as described 0078 above, and possibly one or more calls to its dnd_motion() method; its 0079 dnd_leave() method has not been called since the last call to 0080 dnd_enter(). The target is notified of the drop by a call to its 0081 method dnd_commit(source, event). 0082 0083 If no final target object is selected, and there was an old target 0084 object, its dnd_leave(source, event) method is called to complete the 0085 dnd sequence. 0086 0087 Finally, the source object is notified that the drag-and-drop process 0088 is over, by a call to source.dnd_end(target, event), specifying either 0089 the selected target object, or None if no target object was selected. 0090 The source object can use this to implement the commit action; this is 0091 sometimes simpler than to do it in the target's dnd_commit(). The 0092 target's dnd_commit() method could then simply be aliased to 0093 dnd_leave(). 0094 0095 At any time during a dnd sequence, the application can cancel the 0096 sequence by calling the cancel() method on the object returned by 0097 dnd_start(). This will call dnd_leave() if a target is currently 0098 active; it will never call dnd_commit(). 0099 0100 """ 0101 0102 0103 import Tkinter 0104 0105 0106 # The factory function 0107 0108 def dnd_start(source, event): 0109 h = DndHandler(source, event) 0110 if h.root: 0111 return h 0112 else: 0113 return None 0114 0115 0116 # The class that does the work 0117 0118 class DndHandler: 0119 0120 root = None 0121 0122 def __init__(self, source, event): 0123 if event.num > 5: 0124 return 0125 root = event.widget._root() 0126 try: 0127 root.__dnd 0128 return # Don't start recursive dnd 0129 except AttributeError: 0130 root.__dnd = self 0131 self.root = root 0132 self.source = source 0133 self.target = None 0134 self.initial_button = button = event.num 0135 self.initial_widget = widget = event.widget 0136 self.release_pattern = "<B%d-ButtonRelease-%d>" % (button, button) 0137 self.save_cursor = widget['cursor'] or "" 0138 widget.bind(self.release_pattern, self.on_release) 0139 widget.bind("<Motion>", self.on_motion) 0140 widget['cursor'] = "hand2" 0141 0142 def __del__(self): 0143 root = self.root 0144 self.root = None 0145 if root: 0146 try: 0147 del root.__dnd 0148 except AttributeError: 0149 pass 0150 0151 def on_motion(self, event): 0152 x, y = event.x_root, event.y_root 0153 target_widget = self.initial_widget.winfo_containing(x, y) 0154 source = self.source 0155 new_target = None 0156 while target_widget: 0157 try: 0158 attr = target_widget.dnd_accept 0159 except AttributeError: 0160 pass 0161 else: 0162 new_target = attr(source, event) 0163 if new_target: 0164 break 0165 target_widget = target_widget.master 0166 old_target = self.target 0167 if old_target is new_target: 0168 if old_target: 0169 old_target.dnd_motion(source, event) 0170 else: 0171 if old_target: 0172 self.target = None 0173 old_target.dnd_leave(source, event) 0174 if new_target: 0175 new_target.dnd_enter(source, event) 0176 self.target = new_target 0177 0178 def on_release(self, event): 0179 self.finish(event, 1) 0180 0181 def cancel(self, event=None): 0182 self.finish(event, 0) 0183 0184 def finish(self, event, commit=0): 0185 target = self.target 0186 source = self.source 0187 widget = self.initial_widget 0188 root = self.root 0189 try: 0190 del root.__dnd 0191 self.initial_widget.unbind(self.release_pattern) 0192 self.initial_widget.unbind("<Motion>") 0193 widget['cursor'] = self.save_cursor 0194 self.target = self.source = self.initial_widget = self.root = None 0195 if target: 0196 if commit: 0197 target.dnd_commit(source, event) 0198 else: 0199 target.dnd_leave(source, event) 0200 finally: 0201 source.dnd_end(target, event) 0202 0203 0204 0205 # ---------------------------------------------------------------------- 0206 # The rest is here for testing and demonstration purposes only! 0207 0208 class Icon: 0209 0210 def __init__(self, name): 0211 self.name = name 0212 self.canvas = self.label = self.id = None 0213 0214 def attach(self, canvas, x=10, y=10): 0215 if canvas is self.canvas: 0216 self.canvas.coords(self.id, x, y) 0217 return 0218 if self.canvas: 0219 self.detach() 0220 if not canvas: 0221 return 0222 label = Tkinter.Label(canvas, text=self.name, 0223 borderwidth=2, relief="raised") 0224 id = canvas.create_window(x, y, window=label, anchor="nw") 0225 self.canvas = canvas 0226 self.label = label 0227 self.id = id 0228 label.bind("<ButtonPress>", self.press) 0229 0230 def detach(self): 0231 canvas = self.canvas 0232 if not canvas: 0233 return 0234 id = self.id 0235 label = self.label 0236 self.canvas = self.label = self.id = None 0237 canvas.delete(id) 0238 label.destroy() 0239 0240 def press(self, event): 0241 if dnd_start(self, event): 0242 # where the pointer is relative to the label widget: 0243 self.x_off = event.x 0244 self.y_off = event.y 0245 # where the widget is relative to the canvas: 0246 self.x_orig, self.y_orig = self.canvas.coords(self.id) 0247 0248 def move(self, event): 0249 x, y = self.where(self.canvas, event) 0250 self.canvas.coords(self.id, x, y) 0251 0252 def putback(self): 0253 self.canvas.coords(self.id, self.x_orig, self.y_orig) 0254 0255 def where(self, canvas, event): 0256 # where the corner of the canvas is relative to the screen: 0257 x_org = canvas.winfo_rootx() 0258 y_org = canvas.winfo_rooty() 0259 # where the pointer is relative to the canvas widget: 0260 x = event.x_root - x_org 0261 y = event.y_root - y_org 0262 # compensate for initial pointer offset 0263 return x - self.x_off, y - self.y_off 0264 0265 def dnd_end(self, target, event): 0266 pass 0267 0268 class Tester: 0269 0270 def __init__(self, root): 0271 self.top = Tkinter.Toplevel(root) 0272 self.canvas = Tkinter.Canvas(self.top, width=100, height=100) 0273 self.canvas.pack(fill="both", expand=1) 0274 self.canvas.dnd_accept = self.dnd_accept 0275 0276 def dnd_accept(self, source, event): 0277 return self 0278 0279 def dnd_enter(self, source, event): 0280 self.canvas.focus_set() # Show highlight border 0281 x, y = source.where(self.canvas, event) 0282 x1, y1, x2, y2 = source.canvas.bbox(source.id) 0283 dx, dy = x2-x1, y2-y1 0284 self.dndid = self.canvas.create_rectangle(x, y, x+dx, y+dy) 0285 self.dnd_motion(source, event) 0286 0287 def dnd_motion(self, source, event): 0288 x, y = source.where(self.canvas, event) 0289 x1, y1, x2, y2 = self.canvas.bbox(self.dndid) 0290 self.canvas.move(self.dndid, x-x1, y-y1) 0291 0292 def dnd_leave(self, source, event): 0293 self.top.focus_set() # Hide highlight border 0294 self.canvas.delete(self.dndid) 0295 self.dndid = None 0296 0297 def dnd_commit(self, source, event): 0298 self.dnd_leave(source, event) 0299 x, y = source.where(self.canvas, event) 0300 source.attach(self.canvas, x, y) 0301 0302 def test(): 0303 root = Tkinter.Tk() 0304 root.geometry("+1+1") 0305 Tkinter.Button(command=root.quit, text="Quit").pack() 0306 t1 = Tester(root) 0307 t1.top.geometry("+1+60") 0308 t2 = Tester(root) 0309 t2.top.geometry("+120+60") 0310 t3 = Tester(root) 0311 t3.top.geometry("+240+60") 0312 i1 = Icon("ICON1") 0313 i2 = Icon("ICON2") 0314 i3 = Icon("ICON3") 0315 i1.attach(t1.canvas) 0316 i2.attach(t2.canvas) 0317 i3.attach(t3.canvas) 0318 root.mainloop() 0319 0320 if __name__ == '__main__': 0321 test() 0322
Generated by PyXR 0.9.4