askopenfilename @ tkinter の話ではあるんだけれどもそうでもないような気もする

もっと大仰な見出しにしようかとも思ったんだけれどもね。

名付けるなら「コモンコントロール・シンドローム」、な悩みの話だったりする。今回の話は「聞くぞ開けファイルダイアログ」の件だけど、ほかにもカレンダとかも似た悩みを抱える。

「いつでもどこででも使うので」という動機でもって共通部品として伝統的に整備されてきたのが「コモンコントロール」であり、このノリ自体は Windows 固有のものではない。微妙な差異はあれど、「同じことを何度もプログラムする必要はなかろ」という部品化はどの OS、ウィンドウシステムにおいても行われている。

ゆえ、基本的に「コモンコントロール」は使うのがとても楽だし、「利用者がめっさ慣れとる」というデカ過ぎるメリットがある。だから、ソフトウェア開発者は、基本「サボりたければ安直にコモンコントロール」が正義だし、概ね正しい。

のであるが…。

まぁなんつーか既に同じことを感じて何十年にもなるのだが、「ファイル選択ダイアログ」って、便利でわかりやすくて使いやすいのは場合によるが実に顕著だったりすんのよね。使いやすいと感じる使い方は、「ドキュメントフォルダ」などがほとんど固定で動かさず、なおかつ「ほぼ GUI でしか生活しない」場合。そして「なめとんのかい」と不平を言いたくなるほどファイル選択ダイアログを使いにくく感じるのはコマンドライン主体のユーザ。後者は「カレントディレクトリ」主体の作業・操作となるため、開く場所は大抵「毎度違う」。つまり「いつもマイドキュメントを開く」なんて作業はまぁしない。

Windows の「ファイル選択ダイアログ」の功罪は、「レジストリを利用して、以前開いた場所を記憶してくれている」ことだったりするのだが、このことこそが「CUI での操作」ではなんともウザったい。「いや、カレントを開けよ…」と。当然「カレントを開けよ…」は、プログラマがそう指示すれば実現出来る。けれども「指示すれば」の話。それだけでなく、その「カレント」というよりは「相対パス」の扱いが、特に Windows の「ファイル選択ダイアログ」のそれは非常に不十分で、たとえばユーザがクリップボードにパスをコピーしてあるとしても、その「相対パス」は、Windows のファイル選択ダイアログは受け付けてはくれない。

さらにである。まさに「いや…クリップボードに入ってるんですけど、使いたいパスが…」なのにも関わらずダイアログが立ち上がり、なおかつ「いや、場所知ってるんすけど」にも関わらず、深くて深くて深い木構造をぷちぷち手繰るのはこれはもう「苦行」でしかない。「知ってるっての、直接入れさせろや」。

というジレンマが、そう、何十年と消えてくれないのよね。なのでGUI をサボろうとして askopenfilename を使うと、利用者にストレスを与えてしまうハメになりえて、楽をしようとしたのにムダな労力を費やしてしまいかねない、なんてこともあるかもしれない。

「サボ」る、の内容が「ソースコードの長さ」を意味しないのであれば、パスを入力するテキストボックスとブラウズボタンをセットにしたダイアログを共通化するのも手:

 1 try:
 2     import Tkinter as tkinter  # python 2.x, but sorry, I'm not testing it.
 3     def _askopenfilename(master, *args, **kwargs):
 4         import tkFileDialog
 5         return tkFileDialog.askopenfilename(*args, **kwargs)
 6     from tkSimpleDialog import Dialog
 7 except ImportError:
 8     import tkinter
 9     def _askopenfilename(master, *args, **kwargs):
10         from tkinter.filedialog import askopenfilenames
11         return askopenfilenames(*args, **kwargs)
12     from tkinter.simpledialog import Dialog
13 
14 
15 class AskOpenFilenameDialog(Dialog):
16     """
17     A pair of a text entry for path and a browse button.
18     """
19     def __init__(self,
20                  parent, title,
21                  entry_width,
22                  initialpath,
23                  *args, **kwargs):
24         self._parent = parent
25         self._entry_width = entry_width
26         self._initialpath = initialpath
27         Dialog.__init__(self, parent, title)
28 
29     def body(self, master):
30         self._pathentvar = tkinter.StringVar()
31         self._pathentvar.set(self._initialpath)
32         pathent = tkinter.Entry(
33             master, textvariable=self._pathentvar, width=self._entry_width)
34         pathent.pack(side=tkinter.LEFT)
35         browsebtn = tkinter.Button(master, text="...", command=self._browse)
36         browsebtn.pack()
37         return pathent
38 
39     def _browse(self, *args):
40         initial = self._pathentvar.get()
41         initialdir = None
42         if initial:
43             if os.path.isdir(initial):
44                 initialdir = os.path.abspath(initial)
45             else:
46                 initialdir = os.path.abspath(os.path.dirname(initial))
47         f = _askopenfilename(self._parent, multiple=0, initialdir=initialdir)
48         if f:
49             self._pathentvar.set(f[0])
50 
51     @property
52     def path(self):
53         return self._pathentvar.get()
54 
55 
56 def _my_askopenfilename(master, inidir=""):
57     dlg = AskOpenFilenameDialog(
58         master, "select video", 120, inidir)
59     return dlg.path

ただね、これ、「multiple」の意味で汎用化出来てないのよね。複数ファイルを選ばせたいようなものを作るには、もう一声以上必要。てわけで悩みは尽きない…。

ちなみに、挙げたコードはここ最近取り組んでる ffchopreview.py に組み込んだ。


2020-11-17追記: gist にあげてある方は直してあるけれど、上に貼りつけてるコード、「キャンセル? 何それ食べれんの」しちゃってて「つかえねー」。すまん、無頓着過ぎた。gist にあげたほうを見てもらえればわかるのだが、措置は「apply するまで固定すんなや」てこと、これはいつでもそう。普段はそうしてるんだけど、なんかこれ書いたときは眠かったんだろうか…。