tkinterのEntryにvalidationを実装する part 1 基底クラスを作る。

# この記事を書いた理由
ユーザーに文字入力をさせるアプリケーションの実装に不可欠である入力規則について、`tkinter`の`validatecommand`について日本語で解説しているドキュメントがなかったので、備忘録的な意味で書いた。

# ユーザーって期待する値を入力してくれない
GUIを持つアプリケーションを作ったとき、文字列(数字の時もある)を、作り手の想定通りにはユーザーは入力をしてくれないものである。
受け付けられない値だからと言って、いったん入力して「決定」ボタンを押したらエラーを返されるような実装では、ユーザーはイラつく。
ユーザーが入力して次の項目に行く前に、受け付けられない入力内容であることはユーザーに知らせたほうが良い。
最初から作り手の想定していない入力はできないようにしておけば、この問題は解決する。

# どうやって入力規則を実装するか
`register`メソッドで`Python`の関数をラップした`Tcl`の関数の情報を`configure`メソッド(またはキーワード指定)の`'validatecommand'`オプションに設定し、`'validate'`オプションで発火条件を設定する。

## `register`メソッド
`tkinter`のウィジェットの基底クラスである`Misc`には、`Python`の関数がコールバックされる`Tcl`の関数を**「登録」**して、返り値として`Tcl`の**関数の名前の文字列**が返される`register`メソッドがある。
当然`Entry`にもこのメソッドが存在する。
`register`メソッドで登録された`Tcl`関数が呼び出されると、元の`Python`の関数が呼び出される。
`Tcl`の関数の引数に渡される値は`str`型に変換されて`Python`の関数の引数が受け取る。

```python:tkinter\__init__.py
# ---略---

# 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'`|アクションコード。<br>1: 挿入<br>0: 削除<br>-1: フォーカスイン/アウト、`textvariable`の変化|
|`'%i'`|インデックス。<br>0以上: 挿入/削除しようとしたカーソルの位置<br>-1: カーソル位置の関係ないアクション|
|`'%P'`|変更を許可した場合の文字列。|
|`'%s'`|変更前の文字列。|
|`'%S'`|挿入または削除した文字列。|
|`'%v'`|`Entry`に設定された発火条件。<br>`'focus'`, `'focusin'`, `'focusout'`, `'key'`, <br>`'all'`,  `'none'` |
|`'%V'`|コールバックした理由。<br>`'focusin'`, `'focusout'`, `'key'`, `'forced'`|
|`'%W'`|ウィジェット名の文字列。|


## `'validate'`オプション
`str`型を渡す。
`'validatecommand'`オプションに渡した関数が、そのウィジェットに関するどのきっかけで発火するかを選択できる。
下記の文字列のいずれかが渡せる。

|値           |場合               |
|:------------|:------------------|
| `'focus'`   |フォーカスを得るか失うかしたとき|
| `'focusin'` |フォーカスを得たとき|
| `'focusout'`|フォーカスを失うとき|
| `'key'`     |キーボード入力のとき|
| `'all'`     |理由を問わず、入力内容に変化が生じたとき|
| `'none'`    |何もしない|


##`Python`でコーディング
`Entry`を継承したウィジェットのクラスを作る。
`'vcmd'`は。`'validatecommand'`と同義である。

```python:entries.py
"""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](https://github.com/junkmd/Python-Tkinter-Widgets)

# 参考文献
[Tcl Developer Xchange - Tk Built-In Commands - entry manual page](https://www.tcl.tk/man/tcl8.4/TkCmd/entry.htm#M13)
[effbot.org - The Tkinter Entry Widget](http://effbot.org/tkinterbook/entry.htm)
[New Mexico Tech - Tkinter 8.5 reference: a GUI for Python - 10.2. Adding validation to an Entry widget](http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html)
[effbot.org - Basic Widget Methods](https://effbot.org/tkinterbook/widget.htm)