要約
以前に書いたtkinter
にバリデーションを設定する記事のコードを
- 型アノテーションを使う
-
Entry
を継承したバリデーター付きEntry
を作るのではなく、バリデーションを設定する関数を実装する
ように書き直してみた。
バリデーションを登録するためのregister
メソッド、vcmd
オプション、validate
オプションについては変化がないので下記記事を参照。
コード全容
コード概要
列挙体
enum
ライブラリを使い、定数は列挙体にした。
ちなみにstr
とEnum
を多重継承すると、(IntEnum
が.value
を参照しなくてもint
と値の比較が可能なように)文字列型と.value
を使わなくても比較が可能になる。
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
関数のパラーメータ名そのままだと参照するときマジックナンバー的でわかりにくいので、エイリアス的にプロパティを設定しアクセスがしやすいようにした。
@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
のサブクラス込みで作ってしまうと、バリデーションの変更をしようとしたときに構成するウィジェットのクラスごと変更しなければならない
ことにも気づいた。
ロジックとしては以前のように、configure
でvcmd
とvalidate
のオプションが渡せればいい。
バリデーションによってはEntry
の内容を変更したいときがある(整数しか入力を許さないときに空文字になるなら代わりに0を挿入するetc...)ので、バリデーションが発火した際に呼び出される関数はウィジェットもとるようにした。
_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
を作る場合。
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()