質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.93%

TkinterDND2でttkウィジェットにファイルやテキストをドロップできない。

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 342

Marusoftware

score 13

前提・実現したいこと

環境:
OS:Linux Mint 20
Python:3.8
Tk&Tcl:8.6
TkDND:2.6

発生している問題・エラーメッセージ

tkinterの通常ウィジェットにはドロップできるのですが、
ttkのウィジェットにドロップできません。

該当のソースコード

以下のようなファイル配置です:

/
|-tkdnd.py
|-dnd_test_tkinter.py
--dnd_test_ttk.py

tkdnd.py(こちらで配布されている最新版の改変版(のコメント削除版))

# -*- coding: utf-8 -*-

try:
    import Tkinter as tkinter
    import Tix as tix
    import ttk
except ImportError:
    import tkinter
    from tkinter import tix
    from tkinter import ttk
import os
TkdndVersion = None
# dnd actions
PRIVATE = 'private'
NONE = 'none'
ASK = 'ask'
COPY = 'copy'
MOVE = 'move'
LINK = 'link'
REFUSE_DROP = 'refuse_drop'
# dnd types
DND_TEXT = 'DND_Text'
DND_FILES = 'DND_Files'
DND_ALL = '*'
CF_UNICODETEXT = 'CF_UNICODETEXT'
CF_TEXT = 'CF_TEXT'
CF_HDROP = 'CF_HDROP'
FileGroupDescriptor = 'FileGroupDescriptor - FileContents'# ??
FileGroupDescriptorW = 'FileGroupDescriptorW - FileContents'# ??

def _require(tkroot):
    '''Internal function.'''
    global TkdndVersion
    tkdndlib = os.environ.get('TKDND_LIBRARY')
    if tkdndlib != "__null__":
        if tkdndlib:
            tkroot.tk.eval('global auto_path; lappend auto_path {%s}' % tkdndlib)
    try:
        TkdndVersion = tkroot.tk.call('package', 'require', 'tkdnd')
    except tkinter.TclError:
        raise RuntimeError('Unable to load tkdnd library.')
    return TkdndVersion

class DnDEvent:
    pass

