pythoncheckboxkivyselection

My Kivy code doesn't detect checkbox selection


I have recently been trying Kivy for a personal proyect I wanted to do: an app that lets you create a DnD character with a few details. That character would store on a JSON file and load up on the main screen. The problem is, when using checkboxes for the gender selection, it doesn't work at all and I can't find the problem.

Here is the main.py file:

from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.graphics import Color, Line
from kivy.uix.button import Button
from kivy.properties import ObjectProperty, ListProperty
import random
import json
import os


class MainScreen(Screen):
    def get_random_color(self):
        # Returns a random RGBA color
        return [random.random() for _ in range(3)] + [0.8]

    def add_character_button(self, character_data):
        # Create a button for each saved character with a random color and only the name
        button = Button(
            text=character_data["name"],
            size_hint_y=None,
            height=50,
            background_normal='',
            background_color=character_data['color']
        )
        button.custom_color = button.background_color.copy()
        button.bind(pos=self.update_border, size=self.update_border)

        # Add a border to the button
        with button.canvas.before:
            Color(0, 0, 0, 1)  # Set black color for border
            self.border_line = Line(rectangle=(button.x, button.y, button.width, button.height), width=1)

        button.bind(on_release=lambda btn: self.show_character_details(character_data))

        self.ids.character_list.add_widget(button)

    def update_border(self, button, _):
        # Update border position and size when button size or position changes
        self.border_line.rectangle = (button.x, button.y, button.width, button.height)

    def load_user_characters(self):
        # Load characters from a JSON file and add them to the screen
        if os.path.exists('characters.json'):
            with open('characters.json', 'r') as f:
                characters = json.load(f)
                for character in characters:
                    self.add_character_button(character)

    def show_character_details(self, character_data):
        character_details_screen = self.manager.get_screen('character_details')
        character_details_screen.set_character_data(character_data)  # Pass character data
        self.manager.current = 'character_details'


class SecondScreen(Screen):
    character_name = ObjectProperty(None)
    character_age = ObjectProperty(None)
    character_power1 = ObjectProperty(None)
    character_power2 = ObjectProperty(None)

    def save_character(self):
        main_screen = self.manager.get_screen('main')
        name = self.ids.character_name.text
        age = self.ids.character_age.text
        gender = 'Man' if self.ids.man_checkbox.active else 'Woman' if self.ids.woman_checkbox.active else 'Other'
        power1 = self.ids.character_power1.text
        power2 = self.ids.character_power2.text
        color = main_screen.get_random_color()

        character_data = {
            'name': name,
            'age': age,
            'gender': gender,
            'power1': power1,
            'power2': power2,
            'color': color
        }

        # Save character to the list
        self.save_characters_to_file(character_data)

        # Add button to the main screen dynamically
        main_screen.add_character_button(character_data)

        # Clear the inputs after saving
        self.ids.character_name.text = ''
        self.ids.character_age.text = ''
        self.ids.character_power1.text = ''
        self.ids.character_power2.text = ''
        self.ids.man_checkbox.active = False
        self.ids.woman_checkbox.active = False

        # Switch back to the main screen
        self.manager.current = 'main'

    def save_characters_to_file(self, new_character):
        characters = []

        # Load existing characters from file if it exists
        if os.path.exists('characters.json'):
            with open('characters.json', 'r') as f:
                characters = json.load(f)

        # Add the new character
        characters.append(new_character)

        # Save updated character list to file
        with open('characters.json', 'w') as f:
            json.dump(characters, f)

    selected_gender = None
    def set_gender(self, checkbox, value):
        if value == True:  # If this checkbox is active
            # Uncheck the others
            if checkbox == self.ids.man_checkbox:
                self.ids.woman_checkbox.active = False
            elif checkbox == self.ids.woman_checkbox:
                self.ids.man_checkbox.active = False


class CharacterDetailsScreen(Screen):
    character_color = ListProperty([1, 1, 1, 1]) #default
    character_data = None #default
    def on_enter(self):
        if self.character_data:  # Check if character_data is set
            self.ids.character_name.text = self.character_data['name']
            self.ids.character_age.text = self.character_data['age']
            self.ids.character_gender.text = self.character_data['gender']
            self.ids.character_power1.text = self.character_data['power1']
            self.ids.character_power2.text = self.character_data['power2']
            self.character_color = self.character_data['color']

    def set_character_data(self, character):
        self.character_data = character  # Store character data
        


