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
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"