class DnDWrapper:
    _subst_format_dnd = ('%A', '%a', '%b', '%C', '%c', '{%CST}',
                         '{%CTT}', '%D', '%e', '{%L}', '{%m}', '{%ST}',
                         '%T', '{%t}', '{%TT}', '%W', '%X', '%Y')
    _subst_format_str_dnd = " ".join(_subst_format_dnd)
    tkinter.BaseWidget._subst_format_dnd = _subst_format_dnd
    tkinter.BaseWidget._subst_format_str_dnd = _subst_format_str_dnd

    def _substitute_dnd(self, *args):
        if len(args) != len(self._subst_format_dnd):
            return args
        def getint_event(s):
            try:
                return int(s)
            except ValueError:
                return s
        def splitlist_event(s):
            try:
                return self.tk.splitlist(s)
            except ValueError:
                return s

        A, a, b, C, c, CST, CTT, D, e, L, m, ST, T, t, TT, W, X, Y = args
        ev = DnDEvent()
        ev.action = A
        ev.actions = splitlist_event(a)
        ev.button = getint_event(b)
        ev.code = C
        ev.codes = splitlist_event(c)
        ev.commonsourcetypes = splitlist_event(CST)
        ev.commontargettypes = splitlist_event(CTT)
        ev.data = D
        ev.name = e
        ev.types = splitlist_event(L)
        ev.modifiers = splitlist_event(m)
        ev.supportedsourcetypes = splitlist_event(ST)
        ev.sourcetypes = splitlist_event(t)
        ev.type = T
        ev.supportedtargettypes = splitlist_event(TT)
        try:
            ev.widget = self.nametowidget(W)
        except KeyError:
            ev.widget = W
        ev.x_root = getint_event(X)
        ev.y_root = getint_event(Y)
        return (ev,)
    tkinter.BaseWidget._substitute_dnd = _substitute_dnd

    def _dnd_bind(self, what, sequence, func, add, needcleanup=True):
        if isinstance(func, str):
            self.tk.call(what + (sequence, func))
        elif func:
            funcid = self._register(func, self._substitute_dnd, needcleanup)
            # FIXME: why doesn't the "return 'break'" mechanism work here??
            #cmd = ('%sif {"[%s %s]" == "break"} break\n' % (add and '+' or '',
            #                              funcid, self._subst_format_str_dnd))
            cmd = '%s%s %s' %(add and '+' or '', funcid,
                                    self._subst_format_str_dnd)
            self.tk.call(what + (sequence, cmd))
            return funcid
        elif sequence:
            return self.tk.call(what + (sequence,))
        else:
            return self.tk.splitlist(self.tk.call(what))
    tkinter.BaseWidget._dnd_bind = _dnd_bind

    def dnd_bind(self, sequence=None, func=None, add=None):
        return self._dnd_bind(('bind', self._w), sequence, func, add)
    tkinter.BaseWidget.dnd_bind = dnd_bind

    def drag_source_register(self, button=None, *dndtypes):
        if button is None:
            button = 1
        else:
            try:
                button = int(button)
            except ValueError:
                # no button defined, button is actually
                # something like DND_TEXT
                dndtypes = (button,) + dndtypes
                button = 1
        self.tk.call(
                'tkdnd::drag_source', 'register', self._w, dndtypes, button)
    tkinter.BaseWidget.drag_source_register = drag_source_register

    def drag_source_unregister(self):
        self.tk.call('tkdnd::drag_source', 'unregister', self._w)
    tkinter.BaseWidget.drag_source_unregister = drag_source_unregister

    def drop_target_register(self, *dndtypes):
        self.tk.call('tkdnd::drop_target', 'register', self._w, dndtypes)
    tkinter.BaseWidget.drop_target_register = drop_target_register

    def drop_target_unregister(self):
        self.tk.call('tkdnd::drop_target', 'unregister', self._w)
    tkinter.BaseWidget.drop_target_unregister = drop_target_unregister

    def platform_independent_types(self, *dndtypes):
        return self.tk.split(self.tk.call(
                            'tkdnd::platform_independent_types', dndtypes))
    tkinter.BaseWidget.platform_independent_types = platform_independent_types

    def platform_specific_types(self, *dndtypes):
        return self.tk.split(self.tk.call(
                            'tkdnd::platform_specific_types', dndtypes))
    tkinter.BaseWidget.platform_specific_types = platform_specific_types

    def get_dropfile_tempdir(self):
        return self.tk.call('tkdnd::GetDropFileTempDirectory')
    tkinter.BaseWidget.get_dropfile_tempdir = get_dropfile_tempdir

    def set_dropfile_tempdir(self, tempdir):
        self.tk.call('tkdnd::SetDropFileTempDirectory', tempdir)
    tkinter.BaseWidget.set_dropfile_tempdir = set_dropfile_tempdir

class Tk(tkinter.Tk, DnDWrapper):
    def __init__(self, *args, **kw):
        tkinter.Tk.__init__(self, *args, **kw)
        self.TkdndVersion = _require(self)

class TixTk(tix.Tk, DnDWrapper):
    def __init__(self, *args, **kw):
        tix.Tk.__init__(self, *args, **kw)
        self.TkdndVersion = _require(self)

ttkバージョン(dnd_test_ttk.py - 今回問題が発生しているものです。)

from tkdnd import *
from tkinter import ttk

root = Tk()#ウィンドウ

frame = ttk.Frame(root, width=100, height=100)#ドロップされるフレーム
frame.pack(fill="both",expand=True)

frame.drop_target_register(DND_FILES, DND_TEXT)#対応させるドロップフォーマット
frame.dnd_bind('<<Drop>>', lambda event:print(event.data))#イベントのバインド

root.mainloop()

tkinterバージョン(dnd_test_tkinter.py)

from tkinter import *
import tkinter

root = Tk()#ウィンドウ

frame = tkinter.Frame(root, width=100, height=100)#ドロップされるフレーム
frame.pack(fill="both",expand=True)

frame.drop_target_register(DND_FILES, DND_TEXT)#対応させるドロップフォーマット
frame.dnd_bind('<<Drop>>', lambda event:print(event.data))#イベントのバインド

