Kivy: RecycleView not updating - kivy

I have two screens in my Kivy-based app, each with a RecycleView to display lists. Both RVs should update when I press a button (add_button_clicked()) on one screen. Currently, the first RV (AddRecipe screen) works mostly as intended. However, the RV on the ViewList screen does not update with new data.
I am new to Python and even newer to Kivy - what am I missing here?
.py:
#! python3
# GroceryList.py
import kivy
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.spinner import Spinner
from kivy.properties import ListProperty
from kivy.properties import ObjectProperty
from kivy.uix.recycleview import RecycleView
selectedMeals = []
ingredients = []
class ViewList(Screen):
def updateList(self, portions, recipe):
ingredients.append((portions, recipe))
print(ingredients) # This proves updateList is getting called
##THE FOLLOW RV THINGS DON'T WORK:
self.ids.shoplist.data = [{'text': '%s (%s)' %(ingredients[i][0], ingredients[i][1])}
for i in range(len(ingredients))]
self.ids.shoplist.refresh_from_data()
##
class AddRecipe(Screen):
recipes = ListProperty()
recipes = {'Guacarole':5, 'Salsa':3, 'Chips':1} # Sample dict for demo
def add_one(self):
if self.addportions.text != '':
value = int(self.addportions.text)
self.addportions.text = str(value+1)
def subtract_one(self):
if self.addportions.text != '':
value = int(self.addportions.text)
self.addportions.text = str(value-1)
def add_button_clicked(self, recipe, portions):
if recipe != '':
selectedMeals.append((recipe, portions))
self.ids.mealslist.data = [{'text': '%s (%s)' %(selectedMeals[i][0], selectedMeals[i][1])}
for i in range(len(selectedMeals))]
self.ids.mealslist.refresh_from_data()
ViewList().updateList(portions, recipe)
def spinner_clicked(self, val):
self.addportions.text = str(self.recipes[val])
class WindowManager(ScreenManager):
pass
class GroceryList(App):
mealsRVdata = ListProperty()
shoppingRVdata = ListProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.sm = ScreenManager()
def build(self):
Builder.load_file("grocerylist.kv")
screens = [ViewList(name='viewlist'), AddRecipe(name='addrecipe')]
for screen in screens:
self.sm.add_widget(screen)
self.sm.current = "addrecipe"
return self.sm
if __name__ == '__main__':
GroceryList().run()
And .kv:
#:kivy 1.11.1
# GroceryList.kv
# Used by GroceryList.py
WindowManager:
AddRecipe:
ViewList:
<ViewList>:
name: "viewlist"
shoplist: shoplist
BoxLayout:
orientation: 'vertical'
BoxLayout:
size_hint: (1, 0.8)
RecycleView:
id: shoplist
data: app.shoppingRVdata
viewclass: 'RVLabel'
RecycleGridLayout:
cols: 1
size_hint: None, None
default_size: sp(200), sp(25)
height: self.minimum_height
width: self.minimum_width
BoxLayout:
size_hint: (1, 0.2)
Button:
text: "View shopping list"
on_release:
app.root.current = "viewlist"
Button:
text: "Add recipes"
on_release:
app.root.current = "addrecipe"
root.manager.transition.direction = "left"
<AddRecipe>:
name: "addrecipe"
addportions: addportions
mealslist: mealslist
BoxLayout:
orientation: 'vertical'
BoxLayout:
size_hint: (1, 0.08)
Label:
size_hint: (0.64, 1)
font_size: 20
text: "Select a meal to add"
Label:
size_hint: (0.36, 1)
font_size: 20
text: "Select portions"
BoxLayout:
size_hint: (1, 0.08)
Spinner:
id: add_spinner
size_hint: (0.64, 1)
text: ""
values: root.recipes.keys()
on_text:
root.spinner_clicked(add_spinner.text)
Button:
size_hint: (0.12, 1)
font_size: 36
text: "-"
on_release: root.subtract_one()
Label:
id: addportions
size_hint: (0.12, 1)
font_size: 24
text: ''
Button:
size_hint: (0.12, 1)
font_size: 36
text: "+"
on_release: root.add_one()
FloatLayout:
size_hint: (1, 0.08)
Button:
size_hint: (0.4, 1)
pos_hint: {"x": 0.3, "top": 1}
text: "Add to shopping list"
on_release:
root.add_button_clicked(add_spinner.text, addportions.text)
BoxLayout:
size_hint: (1, 0.68)
RecycleView:
id: mealslist
data: app.mealsRVdata
viewclass: 'RVLabel'
RecycleGridLayout:
cols: 1
size_hint: None, None
default_size: sp(200), sp(25)
height: self.minimum_height
width: self.minimum_width
BoxLayout:
size_hint: (1, 0.08)
Button:
text: "View shopping list"
on_release:
app.root.current = "viewlist"
root.manager.transition.direction = "right"
Button:
text: "Add recipes"
on_release:
app.root.current = "addrecipe"
<RVLabel#Label>:
text_size: self.size

