I wrote an IPython Notebook-like Tkinter wrapper [Python]

As the title suggests, I wrote a wrapper for Tkinter that can easily call the GUI like the interactive widget of IPython Notebook, so I will give it up. I'm worried because it's behaving a little unexpectedly, so I'd like to ask for your teaching. Apart from that, I think it's much easier to use, so I'm thinking of porting it to Qt or Gtk next time.

Postscript: Ported to Gtk (link)

out-2_65-139.gif

Motivation for creation

I've been using IPython notebooks a lot these days, but when I think about what features I'm attracted to, I can interactively change variables and execute functions, such as interactive and interactive. I think it depends largely on the existence. The rest is creating slides. I've been writing my own wrapper in Tkinter, but I was impressed by the ease of use of the interactive widget in IPython Notebook, so I decided to make something that could display the Tk widget in a similar way. .. Therefore, how to use

It is almost the same as the one listed in. Due to the Tkinter module, the drop-down list has been changed to a radio button. However, some people are creating dropdowns with Tk,

It seems that there is nothing you can't do if you incorporate something, but this time I gave it up. (I'm going to use Gtk as the main ...)

how to use

The type to call is the same as [Using Interact] interact as mentioned earlier. It is grouped as a class interactive, and when called from a script, it is necessary to execute w.display () for display.


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

(in console, tk widget appear here)
(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}

Also, at the head family, when specifying arguments with interactive, an error will occur if all the same arguments as the arguments of the function f to be linked are not included. You need to specify the fixed class as c = fixed (10) for the arguments that you do not want to change. Since it was difficult to implement this time and I didn't have many chances to use it, if the default value is set in the argument of the function f linked by the variable not specified by interactive, it can be executed using that value as it is. Yes (of course, if you don't specify it, you will get a type error saying "There are not enough arguments").

Whole script

It's long (although it's a lot simpler than the original one), but please take a look if you like. It has been confirmed to work with Python 2.7.6, so please try it. Even in Python3.4.0, if you change "Tkinter" to "tkinter", make the print statement a function notation, and then take out the dictionary value in 3 series

>>> a = {"d": 1, "c":4, "b":5}
>>> a.values()
dict_values([4, 5, 1])

Because it becomes like

>>> list(a.values())
[4, 5, 1]

I was able to execute it just by replacing the part of arg.value () (lines 97,116) with list (arg.value ()) as shown in.

tk_wrapper.py


#! /usr/bin/env python
# -*- coding:utf-8 -*-
#
# written by ssh0, October 2014.
__doc__ = '''IPython-like Tkinter wrapper class.

You can create Scalebar, Checkbutton, Radiobutton via simple interface.

usage example:

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

(in console, tk widget appear here)
(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}
'''

import Tkinter
import inspect


