Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kivy: How to initialize the viewclass of the RecycleView dynamically?

Recently I have been struggling with using RecycleView in kivy. I have made quite many test and got a bit confused.

My goal I want to create a Recycle view where each Row contains few Label (i.e. columns), but the number of columns is set dynamically (but is the same for all the rows of course). Typically, I have a database and I want to display the table but I want to dynamically change the columns that I display.

There have been similar questions, but they all deal a fixed number of value to set.

What works

  • If the number of columns is fixed, the values can be linked using properties
  • I can adding widget dynamically using add_widget

What doesn't work

  • If the cell are added dynamically, I cannot link the content of value in the kv widget to the python variable
  • If the number of Label widget is fixed, I can updated things dynamically (like the examples) but it cannot be directly initialized.

Here is an example with all the different possibilities that I have tried:

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import StringProperty, ObjectProperty
from kivy.uix.label import Label
from kivy.uix.recycleview import RecycleView

kv = """
<Row>:
    id: row
    canvas.before:
        Color:
            rgba: 0.5, 0.5, 0.5, 1
        Rectangle:
            size: self.size
            pos: self.pos
    value1: ''
    value2: ''
    value3: ''
    value4: ''
    value5: ''
    Label: #Label 1
        text: root.value1


<Test>:
    canvas:
        Color:
            rgba: 0.3, 0.3, 0.3, 1
        Rectangle:
            size: self.size
            pos: self.pos
    orientation: 'vertical'
    GridLayout:
        cols: 3
        rows: 2
        size_hint_y: None
        height: dp(108)
        padding: dp(8)
        spacing: dp(16)
        Button:
            text: 'Update list'
            on_press: root.update()
    RecycleView:
        id: rvlist
        scroll_type: ['bars', 'content']
        scroll_wheel_distance: dp(114)
        bar_width: dp(10)
        viewclass: 'Row'
        RecycleBoxLayout:
            default_size: None, dp(56)
            default_size_hint: 1, None
            size_hint_y: None
            height: self.minimum_height
            orientation: 'vertical'
            spacing: dp(2)
"""

Builder.load_string(kv)

class Row(BoxLayout):
    constant_value = 'constant content from class data'
    value1 = StringProperty('default 1')
    value2 = StringProperty('default 2')
    value3 = StringProperty('default 3')
    value4 = StringProperty('default 4')
    value5 = StringProperty('default 5')
#  value5,....

    def __init__(self,**kwargs):
        super().__init__(**kwargs)
        self.add_widget(Label(text = 'constant content')) # Label 2
        self.add_widget(Label(text = self.constant_value)) #Label 3
        self.add_widget(Label(text = self.value4)) # Label 4
        self.add_widget(Label(text = self.value5)) # Label 5
        for x in kwargs:
            self.add_widget(Label(text = 'content from **kwargs'))# Label 6


class Test(BoxLayout):

    def __init__(self,**kwargs):
        super().__init__(**kwargs)
        self.ids.rvlist.data = [{'value1': 'init content from parent class',
        'value4': 'init content from parent class'} for x in range(15)]

    def update(self):
        self.ids.rvlist.data = [{'value1': 'updated content from parent class dynamic widget',
        'value4': 'updated content from parent class dynamic widget',
        'value5': 'updated content from parent class static widget'} for x in range(15)]


class TestApp(App):
    def build(self):
        return Test()


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

The behavior are the following:

  • Label1: this is defined in the same as in the documentation. This works well but I would need to define as many value in the Row class as the maximum possible value of columns and only use few of them. It looks a bit hackish...
  • Label2: It works as expected. This is a constant content. But it's not what I need (I want to fill these row with dynamically generated content)
  • Label3: a variation of Label2, not what I need. But works.
  • Label4: Here the label is set at initialization. the initialization and update don't work, likely because the widget is not yet created so the self.value4 does not exist yet. Is there a workaround?
  • Label5: the label is set on the update. Both the initialization and update don't work. It is similar to 4.
  • Label6: is not even created. I would have expected that the data would be one of the argument pass to the class for initialization such that I could created a Label for each of the data. That would be the ideal case I guess.

