Python+TkでD&D

標準ライブラリリファレンスを読むとTkdndが使えるみたいなことが書いてあるが、リファレンスには説明がなくソースを読めと言わんばかりの状態。Tkdnd.pyを見ても要素をウィンドウ間でやり取りできるがD&Dされたファイルの情報の受け取り方はさっぱり。結局win32 API直接呼出しという品のない方法に。
基本方針としては、以下のとおり

  1. 何はなくともWindowハンドルの取得
  2. DragAcceptFilesでD&Dを許可
  3. GetWindowLongWでWindowプロシージャを取得
  4. SetWindowLongWでD&D用のコールバック関数を登録、この際コールバック関数内でTkに関するものをいじるともれなく落ちるのでモジュールスコープの変数かクラス変数に保存する
  5. 保存先をチェックしてTk側に反映させる関数をTk.after()で定期的に実行させる

コードとしてはこんな感じ

import Tkinter
from Tkconstants import *
import ctypes
from ctypes.wintypes import HWND, UINT, WPARAM, LPARAM

prototype = ctypes.WINFUNCTYPE(ctypes.c_long, HWND, 
                                UINT, WPARAM, LPARAM)
WM_DROPFILES = 0x0233
GWL_WNDPROC = -4
WINPROC = None

def py_drop_func(hwnd, msg, wp, lp):
    u"""D&D用のコールバック
    ファイルのドラッグアンドドロップイベント(WM_DROPFILES)を検出して、
    ドロップされたファイル名を保持する。
    ここでウィンドウ(tk)を使用するとハングアップするのでデータ保存だけ行う。
    """
    if msg == WM_DROPFILES:
        i = ctypes.windll.shell32.DragQueryFile(wp, -1, \
                                        None, None)
        print i
        for tmp in range(i):
            szFile = ctypes.c_buffer(260)
            ctypes.windll.shell32.DragQueryFile(wp, \
                    tmp , szFile, ctypes.sizeof(szFile))
            TkApp.dropnames.append(\
                                szFile.value.decode(\
                                    sys.getfilesystemencoding()))
        ctypes.windll.shell32.DragFinish(wp)
    return ctypes.windll.user32.CallWindowProcW(\
                    WINPROC, hwnd, msg, wp, lp)

class TkApp(Tkinter.Frame):
    dropnames = []
    
    def __init__(self, *args, **kargs):
        Tkinter.Frame.__init__(self, *args, **kargs)
        self.createwidget()

        def drop_check():
            if TkApp.dropnames:
                tmp = TkApp.dropnames
                TkApp.dropnames = []
                self.open(tmp)
            self._root().after(10,drop_check)
        
        self._root().after(10,drop_check)

    def createwidget(self):
        u"何かウィジット作成"
        pass

    def open(self, filenames):
        u"ファイルを開く"
        pass

if __name__ == "__main__":
    a = TkApp()
    hwnd = a._root().winfo_id()
    ctypes.windll.shell32.DragAcceptFiles(hwnd, True)
    WINPROC = ctypes.windll.user32.GetWindowLongW(\
                        hwnd, GWL_WNDPROC)
    drop_func = prototype(py_drop_func)
    ctypes.windll.user32.SetWindowLongW(hwnd, \
                        GWL_WNDPROC, drop_func)
    a.mainloop()

これでファイルをD&Dされたとき反応できる。

2019/09/08追記

新しめの環境にも対応できるよう書き直しました。
masahero.hatenablog.jp