tkinter の Entry で「changed」がない、のはまぁいいのだが。

よくない?

まぁやりたいことによるんだわ。元も子もないことを言えば、入力テキストを要求するコントロールがある際に、これへの「正しい入力を前提にしてなんらかのアクションを実行する」のに相応しいタイミングは、これはいつだって「なんらかのアクションを実行してくれよ、と頼まれたとき」であって、その実行タイミングをソフトウェアが勝手に判断するのはいつでも何かしらの問題を抱え込む。つまり「そのテキスト入力コントロール操作中」であることはなくて、たとえば「実行ボタンが押されたとき」である。入力チェックも同じね。一番適切なのは「編集真っ最中」ではなくて、「さぁ歌えや踊れや」とユーザが明示的に指示してくれた場合。つまりこのタイミングというのは、ユーザも「準備万端なのだ、さぁ、さぁ」という相思相愛状態なわけだ、ソフトウェア側の一方的な片思いではない。

ただそれは、「常に最適とは限らない」という話であって、入力の中途にアクションを起こす必要がある、もしくは「そうすると便利」なことも多いわけだ。最近でよくお目にかかるのは無論検索ボックスの「サジェッション」で、ユーザの入力を「予想」して候補を選定するというアクションを、「ユーザが絶賛入力中に」ガンガン実行するわけだ。

tkinter なのだが、そうしたことを行うのに「最底辺」のシカケはあるにはあるわけだが、なんとも色々と中途半端で、とても使いにくい。「中途入力/入力終了」のどちらを扱いたくてもどちらもなんだか不完全で困る。特に後者が困る。これに追記した通りなのだが、「FocusOut」が通知されるタイミングが、一つ致命的に欠落しているので、結論としては「テキストへの入力が終了した」という判定でもって入力テキストの「完全性」を担保することは出来ない。そう、それに完全性を求めるのであれば「実行ボタンが押されたらチェックするとかいろいろ」とする必要がある。この stackoverflow での議論はそこまでのことは踏まえていないため、あたかも「trace/trace_add は不完全だから FocusOut まんせー」と言っちゃいかねないが、そうではないわけである。

「絶賛編集中」を追跡するのに「trace」を使えるので、前者(中途入力)をとらえ続けて「タイムアウトを上手に利用」することで「編集を終えたに違いない」を判定する、という発想で「changed、というか paused writing」:

 1 try:
 2     import Tkinter as tkinter  # python 2.x, but sorry, I'm not testing it.
 3 except ImportError:
 4     import tkinter
 5 
 6 class _MyStringVar(tkinter.StringVar):
 7     _WRITING_TIMEOUT = 500
 8     def __init__(self, observer, paused_writing_callback, master=None, value=None, name=None):
 9         tkinter.StringVar.__init__(self, master, value, name)
10         if not hasattr(self, "trace_add"):
11             def _trace(mode, callback):
12                 mode = {"write": "w", "read": "r", "undefine": "u"}[mode]
13                 self.trace(mode, callback)
14             self.trace_add = _trace
15         self.trace_add("write", functools.partial(self._track_writing, "trace"))
16         self._observer = observer  # the target for invoking "after"
17         self._paused_writing_callback = paused_writing_callback
18         self._now_writing = None
19 
20     def _track_writing(self, entrypoint, *args):
21         f = functools.partial(self._track_writing, "after")
22         setattr(f, "__name__", "_MyStringVar_track_writing_after")
23         if entrypoint == "trace":  # now writing
24             if self._now_writing:
25                 self._observer.after_cancel(self._now_writing)
26             self._now_writing = self._observer.after(self._WRITING_TIMEOUT, f)
27         else:
28             self._paused_writing_callback("paused writing")
29             self._now_writing = None

「stop writing」とか「finish writing」ではないよ。うーん、と考えながら入力して立ち止まれば「ささ、終わったよね、終わったんでしょ?」と早とちってしまうわけだが、要は「それが許される場合に使う」ものね。StringVar とかの「Variable」の考え方がわかってないとわからんかもしれん。今プログラミング教育では「MVC」とかって教えるのかな? ~Var は tcl/tk における「M」(Model)にあたるものね。どこかにキレイに説明してくれてる人はきっといると思うよ、わからない人は探してね。上の「_MyStringVar」は StringVar の代わりに使える、てもの。一応 ffchopreview.py に実際に組み込んだが、ほかにやってることが多いのでわかりにくいかもしらん、すまんね。

ワタシのこれを必要とする動機は「時刻操作のエントリがあり、それをイジくるとビデオの対応時刻部分のサムネイルを生成する」というもので、カーソルキーとかで操作できるようにしてるので、「カーソルキーを動かせばサムネイルが刻々変わる」というようにしたかったということ。これを導入前は「編集→タブキーでフォーカスアウト→タブキーでフォーカスイン→編集」という操作を余儀なくされていたが、これがまぁ繰り返すとかなり指がつる。

実は最初「after_cancel」に気付かずに、間違った投稿を仕立て上げてしまうところだったが、「after_cancel」の存在のおかげでワタシの狙い通りのものになった。このワタシの早とちりを観察したければ、ffchopreview.py の「Rev147→148」参照。それよか「__name__がないんだからあんた死ねばいいと思うよ」問題ね。after に渡すコールバックは「__name__」が必要。これが地味に毎度毎度ウザい。特にワタシはここんとこ functools.partial を多用してるので。