This is a tutorial about TkDND, an extension that adds drag and drop capabilities to Tk.
The first step is of course to load the TkDND package:
package require tkdnd
A "drop target" is a widget that can accept drops. What we must specify when creating a drop target, is a list of "types" that our drop target can accept. Lets create a simple Tk widget (in this case a ttk::button):
pack [ttk::button .drop_target -text " Drop Target (I can accept anything!) "] \ -fill x -padx 20 -pady 20
In order to register widget ".drop_target" as a drop target, we need to call the "tkdnd::drop_target" command, which needs 3 arguments: "register" or "unregister" (according to whether we create a drop target or we delete an existing one), the path of the Tk widget to be registered/unregistered, and a list of types, the drop target will accept:
tkdnd::drop_target register .drop_target *
The type "*" is a special type, which matches all types the TkDND package can handle. Other available types are:
So, we have created a single drop target, that can accept anything, which we can try:
package require tkdnd pack [ttk::button .drop_target -text " Drop Target (I can accept anything!) "] \ -fill x -padx 20 -pady 20 tkdnd::drop_target register .drop_target *
We can see that we can drop from other applications data on our drop target, but nothing actually happens if we perform the drop.
During the drop, the TkDND package delivered many Tk virtual events to our widget (".drop_target"), but we haven't registered or handled any of them. The virtual events that will be delivered to a drop target, are:
All of these events are optional, but usually we want to implement at least a binding for the <<Drop>> event, so as to get the dropped data. So, lets define a callback for this event, that simply prints the received data:
bind .drop_target <<Drop>> {puts %D; return %A}
(%A will be substituted with the "negotiated action" between the drag source and the drop target. Actions will be discussed in a few paragraphs.)
If we test our demo script now, we can see that dropped data are print to the console/terminal. However, there is no way we can tell if the dropped data is text, or a set of file/directory names, as our drop target accepts anything. An easy way to do this, is to define a callback for one of the "specialised" virtual events, that also contain a type:
So, lets add some more callbacks:
package require tkdnd catch {console show} pack [ttk::button .drop_target -text " Drop Target (I can accept anything!) "] \ -fill x -padx 20 -pady 20 tkdnd::drop_target register .drop_target * bind .drop_target <<Drop>> {puts "Generic data drop: \"%D\""; return %A} bind .drop_target <<Drop:DND_Text>> {puts "Dropped text: \"%D\""; return %A} bind .drop_target <<Drop:DND_Files>> {puts "Dropped files: \"[join %D {, }]\""; return %A} bind .drop_target <<Drop:DND_Color>> {puts "Dropped color: \"%D\""; return %A}
TkDND provides various pieces of information through "%*" codes, which get substituted with actual values, just like Tk. "%D" means dropped data. Testing with this script, we can see that TkDND will never deliver a generic <<Drop>> event, if a more specialised binding, like <<Drop:DND_Text>>, exists.
Although not required, usually widgets provide visual feedback when a drop is above them, specifying whether they will accept the drop, or not. In order to provide some simple feedback, we need to add callbacks to the <<DropEnter>> & <<DropLeave>> events:
bind .drop_target <<DropEnter>> {%W state active} bind .drop_target <<DropLeave>> {%W state !active}
But this is not enough, as during drops, <<DropLeave>> will not be delivered, so we need to reset the widget appearance in the <<Drop>> callbacks:
package require tkdnd catch {console show} pack [ttk::button .drop_target -text " Drop Target (I can accept anything!) "] \ -fill x -padx 20 -pady 20 tkdnd::drop_target register .drop_target * ## Visual feedback: bind .drop_target <<DropEnter>> {%W state active} bind .drop_target <<DropLeave>> {%W state !active} ## Drop callbacks: bind .drop_target <<Drop>> { puts "Generic data drop: \"%D\"" %W state !active return %A } bind .drop_target <<Drop:DND_Text>> { puts "Dropped text: \"%D\"" %W state !active return %A } bind .drop_target <<Drop:DND_Files>> { puts "Dropped files: \"[join %D {, }]\"" %W state !active return %A } bind .drop_target <<Drop:DND_Color>> { puts "Dropped color: \"%D\"" %W state !active return %A }
Our example so far accepts everything, as we have specified the type "*" when we register our drop target. We can specify a list of types, if we want to accept specific types:
## Accept only text: tkdnd::drop_target register .drop_target1 DND_Text ## Accept only text & files: tkdnd::drop_target register .drop_target2 {DND_Text DND_Files}
During a drop and drop operation there are two types of negotiations that take place between drag sources and drop targets. One is the negotiations about types, so as the types offered by the drag source match those accepted by the drop target (else no drop can occur). But besides types, there is also a negotiation about actions. Actions specify what will happen to the data after it is dropped. It can be copied, or it can be moved, meaning that the drag source should delete the dropped data. The actions supported by TkDND are:
In general, it is required by TkDND that all callbacks must return an action, despite the fact that "reasonable" assumptions will be made by TkDND if the callback does not return an action. It is important that callbacks registered to <<Drop*>> and <<DropPosition>> events must return an action (as the default is "refuse_drop"). The action return must be one of the actions supported by the drag source. This action list can be retrieved through "%a", and the currently agreed action with "%A". It is common practice that the action must be changed according to the keys pressed. For example, under Windows a pressed "Shift" key suggest a move actions, and a pressed "Alt" key suggests a link action. The pressed keys can be retrieved with "%b". In the following example, we show how this behavior can be implemented:
proc handle_position {widget mouse_x mouse_y drag_source_actions buttons} { if {"alt" in $buttons && "link" in $drag_source_actions} { return link } elseif {"ctrl" in $buttons && "move" in $drag_source_actions} { return move } elseif {"copy" in $drag_source_actions} { return copy } else { return refuse_drop } };# handle_position bind .drop_target <<DropPosition>> [list handle_position %W %X %Y %a %b]
The action "refuse_drop" is quite handy if we want to refuse drops to specific parts of the widget, which can be found from the mouse coordinates %X and %Y during <<DropPosition>> events (which are in root coordinates). Suppose we wanted drops to be allowed only on the left half of the widget:
proc handle_position {widget mouse_x mouse_y drag_source_actions buttons} { ## Limit drops to the left half part of the window... set x [winfo rootx $widget] set w [winfo width $widget] set middle [expr {$x + $w / 2.}] if {$mouse_x > $middle} {return refuse_drop} if {"alt" in $buttons && "link" in $drag_source_actions} { return link } elseif {"ctrl" in $buttons && "move" in $drag_source_actions} { return move } elseif {"copy" in $drag_source_actions} { return copy } else { return refuse_drop } };# handle_position bind .drop_target <<DropPosition>> [list handle_position %W %X %Y %a %b]
package require tkdnd catch {console show} pack [ttk::button .drop_target -text " Drop Target (I can accept anything!) "] \ -fill x -padx 20 -pady 20 tkdnd::drop_target register .drop_target * ## Visual feedback: bind .drop_target <<DropEnter>> {%W state active} bind .drop_target <<DropLeave>> {%W state !active} ## Position events: proc handle_position {widget mouse_x mouse_y drag_source_actions buttons} { ## Limit drops to the left half part of the window... set x [winfo rootx $widget] set w [winfo width $widget] set middle [expr {$x + $w / 2.}] if {$mouse_x > $middle} {return refuse_drop} if {"alt" in $buttons && "link" in $drag_source_actions} { return link } elseif {"ctrl" in $buttons && "move" in $drag_source_actions} { return move } elseif {"copy" in $drag_source_actions} { return copy } else { return refuse_drop } };# handle_position bind .drop_target <<DropPosition>> [list handle_position %W %X %Y %a %b] ## Drop callbacks: bind .drop_target <<Drop>> { puts "Generic data drop: \"%D\"" %W state !active return %A } bind .drop_target <<Drop:DND_Text>> { puts "Dropped text: \"%D\"" %W state !active return %A } bind .drop_target <<Drop:DND_Files>> { puts "Dropped files: \"[join %D {, }]\"" %W state !active return %A } bind .drop_target <<Drop:DND_Color>> { puts "Dropped color: \"%D\"" %W state !active return %A }
A "drag source" is a widget which can initiate a drag and drop operation. Drag sources are created with the "tkdnd::drag_source" command. For example, lets create two drag sources, one for text and one for files:
package require tkdnd catch {console show} pack [ttk::button .drag_source_text -text " Drag Source (Text) "] \ -fill x -padx 20 -pady 20 pack [ttk::button .drag_source_files -text " Drag Source (Files) "] \ -fill x -padx 20 -pady 20 tkdnd::drag_source register .drag_source_text DND_Text tkdnd::drag_source register .drag_source_files DND_Files
(There is also another optional argument, the mouse button that will initiate a drag operation, which defaults to mouse button 1.)
When a drag operation is initiated by the user, the virtual event <<DragInitCmd>> is delivered to the drag source. If no binding is found, the drag operation is canceled. The callback for this events is expected to return 3 things:
So, lets add some callbacks to our drag sources:
## Event <<DragInitCmd>> set filename [info script] bind .drag_source_text <<DragInitCmd>> \ {list copy DND_Text {Some nice dropped text!}} bind .drag_source_files <<DragInitCmd>> \ {list {copy move} DND_Files [list $filename $filename]}
The last step in defining a drag source, is to provide a callback for performing the action asked by the drop target (so as to delete the data in case of a move, etc.). This is done by adding a callback to the <<DragEndCmd>> virtual event:
## Event <<DragEndCmd>> bind .drag_source_files <<DragEndCmd>> { puts "Drop action: %A" ## Do something if the drop action is not "copy", ## i.e. delete dropped data on "move". }
package require tkdnd catch {console show} pack [ttk::button .drag_source_text -text " Drag Source (Text) "] \ -fill x -padx 20 -pady 20 pack [ttk::button .drag_source_files -text " Drag Source (Files) "] \ -fill x -padx 20 -pady 20 tkdnd::drag_source register .drag_source_text DND_Text tkdnd::drag_source register .drag_source_files DND_Files ## Event <<DragInitCmd>> set filename [info script] bind .drag_source_text <<DragInitCmd>> \ {list copy DND_Text {Some nice dropped text!}} bind .drag_source_files <<DragInitCmd>> \ {list {copy move} DND_Files [list $filename $filename]} ## Event <<DragEndCmd>> bind .drag_source_files <<DragEndCmd>> { puts "Drop action: %A" ## Do something if the drop action is not "copy", ## i.e. delete dropped data on "move". }
Googie - 2012-07-14 - It's not clear to me who is responsible for executing requested action when I've defined the DND_Files Drag source and dropped the file to some external application. The only thing I serve in <<DragIniCmd>> is the name of the script, not the actual script, so who does the actual "move" or "copy" actions - tkdnd or external application?
Btw. There are no 2.5 binaries for OSX - there's an empty dir on SF.
George Petasis - 2012-07-14 - It is totally up to you to decide. If during a drop the drop target simply copies the data, it must return the move action. In that case, the drag source must delete the data. If the drop target moves the data, it returns copy as the action (and the drag source does nothing). In general, if you are a drag source and you receive move or link in the <<DragEndCmd>> callback, you must perform the respective action. If you are a drop target and you must perform a move/link action, its up to you to decide whether you will perform the action. If you do everything, return copy. If you only copied the data, return move/link. Just see the action the drop target returns in its <<Drop>> callback, as a command to the drag source.
Regarding OS X binaries, I don't own such a machine to generate them. And there is no cross-compiler for OS X in my Fedora box (there is one for 32/64 bit Windows, and this is how I generated the corresponding binaries). Somebody who owns such a machine has to donate the binaries.
Googie - 2012-07-14 - Thanks for explanation, but I'm still a little confused. If a drop target "copies the data", then why would it return "move", while it's copying? Shouldn't it return "move" when it tries to move the data and copy when it tries to copy it?
George Petasis - 2012-07-14 - The life-cycle of actions is this:
See for example here: [L1 ] "Notice that DROPEFFECT_MOVE does not mean, "The drop target performed a move." Rather, it tells you that the drop target wants you to delete the original. If the drop target was able to delete the original (or move it directly), then you will not get DROPEFFECT_MOVE back."
The drop target should return "move" in its drop callback only if the user has triggered the move action, the drag source supports the move action, and the drop target has only copied the data. In this case, returning "move" is an instruction for the drag source to delete the data, which the drop target didn't want to (or could not) delete.
It all comes down to what you want to do. If you are writing a file manager, that accepted a drop of files, there is no point in copying the data, and returning "move" to the caller to delete them. The drop target can simply call "mv" and just return "copy". But if the files cannot be deleted by the drop target (i.e. because the users running the two applications are different), then there is no other way than the target to only copy the data, and return "move" to the source in order to delete the data.
Googie - 2012-07-15 - Thanks, now it's clear.
iva2k - 2012-11-22 - I'm using tkdnd for few years (on v2.5 now) for simple files drop into entry widgets - it works great! Now I'm trying to figure out how to drop something on canvas objects using tkdnd and cannot find any example, discussion or even a concept. So far I'm thinking to bind <<DropPosition>> to the canvas, translate the X,Y to the object and save for <<Drop>>. I will appreciate any pointers.
George Petasis - 2012-11-25 - I haven't yet ported the portion fro TkDND 1.x for dropping on canvases. I will try to look into this issue, and perhaps make a new release, if I find some time to do it. The idea is simple: since the various DnD protocols relate onlly to widgets, I have written some code in tha past, to simulate events into items (i.e. http://tkdnd.cvs.sourceforge.net/viewvc/tkdnd/tkdnd/library/tkdnd.tcl?revision=1.3&view=markup ). I will try to update this, and perhaps enhance it for TkDND 2.x...
Googie - 2012-11-26 - I wonder if there's a way to reject drops depending on the source. I mean I would like to have a drop target that accepts only drops from certain drag sources. Is it currently doable?
George Petasis - 2012-11-27 - Unfortunately, this is not possible, due to the platform protocols. It is possible in XDND (unix & OSX), but not under windows. In the binding for the <<DropPosition>>, you can "peek" the variable tkdnd::xdnd::_drag_source. It will be an id for the drag source window (something like an integer from the X server), that may belong to this, or a foreign application. But you have to somehow find the application that has this window, where the drag started. Just return refuse_drop to refuse the drop.
LES on 2023-05-26: I have these two pieces of code. Can someone please explain why the first one works and why the second one doesn't when I drag a file onto the button?
PO Both pieces work for me on Windows with Tcl/Tk 8.6.13 and tkdnd 2.9.3. You should specify, which platform and versions of Tcl/Tk and tkdnd you are using.
LES I have Tcl/Tk 8.6.6 and tkdnd 2.6 on Linux. I might be able to upgrade Tcl/Tk, but not tkdnd. I have problems in my system and cannot compile tkdnd. I have tried. This is exactly why I always try to avoid using packages.
# CODE 1: works package require Tk package require tkdnd pack [ttk::button .drop_target -text " Drop Target (I can accept anything!) "] -fill x -padx 20 -pady 20 tkdnd::drop_target register .drop_target * bind .drop_target <<Drop>> {tk_messageBox -message "YES"}
# CODE 2: doesn't work package require Tk package require tkdnd pack [frame .outerframe] pack [ttk::button .outerframe.drop_target -text " Drop Target (I can accept anything!) "] -fill x -padx 20 -pady 20 tkdnd::drop_target register .outerframe.drop_target * bind .outerframe.drop_target <<Drop>> {tk_messageBox -message "YES"}