9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

目的

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

ことの経緯

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

実装

最初の実装

StackOverflowで見つけた記事 Python tkinter text modified callbackを参考にEntry中のcreateCommandを監視し、"insert", "delete", "replace"の3つのコマンドが発行されたら<<TextModified>>イベントを発生させるようにした。

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も関係ないので例外処理をしなくても良い。
この方法でカスタムコンポーネントを作ってみる。

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の紐づけに限定しているのでそこまで気にする必要はないが、気に留めておく必要はある。

実行可能サンプルコード

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()
9
9
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?