tkDND

 tkDND(Tk Drag & Drop)は、 Tkウィジェット(GUI部品)でドラッグやドロップ、 それにドリッピーまでも可能にする拡張ライブラリです (うそつけコラ) 元々ドラッグ&ドロップというのはプラットフォーム依存が強い処理で、 WindowsとXウィンドウシステムではそれこそ見かけの動作が同じだけで、 プログラミング手法も使うAPIもまるっきり違うので、ネイティヴャのAPI で書こうとすると相当大変なはずです。 が、この tkDND は両者の違いをうまく吸収していて、 全く同じTclプログラムでWindowsでもXウィンドウシステムでもドラッグ&ドロップを簡単に実現できます。 しかも、単にTkウィジェット同士だけでなく、 Tkウィジェットと他のアプリケーションのアイコンなどとの間でドラッグ&ドロップ動作をさせることもできるのです。 これで便利なデスクトップツールをわれもわれもと作ることができますね。 まさに目指せドラッグクイーンって感じですか(おーい)
 …tkDNDは George Petasis さんによって開発されたフリーウェアのライブラリで、 このページでダウンロードできます。 またSourceForgeにもオープンソースプロジェクトとしてのページがあり、 こちらからも入手できます。 最新バージョンは2.8です。WindowsバイナリDLLも配布されており、 2015/02/28現在、32ビット版は2.8、64ビット版は2.7が配布されています。


Windows用バイナリDLLのインストール

 WindowsではバイナリDLLが配布されているので、とても簡単です。 配布アーカイブを展開して、lib\tkdnd2.7 というディレクトリを Tcl/Tk のインストールライブラリの下、 たとえば C:\Program Files\Tcl\lib\tkdnd2.7 に移せば準備はOKです。


UNIXでのインストール

 UNIXではソースアーカイブからのコンパイル・インストール作業が必要になりますが、 configure→make→make installで済むので簡単です。

% tar xozf tkdnd.tar.gz
% cd unix
% sh configure
% make
...
                  ***** tkdnd Made Successfully! ***
% su root
# make install


tkDNDでドラッグ&ドロップ

●ウィジェット同士のドラッグ&ドロップ
ではこれから、そのドリッピー、じゃなかったドラッグ&ドロップの実際のコーディングをご紹介してまいりましょう。 まずは、簡単なサンプルから。tkDNDを使うと

  • Tkウィジェット同士
  • 他のアプリケーション(エクスプローラなど)からTkウィジェットへ
  • Tkウィジェットから他のアプリケーション(圧縮解凍ツールなど)へ
のドラッグ&ドロップが実現できますが、基本は一番上のTkウィジェット同士。 というわけで、その簡単なサンプルは次のようになります。

package require tkdnd
wm title . "Example"
label .s1 -text "Source" -fg #700040 -rel raised -bd 2
label .t1 -text "Target" -fg #700040 -rel raised -bd 2

dnd bindsource .s1 text/plain {return "Hello D&D World!"}
dnd bindtarget .t1 text/plain <Drop> {showmsg "%A" "%T" "%D"}
bind .s1 <Button-1> {dnd drag %W}

pack .s1 .t1 -side top -ipadx 20 -pady 10

proc showmsg {action type data} {
    tk_messageBox -message "Action:$action Type:$type Data:$data"
}
# end.

スクリーンショット

tkDNDで最低限使い方を覚えておくべきコマンドは dnd bindsource、 dnd bindtarget、 dnd dragの3つです。使い方の手順としては、

  1. ドラッグ元(ラベル、エントリ、キャンバス、リストボックスなど) を作ります。
  2. ドロップ先(同)を作ります。
  3. ドラッグ元に対し、dnd bindsource コマンドを使い、「D&Dデータ」を定義します。
  4. ドラッグ先に対し、dnd bindtarget コマンドを使い、ドロップされたときの処理を書きます。
  5. ドラッグ元の何かのイベント(典型的には<Button-1>です) に対し、dnd dragコマンドを結び付けて、 その操作を行ったときにドラッグ動作が始まることを指示します。
