lemondifficult
lemondifficult

Reputation: 229

Kivy Layout Background Not Painted on ScrollView

Please consider the following code:

Builder.load_string(
"""
<TestView>:
    canvas.before:
        Color:
            rgba: 0, 1, 0, 1
        Rectangle:
            pos: self.pos
            size: self.size
""")

class TestView(GridLayout):

    def __init__(self, **kwargs):
        super(TestView, self).__init__(**kwargs)

        self.cols = 1
        self.spacing = 20

        self.add_widget(Label(text="I'm on the TestView", height=80 ))

class TestScreen(Screen):

    def __init__(self, **kwargs):
        super(TestScreen, self).__init__(**kwargs)

        self.content_grid_layout = GridLayout(cols=1, spacing=20, size_hint_y=None)
        self.content_grid_layout.bind(minimum_height=self.content_grid_layout.setter('height'))

        self.content_grid_layout.add_widget(Label(text="A"))
        self.content_grid_layout.add_widget(Label(text="B"))

        self.content_grid_layout.add_widget(TestView())

        self.scroll_view = ScrollView(size_hint=(1, 1), size=(self.width, self.height))
        self.scroll_view.add_widget(self.content_grid_layout)

        self.add_widget(self.scroll_view)

sm = ScreenManager()
sm.add_widget(TestScreen(name="test_screen"))

class TestApp(App):

    def build(self):
        return sm

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

(I've left out the imports to save space). So my specific problem here is that I expect the background of TestView to be painted green, but it isn't here. It would be if you just add TestView straight onto the Screen's layout, i.e. change the init of TestScreen to just do:

self.content_grid_layout = GridLayout(cols=1, spacing=20, size_hint_y=None)
self.content_grid_layout.add_widget(TestView())
self.add_widget(self.content_grid_layout)

However, I need my content to be on a ScrollView here. So I also note that if I just comment out this line:

self.content_grid_layout.bind(minimum_height=self.content_grid_layout.setter('height'))

from the original code, everything appears to work as expected and the background of the TestView is painted green. However, if I do that in turns out the ScrollView isn't doing what I expect (it doesn't scroll).

If I give the TestView an explicit height, then everything works as expected, e.g. add:

self.size_hint_y = None
self.height = 40

to the TestView init. So I guess the problem is something not really knowing what height to give the TestView, but then weirdly being able to put the Label in approximately the right place, but not paint the background, or paint it off the screen or something. Anyway, hard-coding the TestView doesn't work for me because IRL it will have some dynamic content that will have varying heights.

I feel like in a sane world the height of a layout would be the height of its contents, unless otherwise specified, but maybe that's just me. I think if I can size the TestView to its contents then I'd get the behaviour I'm expecting?


Edit:

OK, I think I can get it to do what I want by following the strategy described here: https://groups.google.com/d/msg/kivy-users/xRl2l8-1qLs/zzpk-QG4C0MJ (in particular the CustomGridLayout described there which is like my TestView I guess).

So the basic idea it seems is that we set the custom GridLayout (TestView) to zero height initially, then manually update its height for each child we add, so something like:

class TestView(GridLayout):

    def __init__(self, **kwargs):
        super(TestView, self).__init__(**kwargs)

        self.cols = 1
        self.spacing = 20, 20

        self.size_hint_y = None
        self.height = 0

        self.height = self.height + 30 + self.spacing[1]
        self.add_widget(Label(text="I'm on the TestView 1", size_hint_y=None, height=30))
        self.height = self.height + 30 + self.spacing[1]
        self.add_widget(Label(text="I'm on the TestView 2", size_hint_y=None, height=30))
        self.height = self.height + 30 + self.spacing[1]
        self.add_widget(Label(text="I'm on the TestView 3", size_hint_y=None, height=30))

