Kivy: Generate dynamically buttons on the second, third... etc screens - python

I am new in Kivy: python. I am trying to do an application using ScreenManager. The idea, that I have a map of maps and keys of maps should generate buttons on several screens. For instance:
map: {user1: {thing1: value1, thing2, value2}, user2: {thing1: value1...}}
The first screen will have two buttons in scrollview: user1 and user2.
Collection of buttons on the next screen will be thing1, thingN depending on what "user_name" user has pressed. This is just "toy" project, here I am studying the Kivy
I have some global variables. In the first screen the global variable "user_name" is initialized depending on what user has pressed on the first screen. Then in the class of the second screen I tried using map_of_maps[user_name].keys() and place these keys as buttons on the second screen.
# kivy_test.py
class MyMainApp(App):
gapi = gAPI("tilit.txt")
gapi.file_download()
gapi.parse_downloaded_file()
global data
global user_name
global account
data = gapi.get_data()
user_name = None
account = None
def build(self):
return kv
class MainWindow(Screen):
f_view = ObjectProperty(None)
def __init__(self,**kwargs):
super(MainWindow, self).__init__(**kwargs)
Clock.schedule_once(self.create_scrollview)
def create_scrollview(self, inst):
base = data.keys()
layout = GridLayout(cols=1)
layout.bind(minimum_height=layout.setter("height"))
for element in base:
button = Button(text=str(element), size_hint=(1, 0.1))
button.bind(on_press=self.on_scrbutton_pressed)
layout.add_widget(button)
scrollview = ScrollView(size=(Window.width, Window.height))
scrollview.add_widget(layout)
self.f_view.add_widget(scrollview)
def on_scrbutton_pressed(self, instance):
user_name = instance.text
print(instance.text)
class SecondWindow(Screen):
s_view = ObjectProperty(None)
def __init__(self,**kwargs):
super(SecondWindow,self).__init__(**kwargs)
Clock.schedule_once(self.create_scrollview)
def create_scrollview(self, inst):
base = data[user_name].keys() # Here I have a problem
layout = GridLayout(cols=1)
layout.bind(minimum_height = layout.setter("height"))
for element in base:
button = Button(text = str(element), size_hint=(1,0.1))
button.bind(on_press=self.on_scrbutton_pressed)
layout.add_widget(button)
scrollview = ScrollView(size=(Window.width, Window.height))
scrollview.add_widget(layout)
self.s_view.add_widget(scrollview)
def on_scrbutton_pressed(self, instance):
print(instance.text)
While running the code I have an error: base = data[user_name].keys(). KeyError: None
# my.kv
# Filename: my.kv
WindowManager:
MainWindow:
SecondWindow:
ThirdWindow:
<MainWindow>:
name: "main"
f_view: f_view
GridLayout:
cols: 1
ScrollView:
id : f_view
# Button:
# text: "User1"
# on_release:
# app.root.current = "second"
# root.manager.transition.direction = "left"
# Button:
# text: "User2"
# on_release:
# app.root.current = "second"
# root.manager.transition.direction = "left"
<SecondWindow>:
name: "second"
s_view : s_view
GridLayout:
cols: 1
ScrollView:
id : s_view
Button:
text: "Add"
<ThirdWindow>:
name: "third"
t_view : t_view
GridLayout:
cols: 1
id : t_view
And one more question: How to perform transitions between screens in this case?