I stumbled across the fix while browsing other questions - specify the RV more fully:
App.get_running_app().root.get_screen('viewlist').ids.shoplist.data = ....
I'm going to leave the question up, though, because now I want to know why this RV needs to be specified so completely, while the one in the AddRecipe screen doesn't.

Related

How can a dynamic class access another class' attributes such as root.width/root.height?

Here is a class NewWindow with a GridLayout in a FloatLayout, a dynamic class NewButton inheriting from ToggleButton, and a class in the Python file that receives the GridProperty in order to add_widget NewButton into it.
The overall goal is to have a button that adds a toggle button into the grid layout, and for all button/toggle buttons to be able to dynamically change size for different screen resolutions. The issue I'm having is that I can't simply write (root.width**2 + root.height**2) / 12**4 for NewButton. I think it's because "root" in NewWindow is not the same as "root" in NewButton, but I do not have a solution.
Is there a way for me to access NewWindow's "root" inside of NewButton? I ultimately just want all my buttons to match each other's sizes and for it to be dynamic, so I don't want to simply write a static "30" for font_size.
# File: main.kv
<NewWindow>:
parentGrid: parentGrid
FloatLayout:
Button:
text: "Add New Button"
pos_hint: {"x": 0.05, "top": 0.7}
size_hint: .4, 0.05
font_size: (root.width**2 + root.height**2) / 12**4
on_release:
root.addButton()
GridLayout:
id: parentGrid
cols: 1
pos_hint: {"x": 0.5, "top": 0.7
size_hint: .4, .5
<NewButton#ToggleButton>:
text: "BUTTON"
size_hint_y: ???
#height: ???
font_size: ???
# Cannot simply use root.width or root.height, also size_hint: .4, 0.05 doesn't work
# File: MainApp.py
class NewWindow(Screen):
parentGrid = ObjectProperty(None)
def AddButton(self):
self.parentGrid.add_widget(NewButton())
You are right the two roots are different. One is as you say the size of the new window, but the other would only be the size of GridLayout. The problem you have with the size_hint is because you are using a GridLayout. If you can change to example a BoxLayout that would work if not the size_hint: None, None and then set the size: would work:
To get the the NewWindow Root use app.root.width and app.root.heigth
main.py:
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import ObjectProperty
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.togglebutton import ToggleButton
class NewButton(ToggleButton):
pass
class NewWindow(Screen):
parentGrid = ObjectProperty(None)
def __init__(self, **kwargs):
super(NewWindow, self).__init__(**kwargs)
def addButton(self):
print('test')
self.parentGrid.add_widget(NewButton())
class Manager(ScreenManager):
pass
class TestApp(App):
def build(self):
return Builder.load_file('test.kv')
if __name__ == '__main__':
TestApp().run()
.kv:
Manager:
NewWindow:
name: 'new'
<NewButton#ToggleButton>:
text: "BUTTON18"
pos_hint: {"x": 0.05, "top": 0.7}
size_hint_x: None
size_hint: 1, 1
#size: 10, 10
font_size: (app.root.width**2 + app.root.height**2) / 12**4
<NewWindow>:
parentGrid: parentGrid
FloatLayout:
Button:
text: "Add New Button"
pos_hint: {"x": 0.05, "top": 0.7}
size_hint: .4, 0.05
font_size: (root.width**2 + root.height**2) / 12**4
on_release:
root.addButton()
BoxLayout:
id: parentGrid
cols: 1
pos_hint: {"x": 0.5, "top": 0.7}
size_hint: .4, .5
canvas:
Color:
rgba: 1,1,1,1
Rectangle:
size: self.size
pos: self.pos
# Cannot simply use root.width or root.height, also size_hint: .4, 0.05 doesn't work
I did also add the screenmanager to be able to access the width and height
Hope this helps

How can I add an MDDataTable to a Screen in a class that's not the main app class?

I'm trying to create an app where after you log in you're shown a table of users. If the login is successful I want the user to be sent to another screen that has an MDDataTable in it.
All of the examples I've found online only show how to display the table using the build method in the main app.
Code example:
class ScreenOne(Screen):
# Displays MDDataTable without the need to press anything to view the table.
class ScreenTwo(Screen):
# When this screen validates user successfully it sends
# me to the other screen that shows an
# MDDataTable and not a blank screen etc...
sm = ScreenManager()
class MainApp(MDApp):
def build(self):
sm.add_widget(ScreenOne(name='screenone'))
sm.add_widget(ScreenTwo(name='screentwo'))
return sm
MainApp().run()
I keep getting either a blank screen or the following error:
ValueError: KivyMD: App object must be initialized before loading root widget. See https://github.com/kivymd/KivyMD/wiki/Modules-Material-App#exceptions
I kept changing the code trying to fix it but I just kept running into all sorts of errors and problems and I just don't understand enough to make my question any clearer.
Here's my .py file code:
from kivy.properties import ObjectProperty
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.stacklayout import MDStackLayout
from kivymd.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import Screen, ScreenManager
from kivymd.uix.datatables import MDDataTable
from kivy.metrics import dp
from kivy.uix.anchorlayout import AnchorLayout
class ClientsTable(Screen):
def load_table(self):
layout = Screen()
data_tables = MDDataTable(
size_hint=(0.9, 0.6),
use_pagination=True,
check=True,
column_data=[
("No.", dp(30)),
("Column 1", dp(30)),
("Column 2", dp(30)),
("Column 3", dp(30)),
("Column 4", dp(30)),
("Column 5", dp(30)),],
row_data=[
(f"{i + 1}", "2.23", "3.65", "44.1", "0.45", "62.5")
for i in range(50)],)
layout.add_widget(data_tables)
return layout
class LoginPage(Screen):
username = ObjectProperty()
password = ObjectProperty()
def validate_user(self):
if self.username.text == "m":
sm.current = "Clientstable"
self.username.text = ""
self.password.text = ""
else:
print("Not here!")
sm = ScreenManager()
class MainWindow(MDApp):
def build(self):
self.title = "EasySport"
sm.add_widget(LoginPage(name='Loginpage'))
sm.add_widget(ClientsTable(name='Clientstable'))
return sm
if __name__ == "__main__":
MainWindow().run()
.kv code:
ScreenManager:
LoginPage:
ClientsTable:
<LoginPage>:
username: User
password: Pass
MDTextField:
id: User
hint_text: "Username"
size_hint: 0.5, 0.09
pos_hint: {"center_x": 0.5, "center_y": 0.7}
MDTextField:
id: Pass
hint_text: "Password"
size_hint: 0.5, 0.09
pos_hint: {"center_x": 0.5, "center_y": 0.6}
MDFillRoundFlatButton:
text: "Login"
size_hint: 0.5, 0.06
pos_hint: {"center_x": 0.5, "center_y": 0.4}
on_release:
root.validate_user()
root.manager.transition.direction = 'left'
<ClientsTable>:
MDFillRoundFlatButton:
text: "Back"
size_hint: 0.5, 0.06
pos_hint: {"center_x": 0.5, "center_y": 0.4}
on_release:
root.manager.transition.direction = 'right'
root.manager.current = 'Loginpage'
MDFillRoundFlatButton:
text: "Load Table"
size_hint: 0.5, 0.06
pos_hint: {"center_x": 0.5, "center_y": 0.3}
on_release:
root.load_table()
Now it doesn't do anything when I press the Load Table button!
I SOLVED IT!
Here's the .py file code:
from kivy.lang import Builder
from kivy.properties import ObjectProperty
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.stacklayout import MDStackLayout
from kivymd.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import Screen, ScreenManager
from kivymd.uix.datatables import MDDataTable
from kivy.metrics import dp
from kivy.uix.anchorlayout import AnchorLayout
class ClientsTable(Screen):
def load_table(self):
layout = AnchorLayout()
self.data_tables = MDDataTable(
pos_hint={'center_y': 0.5, 'center_x': 0.5},
size_hint=(0.9, 0.6),
use_pagination=True,
check=True,
column_data=[
("No.", dp(30)),
("Column 1", dp(30)),
("Column 2", dp(30)),
("Column 3", dp(30)),
("Column 4", dp(30)),
("Column 5", dp(30)),],
row_data=[
(f"{i + 1}", "2.23", "3.65", "44.1", "0.45", "62.5")
for i in range(50)],)
self.add_widget(self.data_tables)
return layout
def on_enter(self):
self.load_table()
class LoginPage(Screen):
username = ObjectProperty()
password = ObjectProperty()
def validate_user(self):
if self.username.text == "m":
sm.current = "Clientstable"
self.username.text = ""
self.password.text = ""
else:
print("Not here!")
sm = ScreenManager()
class MainWindow(MDApp):
def build(self):
sm.add_widget(LoginPage(name='Loginpage'))
sm.add_widget(ClientsTable(name='Clientstable'))
return sm
if __name__ == "__main__":
MainWindow().run()
And the .kv file code:
ScreenManager:
LoginPage:
ClientsTable:
<LoginPage>:
username: User
password: Pass
MDTextField:
id: User
hint_text: "Username"
size_hint: 0.5, 0.09
pos_hint: {"center_x": 0.5, "center_y": 0.7}
MDTextField:
id: Pass
hint_text: "Password"
size_hint: 0.5, 0.09
pos_hint: {"center_x": 0.5, "center_y": 0.6}
MDFillRoundFlatButton:
text: "Login"
size_hint: 0.5, 0.06
pos_hint: {"center_x": 0.5, "center_y": 0.4}
on_release:
root.validate_user()
I just needed to add the Screen's on_enter() method to my class like it says in the documentation. See here

Kivy: how to update a label when a button is clicked

I use a button to retrieve the paths of some folders selected with the filechooser. When the button is clicked I would like to update the text of the label so that it dispays the selected paths.
In my Kv:
Button:
text:'OK'
on_press: root.selected(filechooser.path, filechooser.selection)
Label:
id: Lb_ListViewFolder
text: root.Lb_ListViewFolder_text
color: 0, 0, 0, 1
size_hint_x: .75
In .py:
class MyWidget(BoxLayout):
Lb_ListViewFolder_text = ObjectProperty("Text")
def selected(self, a, b):
global Lb_ListViewFolder_text
Lb_ListViewFolder_text = b
print(a,b)
This doesn't give me any error but the label text isn't changed.
I also tried self.ListViewFolder.text = b like recommended here but I get this error: MyWidget' object has no attribute 'Lb_ListViewFolder'.
I have seen this answer, but I have trouble applying in my code
I use python 3.6 and Kivy 1.9.2.dev0
In case, this is my entire code:
from kivy.properties import ObjectProperty
from kivy.core.window import Window
from kivy.event import EventDispatcher
from kivy.lang import Builder
root = Builder.load_string('''
<MyWidget>
id: BL_Main
orientation: "horizontal"
padding: 10
spacing: 10
BoxLayout:
id: BL_folder
orientation: "vertical"
Button:
id:ok
text:'OK'
background_color: 0,0,1,1
height: 5
size_hint: 0.1, 0.1
on_press: root.selected(filechooser.path, filechooser.selection)
BoxLayout:
orientation:"horizontal"
size_hint: None, 0.9
width:150
canvas.before:
Color:
rgb: .4,.5,.5
Rectangle:
pos: self.pos
size: self.size
## multiple select folder not possible with FileChooserListView
FileChooserIconView:
id: filechooser
pos:self.pos
multiselect: True
dirselect: True
Label:
id: Lb_ListViewFolder
text: root.Lb_ListViewFolder_text
color: 0, 0, 0, 1
size_hint_x: .75
''')
class MyWidget(BoxLayout):
Lb_ListViewFolder_text = ObjectProperty("Text")
def selected(self, a, b):
global Lb_ListViewFolder_text
Lb_ListViewFolder_text = b
print(a,b)
class MyApp(App):
def build(self):
Window.clearcolor = (1, 1, 1, 1)
return MyWidget()
MyApp().run()
You can use StringProperty here:
from kivy.app import App
from kivy.uix.filechooser import FileChooserListView
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.properties import StringProperty
Builder.load_string('''
<MyLayout>:
orientation: "vertical"
Label:
text: root.label_text
Button:
id:ok
text:'OK'
on_press: root.selected(filechooser.path, filechooser.selection)
FileChooserIconView:
id: filechooser
pos:self.pos
multiselect: True
dirselect: True
''')
class MyLayout(BoxLayout):
label_text = StringProperty("File name")
def selected(self, a, b):
self.label_text = b[0]
class MyApp(App):
def build(self):
return MyLayout()
MyApp().run()
Or you can change it directly in kvlang:
<MyLayout>:
orientation: "vertical"
Label:
id: dirlabel
text: root.label_text
Button:
id:ok
text:'OK'
on_press: dirlabel.text = filechooser.selection[0]
FileChooserIconView:
id: filechooser
pos:self.pos
multiselect: True
dirselect: True

Kivy: How can I combine the two screens into one?

I am new to kivy. I have two screens and I want to combine them into one.
The code for the first screen:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.scrollview import ScrollView
from kivy.lang import Builder
Builder.load_string('''
<Button>:
font_size: 40
color: 0,1,0,1
<Widgets>:
Button:
size: root.width/2, 75
pos: root.x , root.top - self.height
text: 'OK'
Button:
size: root.width/2, 75
pos: root.x + root.width/2, root.top - self.height
text: 'Cancel'
Label:
text: "bbbbbaäa üß AäÄ"
size: root.width, 75
valign: 'middle'
halign: 'center'
anchor: 'left'
pos: root.x, root.top - 150
font_size: 50
height: 75
Label:
text: "Tssssssssssa #aaäa Äaaäa Üaa Maaäa a"
size: root.width, 75
valign: 'middle'
halign: 'left'
anchor: 'left'
pos: root.x, root.top - 150 - 50
font_size: 30
height: 50
''')
class ScrollableLabel(ScrollView):
pass
class Widgets(Widget):
def build(self):
return Widgets()
class MyApp(App):
def build(self):
return Widgets()
if __name__ == "__main__":
MyApp().run()
The code for the second screen is from Alexander Taylor: https://github.com/kivy/kivy/wiki/Scrollable-Label
I paste the code here again:
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.scrollview import ScrollView
from kivy.properties import StringProperty
from kivy.lang import Builder
long_text = 'yay moo cow foo bar moo baa ' * 100
Builder.load_string('''
<ScrollableLabel>:
Label:
size_hint_y: None
height: self.texture_size[1]
text_size: self.width, None
text: root.text
''')
class ScrollableLabel(ScrollView):
text = StringProperty('')
class ScrollApp(App):
def build(self):
return ScrollableLabel(text=long_text)
if __name__ == "__main__":
ScrollApp().run()
Question one:
I want to combine those two screens into one, to have the Scrollbale Label from the second screen below the Label from the first screen. How can I do it?
Question two:
I want to have the Label text from first code started from left. Now the text is in the middle of the Label. It seems that anchor: 'left' is not working. How can I do it?
And help will be appreciated. Thanks.
You can do that like this:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.scrollview import ScrollView
from kivy.lang import Builder
from kivy.properties import StringProperty
Builder.load_string('''
<Button>:
font_size: 40
color: 0,1,0,1
<Widgets>:
GridLayout:
size_hint: 1,0.4
cols: 2
rows: 2
Button:
text: 'OK'
Button:
text: 'Cancel'
Label:
text: "bbbbb"
font_size: 50
halign: "left"
size: self.texture_size
text_size: root.width/2, None
Label:
text: "Tssssssssssa #aaäa Äaaäa Üaa Maaäa a"
font_size: 30
halign: "left"
size: self.texture_size
text_size: root.width/2, None
ScrollView:
Label:
size_hint_y: None
height: self.texture_size[1]
text_size: self.width, None
text: root.text
''')
class Widgets(BoxLayout):
text = StringProperty("")
def __init__(self,**kwargs):
super(Widgets,self).__init__(**kwargs)
self.orientation = "vertical"
self.text = 'yay moo cow foo bar moo baa ' * 100
class MyApp(App):
def build(self):
return Widgets()
if __name__ == "__main__":
MyApp().run()

Kivy app with .kv file doesn't display

I am trying to modify this example: https://github.com/inclement/kivycrashcourse/blob/master/video14-using_a_screenmanager/after.py to make it work with a .kv file. This is my myscreenmanager.py file:
from kivy.app import App
from kivy.base import runTouchApp
from kivy.lang import Builder
from kivy.properties import ListProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
import time
import random
class FirstScreen(Screen):
pass
class SecondScreen(Screen):
pass
class ColourScreen(Screen):
colour = ListProperty([1., 0., 0., 1.])
class MyScreenManager(ScreenManager):
def new_colour_screen(self):
name = str(time.time())
s = ColourScreen(name=name,
colour=[random.random() for _ in range(3)] + [1])
self.add_widget(s)
self.current = name
class MyScreenManagerApp(App):
def build(self):
return MyScreenManager()
if __name__ == "__main__":
MyScreenManagerApp().run()
And this is my myscreenmanager.kv file:
#:import FadeTransition kivy.uix.screenmanager.FadeTransition
MyScreenManager:
transition: FadeTransition()
FirstScreen:
SecondScreen:
<FirstScreen>:
name: 'first'
BoxLayout:
orientation: 'vertical'
Label:
text: 'first screen!'
font_size: 30
BoxLayout:
Button:
text: 'goto second screen'
font_size: 30
on_release: app.root.current = 'second'
Button:
text: 'get random colour screen'
font_size: 30
on_release: app.root.new_colour_screen()
<SecondScreen>:
name: 'second'
BoxLayout:
orientation: 'vertical'
Label:
text: 'second screen!'
font_size: 30
BoxLayout:
Button:
text: 'goto first screen'
font_size: 30
on_release: app.root.current = 'first'
Button:
text: 'get random colour screen'
font_size: 30
on_release: app.root.new_colour_screen()
<ColourScreen>:
BoxLayout:
orientation: 'vertical'
Label:
text: 'colour {:.2},{:.2},{:.2} screen'.format(*root.colour[:3])
font_size: 30
Widget:
canvas:
Color:
rgba: root.colour
Ellipse:
pos: self.pos
size: self.size
BoxLayout:
Button:
text: 'goto first screen'
font_size: 30
on_release: app.root.current = 'first'
Button:
text: 'get random colour screen'
font_size: 30
on_release: app.root.new_colour_screen()
After running the app nothing is displayed on the screen. No errors in console. Switching back to Builder.load_string displays the app as expected.
Found my mistake: when using a .kv file the root widget needs to be surrounded in <>, like this:
#:import FadeTransition kivy.uix.screenmanager.FadeTransition
<MyScreenManager>:
transition: FadeTransition()
FirstScreen:
SecondScreen:
Not sure why the discrepancy between load_string and .kv files, but it works now.

Resources