ろうとるがPythonを扱う、、（その31：tkinterドラッグアンドドロップ処理で気がついた）

# ドラッグアンドドロップされたテキストファイルを表示
tkinterにて、ドラッグアンドドロップされた複数のテキストファイルの内容をWindow（テキストエリア）に表示するだけだが、例外ケースが多少複雑だったので記録。なお、ここでは、テキストファイル以外のファイル読み込み時のエラー処理は考慮していない。

## 結果
Windowにファイルをドラッグアンドドロップすると下記のように表示するもの。

![Capture1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/2723cef6-1c16-bed2-b25a-9651dd575f75.png)

## 最初は単純
### ソースコード

```python:DragAndDropFile0.py
# -*- coding: utf-8 -*-
###########################################################
# ドラッグアンドドロップされたテキストファイルの内容を表示
###########################################################

###########################################################
# Library Import
###########################################################
import tkinter as tk
import tkinterdnd2 as tkdnd				# tkinterにてDrag&Drop利用
  
###########################################################
# Drag&Drop時にCallされる関数
###########################################################
def ListAndOpenFile0(event):
  target_files = []						# Open/ReadするFileのリスト
  if event.data:						# Drag&DropされたFile名の文字列
    textarea.configure(state='normal')	# 表示エリアを書き込み可能へ
    textarea.delete('1.0', 'end')		# 表示エリアをクリア
    print('event.data:', event.data)	# @A Debug
    s = event.data						# @B Debug
    len_s = len(s)						# @B Debug
    for i in range(len(s)):				# @B Debug
      print(i, s[i], hex(ord(s[i])))	# @B Debug
    target_files = event.data.split()	# File名の分割
    print('File List:', target_files)	# @C Debug
    for file in target_files:			# Fileごとの処理
      textarea.insert('end', '<< File: '+file+' >>\n')	# File名表示
      fd = open(file, 'r', encoding='utf-8')	# File Open
      for line in fd:					# 1行ずつ処理
        textarea.insert('end', line)	# テキストエリアに1行表示
      textarea.insert('end', '\n\n')	# File最後に空行挿入
      fd.close()						# File Close
    textarea.configure(state='disabled')# 表示エリアの書き込み禁止
  return event.action

###########################################################
# Main
###########################################################
root = tkdnd.TkinterDnD.Tk()					# Drag&Drag用の設定
root.title("Show Content of Drag&Drop Files")	# Windowタイトル
root.geometry("600x400")						# Windowサイズ

## 表示エリア設定
frame =tk.Frame(root)
textarea = tk.Text(frame, height=30, width=80)	# 表示（テキスト）エリア
textarea.drop_target_register(tkdnd.DND_FILES)	# Drag&Drag用の設定
textarea.dnd_bind('<<Drop>>', ListAndOpenFile0)	# Drag&Drag時にCallされる関数の定義
scroll = tk.Scrollbar(frame, orient=tk.VERTICAL)# 縦スクロールバー
textarea.configure(yscrollcommand=scroll.set)	# スクロールバー設定
textarea.configure(state='disabled')			# 表示エリアの書き込み禁止
scroll.config(command=textarea.yview)			# スクロールバーを表示エリアとリンク

## Widget表示
frame.pack()
textarea.pack(side=tk.LEFT)
scroll.pack(side=tk.RIGHT, fill=tk.Y)

## Loop
root.mainloop()
```

- ドラッグアンドドロップ時にコールされる関数ListAndOpenFile0()
-  ”event.data”にドラッグアンドドロップされたファイル名の文字列が格納
- 文字列では、空白によりファイル名が分割
    - ”event.data.split()”によりファイル名の分割を実施
- ”@A Debug”、”@B Debug”、”@C Debug”は、後述する不具合の状況確認用
- 本質的に、ファイル名のリスト化は不要だが、他のコードへの転用のため、ここではリスト化実施
    - ファイル名取得ごとにファイル読み込み、テキストエリアへ表示

その他の内容およびtkinter部分にについては、コード中のコメントを参照。

### 何が問題か？
ファイル名に特殊文字があると、不具合が起こる。上記”@A Debug”、”@B Debug”、”@C Debug”により状況確認。