Problem 1 - KeyError
base = data[user_name].keys() # Here I have a problem
KeyError: None
Root Cause
When Kivy process your kv file, it instantiates WindowManager object as the root, and it also instantiates its children, MainWindow and SecondWindow. While instantiating SecondWindow, it invoked the constructor (__init__) and method create_scrollview. It threw a KeyError because the global variable, user_name contains the initial value of None which does not exist in the dictionary type, data.
Solution
Add global user_name in method on_scrbutton_pressed of class MainWindow before populating user_name. Without global user_name, Python creates a new local variable when instance.text is assigned to user_name.
Problem 2
How to perform transitions between screens in this case?
Solution
Each screen has by default a property manager that gives you the
instance of the ScreenManager used. Add the following into method
on_scrbutton_pressed of class MainWindow.
self.manager.current = "second"
self.manager.transition.direction = "left"
In class Secondwindow, remove constructor and replace method create_scrollview(self, inst) with on_pre_enter(self)
In class MainWindow, replace constructor, __init__ with on_pre_enter(self):
Add method on_pre_leave to remove widgets added to prevent duplicates
Snippets
class MainWindow(Screen):
f_view = ObjectProperty(None)
def on_pre_enter(self):
Clock.schedule_once(self.create_scrollview)
...
def on_pre_leave(self):
self.f_view.clear_widgets()
def on_scrbutton_pressed(self, instance):
global user_name
user_name = instance.text
print(instance.text)
self.manager.current = "second"
self.manager.transition.direction = "left"
class SecondWindow(Screen):
s_view = ObjectProperty(None)
def on_pre_enter(self):
base = data[user_name].keys()
...
def on_pre_leave(self):
self.s_view.clear_widgets()

I've implemented almost the same thing in my app too.
I have a list of chapter names as keys and their summaries as the
values.
When the user enters the screen, the keys() are called and
buttons are created.
Depending on the button clicked, the summary is
displayed on the next screen
Here's how I coded it (Snippets)
class chapter_list(Screen):
def on_pre_enter(self):
self.buttons = []
for i in flamingo.keys(): #for loop to generate buttons
button = Button(text=str(i),height=150) #the text on the button is the chapter name
button.bind(on_press=self.pressed) #when the button is clicked, the function pressed on called
self.ids.fgrid.add_widget(button) #added to the flamingogrid
self.buttons.append(button)
Here the fgrid is the id: of the Layout(GridLayout in this case) I choose for this screen in the kv file
The self.buttons = [] is just to collect the buttons so that I can remove them on the screen exit.
Let me know if this was helpful.

Related

How to make changes to global object attribute with Kivy TextInput and have it save for later use?