class RosterApp(App):
    def build(self):
        sm = ScreenManager()
        sm.add_widget(MainScreen(name='main'))
        sm.add_widget(SecondScreen(name='second'))
        sm.add_widget(CharacterDetailsScreen(name='character_details'))

        # Load characters into the main screen when the app starts
        sm.get_screen('main').load_user_characters()

        return sm


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

And my Kivy file:

<MainScreen>:
    FloatLayout:
        orientation: 'vertical'

        # Light yellow background
        FloatLayout:
            canvas.before:
                Rectangle:
                    pos: self.pos
                    size: self.size
                    source: 'img.png'

            # Round button in the bottom right corner
            Button:
                text: ""
                size_hint: None, None
                size: 50, 50
                pos_hint: {'right': 0.95, 'bottom': 0.05}
                background_normal: 'plus.webp'
                background_color: 0.737, 0.635, 0.514, 1 # light almost-off golden
                border: (50, 50, 50, 50)  # Simulates a round border
                canvas.before:
                    Color:
                        rgba: 0.086, 0.086, 0.086, 0.75  # Same dark gray color
                    Ellipse:  # Draw an ellipse to make the button round
                        pos: self.pos
                        size: self.size
                on_release:
                    app.root.current = 'second'
                
        # dark gray top bar with app name
        FloatLayout:
            size_hint_y: None
            height: 50
            pos_hint: {'top': 1}
            canvas.before:
                Color:
                    rgba: 0.086, 0.086, 0.086, 0.75  # dark gray color
                Rectangle:
                    pos: self.pos
                    size: self.size
            Label:
                text: "DnD Roster Panel"
                color: 1, 1, 1, 1  # white text
                pos_hint: {'center_x': 0.5, 'center_y': 0.5}
        
        ScrollView:
            size_hint: (1, 0.8)
            pos_hint: {'top': 0.9}
            BoxLayout:
                id: character_list
                orientation: 'vertical'
                size_hint_y: None
                height: self.minimum_height  # Adjusts height to fit content
                spacing: 10
                padding: 10


<SecondScreen>:
    FloatLayout:
        canvas.before:
            Rectangle:
                pos: self.pos
                size: self.size
                source: 'img.png'

    FloatLayout:
        orientation: 'vertical'
        FloatLayout:
            size_hint_y: None
            height: 50
            pos_hint: {'top': 1}
            canvas.before:
                Color:
                    rgba: 0.086, 0.086, 0.086, 0.75  # dark gray color
                Rectangle:
                    pos: self.pos
                    size: self.size
            Label:
                text: "DnD Roster Panel"
                color: 1, 1, 1, 1  # white text
                pos_hint: {'center_x': 0.5, 'center_y': 0.5}

    ScrollView:
        size_hint: 1, 0.85  # Make it take up the remaining space
        pos_hint: {'top': 0.85}
        do_scroll_x: False  # Disable horizontal scrolling

        BoxLayout:
            orientation: 'vertical'
            size_hint_y: None
            height: self.minimum_height
            padding: [20, 20, 20, 20]  # Add some padding for spacing
            spacing: 20  # Add some space between elements

            Label:
                text: 'Name'
                color: 1, 1, 1, 1
                halign: "center"
        
            TextInput:
                id: character_name
                hint_text: ""
                multiline: False
                size_hint_y: None
                height: 40

            Label:
                text: 'Age'
                color: 1, 1, 1, 1
                halign: "center"

            TextInput:
                id: character_age
                hint_text: ""
                multiline: False
                input_filter: 'int'
                size_hint_y: None
                height: 40

            Label:
                text: 'Select Gender'
                color: 1, 1, 1, 1
                halign: "center"

            GridLayout:
                cols: 2
                spacing: 40
                padding: 20
                size_hint_y: None
                height: self.minimum_height
                Label:
                    text: "Man"
                CheckBox:
                    id: man_checkbox
                    on_active: root.set_gender(self, self.active)  # Uncheck female if male is checked       

                Label:
                    text: "Woman"
                CheckBox:
                    id: woman_checkbox
                    on_active: root.set_gender(self, self.active)  # Uncheck male if female is checked         


            Label:
                text: 'First Power'
                color: 1, 1, 1, 1
                halign: "center"

            TextInput:
                id: character_power1
                hint_text: ""
                multiline: False
                size_hint_y: None
                height: 40
            
            Label:
                text: 'Second Power'
                color: 1, 1, 1, 1
                halign: "center"

            TextInput:
                id: character_power2
                hint_text: ""
                multiline: False
                size_hint_y: None
                height: 40

            Button:
                text: "Save Character"
                size_hint_y: None
                height: 50
                on_release:
                    root.save_character()  # Call save_character method

