Search code examples
pythoncanvasdynamickivykivy-language

How do I dynamically update a canvas.before widget?


To the best of my knowledge, to set a background image in a Kivy application, you should define a Rectangle widget as a child of canvas.before and set its source. This works for a static value.

However, I would like to change the background from time to time. I expected this MRE to do just that by invoking canvas.ask_update() but that doesn't work. The debug statement Got background X is printed only once.

How can I dynamically update the background? I would prefer to define widgets in kv rather than programmatically, if at possible.

sample2.kv

#:kivy 1.0.9

<Sample2Gui>:
    canvas.before:
        Rectangle:
            pos: self.pos
            size: self.size
            source: app.get_background_source()

    Button:
        font_size: sp(50)
        pos_hint: {"center_x": 0.5, "center_y": 0.5}
        size_hint: 0.2, 0.1
        text: "Cycle background"
        on_press: app.update_background_source()

sample2.py

from kivy.app import App
from kivy.config import Config
from kivy.uix.floatlayout import FloatLayout


class Sample2Gui(FloatLayout):
    pass


class Sample2App(App):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.index = 0
        print("Initialised index to 0.")

    def build(self):
        return Sample2Gui()

    def get_background_source(self):
        source = f"background{self.index}.png"
        print(f"Got background {source}.")
        return source

    def update_background_source(self):
        self.index = (self.index + 1) % 3  # 0, 1, 2
        print(f"Set index to {self.index}.")
        self.root.canvas.ask_update()


if __name__ == '__main__':
    Config.set('graphics', 'window_state', 'maximized')
    Sample2App().run()

Solution

  • From a comment buried underneath this helpful blog post about the Kivy canvas:

    The short answer is that you need to create a property in your python code that you will reference in the kv language. You will use that property to modify the string of the source.

    This is not a Python property (defined with the @property decorator), but a type of Kivy class. From a related answer:

    General rule for programming in Kivy, if you want to change code depending on a property of a Widget/Object use a Kivy Property. [A Kivy property] provides you the option to Observe Property changes... implicitly through kv language as mentioned above.

    In this case we can define the source as a StringProperty:

    sample2.kv

    #:kivy 1.0.9
    
    <Sample2Gui>:
        canvas.before:
            Rectangle:
                pos: self.pos
                size: self.size
                source: app.background_source
                # NOTE: no longer a method, see Python
                # code for matching definition
    
        Button:
            font_size: sp(50)
            pos_hint: {"center_x": 0.5, "center_y": 0.5}
            size_hint: 0.2, 0.1
            text: "Cycle background"
            on_press: app.update_background_source()
    

    sample2.py

    from kivy.app import App
    from kivy.config import Config
    from kivy.properties import StringProperty
    from kivy.uix.floatlayout import FloatLayout
    
    
    class Sample2Gui(FloatLayout):
        pass
    
    
    class Sample2App(App):
        background_source = StringProperty()  # default is ""
    
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
            self.index = -1
            self.update_background_source()
    
        def build(self):
            return Sample2Gui()
    
        def update_background_source(self):
            self.index = (self.index + 1) % 3  # 0, 1, 2
            self.background_source = f"background{self.index}.png"
    
    
    if __name__ == '__main__':
        Config.set('graphics', 'window_state', 'maximized')
        Sample2App().run()