IPython NotebookライクなGtkラッパーを書いてみた【Python】

先日、

[IPython NotebookライクなTkinterラッパーを書いてみた【Python】](http://qiita.com/ssh0/items/1835213f011c62de0767)

というのを書きました。今回はその中でも触れていたように、同じことをGtkに移植したものを紹介します。呼び出し方などは上の記事を参考にしていただければと思います。Gtkの日本語のチュートリアルって、ホント見かけないので、意外と簡単に使えるよーってことが分かっていただければ。

![gtk_wrapper.png](https://qiita-image-store.s3.amazonaws.com/0/54869/7a5a5762-278f-aab9-54a5-04f44e62143a.png)


#仕様

今回は、Python3に完全対応しております。コードの最初に\_\_future\_\_からprint_functionを持ってきていますが、モジュールとして呼び出す際にはこれは必要ではありません。テストでも３系で動くようにしてあるだけです。

前回、またはIPython Notebookと比べて大きく変更したのは、スケールバーの呼び出しの際の３つ目の引数についてです。これまでは刻み幅を自分で設定する形（例：x=（0., 10., 0.5）とすると0.0から10.0までの0.5刻みのスケールバーが作成される）でしたが、Gtkの機能としてset_digitsというものがあり、これを利用しております。すなわち、この部分には小数点以下何桁まで取るかを指定する形式に変更してあります。もちろん、今まで通りこの部分は省略することができて、第１引数として整数値を与えた場合には整数で指定できますし、これに小数を与えた場合には小数点２桁まで取るようにしてあります。ここらへんの値の丸め込みに関しては、レファレンスを読むともっと細かく設定することも分かりますが、今回いじっていません。

したがって、interactiveに与えるキーワード引数の中身については、以下の用にウィジェットに変換されます。

<table class="table table-condensed table-bordered">
  <tr><td><strong>Keyword argument</strong></td><td><strong>Widget</strong></td></tr>  
  <tr><td>`True` or `False`</td><td>SwitchWiget</td></tr>  
  <tr><td>`value` or `(min,max)` or `(min,max,digit)` if integers are passed</td><td>IntSliderWidget</td></tr>
  <tr><td>`value` or `(min,max)` or `(min,max,digit)` if floats are passed</td><td>FloatSliderWidget</td></tr>
  <tr><td>`('orange','apple')` or `{'one':1,'two':2}`</td><td>ComboBoxWidget</td></tr>
</table>


#使用感

実際に動かしてみた様子をスクリーンショットで取ったのでいくつか紹介します。
やっぱりTkに比べるとだいぶ綺麗に表示されますね。Gtkテーマが適用されていることも嬉しいです。


tk_wrapperを使ってGtkで表示すると、

![Selection_014.png](https://qiita-image-store.s3.amazonaws.com/0/54869/8c0e841a-70f0-fa2d-e075-5fe68892dfa7.png)

このようにウィジェットが生成されます。

スライダーを動かしたり、スイッチをオンオフすると、関数の中身が実行されます。（今は変数の中身を表示するだけ）
ボタンを押すと、そのタイミングで別の関数を実行でき、そのときの変数の値などを参照できます。

![Selection_015.png](https://qiita-image-store.s3.amazonaws.com/0/54869/738f0a0e-9b86-b383-7ce4-ffc94c887260.png)


#コード全体

最後にコード全体を晒します。相変わらずコメントが少なくて恐縮ですが、何かのお役に立てれば幸いです。

[gist](https://gist.github.com/d71dd52cc58763604dc6)

```py:gtk_wrapper.py

#! /usr/bin/env python
# -*- coding:utf-8 -*-
#
# written by ssh0, October 2014.

from __future__ import print_function

__doc__ = '''IPython-like Gtk wrapper class.

You can create Scalebar, Switch, ComboBox via simple interface.

usage example:

>>> from gtk_wrapper import interactive
>>> def f(a,b=20):
...     return a + b
...
>>> w = interactive(f, a=(0, 20), b=20)

(you can add buttons and label here manually)

(then, you should add the next line)
>>> w.display()

(and you can get the result for f(a,b) by)
>>> w.result
32

(or get the arguments with a dictionary)
>>> w.kwargs
{'a':8, 'b':24}
'''

from gi.repository import Gtk
import inspect


class interactive(Gtk.Window):

    def __init__(self, func, title='title', **kwargs):
        self.__doc__ = __doc__
        self.func = func
        self.kwargs = dict()
        args, varargs, keywords, defaults = inspect.getargspec(self.func)
        d = []
        for default in defaults:
            d.append(default)
        self.kwdefaults = dict(zip(args[len(args) - len(defaults):], d))

        Gtk.Window.__init__(self, title=title)
        hbox = Gtk.Box(spacing=6)
        self.add(hbox)

        self.listbox = Gtk.ListBox()
        self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        hbox.pack_start(self.listbox, True, True, 0)
        self.status = Gtk.Label()

        for kw, arg in kwargs.items():
            kw = str(kw)
            arg_type = type(arg)
            if arg_type == tuple:
                # type check for elements in tuple
                argtype = self.type_check(arg)
                if argtype == str:
                    self.combobox_str(kw, arg)
                else:
                    self.scale_bar(kw, arg, argtype)
            elif arg_type == int or arg_type == float:
                self.scale_bar(kw, [arg], arg_type)
            elif arg_type == bool:
                self.switch(kw, arg)
            elif arg_type == str:
                self.label(arg)
                self.kwargs[kw] = arg
            elif arg_type == dict:
                self.combobox_dict(kw, arg)
            else:
                raise TypeError

        row = Gtk.ListBoxRow()
        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
        row.add(hbox)
        hbox.pack_start(self.status, True, True, 10)
        self.status.set_text(str(self.kwargs))
        self.listbox.add(row)

    def display(self):
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()
        Gtk.main()

    def status_change(self):
        self.status.set_text(str(self.kwargs))
        self.kwargs_for_function = self.kwdefaults
        for k, v in self.kwargs.items():
            self.kwargs_for_function[k] = v
        self.result = self.func(**self.kwargs_for_function)

    def set_value(self, kw, new_value):
        # if argument is already given in func, use it for a default one
        if kw in self.kwdefaults:
            self.kwargs[kw] = self.kwdefaults[kw]
            return self.kwdefaults[kw]
        else:
            self.kwargs[kw] = new_value
            return new_value

    def type_check(self, arg):
        argtype = type(arg[0])
        if not all([type(a) == argtype for a in arg]):
            raise TypeError("""types in a tuple must be the same.
            int or float: Scalebar
            str         : Combobox""")
        return argtype

    def add_label(self, kw, parent):
        label = Gtk.Label(kw, xalign=0)
        parent.pack_start(label, True, True, 10)

    def scale_bar(self, kw, arg, argtype):

        def scale_interact(scale, _type):
            if _type == int:
                self.kwargs[kw] = int(scale.get_value())
            else:
                self.kwargs[kw] = float(scale.get_value())
            self.status_change()

        # length check for tuple
        len_arg = len(arg)
        if len_arg > 3 or len_arg == 0:
            raise IndexError("tuple must be 1 or 2 or 3 element(s)")

        if argtype == int:
            scale_digit = 0
        elif argtype == float:
            scale_digit = 2
        else:
            raise TypeError("arg must be int or float")

        # set the values
        if len_arg == 3:
            scale_from = arg[0]
            scale_to = arg[1]
            scale_digit = arg[2]
        elif len_arg == 2:
            scale_from = arg[0]
            scale_to = arg[1]
        else:
            scale_from = arg[0] * (-1)
            scale_to = arg[0] * 3

        # create scale widget in listbox
        row = Gtk.ListBoxRow()
        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
        row.add(hbox)

        self.add_label(kw, hbox)
        scale = Gtk.Scale()
        scale.set_range(scale_from, scale_to)
        scale.set_digits(scale_digit)
        scale.set_value(self.set_value(kw, arg[0]))
        scale.set_draw_value(True)
        scale.connect('value-changed', scale_interact, argtype)
        hbox.pack_start(scale, True, True, 10)

        self.listbox.add(row)

    def switch(self, kw, arg):

        def on_switch_activated(switch, gparam):
            self.kwargs[kw] = switch.get_active()
            self.status_change()

        # create switch widget in listbox
        row = Gtk.ListBoxRow()
        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
        row.add(hbox)
        self.add_label(kw, hbox)
        switch = Gtk.Switch()
        switch.connect("notify::active", on_switch_activated)
        switch.set_active(self.set_value(kw, arg))
        hbox.pack_start(switch, False, False, 10)
        self.listbox.add(row)

    def combobox_str(self, kw, arg):

        def on_combo_changed(combo):
            tree_iter = combo.get_active()
            if tree_iter is not None:
                self.kwargs[kw] = arg[tree_iter]
                self.status_change()

        argstore = Gtk.ListStore(str)
        for a in arg:
            argstore.append([a])

        # create combobox widget in listbox
        row = Gtk.ListBoxRow()
        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
        row.add(hbox)
        self.add_label(kw, hbox)
        combo = Gtk.ComboBox.new_with_model(argstore)
        combo.connect("changed", on_combo_changed)
        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, "text", 0)
        combo.set_active(arg.index(self.set_value(kw, arg)))
        hbox.pack_start(combo, False, False, True)
        self.listbox.add(row)

    def combobox_dict(self, kw, arg):

        def on_combo_changed(combo):
            tree_iter = combo.get_active()
            if tree_iter is not None:
                self.kwargs[kw] = values[tree_iter]
                self.status_change()

        argstore = Gtk.ListStore(str)
        keys = list(arg.keys())
        values = list(arg.values())
        for a in keys:
            argstore.append([a])

        # create combobox widget in listbox
        row = Gtk.ListBoxRow()
        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
        row.add(hbox)

        self.add_label(kw, hbox)
        combo = Gtk.ComboBox.new_with_model(argstore)
        combo.connect("changed", on_combo_changed)
        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, "text", 0)
        combo.set_active(values.index(self.set_value(kw, arg)))
        hbox.pack_start(combo, False, False, True)

        self.listbox.add(row)


if __name__ == '__main__':
    from gi.repository import Gtk

    def f(x=12, y=20, z='III', o=False, i=20):
        print("x: {0}, y: {1}, z: {2}, o: {3}, i: {4}".format(x, y, z, o, i))

    def b1(button):
        print(w.kwargs)

    buttons = [('b1', b1), ('exit', Gtk.main_quit)]
    w = interactive(f, x=10, y=(1., 100.),
                    z=("ZZZ", "III", "foo", "bar"),
                    i={'0': 0, '10': 10, '20': 20},
                    o=True
                    )
    row = Gtk.ListBoxRow()
    hbox = Gtk.HBox(spacing=10)
    row.add(hbox)
    for b in buttons:
        button = Gtk.Button(b[0])
        button.connect('clicked', b[1])
        hbox.pack_start(button, True, True, 0)
    w.listbox.add(row)

    w.display()

```

#課題と展望

メインで使いたいのはGtkだったので、これで一段落です。ただ、あいかわらずスライドバーの数だけ先に関数が実行されてしまっているし（つまりTkだけの問題ではない）、表示順もバラバラになってしまっています。今後改善する点は以上の点と、メニューバーの充実でしょうか。ファイルの読み出しなんかも簡単に作れると思います。Qtは個人的にあまり使う機会がないので（Ubuntuですし・・・）、保留です。