mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	
		
			
	
	
		
			322 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			322 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | """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 higlight 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() |