Search code examples
pythonkivykivy-language

Kivy checkbox issue using custom widget


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'

Solution

  • 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}')