Search code examples
pythonpython-3.xkivykeyerror

access dynamically added widget with id


The Goal
I want to create a small script that adds buttons dynamically, but still lets me perform functions on specific ones via root.


My Methods
I made this script.

It is capable of dynamically adding large buttons along the top.
Each of these buttons slightly changes its own color when pressed.

It has two smalls buttons at the bottom.
The first button dynamically adds new large buttons along the top.
The second button resets the color of the first large button on the top.

My Code

#!/usr/bin/env python3
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout

Builder.load_string('''
<RootWidget>:  
    Button:
        text: 'Add'
        size_hint: (None, None)
        size: (40, 40)
        pos: (40, 40)
        group: 'action'
        on_press: root.createNextTarget()
    Button:
        text: 'res'
        size_hint: (None, None)
        size: (40, 40)
        pos: (100, 40)
        group: 'action'
        on_press: root.resetTarget()
''')

class RootWidget(FloatLayout):
    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)
        #note: self.ids isn't populated yet. I guess we can't use it yet.
        self.createNextTarget()

    def resetTarget(self):
        f_target = self.ids['targetbutton0']
        f_target.background_color = (1.0, 1.0, 1.0, 1.0)
        return True

    def countTargets(self):
        return [str(x.__class__.__name__) for x in self.children if x != None].count('TargetButton')

    def createNextTarget(self):
        f_nextButton = TargetButton(id="targetbutton"+str(self.countTargets()),
                               size_hint=(None, None),
                               pos=(80 + (10 + 60) * self.countTargets(), 100),
                               size=(60, 60),
                               background_normal = '',
                               background_color = (1, 1, 1, 1),
                               group = 'target')
        self.add_widget(f_nextButton)
        f_nextButton.bind(on_press=TargetButton.lowerAllRGB)

class TargetButton(Button):
    def __init__(self, **kwargs):
        super(TargetButton, self).__init__(**kwargs)

    def lowerAllRGB(self):
        f_r, f_g, f_b, f_a = self.background_color
        if f_r >= 0.1: f_r = f_r - 0.1
        if f_g >= 0.1: f_g = f_g - 0.1
        if f_b >= 0.1: f_b = f_b - 0.1
        self.background_color = (f_r, f_g, f_b, f_a)
        return True

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

    def on_stop(self):
        print("TestApp.on_stop: finishing", self.root.ids)

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

The Problem
If I try to hit the reset button (that accesses the widget via root.ids), I get the error: KeyError: 'targetbutton0'

After finding a post about a similar problem, I thought root.ids just wouldn't work during RootWidget.__init__.
But when I use the button to add buttons after RootWidget.__init__ is finished, TestApp.on_stop() still prints: TestApp.on_stop: finishing {}

So root.ids is still empty, and doesn't seem to include any dynamically added widgets despite me assigning an id attribute to each of them.

My questions to you

  1. Given the way I am dynamically adding widgets, is using root.ids just worthless for my purposes?
  2. Is there a decent way for me to access my widgets via id?
    I saw another question here asking something similar. But it didn't answer my question about dynamically added widgets.

Solution

  • Question 1 - root.ids / self.ids

    Given the way I am dynamically adding widgets, is using root.ids just worthless for my purposes?

    Answer

    id assigned to dynamically added widgets are not store in self.ids or root.ids. Therefore, you cannot access dynamically added widgets using self.ids['targetbutton0'] or self.ids.targetbutton0. If you do that, you will get a KeyError because it is not found in self.ids which is a dictionary type property.

    When your kv file is parsed, Kivy collects all the widgets tagged with id’s and places them in this self.ids dictionary type property.

    Note: These type of id (i.e. id assigned to dynamically created widget) is deprecated and will be removed in a future Kivy version.

    [WARNING] Deprecated property "<StringProperty name=id>" of object "<kivy.uix.button.Button object at 0x7feeec0968d0>" has been set, it will be removed in a future version
    

    Question 2

    Is there a decent way for me to access my widgets via id?

    Solution

    You could create your own list of ids of dictionary type property.

    Snippets

    from kivy.properties import DictProperty
    
    class RootWidget(FloatLayout):
        dynamic_ids = DictProperty({})    # declare class attribute, dynamic_ids
    
        def __init__(self, **kwargs):
            super(RootWidget, self).__init__(**kwargs)
            self.createNextTarget()
    
        def resetTarget(self):
            f_target = self.dynamic_ids['targetbutton0']
            f_target.background_color = (0.0, 1.0, 1.0, 1.0)    # cyan colour
            return True
    
        ...
    
        def createNextTarget(self):
            id = "targetbutton" + str(self.countTargets())
            f_nextButton = TargetButton(id=id,
                                   size_hint=(None, None),
                                   pos=(80 + (10 + 60) * self.countTargets(), 100),
                                   size=(60, 60),
                                   background_normal = '',
                                   background_color = (1, 1, 1, 1),    # white colour
                                   group = 'target')
            self.add_widget(f_nextButton)
            self.dynamic_ids[id] = f_nextButton
            f_nextButton.bind(on_press=TargetButton.lowerAllRGB)
    

    Output

    Result