#### ケース１：ファイル名に空白が含まれると”{}”で囲まれる
この場合、ドラッグアンドドロップされた文字列が”{}”で囲まれる。下記では、2つのファイルをドラッグアンドドロップしている。

```shell-session
C:\t>python DragAndDropFile0.py
event.data: C:/t/+.txt {C:/t/ .txt}
0 C 0x43
1 : 0x3a
2 / 0x2f
3 t 0x74
4 / 0x2f
5 + 0x2b
6 . 0x2e
7 t 0x74
8 x 0x78
9 t 0x74
10   0x20
11 { 0x7b
12 C 0x43
13 : 0x3a
14 / 0x2f
15 t 0x74
16 / 0x2f
17   0x20
18 . 0x2e
19 t 0x74
20 x 0x78
21 t 0x74
22 } 0x7d
File List: ['C:/t/+.txt', '{C:/t/', '.txt}']
Exception in Tkinter callback

...

    fd = open(file, 'r', encoding='utf-8')
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OSError: [Errno 22] Invalid argument: '{C:/t/'
```

Openするファイル名が不適切。2番目のファイルに空白が含まれ、split()によりそのファイルが分割されてしまい、ファイルリスト（File List）にファイル3つ分が格納されてしまう。

#### ケース２：空白で終わるファイルは”{}”で囲まれない
こちらも2つのファイルをドラッグアンドドロップしている。

```shell-session
C:\t>python DragAndDropFile0.py
event.data: C:/t/\}\  C:/t/\{
0 C 0x43
1 : 0x3a
2 / 0x2f
3 t 0x74
4 / 0x2f
5 \ 0x5c
6 } 0x7d
7 \ 0x5c
8   0x20
9   0x20
10 C 0x43
11 : 0x3a
12 / 0x2f
13 t 0x74
14 / 0x2f
15 \ 0x5c
16 { 0x7b
File List: ['C:/t/\\}\\', 'C:/t/\\{']
Exception in Tkinter callback

...

    fd = open(file, 'r', encoding='utf-8')
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OSError: [Errno 22] Invalid argument: 'C:/t/\\}\\'
```

Openするファイル名が不適切。1番目のファイル名の最後に空白がある（index 8がその空白、index 9はファイル名の区切り文字）。最後に空白がある場合、”{}”で囲まれないようだ。

#### ケース３：空白が途中にあっても”{}”で囲まれない
なぜこのようなことが発生するか不明だが、トライ＆エラー中に見つけたもの。

```shell-session
C:\t>python DragAndDropFile0.py
event.data: C:/t/ト\ \}ド.txt
0 C 0x43
1 : 0x3a
2 / 0x2f
3 t 0x74
4 / 0x2f
5 ト 0x30c8
6 \ 0x5c
7   0x20
8 \ 0x5c
9 } 0x7d
10 ド 0x30c9
11 . 0x2e
12 t 0x74
13 x 0x78
14 t 0x74
File List: ['C:/t/ト\\', '\\}ド.txt']
Exception in Tkinter callback

...

    fd = open(file, 'r', encoding='utf-8')
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'C:/t/ト\\'
```

ファイルが存在しない。

#### ケース４：バックスラッシュが挿入されてしまうファイル
ドラッグアンドドロップで空白が含まれている場合に囲む文字（'{'や'}'）をファイル名に入れた時に見つけた不具合。

```shell-session
C:\t>python DragAndDropFile0.py
event.data: C:/t/\}\}
0 C 0x43
1 : 0x3a
2 / 0x2f
3 t 0x74
4 / 0x2f
5 \ 0x5c
6 } 0x7d
7 \ 0x5c
8 } 0x7d
File List: ['C:/t/\\}\\}']
Exception in Tkinter callback

...

    fd = open(file, 'r', encoding='utf-8')
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'C:/t/\\}\\}'
```

ファイルが存在しない。

## 今のところ不具合なしのソースコード

