2
0

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.

`tkinter`の`Entry`にバリデーションを設定する アップデート版

Last updated at Posted at 2021-05-09

要約

以前に書いたtkinterにバリデーションを設定する記事のコードを

  • 型アノテーションを使う
  • Entryを継承したバリデーター付きEntryを作るのではなく、バリデーションを設定する関数を実装する

ように書き直してみた。
バリデーションを登録するためのregisterメソッドvcmdオプションvalidateオプションについては変化がないので下記記事を参照。

コード全容

コード概要

列挙体

enumライブラリを使い、定数は列挙体にした。
ちなみにstrEnumを多重継承すると、(IntEnum.valueを参照しなくてもintと値の比較が可能なように)文字列型と.valueを使わなくても比較が可能になる。

tk_widget_utils/entries.py
class _StrEnum(str, Enum):
    """Enum where members are also (and must be) strs"""

    pass


class ActionType(IntEnum):
    INSERT = 1
    DELETE = 0
    FOCUS_OR_FORCE = -1


class TriggerType(_StrEnum):
    FOCUS_IN = "focusin"
    FOCUS_OUT = "focusout"
    FORCED = "forced"
    KEY = "key"


class ValidationType(_StrEnum):
    ALL = "all"
    FOCUS = "focus"
    FOCUS_IN = "focusin"
    FOCUS_OUT = "focusout"
    FORCED = "forced"
    KEY = "key"
    NONE = "none"

validatecommandで指定した関数に渡されるパラメータのデータクラス

dataclassesライブラリを使い、コンストラクタで渡された各パラメータを属性にする部分を省略した。
tk関数のパラーメータ名そのままだと参照するときマジックナンバー的でわかりにくいので、エイリアス的にプロパティを設定しアクセスがしやすいようにした。

tk_widget_utils/entries.py
@dataclass(frozen=True)
class VcmdParams:
    """Dataclass for tkinter entry validate command params."""

    d: str  # action_type
    i: str  # index
    P: str  # text_allowed_to_edit
    s: str  # text_prior_to_edit
    S: str  # text_being_changed
    v: str  # validation_type
    V: str  # trigger_type
    W: str  # widget_name

    @property
    def action_type(self) -> ActionType:
        """Type of action:
        1 for insert,
        0 for delete,
        or -1 for focus, forced or textvariable validation.
        """
        return ActionType(int(self.d))

    @property
    def index(self) -> int:
        """Index of char string to be inserted/deleted,
        if any, otherwise -1.
        """
        return int(self.i)

    @property
    def text_allowed_to_edit(self) -> str:
        """The value of the entry if the edit is allowed.
        If you are configuring the entry widget to have a new textvariable,
        this will be the value of that textvariable.
        """
        return self.P

    @property
    def text_prior_to_edit(self) -> str:
        """The current value of entry prior to editing."""
        return self.s

    @property
    def text_being_changed(self) -> str:
        """The text string being inserted/deleted,
        if any, empty string otherwise.
        """
        return self.S

    @property
    def validation_type(self) -> ValidationType:
        """The type of validation currently set."""
        return ValidationType(self.v)

    @property
    def trigger_type(self) -> TriggerType:
        """The type of validation that triggered the callback
        (key, focusin, focusout, forced).
        """
        return TriggerType(self.V)

    @property
    def widget_name(self) -> str:
        """The name of the entry widget."""
        return self.W

バリデーション設定関数

以前の記事では、バリデーション用のメソッドをサブクラスでオーバーライドするようなEntryの派生クラスを実装していた。
しかし考えてみれば

  • configureで設定できるのにわざわざEntry自身がバリデーション用のメソッドを持つ必要がない
  • 様々なウィジェットを組み合わせた「メガウィジェット」をEntryのサブクラス込みで作ってしまうと、バリデーションの変更をしようとしたときに構成するウィジェットのクラスごと変更しなければならない

ことにも気づいた。

ロジックとしては以前のように、configurevcmdvalidateのオプションが渡せればいい。
バリデーションによってはEntryの内容を変更したいときがある(整数しか入力を許さないときに空文字になるなら代わりに0を挿入するetc...)ので、バリデーションが発火した際に呼び出される関数はウィジェットもとるようにした。

tk_widget_utils/entries.py
_TkEntryType = TypeVar("_TkEntryType", bound=tk.Entry)


def set_vcmd(
    widget: _TkEntryType,
    func: Callable[[_TkEntryType, VcmdParams], bool],
    type_: Literal[
        "none", "focus", "focusin", "focusout", "key", "all"
    ] = ValidationType.ALL.value,
) -> _TkEntryType:
    """Configures validate command to entry widget.

    Returns widget that validate command configured.
    """

    @ft.wraps(func)
    def _func(d, i, P, s, S, v, V, W) -> bool:
        return func(widget, VcmdParams(d, i, P, s, S, v, V, W))

    vcmd = (widget.register(_func), *(f"%{a}" for a in "diPsSvVW"))
    widget.configure(validate=type_, vcmd=vcmd)
    return widget

サンプルスクリプト

アルファベットと空文字しか許容しないEntryを作る場合。

sample.py
import tkinter as tk

from tk_widget_utils import entries as wue


def main():
    root = tk.Tk()
    tk.Label(master=root, text="alpha").grid(column=0, row=0)
    ent = tk.Entry(master=root)

    def _validate(w: tk.Entry, vp: wue.VcmdParams) -> bool:
        if vp.text_allowed_to_edit.isalpha():
            return True
        if vp.text_allowed_to_edit == "":
            return True
        return False

    wue.set_vcmd(ent, _validate)
    ent.grid(column=1, row=0)
    root.mainloop()


if __name__ == "__main__":
    main()

2
0
1

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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?