My app currently has 10 "players" with a name, offensive rating, and defensive rating. I want to give the user the opportunity to change the names of the players before running my game simulation code, but it appears I can only temporarily change the name inside of the current screen. Here's my current python code for when the user inputs the name from the gui:
class SecondWindow(Screen):
def pg1_name_val(self, widget):
pg1.name = widget.text
If I code in a print(pg1.name) right below that, it will print the correct name, but when the user hits "play" to shift to a results screen, the names go back to the default names.
How do I make pg1.name from my SecondWindow(Screen) become the global pg1.name? Is there a simple line of code I can use to return pg1.name as the global variable?
EDIT ----
So I've came a long way with my app this week, but now that I want to have a separate "results" screen, I've realized that I lost all of my data from the previous screen. It looks like all of the changes I make to my "players" are going back to defaults once the screen changes. I'll paste some code below for reference. Basically, my question now is this: Is there a way to keep the objects from one screen and return them back as the global instance of the object. Not just the local instance of the object.
.py -
class Player():
def __init__(self,name,off,deff):
self.name = name
self.off = off
self.deff = deff
self.stats = 0
class Team():
def init(self,pg, sg, sf, pf, c):
self.pg = pg
self.sg = sg
self.sf = sf
self.pf = pf
self.c = c
self.score = 0
self.results = None
self.to = 0
On the screen where I'm creating my team, I have a button to run the simulation. This button runs the sim correctly, records all of the game results in a string, prints all of the results to the console, attributes, and score correctly. The button also changes the screen to where I want to display a scroll view of the results string, but the results are empty.
I'd really appreciate any help on this, as I'm at a roadblock. Thanks!
It would kinda look the same. Depending on how exactly you wanna keep the 10 different players.
class Player():
def __init__(self, name, off, deff):
self.name = name
self.off = off
self.deff = deff
self.stats = 0
player = [Player('', 0, 0)] * 10
class SecondWindow(Screen):
def __init__(self, **kwargs):
super(SecondWindow, self).__init__(**kwargs)
self.player = player
def name_change(self):
self.player[0].name = self.ids.name.text
player[0].name = self.player[0].name
def next_screen(self):
self.manager.current = 'result'
class ResultWindow(Screen):
def __init__(self, **kwargs):
super(ResultWindow, self).__init__(**kwargs)
def on_enter(self, *args):
print(player[0].name)
class Manager(ScreenManager):
pass
class Player(App):
def build(self):
sm = ScreenManager()
sm.add_widget(SecondWindow(name='second'))
sm.add_widget(ResultWindow(name='result'))
return sm
if __name__ == '__main__':
Player().run()
You would have to work with OOP, here I created a list of you player object for all ten players it is an easy way to keep the code short.
You might like to keep them as 10 different objects the good part of a list it is easy to iterate through in a for loop and then get all the data for a stats board on a screen and also easy to expand later if you need 20 players or more.
I didn't change anything in the .kv file.
.kv
<SecondWindow>:
TextInput:
id: name
hint_text: 'Player name'
size: 260, 32
pos: (root.width / 2) - (self.width / 2), 248
multiline: False
on_text_validate: root.name_change()
Button:
id: create_btn
text: 'Next Screen'
size: 260, 32
pos: (root.width / 2) - (self.width / 2), 206
color: utils.get_color_from_hex('#ffffff')
background_color: utils.get_color_from_hex('#016bf8')
on_press: root.next_screen()
<ResultWindow>:
Label:
id: name
The on_text_validate only works with multiline false and when enter is pressed if you don't want to press enter user on_text instead.
--Edit--
I did some research this morning and thought maybe to update this a bit with my findings.
You could instead of doing it the import to all screens do it in the screenmanager and just call it from the screen like this:
pyhon.py
import kivy
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
class Player:
def __init__(self, name, off, deff):
self.name = name
self.off = off
self.deff = deff
self.stats = 0
class SecondWindow(Screen):
def __init__(self, **kwargs):
super(SecondWindow, self).__init__(**kwargs)
def name_change(self):
self.manager.player.name = self.ids.name.text
def next_screen(self):
self.manager.current = 'result'
class ResultWindow(Screen):
def __init__(self, **kwargs):
super(ResultWindow, self).__init__(**kwargs)
def on_enter(self):
self.ids.name.text = self.manager.player.name
class Manager(ScreenManager):
player = Player('', 0, 0)
def __init__(self, **kwargs):
super(Manager, self).__init__(**kwargs)
class PlayerApp(App):
def build(self):
return Builder.load_file('test.kv')
if __name__ == '__main__':
PlayerApp().run()
the kv file would look something like this:
.kv
#:import utils kivy.utils
Manager:
SecondWindow:
name: 'second'
ResultWindow:
name: 'result'
<SecondWindow>:
TextInput:
id: name
hint_text: 'Player name'
size: 260, 32
size_hint: None, None
pos: (root.width / 2) - (self.width / 2), 248
multiline: False
on_text_validate: root.name_change()
Button:
id: create_btn
text: 'Next Screen'
size_hint: None, None
size: 260, 32
pos: 0, 0
color: utils.get_color_from_hex('#ffffff')
background_color: utils.get_color_from_hex('#016bf8')
on_press: root.next_screen()
<ResultWindow>:
Label:
id: name
hope it helps

KIvy button press to change label text in python code

