目的
Tcl/Tkには他のGUIインターフェースを持つ言語によくある『テキストボックスの中身が変更されたら発生するイベント』が元から実装されていない。よって、なるべく簡便に実装できる方法を考え、実際に作ってみる。
ことの経緯
Teratailのこの記事
バーコードリーダからキーボードインタフェースで入力されるデータを監視し、Entryにバーコードデータが入ったら自動で次のEntryにフォーカスを持っていきたいという話。
バーコードリーダーは機器側の設定でデータの末尾に改行を付与することができるので、テキストの変更を検知して末尾の改行を読み取ればよい。
実装
最初の実装
StackOverflowで見つけた記事 Python tkinter text modified callbackを参考にEntry中のcreateCommand
を監視し、"insert", "delete", "replace"の3つのコマンドが発行されたら<<TextModified>>
イベントを発生させるようにした。
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も関係ないので例外処理をしなくても良い。
この方法でカスタムコンポーネントを作ってみる。
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の紐づけに限定しているのでそこまで気にする必要はないが、気に留めておく必要はある。
実行可能サンプルコード
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()