読者です 読者をやめる 読者になる 読者になる

StatsFragments

Python, R, Rust, 統計, 機械学習とか

Python traits で型強制 + traitsui でカンタン GUI 作成

Python の Canopy ディストリビューションで有名な Enthought.inc が作っている traits, traitsui というモジュールが結構便利なのだが、日本語の情報がないのでメモ。

概要

  • traitsPython のクラスプロパティに特定の型を強制できるモジュール
  • traitsuitraits の定義に従って、wxPython, PyQt, PysideGUI を簡単にデザインできるモジュール

インストール

pip install traits traitsui

この記事の例ではPyQt を使うので入ってなければ入れる (pip では入らない)。Windows なら MSIインストーラがあるので楽。他OSならソースから build するか、各パッケージ管理で。

traits

Defining Traits: Initialization and Validation — Traits 4 User Manual

ほぼそのままですが。

import traits.api

# class定義の際は HasTraits を継承させる
class Person(traits.api.HasTraits):
    # traits を使うプロパティはクラス変数とし、許可される型を traits.api から設定 

    # 文字列 (str) のみ許可
    name = traits.api.Str

    # 整数のみ許可
    age = traits.api.Int

    # 'M', 'F' もしくは 'X'のみ許可
    sex = traits.api.Enum('M', 'F', 'X')

# インスタンス初期化
p = Person(name='John', age=22, sex='M')

# Str 型が設定されたプロパティを書き換え。str では上書きできるが、別の型を入れるとエラー
p.name = 'Mike'

# NG!
p.name = 0
# TraitError: The 'name' trait of a Person instance must be a string, but a value of 0 <type 'int'> was specified.

# Int 型が設定されたプロパティを書き換え。int では上書きられるが、別の型を入れるとエラー
p.age = 24

# NG!
p.age = 2.0
# TraitError: The 'age' trait of a Person instance must be an integer (int or long), but a value of 2.0 <type 'float'> was specified.

# Enum 型が設定されたプロパティを書き換え。初期化の際に許可した値以外はエラー

# NG!
p.sex = 'B'
# TraitError: The 'sex' trait of a Person instance must be 'M' or 'F' or 'X', but a value of 'B' <type 'str'> was specified.

という感じで、クラス定義の際に設定しておけば、煩雑な入力値チェックを自分で書く必要がなくなる。より複雑な条件を設定したい場合は自分でチェック関数を書くこともできる。

さらに、ある変数の変更を検知する Handler や 読み取り専用のProperty (cache可) を定義したりもできる。

class Person2(Person):
    # Personを継承

    # name + age を表示名にしてみる
    disp = traits.api.Property

    # _xxx_changed は プロパティ xxx の変更時に自動的に実行
    def _name_changed(self, value):
        print('updated with ' + value)

    # _get_xxx は 自動的にプロパティ xxx の getter になる
    def _get_disp(self):
        return '{0} ({1})'.format(self.name, self.age)


# 初期化やプロパティ設定でname を変更すると Person._name_changed が実行される
p = Person2(name='John', age=22, sex='M')
# updated with John

p.name = 'Mike'
# updated with Mike

# disp を読み取ると Person._get_disp が実行される
p.disp
# Mike (22)

# disp は上書きできない
p.disp = 'overwrite'
# TraitError: The 'disp' trait of a Person2 instance is 'read only'.

traitsui

traits で定義したプロパティの型に応じて、適切な GUI を表示してくれる。さきほどの Person2クラスで .configure_traits メソッドを実行すると、自動でレイアウトされた GUI がポップアップしてくる。各フィールドはクラスで定義した型に応じてテキスト/プルダウンとして表示される。

p.configure_traits()

f:id:sinhrks:20141013185835p:plain

traitsui を使うと、この GUI のレイアウトを変えられる。表示方法は HasTraits を継承したクラスの traits_view プロパティで設定する。

表示のレイアウト変更 + ラベル付与 + 性別をラジオボタン選択 + 表示名を読み取り専用にしてみる。どういった設定ができるかは膨大なので 下記ドキュメントを参照。

Introduction to Trait Editor Factories — TraitsUI 4 User Manual

from traitsui.api import View, VGroup, HGroup, Item

class Person3(Person2):

    # VGroupは縦方向のレイアウト
    # HGroupは横方向のレイアウト
    traits_view = View(VGroup(
        HGroup(Item('name', label='氏名')),
        HGroup(Item('age', label='年齢'), Item('sex', label='性別', style='custom')),
        HGroup(Item('disp',label='表示名', style='readonly'))))

p = Person3(name='John', age=22, sex='M')
p.configure_traits()

f:id:sinhrks:20141013191414p:plain

このGUIからユーザがプロパティを変更した場合、traits で行った定義に従って入力値のチェックや Handler の実行なんかが自動的に行われる。たとえば Int で定義された年齢フィールドに文字列を入力しようとすると、エラーとなり入力が許可されない。

f:id:sinhrks:20141014213926p:plain

使い方 (PyQtへの埋め込み)

traits, traitsui でデザインした GUIPyQt のウィンドウ / ダイアログに組み込むことができる。ので PyQt 上で 細かい GUI のレイアウトやイベント制御の処理を書かなくてすむようになる。

import sys

# おまじない
import sip
sip.setapi('QString', 2)
sip.setapi('QVariant', 2)
import PyQt4.QtCore as QtCore
import PyQt4.QtGui as QtGui


class AppForm(QtGui.QMainWindow):

    def __init__(self, parent=None):
        QtGui.QMainWindow.__init__(self, parent)

        # ウィンドウタイトル / サイズを指定
        self.setWindowTitle('Person Config')
        self.resize(400, 200)

        # メインの widget, layout を作成
        self.main_frame = QtGui.QWidget()
        self.main_layout = QtGui.QVBoxLayout()

        p = Person3(name='John', age=22, sex='M')
        # Person3 の画面 widget ( GUI のレイアウト) を取得
        control = p.edit_traits(parent=self, kind='subpanel').control

        # layout に widget 追加
        self.main_layout.addWidget(control)

        # メインの領域に layout 追加
        self.main_frame.setLayout(self.main_layout)
        self.setCentralWidget(self.main_frame)

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    form = AppForm()
    form.show()
    sys.exit(app.exec_())

f:id:sinhrks:20141013191423p:plain

まとめ

ちょっとした GUI のついたツールが作りたいとき、traits, traitsui を使うとラクだし、設計もシンプルにできる。

  • モデル/コントローラの各パーツ部分は traits で作る。Handler も traits で定義しておく。
  • traitsui で各パーツの GUI のスタイル/レイアウトを決める。
  • 作ったパーツを PyQt 上に配置する