I have a Label and a button that I define in the init of my class. In the init I bind my button to a method that should change the label. However the label does not update on button press even tho the variable does.
Why does my variable change but my label text stays the same even tho the text is an ObjectProperty?
class ReviewLayout(BoxLayout):
Price = Price()
textover = ObjectProperty(None)
ordered_products = []
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.get_order()
l = Label(text = str(self.textover))
self.add_widget(l)
b = Button(text= 'button')
b.bind(on_press=lambda x: self.get_order())
self.add_widget(b)
def get_order(self):
ordered_products = self.ordered_products
ordered_products.append("%s")
print("this shall be present", ordered_products)
self.textover = ordered_products
When you declare your label you set its value to self.textover value but when self.textover value changes it doesn't update the label.
You need to change the value of your label by storing it as a class property and updating it whenever you want.
Just refer to this Update label's text when pressing a button in Kivy for Python
class ReviewLayout(BoxLayout):
Price = Price()
textover = ObjectProperty(None)
ordered_products = []
def __init__(self, **kwargs):
super().__init__(**kwargs)
# declare label variable as a class property (in self)
self.label = Label(text = str(self.textover))
self.add_widget(self.label)
self.button = Button(text= 'button')
self.add_widget(self.button)
self.button.bind(on_press=lambda x: self.get_order())
def get_order(self):
ordered_products = self.ordered_products
ordered_products.append("%s")
print("this shall be present", ordered_products)
self.textover = ordered_products
# change class variable text property to be the new textover content
self.label.text = str(self.textover)
I think an easier solution is to use kv to allow it to do the updates for you automatically, like this:
Builder.load_string('''
<ReviewLayout>:
Label:
text: str(root.textover)
Button:
text: 'button'
on_press: root.get_order()
''')
class ReviewLayout(BoxLayout):
Price = Price()
textover = ListProperty() # note change to ListProperty
def get_order(self):
self.textover.append("%s")
print("this shall be present", self.textover)

Why won't my Kivy program update the font size when I tell it to?

I'm making a choose your own adventure game, but sometimes I need to change the font size and Kivy isn't giving me the results I'm expecting. This is the full code so feel free to run it and see what I mean.
Here is the python file:
# A Choose your own adventure game
import kivy
kivy.require('1.11.1')
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.lang import Builder
global root
root = BoxLayout() #If I don't define root immediately the program won't work
#root is given a proper definition in class Main()
#Easily add new pages to the program
def add_page(pagenum):
root.clear_widgets()
root.add_widget(pagenum)
#The main window that encapsulates all other widgets
class RootBoxLayout(BoxLayout):
def __init__(self, **kwargs):
super(RootBoxLayout, self).__init__(**kwargs)
# The Menu that drives the game
class Menu(BoxLayout):
def __init__(self, **kwargs):
super(Menu, self).__init__(**kwargs)
# The Main Menu
class StartMenu(Menu):
def __init__(self, **kwargs):
super(Menu, self).__init__(**kwargs)
#Text Box
self.ids.textbox.text = "Opening Screen"
# Button 1
self.ids.button1.text = "Play"
self.ids.button1.bind(on_press = self.nextpage1)
def nextpage1(self, *args):
add_page(HappyBee())
class HappyBee(Menu):
def __init__(self, **kwargs):
super(Menu, self).__init__(**kwargs)
#############################################
### This is where the problem seems to be ###
#############################################
self.ids.textbox.font_size = self.ids.textbox.height/10 #Kivy says nah I don't feel like doing this
self.ids.textbox.text = "This is a very large block of text that I would like " \
"to decrease the font size of. Pressing the button below changes it but I don't " \
"want users to have to press a button just to get the game to function " \
"how it should function from the start."
# Button 1
self.ids.button1.text = "y tho"
self.ids.button1.bind(on_press = self.nextpage1)
# What to do when each button is pressed
def nextpage1(self, *args):
self.ids.textbox.font_size = self.ids.textbox.height/10 # Kivy says ok I can change it now lol
# An App class that will be used to umbrella everything else in the application
class Main(App):
def build(self):
Builder.load_file("cyoa.kv")
global root # Other classes and functions need to easily access root
root = RootBoxLayout()
first_screen = StartMenu()
add_page(first_screen) # Add the Main Menu to the root window
return root
if __name__ == '__main__':
Main().run()
and here is the corresponding kv file, which I have saved as cyoa.kv
<RootBoxLayout>:
orientation: 'vertical'
# Create the background color of the root layout
canvas.before:
Color:
rgba: 0,0,0,1 # black
Rectangle:
pos: self.pos
size: self.size
# This custom button allows to font size to change dynamically with the window
<MyButton#Button>:
font_size: self.height/3
halign: 'center'
valign: 'center'
text_size: self.size
size_hint_y: 0.14
<Menu>:
BoxLayout:
orientation: 'vertical'
Label:
id: textbox
font_size: self.height/6
text_size: self.size # Allows text to wrap
halign: 'center'
valign: 'center'
size_hint_y: 0.6
MyButton:
id: button1
text: 'Play'
I can change font_size in __init__ only if I remove font_size from .kv. It seems it gets value from .kv after running __init__ and this makes problem. There is also other problem: height (and width) in __init__ is 100 instead of expected size. Probably it calculates it after running __init__.
Searching in internet I found on Reddit: How can I use init for screen ids?
It uses Clock to run some function after all updates and in this function change values.
def __init__(self, **kwargs):
#super(...)
Clock.schedule_once(self._do_setup)
def _do_setup(self, *l):
self.ids.something = '....'
In your code it would be
from kivy.clock import Clock # <---
class HappyBee(Menu):
def __init__(self, **kwargs):
super(Menu, self).__init__(**kwargs)
self.ids.textbox.text = "This is a very large block of text that I would like " \
"to decrease the font size of. Pressing the button below changes it but I don't " \
"want users to have to press a button just to get the game to function " \
"how it should function from the start."
self.ids.button1.text = "y tho"
self.ids.button1.bind(on_press = self.nextpage1)
Clock.schedule_once(self.on_init_complete) # <---
def on_init_complete(self, *args, **kwargs):
self.ids.textbox.font_size = self.ids.textbox.height/10 # <---
It works but has one small problem - it display text in original size for few milliseconds. But if you don't know this then you may not notice this.
EDIT: Similar problem: How to Load Kivy IDs Before Class Method is Initialized (Python with Kivy)