1. と 2. は別段配慮は不要で、普通の感じでウィジェットを作ればよいです。 上の例ではドラッグ元もドロップ先もラベルですが、 当然他のウィジェットも使えます (通常のデスクトップツールを考えると、ドラッグ元はリストボックス、 ドロップ先はキャンバス、テキスト、リストボックスなどが典型的でしょう)

でいよいよ問題の 3. ですが、

dnd bindsource .s1 text/plain {return "Hello D&D World!"}

.s1はドラッグ元のウィジェット名、text/plainは D&Dタイプと呼ばれるもので、 その後ろのTcl文がD&Dデータの定義です。 D&Dタイプはデフォルトでは
  • text/plain
  • text/plain;charset=UTF-8
  • text/uri-list
の3つが定義されていて、より詳しくは後述します。 D&Dデータの定義文は、必ずreturnコマンドを使うことになります。

次に 4. のドロップ先は

dnd bindtarget .t1 text/plain <Drop> {showmsg "%A" "%T" "%D"}

こんな感じになります。.t1はドロップ先のウィジェット名、 text/plain はD&Dタイプです。このD&Dタイプはドラッグ先のそれと一致していないとドラッグ&ドロップできないので注意が必要です。 その後ろの<Drop>は最初は決め打ちでよいでしょう。 最後のTclスクリプトがドロップされたときの実行コマンドです。 bindコマンドのような%文字列が使えます。一例を挙げると

  • %A : アクション (copy、move、link、askがあります。この例では必ずcopyになります)
  • %T : D&Dタイプ (上の例では text/plain )
  • %D : D&Dデータ (上の例では Hello, D&D World!)
  • %W : ドロップ先のウィジェット名 (上の例では .t1)
つまり、ウィジェット .t1 にD&Dデータ 'Hello, D&D World!' がドロップされたときの処理を書けばよいわけです。

最後の5. はこの通り決め打ちでよいでしょう。

bind .s1 <Button-1> {dnd drag %W}

 さて今度はもうちょっと進展させてみます。あー進みが遅いとか言わないように! 進展のプロセスを楽しむのが大人の味わいというではないですか。
…言いませんね。 ともかく、Windowsエクスプローラのように、 シフトキーやコントロールキーを押しながらドラッグ&ドロップすると操作が変えられるようにしてみます。

package require tkdnd

label .s1 -text "Source" -fg #700040 -rel raised -bd 2
label .t1 -text "Target" -fg #700040 -rel raised -bd 2

dnd bindsource .s1 text/plain {return "Hello, D&D World!"}
dnd bindtarget .t1 text/plain <Drop> {showmsg "%A" "%T" "%D"}
dnd bindtarget .t1 text/plain <Drag> {return "copy"}
dnd bindtarget .t1 text/plain <Shift-Drag> {return "move"}
dnd bindtarget .t1 text/plain <Control-Drag> {return "link"}
bind .s1 <Button-1> {dnd drag %W -actions {copy move link}}

pack .s1 .t1 -side top -ipadx 20 -pady 10

proc showmsg {action type data} {
    tk_messageBox -message "Action:$action Type:$type Data:$data"
}

 このように、標準の「copy」アクション以外の「move」「link」アクションができるようにするには、 dnd bindtargetサブコマンドで <Shift-Drag> のようなモディファイア+Dragイベントにそのアクションをreturnするスクリプトをつけて指定し、 また dnd drag サブコマンドに「-actions {copy move link}」 を機械的につけてやればOKです。

dnd bindtarget .t1 text/plain <Shift-Drag> {return "move"}
bind .s1 <Button-1> {dnd drag %W -actions {copy move link}}

 さて今度の話題ですが、一般のアプリケーションでは、 2系統以上のドラッグ&ドロップを同時に使い分けるようにできるべきです。 その例をご紹介しましょう。 下の例は上のサンプルのドラッグ元(ソース)とドロップ先(ターゲット)がそれぞれ2個ずつ現れますが、 プログラムの指示により、ソース1からはターゲット2に、 ソース2からはターゲット1にしかドロップできないようにしています。