class interactive(object):

    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))

        self.root = Tkinter.Tk()
        self.root.title(title)
        self.f = Tkinter.Frame(self.root)
        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.radiobuttons_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.checkbutton(kw, arg)
            elif arg_type == str:
                self.label(arg)
                self.kwargs[kw] = arg
            elif arg_type == dict:
                self.radiobuttons_dict(kw, arg)
            else:
                raise TypeError

        self.f.pack()
        self.status = Tkinter.Label(self.root, text=str(self.kwargs))
        self.status.pack()

    def display(self):
        self.root.mainloop()

    def status_change(self):
        self.status.config(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, var, kw, new_value):
        # if argument is already given in func, use it for a default one
        if kw in self.kwdefaults:
            var.set(self.kwdefaults[kw])
            self.kwargs[kw] = self.kwdefaults[kw]
        else:
            var.set(new_value)
            self.kwargs[kw] = 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         : Radiobuttons""")
        return argtype

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

        def scale_interact(new):
            self.kwargs[kw] = var.get()
            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:
            var = Tkinter.IntVar()
            scale_resolution = 1
        elif argtype == float:
            var = Tkinter.DoubleVar()
            scale_resolution = 0.1
        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_resolution = arg[2]
        elif len_arg == 2:
            scale_from = arg[0]
            scale_to = arg[1]
        else:
            scale_from = arg[0]*(-1)
            scale_to = arg[0]*2

        self.set_value(var, kw, arg[0])
        # create scale widget in frame
        scale = Tkinter.Scale(self.f, label=kw, from_=scale_from, to=scale_to,
                              resolution=scale_resolution, variable=var,
                              orient='h', width=10, length=200,
                              command=scale_interact)
        scale.pack()

    def checkbutton(self, kw, arg):

        def check_interact():
            self.kwargs[kw] = var.get()
            self.status_change()

        var = Tkinter.BooleanVar()
        self.set_value(var, kw, arg)
        checkbutton = Tkinter.Checkbutton(self.f, text=kw, variable=var,
                                          command=check_interact)
        checkbutton.pack()

    def radiobuttons_str(self, kw, arg):

        def select():
            self.kwargs[kw] = var.get()
            self.status_change()

        var = Tkinter.StringVar()
        label = Tkinter.Label(self.f, text='select one for '+kw)
        label.pack()
        self.set_value(var, kw, arg[0])
        for x in arg:
            R = Tkinter.Radiobutton(self.f, text=x, variable=var,
                                    value=x, command=select)
            R.pack()

    def radiobuttons_dict(self, kw, arg):

        def select():
            self.kwargs[kw] = var.get()
            self.status_change()

        vtype = self.type_check(arg.values())
        if vtype == str:
            var = Tkinter.StringVar()
        elif vtype == int:
            var = Tkinter.IntVar()
        elif vtype == float:
            var = Tkinter.DoubleVar()
        else:
            raise TypeError("arg must be int or float or str.")
        label = Tkinter.Label(self.f, text='select one for '+kw)
        label.pack()
        self.set_value(var, kw, arg.values()[0])
        for k, d in arg.items():
        
            R = Tkinter.Radiobutton(self.f, text=k, variable=var,
                                    value=d, command=select)
            R.pack()

if __name__ == '__main__':
    import sys
    import Tkinter

    def f(x, y, z, o=True, i=0):
        print x, y, z, o, i

    def b1():
        print w.kwargs

    buttons = [('b1', b1), ('exit', sys.exit)]
    w = interactive(f, x=(0, 10), y=(1, 100, 10),
                    z=("ZZZ", "III", "foo", "bar"),
                    i={'0':0, '10':10, '20':20},
                    o=True
                    )

    for button in buttons:
        button = Tkinter.Button(w.root, text=button[0], command=button[1])
        button.pack()

    w.display()

Problems and future prospects

実行した様子

The problem is that, as you can see, the order in which the variables are displayed is out of order. I think this is due to the fact that it is referenced in dictionary format, but I don't know how to solve it, so I'm leaving it as it is for the time being. This is where I want to do something about it. Maybe you can use Ordered Dict.

Then, as you can see from the gif image and the image above, the contents of the function f have been executed by the number of arguments specified in scalebar at the beginning. Perhaps this is Tkinter's Scale specification, which means that the value was set at the stage of display. It didn't work to enable scale_change on the line just before self.root.mainloop () in the display function. If you get the same problem when porting to Gtk, the cause may be different. Therefore, like the IPython Notebook, the referenced function should be a light function (or a completely dummy function), and I feel that it is more stable to perform heavy calculations when the button is pressed.

As for the future prospects, I would like to extend it to Gtk for the time being, and in that case I would like to incorporate dropdowns, tabs, and progress bars.

Recommended Posts

I wrote an IPython Notebook-like Tkinter wrapper [Python]
I wrote an IPython Notebook-like Gtk wrapper [Python]
I wrote an empty directory automatic creation script in Python
I sent an SMS with Python
I wrote Fizz Buzz in Python
I wrote the queue in Python
I wrote the stack in Python
I tried sending an email with python.
[Python beginner] I collected the articles I wrote
A memo that I wrote a quicksort in Python
I wrote a class in Python3 and Java
I wrote "Introduction to Effect Verification" in Python
[Python] I made a Youtube Downloader with Tkinter.
I tried sending an email with SendGrid + Python
I made a Python Qiita API wrapper "qiipy"
I started python
I wrote matplotlib
[Python] Tkinter template
I got an error that Python couldn't read settings.ini
Basics of I / O screen using tkinter in python3
I wrote an automatic kitting script for OpenBlocks IoT
I get an Import Error in Python Beautiful Soup
I tried to implement an artificial perceptron with python
I wrote an automatic installation script for Arch Linux
[Python, ObsPy] I wrote a beach ball with Matplotlib + ObsPy
I made an action to automatically format python code
Python: I tried a liar and an honest tribe
[Python] I tried to make an application that calculates salary according to working hours with tkinter
[Fundamental Information Technology Engineer Examination] I wrote an algorithm for determining leap years in Python.