I'm new to Kivy (and programming in general) so mucking around with a few little projects to try to learn. In particular I'm trying to understand creating custom widgets in Kivy to cut down on code and make it easier when groups of widgets are always used together (i.e. Label with CheckBox).
I've created a custom widget to combine a Label with a Checkbox. In this example I want to change the image depending on which checkbox is selected.
In the code I have made two versions, one using the custom widget and the other splitting out the Label and Checkbox. The second way works but the custom widget does not with an error:
AttributeError: 'NoneType' object has no attribute 'ids'
In the program that I pulled this snippet from I have done another widget with two labels, where one label is the title and the other is updated via a Clock. It works fine so I can't understand why this won't.
Here is my kv and py files:
#:kivy 2.0.0
<SoundCheckBox@GridLayout>:
cols:2
label_text: ''
check_state: 'normal'
Label:
text: self.parent.label_text
CheckBox:
state: self.parent.check_state
allow_no_selection: False
group: 'sound_option'
on_active: app.sound_stat(self, self.active, self.parent.label_text)
<MyCheckBox@CheckBox>:
state: 'normal'
allow_no_selection: False
group: 'sound_option'
on_active: app.sound_stat_alt(self, self.active, self.label_text)
GridLayout:
rows: 4
Label:
text: 'Change Image Test'
BoxLayout:
orientation:'horizontal'
SoundCheckBox:
id: sound_on
label_text: 'On'
check_state: 'normal'
SoundCheckBox:
id: sound_dings
label_text: 'Ding'
check_state: 'down'
SoundCheckBox:
id: sound_off
label_text: 'Off'
check_state: 'normal'
BoxLayout:
orientation:'horizontal'
Label:
text: 'On (a)'
MyCheckBox:
id: sound_on
label_text: 'On'
Label:
text: 'Ding (a)'
MyCheckBox:
id: sound_dings
label_text: 'Ding'
Label:
text: 'Off (a)'
MyCheckBox:
id: sound_off
label_text: 'Off'
Image:
id: im
source: 'sound-On.png'
And the Python file
from kivy.app import App
class TestApp(App):
def on_start(self):
print(self.root.ids)
def sound_stat(self, instance, value, option):
image_source = f'sound-{option}.png'
# depending on check box is checked I want to set an image
# print(self.root.ids) # Fails if this is not commented out
self.root.ids.im.source = image_source # Fails if this is not commented out
print(f'value is {value} and option is {option} so image source will be {image_source}')
def sound_stat_alt(self, instance, value, option):
image_source = f'sound-{option}.png'
# depending on check box is checked I want to set an image
print(self.root.ids)
self.root.ids.im.source = image_source
print(f'value is {value} and option is {option} so image source will be {image_source}')
if __name__ == "__main__":
TestApp().run()
It won't run unless I comment out the line in the sound_stat method as commented in the code. Any insight much appreciated.
Edit: I just noticed that the above code will work if I don't set the Checkbox state to 'down'. I trieds setting one of the other CheckBox's state to 'down as well and I get the same error. So it works but I can't set a default selection within the kv file. Why would that be? So for this bit of code in the kv file, if I have all the states as 'normal' the code will run. As soon as I set any of them to 'down' it fails with the Attribute Error. I then set the default button as 'down' in the start_up method of the App class. Is this a bug? Sure this should be able to be set in the kv file as I tried.
SoundCheckBox:
id: sound_on
label_text: 'On'
check_state: 'normal'
SoundCheckBox:
id: sound_dings
label_text: 'Ding'
check_state: 'down' # This 'down' causes the crash.
SoundCheckBox:
id: sound_off
label_text: 'Off'
check_state: 'normal'
The problem is that when you set the check_state
of a SoundCheckBox
in your kv
file the on_active
action is triggered. But that happens even before the root
widget of the App
is assigned. The fix is either to delay the setting of check_state
using something like Clock.schedule_once()
or to just ignore any change to check_state
that happens before the root
widget is assigned, like this:
def sound_stat(self, instance, value, option):
# ignore calls to this method that happen before the root widget is assigned
if self.root is None:
return
image_source = f'sound-{option}.png'
# depending on check box is checked I want to set an image
# print(self.root.ids) # Fails if this is not commented out
self.root.ids.im.source = image_source # Fails if this is not commented out
print(f'value is {value} and option is {option} so image source will be {image_source}')