この記事を書いた理由
ユーザーに文字入力をさせるアプリケーションの実装に不可欠である入力規則について、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