Nesting widgets in kivy

Viewed 2557

I'm trying to make an interface in kivy and I think there are some fundamental things I don't understand about custom widgets and how to hierarchy them, even after going through the tutorial. I think I have more of a box-model html mindset, so the way widgets are nested in native GUIs are still kind of foreign to me.

Some background:

  1. I consulted this entry on how to add a background (It answers the question: "How to add a background image/color/video/... to a Layout", which I believe I was replicating with the _update_rect methods).

  2. This one that has an on_touch_down event.

K, I'm trying to get MyApp to look like this...

enter image description here

As I understand it, here are the things I'd need for that:

  1. You have an app.
  2. The app has a root.
  3. The root has a background, say the background is white.
  4. The background contains a holder, say the holder has a little margin from the background and is gray. I do want this to be a widget rather than just a non-widget canvas because I want the holder to react to click events, as well. It turns random colors when clicked. (Just to show it's doing something.)
  5. The holder contains two custom widgets. These are circles with labels, each colored green. They turn random colors when clicked (just to show they're doing something).

Here's my code, which doesn't crash anymore, but doesn't display any colors or objects of any kind.

#!/usr/bin/kivy
import kivy
kivy.require('1.7.2')

from random import random
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.floatlayout import FloatLayout
from kivy.graphics import Color, Ellipse, Rectangle

class MyApp(App):
    title = 'My App'
    def build(self):
        root = RootWidget()
        root.bind(
            size=self._update_rect,
            pos=self._update_rect)
    def _update_rect(self, instance, value):
        self.rect.pos = instance.pos
        self.rect.size = instance.size

class RootWidget(FloatLayout):
    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)
        with self.canvas.before:
            Color(1, 0, 0, 1) # This RED does not display.
            self.rect = Rectangle(
                                    size=self.size,
                                    pos=self.pos,
                                    text="some junk!")  # This label does not display.
        mybackground = Background()
        self.add_widget(mybackground)
    def _update_rect(self, instance, value):
        self.rect.pos = instance.pos
        self.rect.size = instance.size

class Background(Widget):
    def __init__(self, **kwargs):
        super(Background, self).__init__(**kwargs)
        with self.canvas.before:    
            Color(1, 1, 1, 1)       # This WHITE does not display
            self.rect = Rectangle(
                                    size=self.size,
                                    pos=self.pos,
                                    text="More stuff!")  # This label does not display.
        myholder = Holder()
        self.add_widget(myholder)
    def _update_rect(self, instance, value):
        self.rect.pos = instance.pos
        self.rect.size = instance.size

class Holder(Widget):
    def __init__(self, **kwargs):
        super(Holder, self).__init__(**kwargs)
        with self.canvas.before:    
            Color(0.25, 0.25, 0.25, 1) # This GRAY does not display
            self.rect = Rectangle(
                                    size=self.size,
                                    pos=self.pos,
                                    text="More stuff!")  # This does not display.
        c1 = Circley(label="Label 1")  # I see I'd need to do some size/pos math here to center
        c2 = Circley(label="Label 2")  # but since everything isn't working, I've tabled this.
        self.add_widget(c1)
        self.add_widget(c2)
    def _update_rect(self, instance, value):
        self.rect.pos = instance.pos
        self.rect.size = instance.size
    def on_touch_down(self, touch):
        rcolor = Color(random(), random(), random(), 1)
        with self.canvas:
            self.color = rcolor

class Circley(Widget):
    def __init__(self, label='label', **kwargs):
        super(Circley, self).__init__(**kwargs)
        with self.canvas.before:    
            Color(0, 1, 0, 1) # This GREEN does not display
            self.circ = Ellipse(
                        size=self.size,
                        pos=self.pos,
                        text=label
            )
    def _update_circ(self, instance, value):
        self.circ.pos = instance.pos
        self.circ.size = instance.size
    def on_touch_down(self, touch):
        rcolor = Color(random(), random(), random(), 1)
        with self.canvas:
            self.color = rcolor

if __name__ == '__main__':
    MyApp().run()

Any pointers on what I'm doing wrong and how to nest these widgets correctly?

2 Answers

The reason you get a blank screen is that your app's build() method does not return anything. Whatever it returns would be the root widget, but even though you make some widgets you don't return one so nothing is displayed.

If you fix this, you can run the app again but you'll immediately get an error something like MyApp has no attribute rect. This is because your root widget is immediately sized and positioned to fill the window, which (as per your root.bind line) triggers MyApp._update_rect. However, this method try to modify MyApp.rect.pos...but the app doesn't have a self.rect! You presumably intended to bind to root._update_rect, not the app's method. (Edit: I bound to root._update_rect instead and now at least the red background and green circle do appear.)

Edit: And as a side note, this would be a lot easier using the kv language, which could automatically take care of a lot of the widget creation, rectangle binding etc.

I don't have time to fix it all right now, but perhaps these two problems can help you fix the overall flow. I'll try to post a more complete answer later if nobody else has.


Here's an updated MyApp, as per the comments.

class MyApp(App):
    title = 'My App'
    def build(self):
        root = RootWidget()
        root.bind(
            size=root._update_rect,
            pos=root._update_rect)
Related