It raises the questions: - how to create dynamically a kv property and link it to another property? (Something like self.add_widget(Label(value5 = parent.value5,text = self.value5)). This would allow to have an update of the text after the initialization (like my label1 somehow) - how to access the data during the initialization? - how to use a self.value to initialize the Label?

I hope it is clear enough... I was confused enough myself.

like image 854
L. C. Avatar asked Nov 24 '25 06:11

L. C.


1 Answers

After a lot of head-scratching I finally understand how to achieve what I wanted, this was not so straightforward.

There are two things which were wrong in my initial post: - As mentioned, the widget was not yet initialized when I assigned the property. The trick is to use Clock.schedule_once to delay the initialization to the next frame. I have seen this trick in another question (sorry, I can't find it agin right now). - An issue which arise from that is that the widgets are created but cannot be updated/accessed. For that I created a callback method which updates the labels every time the data in the viewclass is updated.

Here is the code:

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty
from kivy.uix.label import Label
from kivy.uix.recycleview import RecycleView
from kivy.clock import Clock

kv = """
<Row>:
    id: row
    canvas.before:
        Color:
            rgba: 0.5, 0.5, 0.5, 1
        Rectangle:
            size: self.size
            pos: self.pos


<Test>:
    canvas:
        Color:
            rgba: 0.3, 0.3, 0.3, 1
        Rectangle:
            size: self.size
            pos: self.pos
    orientation: 'vertical'
    GridLayout:
        cols: 3
        rows: 2
        size_hint_y: None
        height: dp(108)
        padding: dp(8)
        spacing: dp(16)
        Button:
            text: 'Update list'
            on_press: root.update()
    RecycleView:
        id: rvlist
        scroll_type: ['bars', 'content']
        scroll_wheel_distance: dp(114)
        bar_width: dp(10)
        viewclass: 'Row'
        RecycleBoxLayout:
            default_size: None, dp(56)
            default_size_hint: 1, None
            size_hint_y: None
            height: self.minimum_height
            orientation: 'vertical'
            spacing: dp(2)
"""

Builder.load_string(kv)

class Row(BoxLayout):
    row_content = ObjectProperty()


    def __init__(self,**kwargs):
        super().__init__(**kwargs)
        Clock.schedule_once(self.finish_init,0)
# it delays the end of the initialization to the next frame, once the widget are already created
# and the properties properly initialized

    def finish_init(self, dt):
        for elt in self.row_content:
            self.add_widget(Label(text = elt))
            # now, this works properly as the widget is already defined
        self.bind(row_content = self.update_row)


    def update_row(self, *args):
        # right now the update is rough, I delete all the widget and re-add them. Something more subtle
        # like only replacing the label which have changed
        print(args)
        # because of the binding, the value which have changed are passed as a positional argument.
        # I use it to set the new value of the labels.
        self.clear_widgets()
        for elt in args[1]:
            self.add_widget(Label(text = elt))


class Test(BoxLayout):

    def __init__(self,**kwargs):
        super().__init__(**kwargs)
        self.ids.rvlist.data = [{'row_content': [str(i) for i in range(5)]} for x in range(15)]

    def update(self):
        self.ids.rvlist.data[0]['row_content'] =  [str(10*i) for i in range(5)]
        self.ids.rvlist.refresh_from_data()

class TestApp(App):
    def build(self):
        return Test()

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

With this code I can then create a table with multiple columns and initialize each row with the number of cell available. If the number of column change it will change automatically change the number of cells (it might needed to modify a bit the update function).

One limitation: the widget are created dynamically, therefore it is not possible to call them by id directly. If you need more advanced widget inside each Row, you may need to create some function to manage yourself the widgets already created (by storing to which position of self.children the widget corresponds to).

If you see a better way to do things, let me know. I still find that the Clock.schedule_once(self.finish_init,0) looks like a hack and I find it weird there is not something more straightforward implemented in the kivy framework.

like image 120
L. C. Avatar answered Nov 25 '25 18:11

L. C.



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!