root.mainloop()

試したこと

順序の入れ替えなど

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

環境: win10/64 python3.7 TkDnD2.8
同環境ではありませんが、類似する症状を確認。

ファイルはドロップできないけどテキストは可能だったりしませんか?

上記の tkdnd.py を使うとどちらのコードもファイルのドロップは出来ず、
公式?(リンク先からダウンロード) の TkinterDnD2 では、ttk でも利用できました。

差分を確認すると、tkdnd.py には 引数 ttk が追加されてました。

def drop_target_register(self, ttk=0, *dndtypes):

原因: DND_FILES が ttk 引数に入ってしまい、dndtypes に渡っていない。

DND_TEXT は有効そうなので、テキストのドロップは出来ました。


※ 但し、dnd_test_ttk.py だけでドロップ出来ないという現象は、上記のコードからは確認できません。
双方のテストをした時に、ファイルとテキストで異なっていた可能性はないでしょうか。

DND_FILES, DND_TEXT はこのコードでは未定義の変数でエラーになるはずですが、
他に、投稿の際に編集したということはありませんか?


追記: ttk.Notebook でのドロップ確認用のコード

※ drop_target_registerのttk=0 の部分に True を渡しています。ここはtkdnd.py側で修正済であれば適宜対処。

#!/usr/bin/env python3.7

import sys
from tkinter import *
from tkinter import ttk

import tkdnd

def drop(event):
    print(event.data)

root = tkdnd.Tk()
root.geometry("400x400")

note = ttk.Notebook(root)
note.drop_target_register(True, tkdnd.DND_FILES) # XXX
note.dnd_bind('<<Drop>>', drop)
note.pack(fill=BOTH, expand=True)

frame = ttk.Frame(note)
note.add(frame, text="Page1")

button = ttk.Button(note, text="OK")
note.add(button, text="Page1")