I'm not going to lie, I think this is pretty ugly. It really seems that the layout should be able to work out its new height when I add a widget to it without having to spoon feed it like this. Anyway, it seems to work. I'm going to leave the question open in case someone has a not-horrible way of doing this.

Upvotes: 0

Views: 376

Answers (2)

lemondifficult
lemondifficult

Reputation: 229

OK, I think I've cracked it - Edvardas' notion of binding the TestView's minimum height to its height is correct (and I guess maybe this should have been obvious to me since it's also what we do for the 'content_grid_layout'). However, it wasn't working because we also need 'size_hint_y: None'.

I don't think we should take out the height / minimum height binding from 'content_grid_layout' because that stops scrolling working.

Doing this it works as expected and we don't have to manually set the TestView's height. Here's a full working example to save any confusion - I've changed the TestView from a GridLayout to a BoxLayout but it works as a GridLayout too:

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.scrollview import ScrollView

Builder.load_string(
"""
<TestView>:
    size_hint_y: None            # <----------------- MAKE SURE YOU DO THIS!
    height: self.minimum_height  # <----------------- AND THIS!
    canvas.before:
        Color:
            rgba: 0, 0.8, 0.06, 0.5
        RoundedRectangle:
            pos: self.pos
            size: self.size
            radius: [16,16,16,16]
""")

class TestView(BoxLayout):

    def __init__(self, **kwargs):
        super(TestView, self).__init__(**kwargs)

        self.orientation = 'vertical'
        self.padding = [20, 20, 20, 20]
        self.spacing = 20

        self.add_widget(Label(text="I'm on the TestView 1"))
        self.add_widget(Label(text="I'm on the TestView 2"))
        self.add_widget(Label(text="I'm on the TestView 3"))

class TestScreen(Screen):

    def __init__(self, **kwargs):
        super(TestScreen, self).__init__(**kwargs)

        self.content_grid_layout = GridLayout(cols=1, spacing=20, size_hint_y=None)
        self.content_grid_layout.bind(minimum_height=self.content_grid_layout.setter('height'))

        # Give us something to scroll:
        for i in range(20):
            btn = Button(text=str(i), size_hint_y=None, height=40)
            self.content_grid_layout.add_widget(btn)
            if i == 5:
                self.content_grid_layout.add_widget(TestView())

        self.scroll_view = ScrollView(size_hint=(1, 1), size=(self.width, self.height))
        self.scroll_view.add_widget(self.content_grid_layout)

        self.add_widget(self.scroll_view)

sm = ScreenManager()
sm.add_widget(TestScreen(name="test_screen"))

class TestApp(App):

    def build(self):
        return sm

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

Upvotes: 0

Edvardas Dlugauskas
Edvardas Dlugauskas

Reputation: 1489

By commenting out the line you mentioned (self.content_grid_layout.bind(minimum_height=self.content_grid_layout.setter('height'))) and adding height: self.minimum_height to the Builder string I was able to get your TestView to be green. The TestScreen is also scrollable.

Consider changing the TestScreen initializer to this:

def __init__(self, **kwargs):
    super(TestScreen, self).__init__(**kwargs)

    self.content_grid_layout = GridLayout(cols=1, spacing=20, size_hint_y=None)
    # self.content_grid_layout.bind(minimum_height=self.content_grid_layout.setter('height'))

    self.content_grid_layout.add_widget(Label(text="A"))
    self.content_grid_layout.add_widget(Label(text="B"))

    self.content_grid_layout.add_widget(TestView())

    self.scroll_view = ScrollView(size_hint=(1, 1), size=(self.width, self.height))
    self.scroll_view.add_widget(self.content_grid_layout)

    self.add_widget(self.scroll_view)

And the Builder string to this:

Builder.load_string(
"""
<TestView>:
    height: self.minimum_height
    canvas.before:
        Color:
            rgba: 0, 1, 0, 1
        Rectangle:
            pos: self.pos
            size: self.size
""")

Upvotes: 0

Related Questions