```python:DragAndDropFile1.py
# -*- coding: utf-8 -*-

###########################################################
# ドラッグアンドドロップされたテキストファイルの内容を表示
###########################################################

###########################################################
# Library Import
###########################################################
import tkinter as tk
import tkinterdnd2 as tkdnd				# tkinterにてDrag&Drop利用

###########################################################
# 定義
###########################################################
SPACE = ' '
BACKSLASH = '\\'						# EscapeとしてのBackslash追加
START_CURLYBRACKET = '{'
END_CURLYBRACKET = '}'

###########################################################
# Drag&Drop時にCallされる関数
###########################################################
def ListAndOpenFile1(event):
  target_files = []						# Openするファイルのリスト 
  if event.data:						# Drag&DropされたFile名の文字列
    textarea.configure(state='normal')	# 表示エリアを書き込み可能へ
    textarea.delete("1.0","end")		# 表示エリアをクリア
    s = event.data						# Drag&Dropされた文字列の置き換え
    len_s = len(s)						# 文字列の長さ
    #for i in range(len(s)):
    #  print(i, s[i], hex(ord(s[i])))
    
    # << A. 文字列探索 >>
    ref_pt = 0							# 検索基準となる参照Point
    while ref_pt < len_s:				# 文字列の最後まで実行
      # 1. 文字列からFile名を分割
      s_pt = ref_pt						# 検索Point
      if s[ref_pt] == START_CURLYBRACKET:	# '{}'で囲まれたFile名
        while True:
          end_idx = s.find(END_CURLYBRACKET, s_pt)	# '}'を探す
          if end_idx == len_s-1:		# 文字列最後のData
            file = s[ref_pt:end_idx+1]	# File名取得
            break						# 次のFile名を探すためbreak
          elif s[end_idx+1] == SPACE:	# 次の文字が区切り文字SPACE
            file = s[ref_pt:end_idx+1]	# File名取得
            break						# 次のFile名を探すためbreak
          else: 						# File名中に'}'あり、次のend_idxを探す
            							# 必ず'}'はあり、見つからない（end_idx = '-1')はありえない
            s_pt = end_idx+1			# 検索Pointを'}'の次へ
        ref_pt = end_idx+2				# 次のFile名検索、参照Point移動
      else:								# '{}'で囲まれていない場合、区切り文字の空白を探す
                						# File名に空白があると'\ 'となる？
        while True:
          end_idx = s.find(SPACE, s_pt)	# 空白を探す
          if end_idx == -1:				# 空白なしは最後のFileのはず
            file = s[ref_pt:len_s]		# File名取得
            break						# 次のFile名を探すためbreak
          elif s[end_idx-1] == BACKSLASH:	# File名に'\ 'あり（連続空白も対応）
            s_pt = end_idx+1			# 検索Pointを'\ 'の次へ
          else:
            file = s[ref_pt:end_idx]	# File名取得
            break						# 次のFile名を探すためbreak
        if end_idx == -1:				# もう空白が存在しない
          ref_pt = len_s				# 次のFile名検索、参照Pointを文字列最後へ
        else:
          ref_pt = end_idx+1			# 次のFile名検索、参照Point移動

      # 2. 1つのFile名の確定化
      # 2.1 File名が{}で囲まれている場合{}を削除
      if file.startswith(START_CURLYBRACKET):	# '{'で始まる
        file = file[1:-1]				# 前後の'{'、'}'を取り除く
      # 2.2 FileからBackslash削除（ファイルopen時に不要）
      s_pt = 0							# 検索Point
      openFile = ''						# OpenするFile名初期化
      while True:
        found = file.find(BACKSLASH, s_pt)	# Backslashを探す
        if found == -1:					# Backslash見つからず
          openFile += file[s_pt:len(file)]	# 検索Pointから最後までを格納
          break							# 次のFile名検索、参照Pointを文字列最後へ
        else:
          openFile += file[s_pt:found]	# Backslashまでを格納
          s_pt = found+1				# 検索ポイントを'\'の次へ
      # Openする１つのFile名の確定
      #print('openFile', openFile)

      # 3. 確定したFile名のリスト化（Open用）
      target_files.append(openFile)		# File名リストへ追加
    # End of While ref_pt < len_s: すべての文字列探索完了
    # File名のリスト化完了
    
    # << B. File読み込み(Open)＆表示 >>
    for file in target_files:			# Fileごとの処理
      textarea.insert('end', '<< File: '+file+' >>\n')	# File名表示
      fd = open(file, 'r', encoding='utf-8')	# File Open
      for line in fd:					# 1行ずつ処理
        textarea.insert('end', line)	# テキストエリアに1行表示
      textarea.insert('end', '\n\n')	# 最後に空行挿入
      fd.close()						# File Close
    textarea.configure(state='disabled')# 表示エリアの書き込み禁止
  return event.action

###########################################################
# Main
###########################################################
root = tkdnd.TkinterDnD.Tk()					# Drag&Drag用の設定
root.title("Show Content of Drag&Drop Files")	# Windowタイトル
root.geometry("600x400")						# Windowサイズ

## 表示エリア設定
frame =tk.Frame(root)
textarea = tk.Text(frame, height=30, width=80)	# 表示（テキスト）エリア
textarea.drop_target_register(tkdnd.DND_FILES)	# Drag&Drag用の設定
textarea.dnd_bind('<<Drop>>', ListAndOpenFile1)	# Drag&Drag時にCallされる関数の定義
scroll = tk.Scrollbar(frame, orient=tk.VERTICAL)# 縦スクロールバー
textarea.configure(yscrollcommand=scroll.set)	# スクロールバー設定
textarea.configure(state='disabled')			# 表示エリアの書き込み禁止
scroll.config(command=textarea.yview)			# スクロールバーを表示エリアとリンク

## Widget表示
frame.pack()
textarea.pack(side=tk.LEFT)
scroll.pack(side=tk.RIGHT, fill=tk.Y)

## Loop
root.mainloop()
```