Updating Labels across Screens in Kivy (Python) - Problem with (lack of) interactions between .kv and .py files with ScreenManager

I'm trying to build a Kivy application that has 2 screens which are re-used over and over again with different text.
So I go from a FirstScreen with a Label that says "First1" to a SecondScreen with a Label that says "Second1", and then back to the FirstScreen but this time with the Label "First2", then SecondScreen and "Second2", and so on and so forth.
The code for this is pretty straightforward, but there seems to be a problem in updating the Label text without a designated update button. For some reason, my Python code manages to update the text, but it isn't updated in my .kv file. So for instance, my print statements will tell me that the Label text is "First2", but Kivy displays "First1" for me. I've illustrated this in the Screenshot below:
By adding a Button that updates the text on press, everything is updated, synced up and works, but I'd really like it to work without the extra user input. Does anybody know how I can go about this? I've scoured the docs and stackoverflow questions left and right but can't seem to find the answer to my seemingly simple problem.
Here's the code:
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.properties import StringProperty, ObjectProperty
from kivy.lang import Builder
S_ID = 1 # global screen ID. I'm using this to detect what text to use.
class FirstScreen(Screen):
text = StringProperty("")
lbl = ObjectProperty(None)
def __init__(self, **kwargs):
super(FirstScreen, self).__init__(**kwargs)
global S_ID
print("\nS_ID is ", S_ID)
self.update()
def update(self):
print("FIRST - UPDATE")
if S_ID == 1:
print("FIRST1")
self.text = "FIRST1"
elif S_ID == 2:
print("FIRST2")
self.text = "FIRST2"
print("self.lbl.text", self.lbl.text)
else:
print("FIRST ELSE")
self.text = "FIRST ELSE"
def pressed(self):
sm.current = "second"
class SecondScreen(Screen):
text = StringProperty("")
def __init__(self, **kwargs):
super(SecondScreen, self).__init__(**kwargs)
self.update()
def update(self):
print("SECOND - UPDATE")
if S_ID == 1:
print("SECOND1")
self.text = "SECOND1"
elif S_ID == 2:
print("SECOND2")
self.text = "SECOND2"
else:
print("SECOND ELSE")
self.text = "SECOND ELSE"
def pressed(self):
global S_ID
S_ID += 1
FirstScreen.update(FirstScreen())
sm.current = "first"
sm = ScreenManager()
kv = Builder.load_file("test.kv")
sm.add_widget(FirstScreen(name='first'))
sm.add_widget(SecondScreen(name='second'))
sm.current = "first"
class MyApp(App):
def build(self):
return sm
if __name__ == '__main__':
MyApp().run()
and here's the .kv file:
<FirstScreen>:
name: "first"
lbl: lbl:
GridLayout:
cols:2
Label:
id: lbl
text: root.text
Button:
text: "next"
on_press: root.pressed()
<SecondScreen>:
name: "second"
GridLayout:
cols:2
Label:
text: root.text
Button:
text: "next"
on_press:
root.pressed()
The problem is your statement:
FirstScreen.update(FirstScreen())
This statement is creating a new instance of FirstScreen and updating that instance. Unfortunately, that instance is not the one shown in your GUI. You can correct that by replacing the above statement with:
first_screen = self.manager.get_screen('first')
first_screen.update()
This code gets the instance of FirstScreen from the ScreenManager and calls update() on that instance.