<CharacterDetailsScreen>:
    FloatLayout:
        canvas.before:
            Rectangle:
                pos: self.pos
                size: self.size
                source: 'img.png'

    FloatLayout:
        orientation: 'vertical'
        FloatLayout:
            size_hint_y: None
            height: 50
            pos_hint: {'top': 1}
            canvas.before:
                Color:
                    rgba: self.parent.parent.character_color
                Rectangle:
                    pos: self.pos
                    size: self.size
            Label:
                text: 'DnD Roster Panel'
                color: 1, 1, 1, 1  # white text
                pos_hint: {'center_x': 0.5, 'center_y': 0.5}
    
    Label:
        id: character_name
        text: 'Name'
        size_hint_y: None
        pos_hint: {'center_x': 0.5, 'top': 0.85}
        height: 20
        font_size: 30


    GridLayout:
        cols: 2
        spacing: 10
        padding: 20
        size_hint_y: None
        height: self.minimum_height  # Adjusts height to fit content

        # Age
        Label:
            text: "Age: "
            size_hint_y: None
            height: 50
            canvas.before:
                Color:
                    rgba: 1, 1, 1, 1  # Black border color
                Line:
                    rectangle: (self.x, self.y, self.width, self.height)
        
        Label:
            id: character_age
            text: "Age:"
            size_hint_y: None
            height: 50
            canvas.before:
                Color:
                    rgba: 1, 1, 1, 1  # Black border color
                Line:
                    rectangle: (self.x, self.y, self.width, self.height)

        # Gender
        Label:
            text: "Gender: "
            size_hint_y: None
            height: 50
            canvas.before:
                Color:
                    rgba: 1, 1, 1, 1  # Black border color
                Line:
                    rectangle: (self.x, self.y, self.width, self.height)

        Label:
            id: character_gender
            text: "Gender:"
            size_hint_y: None
            height: 50
            canvas.before:
                Color:
                    rgba: 1, 1, 1, 1  # Black border color
                Line:
                    rectangle: (self.x, self.y, self.width, self.height)

        # First Power
        Label:
            text: "First Power:"
            size_hint_y: None
            height: 50
            canvas.before:
                Color:
                    rgba: 1, 1, 1, 1  # Black border color
                Line:
                    rectangle: (self.x, self.y, self.width, self.height)

        Label:
            id: character_power1
            text: "First Power:"
            size_hint_y: None
            height: 50
            canvas.before:
                Color:
                    rgba: 1, 1, 1, 1  # Black border color
                Line:
                    rectangle: (self.x, self.y, self.width, self.height)

        # Second Power
        Label:
            text: "Second Power:"
            size_hint_y: None
            height: 50
            canvas.before:
                Color:
                    rgba: 1, 1, 1, 1  # Black border color
                Line:
                    rectangle: (self.x, self.y, self.width, self.height)

        Label:
            id: character_power2
            text: "Second Power:"
            size_hint_y: None
            height: 50
            canvas.before:
                Color:
                    rgba: 1, 1, 1, 1  # Black border color
                Line:
                    rectangle: (self.x, self.y, self.width, self.height)

        Button:
            text: ""
            size_hint: None, None
            size: 50, 50
            pos_hint: {'left': 0.95, 'bottom': 0.05}
            background_normal: 'back.png'
            background_color: 0.737, 0.635, 0.514, 1 # light almost-off golden
            border: (50, 50, 50, 50)  # Simulates a round border
            canvas.before:
                Color:
                    rgba: 0.086, 0.086, 0.086, 0.75  # Same dark gray color
                Ellipse:  # Draw an ellipse to make the button round
                    pos: self.pos
                    size: self.size
            on_release:
                app.root.current = 'main'

I would like the app to let you check and uncheck the checkboxes, but restricting the selection to only one box (only one gender).

The code itself might also be very messy, but for now it was working fine XD


Solution

  • You can get the behavior you want by using the group property of the CheckBox. See the documentation.

    Try this:

            GridLayout:
                cols: 2
                spacing: 40
                padding: 20
                size_hint_y: None
                height: self.minimum_height
                Label:
                    text: "Man"
                CheckBox:
                    id: man_checkbox
                    group: "gender"
    
                Label:
                    text: "Woman"
                CheckBox:
                    id: woman_checkbox
                    group: "gender"