package require tkdnd

foreach e {{s1 {Source 1}} {s2 {Source 2}} {t1 {Target 1}} {t2 {Target 2}}} {
    set a [label .[lindex $e 0] -text [lindex $e 1] -rel raised -bd 2 \
           -fg #700040 ]
    pack $a -side top -ipadx 20 -pady 10
}

dnd bindsource .s1 TYPE1111 {return "This is Source 1"}
dnd bindsource .s2 TYPE2222 {return "This is Source 2"}
dnd bindtarget .t1 TYPE2222 <Drop> {showmsg "%T" "%D"}
dnd bindtarget .t2 TYPE1111 <Drop> {showmsg "%T" "%D"}
bind .s1 <Button-1> {dnd drag %W}
bind .s2 <Button-1> {dnd drag %W}

proc showmsg {type data} { tk_messageBox -message "Type:$type Data:$data" }
# end.

スクリーンショット

このような制御を指示するのは、dnd bindsource、dnd bindtarget サブコマンドのD&Dタイプです。 D&Dタイプの名前は「text/plain」でなくても適当な名前を自分で作れますが、 dnd bindsource と dnd bindtarget で指定したD&Dタイプが同じウィジェットの間同士でのみ、ドラッグ&ドロップ操作が可能になります。 つまりソース1はターゲット1とD&Dタイプが違うので、 これらの間ではドラッグ&ドロップはできません。


他アプリケーションからのドラッグ&ドロップ

 さて、ここからも、というかWindowsではここからが本領発揮です (もちろん、GNOMEなどのUNIX系の新進のデスクトップ環境でも同じですが)。 デスクトップのアイコンや、Windowsエクスプローラのファイルリスト、 GNOMEで置き換えるとするならば GNOME Midnight Commander (gmc) のファイルリストからマウスでぐにーと引っ張ってきて、 Tkウィジェットにドロップすることで何らかの処理をさせることができます。
 奥さん大変大変、これは大変に強力で、 これだけでもすぐにいろいろなツールのアイデアが思い浮かぶことでしょう。 というわけで、下のサンプルはムッチャ短いですが、 「メモ帳」と書かれたラベルにファイルをドロップするとメモ帳が起動する、 という立派な(?) ツールがこれで出来上がっています。 ちなみに少なくともMS-Windowsでは、 日本語のファイル名もちゃんと扱えています。

package require tkdnd
label .la -text メモ帳 -rel groove -bd 2 -fg #700040
button .cmde -text 終了 -command exit

dnd bindtarget .la text/uri-list <Drop> {invokeNotePad %D}

proc invokeNotePad filenames {
    tk_messageBox -message $filenames
    foreach e $filenames { eval exec "notepad $e &" }
}
pack .la -side top -ipadx 20 -pady 10
pack .cmde -side top

このようなツールを書くのは簡単です。単に今までのサンプルからドラッグ元を削り、 ドロップ先だけを残せばよいのです。 一点注意として、dnd bindtargetで指定するD&Dタイプとして text/uri-listを必ず選ぶ必要があります。 そうしないと他のアプリケーションからドロップ先と認識されないようです。

もうひとつ便利なツールを作る題材として、 エクスプローラからTkのキャンバスに画像ファイルをドラッグしてくることで、 その画像をキャンバスに表示するというビューアを作ってみました。 いろいろ応用が利くと思います。 標準以外の画像フォーマットを読むためになもなものpife拡張を使っています。

package require tkdnd
package require pife
image create photo Image1
label .la -width 100 -height 100 -bg #808080 -image Image1
bind .la <Button-3> exit
dnd bindtarget .la text/uri-list <Drop> {dropped %D}
proc dropped filenames {
    set filename [lindex $filenames 0]
    if {[catch { Image1 read $filename } ]} { } else {
        .la configure -width [image width Image1] -height [image height Image1]
    }
}
pack .la -side top -ipadx 20 -pady 10


他アプリケーションへのドラッグ&ドロップ

 さて逆も真なり!玉の輿あれば逆玉だってあり!(力説) というわけで、今度はTkのリストボックスでファイル名の一覧を作り、 これをマウスで他のアプリケーションにドロップしてファイルをコピーしたりアプリケーションを起動したりできるのか、 どうなのか。今、二人の愛が試される。

package require tkdnd
bind Button <Key-Return> "%W invoke"

set fe [frame .fe]
entry $fe.epwda -textvariable cwd
button $fe.cmda -text 更新 -command "updateFileList .fa.lsta"
foreach e {epwda cmda} { pack $fe.$e -side left }
set cwd [pwd]

set fa [frame .fa -rel groove -bd 2]
listbox $fa.lsta -selectmode extended -ysc "$fa.scrv set"
scrollbar $fa.scrv -ori v -command "$fa.lsta yview"
foreach e {lsta scrv} { pack $fa.$e -side left -fill y }

button .cmde -text 終了 -command exit

dnd bindsource $fa.lsta text/uri-list "return \[getFileName $fa.lsta\]"
bind $fa.lsta <Button-1> {
    if {[llength [%W curselection]] > 0} {
        dnd drag %W -actions {copy move link} }
}
bind $fa.lsta <Button1-Leave> { break }
proc getFileName lsta {
    global cwd
    if {[llength [set indexes [$lsta curselection]]] == 0} {return {}}
    set files {}
    foreach e $indexes { lappend files [file join $cwd [$lsta get $e]] }
    return $files
}
proc updateFileList lsta {
    global cwd
    $lsta delete 0 end
    foreach e [glob -nocomplain [file join $cwd *]] {
	$lsta insert end [file tail $e] }
}
pack .fe    -side top
pack .fa    -side top
pack .cmde  -side top -anc e
updateFileList .fa.lsta

スクリーンショット

コードが若干長めですが、注意する点はほとんどありません。 今度はドラッグ元だけを書けばよく、 その dnd bindsource で D&Dタイプをtext/uri-list にすること、D&Dデータとしてファイル名(またはそのリスト) を返すようにすれば万事おっけーっすよ。

dnd bindsource $fa.lsta text/uri-list "return \[getFileName $fa.lsta\]"
proc getFileName lsta {
    global cwd
    if {[llength [set indexes [$lsta curselection]]] == 0} {return {}}
    set files {}
    foreach e $indexes { lappend files [file join $cwd [$lsta get $e]] }
    return $files
}

ところで、
このようにリストボックスのオプション -selectmodeをextended にして複数の項目をシフトキーなどで選択できるようにした場合、 ドロップした後の選択状態が変になってしまいます。 これはリストボックスのデフォルトのイベント処理とぶつかってしまうためのようで、 下の文を豆知識としてご紹介しておきますね。

bind $fa.lsta <Button1-Leave> { break }

これで、デフォルトの<Button1-Leave>イベントハンドラの動作を押さえ込んでおきます。
あとは寝技で。(意味不明)


リストボックス項目のドラッグ&ドロップ

 さて少しずつでありますが佳境に近づいて参りました。 今度は少しアプリケーションフリャに迫ってみます。 これはサウンドプレイヤーのようなツールによく見られる、 演奏トラックの選択ダイアログを模したサンプルです。

package require tkdnd
wm title . "tkDND test"
option add *Listbox.font {Helvetica 12 normal}

proc createFrame {fa initlist} {
    global ListVar
    set fa [frame $fa]
    listbox $fa.lsta -selectmode extended \
            -ysc "$fa.scrv set" -xsc "$fa.scrh set" \
	    -listvar ListVar($fa.lsta) -height 6 -width 35
    scrollbar $fa.scrv -ori v -com "$fa.lsta yview"
    scrollbar $fa.scrh -ori h -com "$fa.lsta xview"
    grid $fa.lsta -row 1 -col 1
    grid $fa.scrv -row 1 -col 2 -sticky ns
    grid $fa.scrh -row 2 -col 1 -sticky ew
    set ListVar($fa.lsta) $initlist
}

createFrame .fa {}
createFrame .fb {
    {'Flowers' (Radio Edit)}
    {'Flowers' (Harry Johansen's Trance Remix)}
    {'Flowers' (Optimistic Overdub)}
    {'Flowers' (Extended Mix)}
    {'Flowers' (Original)}
}

dnd bindsource .fb.lsta text/plain "return \[getSourceData .fb.lsta\]"
dnd bindtarget .fa.lsta text/plain <Drop> "drop %W %A %T %D"
bind .fb.lsta <Button-1> { dnd drag %W }
bind .fb.lsta <B1-Leave> { break }

proc drop {target action type data} {
    global ListVar
    foreach e $data {
	if {[lsearch $ListVar($target) $e] < 0} {
	    lappend ListVar($target) $e	}}}

proc getSourceData lsta {
    set r {}; foreach e [$lsta curselection] {lappend r [$lsta get $e] }
    return $r }

button .cmde -text 終了 -command exit

pack [label .lb -text "トラック一覧:"] -side top -anc w
pack .fb
pack [label .la -text "編集:"] -side top -anc w
pack .fa
pack .cmde -anc e -padx 10 -pady 3
# end.

スクリーンショット

特に注意すべき点はありません。今までご紹介した内容を取り集めた総集編 (といっても大した量ではないですが)チックなサンプルというだけです。


そして寿司ネタへ…

そう言えば、何かを忘れていませんか?

すしねた

そうか、そういうことだったのか…(遠い目)
ついに、その時がやってきたというわけですね… あわわ、そうせがまれてせがまれてせがみ倒されてしまっては仕方ありません (誰もせがんでないってば) 次のサンプルはリストボックスの項目をキャンバスにドロップすると寿司ネタがその場に現れるというエキゾチックオリエンタルなサンプルです。 まさにジャペン!!です。(うるさいよ) ドロー系のツール、Microsoft Visio のステンシルペインからドローオブジェクトを描画領域にドラッグ&ドロップする、 あのような感じのものです。 このように、ドロップした場所の座標(x, y)を使って処理をしたいときには、 dnd bindtargetで指定するスクリプトで%xと%yを使えばOKです。

package require pife
package require tkdnd
wm title . "Don't Drop Your Sushi"
foreach e [set items {いか いわし うなぎ えび かにサラダ さば
  ねぎとろ はまち まぐろ}] {
      image create photo Images($e) -file /usr/icons/freeso/susipng/寿司$e.bmp
  }
set fa [frame .fa -rel ridge -bd 2]
listbox $fa.lsta -selectmode single -ysc "$fa.scrv set" -listvar items
scrollbar $fa.scrv -ori v -command "$fa.lsta yview"
pack $fa.lsta $fa.scrv -side left -fill y
canvas .can -bg white -width 300 -height 200
button .cmde -text 終了 -command exit

dnd bindsource $fa.lsta text/plain "return \[getSelectedItem $fa.lsta\]"
dnd bindtarget .can text/plain <Drop> "dropped .can %D %x %y"
bind $fa.lsta <1> "dnd drag %W"
proc getSelectedItem lsta {
    if {[llength [set index [$lsta curselection]]] == 0} { return "" }
    return [$lsta get $index]
}
proc dropped {can data x y} {
    set item [$can create image $x $y -anc center -image Images($data)]
    $can bind $item <Button1-Motion> "$can coords $item %x %y"
}
pack .cmde -side bottom -anc e -padx 20 -pady 5
pack .fa .can -side left
# end.

スクリーンショット

スクリーンショット左のリストボックスから、 右のキャンバスにドラッグ&ドロップしてアイコンを表示させることができます。 おお、寿司がいっぱいだ。 なおこのアイコンはKAZUTAKAさんの アイコンミュージアム で公開されているものです。 あまりに美味しそうなため、またしても使わせて頂きました。

拡張レビュー分室 top
(first uploaded 2001/05/01 last updated 2015/02/28, MISUMI URANO)