root.mainloop()

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/07/20 23:50

    回答ありがとうございます。

    動作報告対象は前回のコメントのものでしょうか?
    わざわざ動作確認ありがとうございます。
    諸事情によりWindows環境の実行に若干の手間がかかるものでして...大変助かります。

    tkdndはDebian Aptパッケージマネージャ版です。
    次のコマンドでインストールしました。
    sudo apt install tkdnd
    インストール時にエラーは発生していません。
    (使用している環境はLinux Mint 20なので、Ubuntu Focalのパッケージと同一のものです。)
    https://packages.ubuntu.com/focal/tkdnd
    こちらは、バージョンが2.6となっているようです。
    githubのtkdnd最新版、ありがとうございます。
    ただ、実は、このTkDnDを使用する(予定)のアプリケーションをマルチプラットフォーム向けに配布する予定もあり、ソースコードからのビルドに若干消極的なのが現実です。(githubの方にバイナリパッケージはなかった)
    とはいっても、バグ修正等により問題が解決されるのであれば、最適な解決方法であると考えられますので、なんとか試してみます。

    結果はなるべく早めに報告いたします。
    今後もお力添えいただければ幸いです。
    よろしくお願いします。

    キャンセル

  • 2020/07/21 14:42 編集

    >動作報告対象は前回のコメントのものでしょうか?

    はい、ひとつ前のコメントの動作報告です。

    >githubの方にバイナリパッケージはなかった

    x64のみですが、release ページにあります。
    2.6は windows用バイナリがありませんでした。

    virtual box 上 linuxmint-20-cinnamon にて python3.8.2
    apt でインストールした tkdnd で問題を再現出来ました。
    原因自体は調べてません。動作報告のみですが、

    https://github.com/petasis/tkdnd/releases にある
    tkdnd-2.9.2-linux-x64.tgz で動作確認出来ました。

    2.9.2のライブラリはインストールせずに、環境変数 TKDND_LIBRARYを設定して読込。
    tkdnd.py は一カ所 master.tk.call -> tkroot.tk.call に修正。

    因みに、tkdnd v2.6 でも ttk.Notebook上のttk.Frameではなく、
    ttk.Notebook に bind するとファイルのドロップは可能でした。

    キャンセル

  • 2020/07/23 20:43 編集

    回答ありがとうございます。

    説明不足ですみません。
    私の言うところの、"ttk.Notebookにドロップ"とは以下のソースコードのようなものです。
    ```
    from tkdnd import *
    from tkinter import ttk
    import os
    os.environ['TKDND_LIBRARY'] = os.path.abspath("./tkdnd")
    root = Tk()
    root.note = ttk.Notebook(root)
    root.note.pack(fill="both",expand=True)
    root.note.frame = ttk.Frame(root.note,height=100,width=100)
    root.note.frame.pack(fill="both",expand=True)
    root.note.add(root.note.frame)
    root.note.frame.drop_target_register(DND_FILES)
    root.note.frame.dnd_bind('<<Drop>>', lambda event:print(event.data))
    root.mainloop()
    ```
    どうやら、バグ("https://sourceforge.net/p/tkdnd/bugs/27/")がaptからインストールできるtkdnd2.6では修正されていないようです。
    また、こちらでも、tkdnd2.8以降であれば、問題なくドロップできることが確認できました。(2.9.2も確認できました!)
    2.9.2に関しては、ソースコード内のtcl-configを
    ```
    #!/bin/sh
    # The next line is executed by /bin/sh, but not tcl \
    exec tclsh "$0" ${1+"$@"}

    set srcdir [file dirname [file normalize [info script]]]

    puts "TkDND sources: $srcdir"

    set libpath /usr/lib/
    set root /usr/include/tk/
    set tcl_includepath $root
    set tk_includepath $root
    set tcl_libpath $libpath
    set tk_libpath $libpath
    set tk_includepath_int $tk_includepath
    catch {file delete -force config.cache}

    # puts "ROOT: $root"

    catch {
    switch -nocase -- $::tcl_platform(os) {
    darwin {
    set sdk {}
    catch {
    set sdk [exec xcodebuild -version -sdk macosx Path]
    }
    foreach dir [list \
    $root/Headers \
    [string map {Tcl Tk} $root]/Headers \
    /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tcl.framework/Versions/Current \
    /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tcl.framework/Versions/Current/Headers \
    /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Versions/Current \
    /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Versions/Current/Headers \
    /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Versions/Current/Headers/tk-private \
    $sdk/System/Library/Frameworks/Tcl.framework/Versions/Current \
    $sdk/System/Library/Frameworks/Tcl.framework/Versions/Current/Headers \
    $sdk/System/Library/Frameworks/Tk.framework/Versions/Current \
    $sdk/System/Library/Frameworks/Tk.framework/Versions/Current/Headers \
    $sdk/System/Library/Frameworks/Tk.framework/Versions/Current/Headers/tk-private \
    /System/Library/Frameworks/Tcl.framework \
    /System/Library/Frameworks/Tk.framework \
    ] {
    if {[file exists $dir/tclConfig.sh]} {set tcl_libpath $dir}
    if {[file exists $dir/tkConfig.sh]} {set tk_libpath $dir}
    if {[file exists $dir/tcl.h]} {set tcl_includepath $dir}
    if {[file exists $dir/tk.h]} {set tk_includepath $dir}
    if {[file exists $dir/tkInt.h]} {set tk_includepath_int $dir}
    }
    }
    }
    }

    set bits --disable-64bit
    catch {
    switch $::tcl_platform(machine) {
    x86_64 -
    amd64 {set bits --enable-64bit}
    default {set bits --disable-64bit}
    }
    }


    ## Do we have Tcllib's fileutil?
    if {[catch {package require fileutil}]} {
    puts "Tcllib's package \"fileutil\" not found! Simulating..."
    ## From: http://wiki.tcl.tk/2042
    proc rglob { dirpath patterns } {
    set rlist {}
    foreach fpath [glob -nocomplain -types f -directory ${dirpath} \
    {*}${patterns}] {
    lappend rlist ${fpath}
    }
    foreach dir [glob -nocomplain -types d -directory ${dirpath} *] {
    lappend rlist {*}[rglob ${dir} ${patterns}]
    }
    return ${rlist}
    };# rglob
    proc find_file {path filename} {
    set found [rglob $path $filename]
    if {![llength $found]} {
    puts stderr "Cannot find file \"$filename\" within directory (and\
    sub-directories) \"$path\"."
    exit 1
    }
    lindex $found 0
    };# find_file
    } else {
    puts "Tcllib's package \"fileutil\" found!"
    proc find_file {path filename} {
    set found [fileutil::findByPattern $path -glob -- $filename]
    if {![llength $found]} {
    puts stderr "Cannot find file \"$filename\" within directory (and\
    sub-directories) \"$path\"."
    exit 2
    }
    lindex $found 0
    };# find_file
    }
    puts {}

    ## Ensure we can find tclConfig.sh & tkConfig.sh
    if {![file exists $tcl_libpath/tclConfig.sh]} {
    ## Ok, we are missing the configuration file. Can we find it?
    set tcl_libpath [file dirname [find_file $root tclConfig.sh]]
    }
    puts "Found \"tclConfig.sh\" in \"$tcl_libpath\"."

    if {![file exists $tk_libpath/tkConfig.sh]} {
    ## Ok, we are missing the configuration file. Can we find it?
    set tk_libpath [file dirname [find_file $root tkConfig.sh]]
    }
    puts "Found \"tkConfig.sh\" in \"$tk_libpath\"."

    ## Ensure we can find tclConfig.sh & tkConfig.sh
    if {![file exists $tcl_includepath/tcl.h]} {
    ## Ok, we are missing the configuration file. Can we find it?
    set tcl_includepath [file dirname [find_file $root tcl.h]]
    }
    puts "Found \"tcl.h\" in \"$tcl_includepath\"."

    if {![file exists $tk_includepath/tk.h]} {
    ## Ok, we are missing the configuration file. Can we find it?
    set tk_includepath [file dirname [find_file $root tk.h]]
    }
    puts "Found \"tk.h\" in \"$tk_includepath\"."

    puts {}

    if {$tk_includepath_int ne $tk_includepath} {
    puts "+++ Running 'bash configure -srcdir=$srcdir\n\
    \ --prefix=$srcdir/cmake/runtime\n\
    \ --exec-prefix=$srcdir/cmake/runtime\n\
    \ --with-tcl=$tcl_libpath\n\
    \ --with-tk=$tk_libpath\n\
    \ --with-tclinclude=$tcl_includepath\n\
    \ --with-tkinclude=$tk_includepath\n\
    \ PKG_INCLUDES=-I\"$tk_includepath_int\"\n\
    \ $bits'\n\
    \ in directory [pwd]..."
    catch {exec bash configure -srcdir=$srcdir --prefix=$srcdir/cmake/runtime \
    --exec-prefix=$srcdir/cmake/runtime \
    --with-tcl=$tcl_libpath --with-tk=$tk_libpath \
    --with-tclinclude=$tcl_includepath \
    --with-tkinclude=$tk_includepath \
    PKG_INCLUDES=-I"$tk_includepath_int" \
    $bits >@stdout 2>@stderr}
    } else {
    puts "+++ Running 'bash configure -srcdir=$srcdir\n\
    \ --prefix=$srcdir/cmake/runtime\n\
    \ --exec-prefix=$srcdir/cmake/runtime\n\
    \ --with-tcl=$tcl_libpath\n\
    \ --with-tk=$tk_libpath\n\
    \ --with-tclinclude=$tcl_includepath\n\
    \ --with-tkinclude=$tk_includepath\n\
    \ $bits'\n\
    \ in directory [pwd]..."
    catch {exec bash configure -srcdir=$srcdir --prefix=$srcdir/cmake/runtime \
    --exec-prefix=$srcdir/cmake/runtime \
    --with-tcl=$tcl_libpath --with-tk=$tk_libpath \
    --with-tclinclude=$tcl_includepath \
    --with-tkinclude=$tk_includepath \
    $bits >@stdout 2>@stderr}
    }
    exit 0
    ```
    と書き換えたところ、問題なくビルドできました。
    おそらく、32bit版及びarm版のビルドも可能そうなので、試してみます。
    長い間お付き合いいただきありがとうございました。
    大変助かりました。
    またお世話になることもあるかもしれませんが、その際もよろしくお願いします。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.93%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る