この記事を書いた理由
ユーザーに文字入力をさせるアプリケーションの実装に不可欠である入力規則について、tkinterのvalidatecommandについて日本語で解説しているドキュメントがなかったので、備忘録的な意味で書いた。
ユーザーって期待する値を入力してくれない
GUIを持つアプリケーションを作ったとき、文字列(数字の時もある)を、作り手の想定通りにはユーザーは入力をしてくれないものである。
受け付けられない値だからと言って、いったん入力して「決定」ボタンを押したらエラーを返されるような実装では、ユーザーはイラつく。
ユーザーが入力して次の項目に行く前に、受け付けられない入力内容であることはユーザーに知らせたほうが良い。
最初から作り手の想定していない入力はできないようにしておけば、この問題は解決する。
どうやって入力規則を実装するか
registerメソッドでPythonの関数をラップしたTclの関数の情報をconfigureメソッド(またはキーワード指定)の'validatecommand'オプションに設定し、'validate'オプションで発火条件を設定する。
registerメソッド
tkinterのウィジェットの基底クラスであるMiscには、Pythonの関数がコールバックされるTclの関数を**「登録」して、返り値としてTclの関数の名前の文字列**が返されるregisterメソッドがある。
当然Entryにもこのメソッドが存在する。
registerメソッドで登録されたTcl関数が呼び出されると、元のPythonの関数が呼び出される。
Tclの関数の引数に渡される値はstr型に変換されてPythonの関数の引数が受け取る。
# ---略---
# Methods defined on both toplevel and interior widgets
class Misc:
"""Internal class.
Base class which defines methods common for interior widgets."""
# ---中略---
def _register(self, func, subst=None, needcleanup=1):
"""Return a newly created Tcl function. If this
function is called, the Python function FUNC will
be executed. An optional function SUBST can
be given which will be executed before FUNC."""
# ---中略---
register = _register
# ---略---
'validatecommand'オプション
(Tclの関数名の文字列, 代数の文字列1, 代数の文字列2, 代数の文字列3...)となるtuple型を渡す。
Tclの関数名の文字列は、registerメソッドで返された文字列である。
関数名から関数をTclが呼び出し、Pythonの関数をコールバックしてその返り値をTclに返す。
返り値がTrueであれば変更を許可し、Falseであれば許可しない。
'validatecommand'に渡したTclの関数は、以下の様な返り値を各代数で受け取ることができる。
'%W'以外はEntry内の文字列に対する何らかの状態を返す。
| 代数 | 返り値の内容 |
|---|---|
'%d' |
アクションコード。 1: 挿入 0: 削除 -1: フォーカスイン/アウト、 textvariableの変化 |
'%i' |
インデックス。 0以上: 挿入/削除しようとしたカーソルの位置 -1: カーソル位置の関係ないアクション |
'%P' |
変更を許可した場合の文字列。 |
'%s' |
変更前の文字列。 |
'%S' |
挿入または削除した文字列。 |
'%v' |
Entryに設定された発火条件。'focus', 'focusin', 'focusout', 'key', 'all', 'none'
|
'%V' |
コールバックした理由。'focusin', 'focusout', 'key', 'forced'
|
'%W' |
ウィジェット名の文字列。 |
'validate'オプション
str型を渡す。
'validatecommand'オプションに渡した関数が、そのウィジェットに関するどのきっかけで発火するかを選択できる。
下記の文字列のいずれかが渡せる。
| 値 | 場合 |
|---|---|
'focus' |
フォーカスを得るか失うかしたとき |
'focusin' |
フォーカスを得たとき |
'focusout' |
フォーカスを失うとき |
'key' |
キーボード入力のとき |
'all' |
理由を問わず、入力内容に変化が生じたとき |
'none' |
何もしない |
Pythonでコーディング
Entryを継承したウィジェットのクラスを作る。
'vcmd'は。'validatecommand'と同義である。
"""Entries with validator."""
import tkinter as tk
from numbers import Number
class Validation(object):
"""Container for the properties of a validation."""
def __init__(self, widget, **kw):
self.__widget = widget
self.__kw = kw
@property
def type_of_action(self):
"""Type of action.
0: deletion,
1: insertion,
-1: focus in, focus out, or a change to the textvariable."""
return int(self.__kw['d'])
@property
def index(self):
"""Index of the beginning of the insertion or deletion.
-1: focus in, focus out, or a change to the textvariable."""
return int(self.__kw['i'])
@property
def text_if_allowed(self):
"""The text in the entry will have if the change is allowed.
"""
return self.__kw['P']
@property
def text_before_change(self):
"""The text in the entry before the change.
"""
return self.__kw['s']
@property
def text_what_changed(self):
"""The text in the entry being inserted or deleted.
"""
return self.__kw['S']
@property
def type_of_validation(self):
"""The value of the entry's validate option.
"""
return self.__kw['v']
@property
def reason_for_callback(self):
"""The reason for this callback.
Returns 'focusin', 'focusout', 'key', or 'forced'."""
return self.__kw['V']
@property
def widget(self):
"""The entry widget.
"""
return self.__widget
def __repr__(self):
members = [m for m in dir(self) if m[0] != '_']
items = []
for m in members:
val = getattr(self, m)
if isinstance(val, Number):
items.append('%s=%s' % (m, val))
elif isinstance(val, str):
items.append('%s=\'%s\'' % (m, val))
else:
items.append('%s=%s' % (m, val.__repr__()))
return '<validation %s>' % " ".join(items)
class BaseEntryWithValidator(tk.Entry):
"""Internal class for tkinter Entry with valiator."""
def __init__(self, master, **kw):
"""Construct an entry widget with validator."""
tk.Entry.__init__(self, master)
vcmd = (
self.register(self.__vcmd_callback),
'%d', '%i', '%P', '%s', '%S', '%v', '%V'
)
#
# '%d' Type of action.:
# 0: deletion,
# 1: insertion,
# -1: focus in, focus out, or a change to the textvariable.
#
# '%i' Index of the beginning of the insertion or deletion.
# -1: focus in, focus out, or a change to the textvariable.
#
# '%P' The text in the entry will have if the change is allowed.
#
# '%s' The text in the entry before the change.
#
# '%S' The text in the entry being inserted or deleted.
#
# '%v' The current value of the widget's validate option.
#
# '%V' The reason for this callback.
# 'focusin', 'focusout', 'key', or 'forced'.
# 'forced' means the textvariable was changed.
#
# '%W' The name of the widget.
#
tk.Entry.configure(self, validate='all', vcmd=vcmd)
tk.Entry.configure(self, **kw)
def __vcmd_callback(self, d, i, P, s, S, v, V):
"""Internal function.
Callback function of native Entry's 'validatecommand' option."""
validation = Validation(self, d=d, i=i, P=P, s=s, S=S, v=v, V=V)
return self.validate(validation)
def validate(self, validation):
"""Validating the changing text in the entry.
"""
return True
'validatecommand'で登録された関数が受け取ることが可能な引数は8個と多いため、これらの順番を考慮して今後の派生クラスを作ることを考えるとしんどくなる。
そのため、'tkinter'のEventクラスのようなコンテナクラスValidationを作った。
また、'%W'でウィジェット名を受け取るよりは、別の引数でウィジェットのインスタンスを受け取った方が良いと考えたため、Validationのインスタンス化の引数にはwidgetでウィジェットのインスタンスそのものを受け取り、Validationインスタンスのメンバにした。
派生クラスでは、validateメソッドをオーバーライドして、Validationのプロパティを参照して条件を作りながら、返り値をTrueまたはFalseにすることでウィジェットの文字列を変化させるかどうかをコントロールする。
次回以降は、「ありがちな入力規則」を実装していく。
GitHubリポジトリ
tkinterについていろいろ書いたリポジトリ Python-Tkinter-Widgets
参考文献
Tcl Developer Xchange - Tk Built-In Commands - entry manual page
effbot.org - The Tkinter Entry Widget
New Mexico Tech - Tkinter 8.5 reference: a GUI for Python - 10.2. Adding validation to an Entry widget
effbot.org - Basic Widget Methods