Kivy - Label not updating on UI

When I click on the buttons in the left part, the label "Top 10 Plays of 2015" should be changed with the text of the button that I've clicked. I can see the variable changing text but the label is not changing text.
this is my work: when I click the buttons on left side the title in the middle should change:
python
class MainApp(Screen, EventDispatcher):
vid = StringProperty("Videos/Top 10 Plays of 2015.mp4")
title = StringProperty("Top 10 Plays of 2015")
def __init__(self,*args,**kwargs):
super(MainApp,self).__init__(*args, **kwargs)
pass
class OtherVideos(BoxLayout, EventDispatcher):
root = MainApp
def __init__(self, *args, **kwargs):
super(OtherVideos,self).__init__(*args, **kwargs)
self.loadVideos()
def loadVideos(self):
con = MongoClient()
db = con.nba
vids = db.videos.find()
vidnum = 1
for filename in vids:
myid = "vid" + str(vidnum)
getfilename = filename['filename']
button = Button(id=myid,
text=getfilename,
color=[0,0.7,1],
bold=1)
button.bind(on_release=lambda x:(self.change_Title(getfilename), self.change_Vid(getfilename)))
self.add_widget(button)
vidnum += 1
def change_Title(self, title):
self.root.title = title
def change_Vid(self, myfilename):
con = MongoClient()
db = con.nba
vids = db.videos.find()
for filename in vids:
file = os.path.basename(filename['video_path'])
if myfilename == filename['filename']:
self.root.vid = filename['video_path']
break
pass
kivy
BoxLayout:
orientation: 'vertical'
Label:
id: lblTitle
text: root.title
size_hint_y: None
height: 40
font_size: 25
bold: 1
canvas.before:
Color:
rgba: 1, 0, 0, 0.7
Rectangle:
pos: self.pos
size: self.size
VideoPlayer:
id: activeVid
source: root.vid
state: 'play'
when I'm printing root.title it's changing its text but the label is not changing which it should because it's getting its text from that variable.
But when I'm putting the change_title() method in __init__ of OtherVideos like this, the label is changing:
class OtherVideos(BoxLayout, EventDispatcher):
root = MainApp
def __init__(self, *args, **kwargs):
super(OtherVideos,self).__init__(*args, **kwargs)
self.change_Title("my title")
This is already taking me days of work, anyone can help?
There are several problems with your code:
Every widget inherits from EventDispatcher, so inheriting from it the second time is pointless.
This root = MainApp is not a reference to the MainApp (object), but to its class.
You don't need to specify a StringProperty to achieve your goal.
The easiest way to do this is to add a reference of the label you want to change in the function change_Title:
def change_Title(self, title):
# self.root.title = title
self.ids.my_label_to_change.text = title

Categories

Resources