【Python3】tkinterでonChange的なイベントを捕まえたい

# 目的
Tcl/Tkには他のGUIインターフェースを持つ言語によくある『テキストボックスの中身が変更されたら発生するイベント』が元から実装されていない。よって、なるべく簡便に実装できる方法を考え、実際に作ってみる。

## ことの経緯
[Teratailのこの記事](https://teratail.com/questions/268691)
バーコードリーダからキーボードインタフェースで入力されるデータを監視し、Entryにバーコードデータが入ったら自動で次のEntryにフォーカスを持っていきたいという話。
バーコードリーダーは機器側の設定でデータの末尾に改行を付与することができるので、テキストの変更を検知して末尾の改行を読み取ればよい。

# 実装

## 最初の実装
StackOverflowで見つけた記事 [Python tkinter text modified callback](https://stackoverflow.com/questions/40617515/python-tkinter-text-modified-callback)を参考にEntry中の`createCommand`を監視し、"insert", "delete", "replace"の3つのコマンドが発行されたら`<<TextModified>>`イベントを発生させるようにした。

```python:CustomEntry
import tkinter as tk
# OnChangeを実装したEntryクラス
class CustomEntry(tk.Entry):
    def __init__(self, *args, **kwargs):
        """A Entry widget that report on internal widget commands"""
        tk.Entry.__init__(self, *args, **kwargs)

        # create a proxy for the underlying widget
        self._orig = self._w + "_orig"
        self.tk.call("rename", self._w, self._orig)
        self.tk.createcommand(self._w, self._proxy)

    # tkからコマンドが発行されるたびに呼び出される
    def _proxy(self, command, *args):
        cmd = (self._orig, command) + args
        print(cmd)
        # 発生理由のわからない例外が出てくるのでそういうのは全て握りつぶす
        try:
            result = self.tk.call(cmd)
        except tk.TclError as ex:
            return None
        # 文字の変更に関するコマンドが発行された場合、TextModifiedイベントを発火させる
        if command in ("insert", "delete", "replace"):
            self.event_generate("<<TextModified>>")
        return result
```
発生したコマンドはうまく`_proxy`メソッドに入ってくるし、イベントも正常にgenerateされているのだが、一部のinsertコマンドを元のEntryにcallすると例外が発生する。なぜだ。
仕方がないので`try-except`で発生した例外を握りつぶしているが気持ち悪い実装になっている。本当に握りつぶして万事OKとも思えない。
また、実際Entryの中身に変化がなくても（カーソルが動いたりするだけで）イベントが発生する。それもよろしくない。

## traceを使った実装

コメントでEntry内のテキストデータをStringVarとバインドすると`trace`メソッドで変更を監視できるという話をいただいた。
この方法であればテキストデータの変更だけを監視できるし、callも関係ないので例外処理をしなくても良い。
この方法でカスタムコンポーネントを作ってみる。

```python:ModifiedEntry
import tkinter as tk
'''
ModifiedEntryクラス
元のEntryクラスと同様にふるまう
中のテキストが更新されると、"<<TextModified>>"イベントが発生する。
'''
class ModifiedEntry(tk.Entry):
    def __init__(self, *args, **kwargs):
        tk.Entry.__init__(self, *args, **kwargs)
        self.sv = tk.StringVar()
        self.sv.trace('w',self.var_changed)
        self.configure(textvariable = self.sv)

    # argsにはtrace発生元のVarの_nameが入っている
    # argsのnameと内包StringVarの_nameが一致したらイベントを発生させる。
    def var_changed(self, *args):
        if args[0] == self.sv._name:
            s = self.sv.get() 
            self.event_generate("<<TextModified>>")
```
コードも小さくなってスッキリ。
使い方は同じ。
StringVarには自動で名前`_name`が設定される。アプリ全体で被ることはないと思われるので大丈夫だろう。

### traceの仕組みについて
`trace`はStringVarのメソッド。StringVarの内容の変化をキャッチするので紐づけた先のEntryには直接関係はない。
StringVarは複数のコンポーネントと関連付けられるので、**どのコンポーネントで入力されたか**まではわからない。
今回はStringVarとEntry　1対1の紐づけに限定しているのでそこまで気にする必要はないが、気に留めておく必要はある。

# 実行可能サンプルコード
```python
import tkinter as tk
'''
ModifiedEntryクラス
元のEntryクラスと同様にふるまう
中のテキストが更新されると、"<<TextModified>>"イベントが発生する。
'''
class ModifiedEntry(tk.Entry):
    def __init__(self, *args, **kwargs):
        # Entry自体の初期化は元のクラスと同様。
        tk.Entry.__init__(self, *args, **kwargs)
        self.sv = tk.StringVar()
        # traceメソッドでStringVarの中身を監視。変更があったらvar_changedをコールバック
        self.sv.trace('w',self.var_changed)
        # EntryとStringVarを紐づけ。
        self.configure(textvariable = self.sv)

    # argsにはtrace発生元のVarの_nameが入っている
    # argsのnameと内包StringVarの_nameが一致したらイベントを発生させる。
    def var_changed(self, *args):
        if args[0] == self.sv._name:
            s = self.sv.get() 
            self.event_generate("<<TextModified>>")

# サンプルフレームアプリ
class SampleApp(tk.Tk):

    def __init__(self, *args, **kwargs):
        # Entry自体の初期化は元のクラスと同様。
        tk.Entry.__init__(self, *args, **kwargs)
        self.sv = tk.StringVar()
        # traceメソッドでStringVarの中身を監視。変更があったらvar_changedをコールバック
        self.sv.trace('w',self.var_changed)
        # EntryとStringVarを紐づけ。
        self.configure(textvariable = self.sv)
    
    # <<TextModified>>が発生したら呼ばれるメソッド
    def testevent(self,event):
        print(event)

if __name__ == "__main__":
    app = SampleApp()
    app.mainloop()
```