3

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.
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
wimworks
  • 283
  • 3
  • 15

2 Answers2

3

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

ikolim
  • 15,721
  • 2
  • 19
  • 29
  • Oh dang. I was using looking up my widgets in a list of children and checking their `id`. I guess I'll have to do this if they're going to remove that though. – wimworks Jun 13 '19 at 22:27
  • @ikolim, do you have a source for your statement about ids being deprecated? – John Anderson Jun 14 '19 at 19:28
  • @JohnAnderson: Quote: [WARNING] Deprecated property "" of object "<__main__.StreakButton object at 0x0000025E3A666180>" has been set, it will be removed in a future version. – ikolim Jun 14 '19 at 19:40
  • @ikolim, Misunderstood your comment. Thought you were saying that `Kivy` `ids` were deprecated. I see that you are talking about the `Widget` property named `id`, which is totally unrelated to the `Kivy` `ids`. – John Anderson Jun 14 '19 at 21:32
1

Kivy 2.0 throws an error when you use id='value' when dynamically creating kivy widgets from python main file. But by using weak references you achieve the following success.

from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
import weakref


class MyLayout(Widget):
    def use_weakref_id_to_replace_text(self, *args):
        self.ids.AddUserTextBox.text = "shotta"
        print("released and renamed the text to shotta")

    def add_textinpt_with_weak_ref_dyn_id(self, *args):
        print('Pressed and added text input box')

        textinput = TextInput(pos=(380,380),text='123')
        
        self.add_widget(textinput)

        # We'll use a weak ref to add our dynamic id 
        self.ids['AddUserTextBox'] = weakref.ref(textinput)

class MdApp(App):
    def build(self):
        root = MyLayout()
        Btn = Button(size=(250,250), pos=(100,100),text="Dynamic id assign n use id to rename")
        Btn.bind(on_release=root.use_weakref_id_to_replace_text,on_press=root.add_textinpt_with_weak_ref_dyn_id)
        root.add_widget(Btn)
        return root 


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