Monday, July 29, 2013

Prototyping UI in Python

Prototyping UI in Python

I like Python, but I don't like typing. Ever since starting to play around with Python, I've found it easier to write and test programs (running individual modules easily is a really nice feature, and skipping the whole compile step just seems, maybe psychologically, quicker and simpler).

But, paradoxically (or maybe not), I've found myself typing a lot more. I start up python, import a module, make a few object, call some functions etc, invariably find a bug, try to fix it and then repeat the whole process again. If I was using, say, C#, I'd probably have to write some extra UI code, hook up a function to do all the above to a button or menu item, then run the whole program, just to test that bit. It would be less flexible, as I'd have to recompile everytime I wanted to change the test function, but it means I just have to click a single button (or menu item) to test - no typing required.

So why not use a similar approach in python? Write a test function, and hook it up to a button somewhere? There's plenty of UI options to choose from (I've dabbled in TkInter, WxPython and Qt). Personally, I find they all seem to get in the way a bit. Your main function ends up being modified to run an application loop, you need to remember the names of the widgets you want, there's all kinds of advanced options for things I don't need, I just want a button! (I find Qt especially bad in this respect, mainly because Qt is no longer a UI framework, but rather an application framework. If you're willing to use QObjects and QLists and QTime and QInt etc, instead of the language standards, you're fine. Otherwise you need something to convert your model into a Qt compatible model (could this be a new model-qtmodel-qtcontroller-uimodel-uicontroller pattern??))

Ideally, this is what I'd like:

class A:
    @button
    def func1(self):
        ...

a=A()
ui_display(A)

I don't care how the button looks, what color it is, how big it is (as long as I can read the text) or even which framework gets used. I'm happy for it to display the name of the function, and for the ui_display function to hog the main thread til I've dismissed it.

There are two parts to solving this problem: First, how to decorate functions in a framework-independent way, and secondly, how to convert an object with decorated functions into a framework specific UI. It ended up being a little more complicated than the version above, but also a little more featured (eg, set functions update the ui automatically).

(TraitsUI attempts to solve a similar problem. TraitsUI has a far larger scope (eg using Traits to describe types of variables, etc) and a lot more functionality, what's below is a lot simpler, and uses more standard python code)

UI Decorators

The example above of a button decorator is a very simple case: A button has no state to it, it's clicked once and we expect an event to happen once. But how about something like a slider or text box? In this case we have more constraints: The slider has a state (the position of the cursor along the slider) that we want to be kept in sync with the object state. We can achieve this by decorating a set function, and then making sure the slider gets updated every time it's called. There's also the case of finding the state of the object at the time the slider is created. In this case we don't want to rely on waiting for a call to the set function, so instead we supply an optional get function to the decorator.

The decorator for creating a slider ends up looking like:

class Test:
    @slider(getfunc=get_test)
    def test(self, newval):
        ...

    def get_test(self):
        ...
        return number

Internally, all the decorator is doing is adding some attributes to the function: a _slider dictionary with information about the decorator, and a (fake) instance method
listeners, which returns the listeners for the function for the object specified. Eg:

>>> t=Test()
>>> t.get_test.slider
>>> t.test._slider
{'getfunc': , 'scale': 1, 'minimum': 0, 'maximum': 100}
>>> t.test.listeners(t)
[]
>>>

Currently, there's similar decorators for combobox, checkbox, textbox and button. The decorators are designed to be mostly side-effect free, so using them in a case when you don't end up using a UI shouldn't cause any major problems (there's an extra dictionary per function per class, and an extra list per function per instance).

UI Frameworks

To actually use the decorators, there needs to be a framework module to convert a class with decorated functions into a UI object. It supplies a function get_obj_widget, which returns a UI-specific widget for a decorated object, and display_widgets, which displays the given list of widgets (either created via get_obj_widget, or some other means). A helper function, display, calls these in succession, creating and displaying the given object.

Currently only a (PySide-based) Qt framework exists. The widgets created are DockWidgets, and can be moved around and docked independently.

I've written this specifically for another project I was playing around with, I'll post that next.

You can find the files on Github. ui_decorators.py has a little sample application if run as the main module, it requires PySide.

2 comments:

  1. Pretty neat. Have you played with enaml? Not like your solution, but still a pretty cool GUI tool.

    ReplyDelete
  2. Hi Mike,
    I had a quick look at enaml (I think when installing Analyzarr). It looks neat, I'll look into it further.

    ReplyDelete