How do I make my custom widget appear in my splitter using Kivy?

I’m still learning Kivy nuances, & I just can’t seem to make my Chessboard widget appear in my splitter. I substituted a working button widget in the splitter for my custom widget, but it’s not acting the same. What am I doing wrong?

import kivy
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.splitter import Splitter
from kivy.graphics import Color, Rectangle
from kivy.uix.image import Image
from kivy.uix.widget import Widget

kivy.require('2.0.0')

class ChessBoardWidget(Widget):  # FloatLayout
    def __init__(self, **kwargs):
        super(ChessBoardWidget, self).__init__(**kwargs)

        with self.canvas.before:
            Color(0, 1, 0, 1)
            self.rect = Rectangle(size=(self.width, self.height), pos=self.pos)

        self.add_widget(
            Image(source="./data/images/chess-pieces/DarkerGreenGreyChessBoard.png", pos=self.pos,
                  size_hint=(1, 1), keep_ratio=True, allow_stretch=True))


class SplitterGui(BoxLayout):
    def __init__(self, **kwargs):
        super(SplitterGui, self).__init__(**kwargs)
        self.orientation = 'horizontal'

        split1_boxlayout = BoxLayout(orientation='vertical')
        split1 = Splitter(sizable_from='bottom', min_size=100, max_size=1000)
        chessboard_widget = ChessBoardWidget()  # was s1_button = Button(text='s1', size_hint=(1, 1)) WORKED!
        s3_button = Button(text='s3', size_hint=(1, 1))
        split1.add_widget(chessboard_widget)  # was split1.add_widget(s1_button) WORKED!
        split1_boxlayout.add_widget(split1)
        split1_boxlayout.add_widget(s3_button)
        self.add_widget(split1_boxlayout)

        split2 = Splitter(sizable_from='left', min_size=100, max_size=1000)
        s2_button = Button(text='s2', size_hint=(.1, 1))
        split2.add_widget(s2_button)
        self.add_widget(split2)


class SplitterTestApp(App):
    def build(self):
        return SplitterGui()  # root


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

Answer

Your ChessBoardWidget is being drawn, it’s just not where you expect it. Three things to keep in mind:

  1. Using the Widget class as a container for other widgets eliminates the capabilities of widgets that are intended for use as containers such as size_hint and pos_hint. From the documentation:

A Widget is not a Layout: it will not change the position or the size of its children. If you want control over positioning or sizing, use a Layout.

  1. When you reference pos or size of a widget in its __init__() method, you will always get the default values of (0,0) for pos and (100,100) for size.
  2. References to properties (such as pos and size) in a widgets __init__() method use the current values of those properties (see #2 above), and there is no binding to handle changes in those values. So, for example, creating a Rectangle in a widgets __init__() using the widgets pos and size will create an unchanging Rectangle at pos of (0,0) and size of (100,100).

So, a fix for getting the ChessBoardWidget drawn where you expect is to just change the base class of ChessBoardWidget to RelativeLayout. The nice thing about RelativeLayout is that adding a child with the default pos and size_hint will result in the child (in your case, the Image) being drawn with the same position and size as its parent (the ChessBoardWidget). Something like this:

class ChessBoardWidget(RelativeLayout):
    def __init__(self, **kwargs):
        super(ChessBoardWidget, self).__init__(**kwargs)

        with self.canvas.before:
            Color(0, 1, 0, 1)
            self.rect = Rectangle(size=(self.width, self.height), pos=self.pos)

        self.add_widget(
            Image(source="./data/images/chess-pieces/DarkerGreenGreyChessBoard.png", keep_ratio=True, allow_stretch=True))

Note that the green Rectangle will still be drawn at the lower left corner of the ChessBoardWidget with size of (100,100). To fix that, you either need to define the Rectangle in a kivy language file or string that gets loaded before the ChessBoardWidget gets created. Or you need to set up bindings that will redraw the Rectangle whenever the ChessBoardWidget gets re-sized.

I believe this is the easiest way to draw the green background, using Builder.load_string():

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

class ChessBoardWidget(RelativeLayout):
    def __init__(self, **kwargs):
        super(ChessBoardWidget, self).__init__(**kwargs)

        # with self.canvas.before:
        #     Color(0, 1, 0, 1)
        #     self.rect = Rectangle(size=(self.width, self.height), pos=self.pos)

        self.add_widget(
            Image(source="./data/images/chess-pieces/DarkerGreenGreyChessBoard.png", keep_ratio=True, allow_stretch=True))