ドラッグアンドドロップ時にコールされる関数ListAndOpenFile1()について、ポイントとなるところを記載。
- << A. 文字列探索 >> ドラッグアンドドロップされた文字列を文字ごとにチェック
    - １. 文字列からFile名を分割
        - 空白ありファイル名の場合に囲まれる’{’で始まるケースでは、終わりの’}’を探し、’{’から’}’までの文字列を仮のファイル名とする
        - 通常のファイル名の場合、ファイル名の区切りとなる空白’ ’を探し、空白の前までの文字列を仮のファイル名とする
        - ただし、この場合でもファイル名に空白があると’\ ’となるバックスラッシュが空白の前に付加されるようなので、バックスラッシュを取り除く
        - 仮のファイル名：file
    - ２. 1つのFile名の確定化
        - 2.1 ファイル名が{}で囲まれている場合{}を削除
        - 2.2 FileからBackslash削除（ファイルopen時に不要）
        - 確定したファイル名：openFile
    - ３. 確定したFile名のリスト化（Open用）
        - リスト：target_files[]
- << B. File読み込み(Open)＆表示 >> ファイルごとの操作（テキストエリアへ表示）
    - ファイル名表示
    - 1行ごとに表示

コード中の説明も参照。今のところ、このコードで動作している。

## 実はまだ不具合というかどうしようもないケースあり
本内容は、Windows環境で実施している。Windows上のWSLで下記ファイル（例：空白のみのファイル名）を作成したときなどは、不具合が発生している。

```shell-session
$ echo abc > '    '
```

WindowsのExplorer、コマンドプロンプト、Power Shellでは、こういったファイルは作成できないようだ。上記ファイルをドラッグアンドドロップし、openしようとすると、下記エラーが発生する。

```
PermissionError: [Errno 13] Permission denied: 'C:/t/t/    '
```

Windowsのメモ帳やエディタで開くことはできないが、WSL上では、下記のように開くことができる。

```shell-session
$ cat '    '
123
456
```

「' 22 '」というファイル名（数字の22の前後に空白が存在）に対して、実施したときもエラーが発生する。

```
FileNotFoundError: [Errno 2] No such file or directory: 'C:/t/t/ 22 '
```

いずれにせよ、メモ帳などでも開けないので、これらファイルに関しては、Give Up。

なお、他の環境では、異なる結果となる可能性があるだろう。

EOF