TkDND Tutorial

This is a tutorial about TkDND, an extension that adds drag and drop capabilities to Tk.

Loading the package

The first step is of course to load the TkDND package:

package require tkdnd

Creating a drop target

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:

  • DND_Text: this type can be used for transferring text.
  • DND_Files: this type can be used for transferring a list of file/directory names, or URLs. Note that transferring file/directory names or URLs is always made as a set, thus TkDND always expects and returns a list, even for transferring a single item (file/directory name or URL).
  • DND_Color: this type can be used for transferring colors (R G B A). Currently works only under Unix.

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:

  • <<DropEnter>>: this event will be delivered when the mouse enters the widget during a drop.
  • <<DropPosition>>: this event will be delivered when the mouse moves over the widget during a drop.
  • <<DropLeave>>: this event will be delivered when the mouse leaves the widget (signaling that a drop will not occur).
  • <<Drop>>: this event will be delivered when a drop happened over the drop target widget.

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:

  • <<Drop:DND_Text>>: this event will be delivered only during a text drop.
  • <<Drop:DND_Files>>: this event will be delivered only during a drop of file/directory names or URLs.
  • <<Drop:DND_Color>>: this event will be delivered only during a color drop.

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.

Providing visual feedback

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
}

Accepting only specific types

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}

Actions

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:

  • copy: the data will be copied.
  • move: the data will be moved.
  • link: the data will be linked.
  • ask: a dialog will be displayed to the user, in order to select an action (not currently implemented).
  • private: a private action will be performed by the drop target.
  • refuse_drop: a drop cannot occur.
  • default: select the preferred action of the drag source.

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

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]

The complete example

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
}

Creating a drag source

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:

  • The list of actions supported by the drag source (i.e. {copy move}).
  • The list of types supported by the drag source (i.e. {DND_Files}).
  • The data to be dropped (which must match the list of types returned).

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".
}

The complete example

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".
}

Questions and answers

  Responsibility of performing action and returned types: copy vs. 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:

  • The drag source defines what is the set of allowed actions during the whole operation.
  • Then, the user can press various modifiers to select the desired action (among the ones defined by the drag source). Lets suppose that "move" was allowed by the drag source, and selected by the user.
  • The drop occurs with the "move" action.
  • The drop target must decide whether it will copy the data, or move the data. If it copies the data, returns action "move". If it moves the data, returns action "copy".
  • The drag source receives the action from the drop target. If it is "copy" it does nothing. If it is "move", it deletes the data.

See for example here: [1 ] "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.