Forum Archive

Use a .pyui file in another .pyui file

Subject22

If I have a bunch of views defined in .pyui files is it possible for me to reuse these to build more complex views?

I see that in the interface designer I can add a custom view and set a custom subclass, but that doesn't pull in the associated .pyui file.

JonB

You have to create custom Classes to use the ui builder. The custom class would simply ui.load_view() in its init method (when load_view is called inside init, it knows to bind itself to the calling instance)

The class needs to be in scope before calling load_view, which can sometimes make this awkward.

I think there are some example floating around the forum...

Subject22

@JonB said:

when load_view is called inside init, it knows to bind itself to the calling instance

Oh that's useful! I'll give it a shot when I get a chance. Thanks!

Subject22

@JonB said:

when load_view is called inside init, it knows to bind itself to the calling instance

Oh that's useful! I'll give it a shot when I get a chance. Thanks!

EDIT:

Hang on. I see a problem with that. If I put ui.load_view() in the custom view class's __init__ and then attempt to use that view with another ui.load_view("my_view") I'll end up in a recursive loop.

Does that mean that if I do this I can ONLY instantiate my .pyui-defined views by adding them to other views in interface designer? Or have I missed something again? 😄

JonB

Right, ok, memory jogged....

See https://forum.omz-software.com/topic/2154/using-a-custom-pu-pyui-view-in-another-one-using-ui-editor/15

The key to allowing both MyView() and load_view('MyView.pyui') type syntax is to create a wrapper class inside init, who returns the current instance in new, and use the bindings in load_view to override MyView.

A cleaned up example:
https://gist.github.com/dea48790491892ad15eabdb29d780436

Subject22

Thanks! Someone pointed me to that in the Slack channel too :)

The cleaned up example will be handy.

Phuket2

@Subject22 , below is how I use it.

# coding: utf-8

import ui
import os

class PYUILoaderStr(ui.View):
    '''
    loads a pyui file into the class, acts as another ui.View
    class.
    ** Please note that the pyui class must have its
    Custom Class attr set to selfwrapper

    Thanks @JonB
    '''
    def __init__(self, pyui_str, raw = True):

        # black magic here, for me at least...
        class selfwrapper(ui.View):
            def __new__(cls):
                return self

            if raw:
                pyui_str = json.dumps(pyui_str)

            ui.load_view_str(pyui_str,
                    bindings={'selfwrapper':selfwrapper, 'self':self})

def xx():
    print 'hi'
    return True

class PYUILoader(ui.View):
    '''
    loads a pyui file into the class, acts as another ui.View
    class.
    ** Please note that the pyui class must have its
    Custom Class attr set to selfwrapper

    Thanks @JonB
    '''
    def __init__(self, f_name = None):
        print 'in PYUILoader init'

        # black magic here, for me at least...
        class selfwrapper(ui.View):
            def __new__(cls):
                return self

        if not f_name.endswith('.pyui'):
            f_name += '.pyui'

        # make sure the file exists
        if not os.path.isfile(f_name):
            raise OSError

        ui.load_view( f_name ,
            bindings={'selfwrapper':selfwrapper, 'self':self})



class MyClass(PYUILoader):
    def __init__(self, f_name ):
        PYUILoader.__init__(self, f_name)
        self.width = 500
        self.height = 500
        print 'in Myclass'
        self['menu'].bg_color = 'red'

    def xx(self):
        print 'hello from my class'
        return True



if __name__ == '__main__':
    mc = MyClass('StdView')
    mc.present('sheet', animated = False)
JonB

@Phuket2
Partly to understand the black magic you need to look at what ui._view_from_dict does.

If custom_class is set, the loader checks for the custom class's name in the bindings (or current scope's locals and globals), and if that class is an subclass of View will instantiate it, without arguments. Then it proceeds to set various attributes of this new view, and finally returns the view.

        v = ViewClass()
    v.frame = _str2rect(view_dict.get('frame'))
    v.flex = attrs.get('flex', '')
    v.alpha = attrs.get('alpha', 1.0)
    v.name = attrs.get('name')
        ...
        return v
...

So the issue is that this creates a whole new instance of our custom class, and returns it... and if the constructor for that class is trying to call load_view, you get an infinite loop. But since we can override the bindings dict, we create a wrapper class whose new constructor simply returns the current instance.

If you also want to be able to use load_view to instantiate the view, rather than use selfwrapper as the custom class name, you would use the actual custom class name, as in my example above. adding self to bindings as you have done is also a good idea, since it lets you reference instance methods as actions.

JonB

Actually, here is a slightly less verbose example which hides some of the details so you have less to remember... basically create a factory function to do the same in one line.

# wrapper.py
def WrapInstance(obj):
   class Wrapper(obj.__class__):
      def __new__(cls):
         return obj
   return Wrapper

#MyView.py
from wrapper import WrapInstance
class MyView(ui.View):
    def __init__(self):
      ui.load_view('MyView',bindings={'MyView':WrapInstance(self),'self':self})
Phuket2

@JonB , thanks so much . Sorry for the delay. Just needed to get around to it.
But I created a snippet for myself.

import ui

# wrapper.py, Pythonista Forum @JonB
# https://forum.omz-software.com/topic/3176/use-a-pyui-file-in-another-pyui-file
# remember to add the the name of the class to the 'Custom View Class'
# in the .pyui

_pyui_file_name = 'find.pyui'


def WrapInstance(obj):
    class Wrapper(obj.__class__):
        def __new__(cls):
            return obj
    return Wrapper


class MyClass(ui.View):
    def __init__(self, *args, **kwargs):
        ui.load_view(_pyui_file_name,
        bindings={'MyClass': WrapInstance(self), 'self': self})

        super().__init__(*args, **kwargs)

if __name__ == '__main__':
    w = 600
    h = 325
    f = (0, 0, w, h)
    mc = MyClass(bg_color='deeppink')
    mc.present('sheet')
Phuket2

@JonB , one small improvement below. Not a big deal, but all constants removed.

ui.load_view(_pyui_file_name,
        bindings={self.__class__.__name__: WrapInstance(self), 'self': self})
Phuket2

Ok, I am an idiot. We got to this before 😱😱😱
But the below I think is a small improvement with a small refactor.
I tested it and the globals are handled correctly , with the custom view assignments as well as custom attributes.
Just separation between the custom class and the binding creation. I realise not a huge leap. But it does make it more tidy to call. In my opinion anyway.

def pyui_bindings(obj):
    def WrapInstance(obj):
        class Wrapper(obj.__class__):
            def __new__(cls):
                return obj
        return Wrapper

    bindings = globals().copy()
    bindings[obj.__class__.__name__]=WrapInstance(obj)
    return bindings

class PYUIClass(ui.View):
    def __init__(self, pyui_fn, *args, **kwargs):
        ui.load_view(pyui_fn, pyui_bindings(self))
        super().__init__(*args, **kwargs)