31

I'm fairly new to Python. I'm trying to input a file name (complete with full path) to a TKinter entry widget. Since the path to the file name can be very long I would like to be able to drag and drop the file directly from Windows Explorer. In Perl I have seen the following:

use Tk::DropSite;
.
.
my $mw = new MainWindow;
$top = $mw->Toplevel;
$label_entry = $top->Entry(-width => '45',. -background => 'ivory2')->pack();
$label_entry->DropSite(-dropcommand => \&drop,-droptypes => 'Win32',);

Is there something similar I can do using TKinter in Python?

martineau
  • 119,623
  • 25
  • 170
  • 301
George Joseph
  • 311
  • 1
  • 3
  • 4
  • 1
    Not really, there is the `Tkdnd` module but that only works inside the application (i.e., drag & drop in the window(s) created by the application). There is a Tk extension which is called TkDND (http://sourceforge.net/projects/tkdnd/) which supports what you want, making a Python binding for it (if there isn't one) should be easy. – mmgp Jan 11 '13 at 14:19

3 Answers3

23

Tk does not have any command to handle that, and Python doesn't include any extra Tk extension to perform drag & drop inter-applications, therefore you need an extension to perform the operation. Tkdnd (the Tk extension at http://sourceforge.net/projects/tkdnd, not the Tkdnd.py module) works for me. To use it from Python, a wrapper is required. Quickly searching for one, it seems http://mail.python.org/pipermail/tkinter-discuss/2005-July/000476.html contains such code. I did another one because I didn't like that other one. The problem with my wrapper is that it is highly untested, in fact I only used the function bindtarget and only for 10 seconds or so.

With the wrapper below, you can create some widget and announce that it supports receiving dragged files. Here is one example:

# The next two lines are not necessary if you installed TkDnd
# in a proper place.
import os
os.environ['TKDND_LIBRARY'] = DIRECTORYTOTHETKDNDBINARY

import Tkinter
from untested_tkdnd_wrapper import TkDND

root = Tkinter.Tk()

dnd = TkDND(root)

entry = Tkinter.Entry()
entry.pack()

def handle(event):
    event.widget.insert(0, event.data)

dnd.bindtarget(entry, handle, 'text/uri-list')

root.mainloop()

And here is the code for untested_tkdnd_wrapper.py:

import os
import Tkinter

def _load_tkdnd(master):
    tkdndlib = os.environ.get('TKDND_LIBRARY')
    if tkdndlib:
        master.tk.eval('global auto_path; lappend auto_path {%s}' % tkdndlib)
    master.tk.eval('package require tkdnd')
    master._tkdnd_loaded = True


class TkDND(object):
    def __init__(self, master):
        if not getattr(master, '_tkdnd_loaded', False):
            _load_tkdnd(master)
        self.master = master
        self.tk = master.tk

    # Available pre-defined values for the 'dndtype' parameter:
    #   text/plain
    #   text/plain;charset=UTF-8
    #   text/uri-list

    def bindtarget(self, window, callback, dndtype, event='<Drop>', priority=50):
        cmd = self._prepare_tkdnd_func(callback)
        return self.tk.call('dnd', 'bindtarget', window, dndtype, event,
                cmd, priority)

    def bindtarget_query(self, window, dndtype=None, event='<Drop>'):
        return self.tk.call('dnd', 'bindtarget', window, dndtype, event)

    def cleartarget(self, window):
        self.tk.call('dnd', 'cleartarget', window)


    def bindsource(self, window, callback, dndtype, priority=50):
        cmd = self._prepare_tkdnd_func(callback)
        self.tk.call('dnd', 'bindsource', window, dndtype, cmd, priority)

    def bindsource_query(self, window, dndtype=None):
        return self.tk.call('dnd', 'bindsource', window, dndtype)

    def clearsource(self, window):
        self.tk.call('dnd', 'clearsource', window)


    def drag(self, window, actions=None, descriptions=None,
            cursorwin=None, callback=None):
        cmd = None
        if cursorwin is not None:
            if callback is not None:
                cmd = self._prepare_tkdnd_func(callback)
        self.tk.call('dnd', 'drag', window, actions, descriptions,
                cursorwin, cmd)


    _subst_format = ('%A', '%a', '%b', '%D', '%d', '%m', '%T',
            '%W', '%X', '%Y', '%x', '%y')
    _subst_format_str = " ".join(_subst_format)

    def _prepare_tkdnd_func(self, callback):
        funcid = self.master.register(callback, self._dndsubstitute)
        cmd = ('%s %s' % (funcid, self._subst_format_str))
        return cmd

    def _dndsubstitute(self, *args):
        if len(args) != len(self._subst_format):
            return args

        def try_int(x):
            x = str(x)
            try:
                return int(x)
            except ValueError:
                return x

        A, a, b, D, d, m, T, W, X, Y, x, y = args

        event = Tkinter.Event()
        event.action = A       # Current action of the drag and drop operation.
        event.action_list = a  # Action list supported by the drag source.
        event.mouse_button = b # Mouse button pressed during the drag and drop.
        event.data = D         # The data that has been dropped.
        event.descr = d        # The list of descriptions.
        event.modifier = m     # The list of modifier keyboard keys pressed.
        event.dndtype = T
        event.widget = self.master.nametowidget(W)
        event.x_root = X       # Mouse pointer x coord, relative to the root win.
        event.y_root = Y
        event.x = x            # Mouse pointer x coord, relative to the widget.
        event.y = y

        event.action_list = str(event.action_list).split()
        for name in ('mouse_button', 'x', 'y', 'x_root', 'y_root'):
            setattr(event, name, try_int(getattr(event, name)))

        return (event, )

Together with Tkdnd, you will find a tkdnd.tcl program which is a higher level over the own C extension it provides. I didn't wrap this higher level code, but it could be more interesting to replicate it in Python than to use this lower level wrapper.

mmgp
  • 18,901
  • 3
  • 53
  • 80
  • Many thanks for that @mmgp! Just wanted to note that `tkDND` doesn't seem to be able to able to bind a widget which has a master/parent any other than `root` (_I have noted some of my problems in the script [youtube-dl-gui-tkdnd.py](http://sdaaubckp.svn.sourceforge.net/viewvc/sdaaubckp/xtra/youtube-dl-gui-tkdnd/youtube-dl-gui-tkdnd.py?view=markup)_). Many thanks again - cheers! – sdaau Jan 22 '13 at 07:19
  • 1
    @sdaau I just created a widget as in: `main = ttk.Frame(); entry = ttk.Entry(main)`. And I run `bindtarget` just fine on `entry`, and it accepts the drop events. Are you sure the problem is not somewhere else ? Can you show a minimal example that fails ? – mmgp Jan 22 '13 at 13:42
19

Things have progressed since this question was originally posted, and tkdnd2.8 (Tcl extensions) and TkinterDnD2 (Python wrapper for tkdnd2.8) are readily available on SourceForge.net.

Here's minimal sample code to do exactly what you've asked.

import Tkinter
from TkinterDnD2 import *

def drop(event):
    entry_sv.set(event.data)

root = TkinterDnD.Tk()
entry_sv = Tkinter.StringVar()
entry = Tkinter.Entry(root, textvar=entry_sv, width=80)
entry.pack(fill=Tkinter.X)
entry.drop_target_register(DND_FILES)
entry.dnd_bind('<<Drop>>', drop)
root.mainloop()

You can see How to Install and Use TkDnD with Python 2.7 Tkinter on OSX for download and installation info for both Windows and Mac on Python 2.7 and 3.6.

GaryMBloom
  • 5,350
  • 1
  • 24
  • 32
  • when 'installing' tkdnd this way to the base interpreter you can not build exe using pyinstaller because it's falling to resolve tkdnd. how can i properly install this package (in a python'ic way) so it will not break things later in the build? – Eliav Louski Apr 05 '21 at 12:47
  • 3
    BIG NOTE: I [forked](https://stackoverflow.com/a/66959998/10577976) the project and published to Pypi. one could now just install this extension with `pip install tkinterdnd2 `. enjoy! – Eliav Louski Jun 30 '21 at 20:29
  • @EliavLouski - does your package install the underlying Tcl files, as well? If not, users will still have to manually perform those steps, as well. – GaryMBloom Jan 25 '23 at 20:26
11

you can now simple use tkinterdnd2. I've forked it, built it, and upload it to pypi so you could simply pip install tkinterdnd2. usage examples here

or if you are too lasy here's a quick example:

import tkinter as tk

from tkinterdnd2 import DND_FILES, TkinterDnD

root = TkinterDnD.Tk()  # notice - use this instead of tk.Tk()

lb = tk.Listbox(root)
lb.insert(1, "drag files to here")

# register the listbox as a drop target
lb.drop_target_register(DND_FILES)
lb.dnd_bind('<<Drop>>', lambda e: lb.insert(tk.END, e.data))

lb.pack()
root.mainloop()

this will open up this enter image description here you simply drag over files

(by the way I've used listbox instead of entry but it will work exactly the same)

Eliav Louski
  • 3,593
  • 2
  • 28
  • 52