i was just playing with Python and kivy , I've loaded my String data into a RecyclerView as per the kivy official documentation. but I've faced trouble on loading an object to multiple columns inside the list like a form data. for example i wanted to have name,family name and age to three columns with title headers row by row , I've also tried RecyclerGridLayout with 3 columns , but it can load just name into grids regardless of row by row requirement
<RV>:
viewclass: 'Label'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
Will appreciate any hint or sample code to learn how RecyclerView works on kivy
I was also looking for this and I could not find a specific example, so I have provided my solution. As el3ien has said, you will need to create a custom class which will represent each row of your selectable label.
<SelectableLabel>:
# Draw a background to indicate selection
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
Rectangle:
pos: self.pos
size: self.size
label1_text: 'label 1 text' # I have included two methods of accessing the labels
label2_text: 'label 2 text' # This is method 1
label3_text: 'label 3 text'
pos: self.pos
size: self.size
Label:
id: id_label1 # method 2 uses the label id
text: root.label1_text
Label:
id: id_label2
text: root.label2_text
Label:
id: id_label3
text: root.label3_text
In applying your data into the RV, you will need to restructure the dictionary to reflect the label layout
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
paired_iter = zip(items_1, items_2) # items_1 and items_2 are defined elsewhere
self.data = []
for i1, i2 in paired_iter:
d = {'label2': {'text': i1}, 'label3': {'text': i2}}
self.data.append(d)
Finally in the refresh_view_attrs, you will specify .label_text which is bound to each label, or you can use label id's.
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
self.label1_text = str(index)
self.label2_text = data['label2']['text']
self.ids['id_label3'].text = data['label3']['text'] # As an alternate method of assignment
return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data)
The entire code is below:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.properties import BooleanProperty
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
Builder.load_string('''
<SelectableLabel>:
# Draw a background to indicate selection
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
Rectangle:
pos: self.pos
size: self.size
label1_text: 'label 1 text'
label2_text: 'label 2 text'
label3_text: 'label 3 text'
pos: self.pos
size: self.size
Label:
id: id_label1
text: root.label1_text
Label:
id: id_label2
text: root.label2_text
Label:
id: id_label3
text: root.label3_text
<RV>:
viewclass: 'SelectableLabel'
SelectableRecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
''')
items_1 = {'apple', 'banana', 'pear', 'pineapple'}
items_2 = {'dog', 'cat', 'rat', 'bat'}
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableLabel(RecycleDataViewBehavior, GridLayout):
''' Add selection support to the Label '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
cols = 3
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
self.label1_text = str(index)
self.label2_text = data['label2']['text']
self.ids['id_label3'].text = data['label3']['text'] # As an alternate method of assignment
return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data)
def on_touch_down(self, touch):
''' Add selection on touch down '''
if super(SelectableLabel, self).on_touch_down(touch):
return True
if self.collide_point(*touch.pos) and self.selectable:
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, rv, index, is_selected):
''' Respond to the selection of items in the view. '''
self.selected = is_selected
if is_selected:
print("selection changed to {0}".format(rv.data[index]))
else:
print("selection removed for {0}".format(rv.data[index]))
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
paired_iter = zip(items_1, items_2)
self.data = []
for i1, i2 in paired_iter:
d = {'label2': {'text': i1}, 'label3': {'text': i2}}
self.data.append(d)
# can also be performed in a complicated one liner for those who like it tricky
# self.data = [{'label2': {'text': i1}, 'label3': {'text': i2}} for i1, i2 in zip(items_1, items_2)]
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
Instead of using Label as viewclass, create a custom class. That could be a horizontal box layout with two boxes.
<CustomClass#BoxLayout>:
orientation: "horizontal"
Label:
Label:
I used the above idea from #el3ien. My code is below.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
Builder.load_string('''
<RV>:
viewclass: 'myView'
RecycleBoxLayout:
default_size: None, dp(200)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
<myView#BoxLayout>:
BoxLayout:
orientation: 'horizontal'
BoxLayout:
orientation: 'vertical'
on_release:
Button:
size_hint: (1,1)
background_normal: 'C:/Users/Arsalan/Desktop/dummyImage2.jpg'
background_down: 'C:/Users/Arsalan/Desktop/dummyImage1.png'
text:
text_size: self.size
halign:
valign: 'middle'
Label:
size_hint: (1,0.3)
text: 'Product summary'
text_size: self.size
halign:
valign: 'middle'
canvas.before:
Color:
rgba: (0.6, 0.7, 0.4, 1)
Rectangle:
size: self.size
pos: self.pos
BoxLayout:
size_hint :(1,0.01)
Label:
size_hint: (1,0.3)
text: 'Rs 600'
text_size: self.size
halign:
valign: 'middle'
BoxLayout:
orientation: 'vertical'
size_hint: (0.001,1)
BoxLayout:
orientation: 'vertical'
on_release:
Button:
size_hint: (1,1)
background_normal: 'C:/Users/Arsalan/Desktop/dummyImage2.jpg'
background_down: 'C:/Users/Arsalan/Desktop/dummyImage1.png'
text:
text_size: self.size
halign:
valign: 'middle'
Label:
size_hint: (1,0.3)
text: 'Product summary'
text_size: self.size
halign:
valign: 'middle'
canvas.before:
Color:
rgba: (0.6, 0.7, 0.4, 1)
Rectangle:
size: self.size
pos: self.pos
BoxLayout:
size_hint :(1,0.01)
Label:
size_hint: (1,0.3)
text: 'Rs 600'
text_size: self.size
halign:
valign: 'middle'
''')
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(x)} for x in range(100)]
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
Try it out and let me know if you still have any question.
I would like to add a headers row to the table; and, I am trying the following:
<RV>:
BoxLayout:
orientation: "vertical"
BoxLayout:
orientation: 'horizontal'
size_hint: 1, None
size_hint_y: None
height: 25
Label:
text: "Item"
Label:
text: "User ID"
Label:
text: "User Name"
BoxLayout:
RecycleView:
id: review
viewclass: 'SelectableLabel'
SelectableRecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
But then the :SelectableRecycleBoxLayout moves deeper into the hierarchy and I no longer get the table, only the headers...how to fix this? I still don't quite get how to get to kv stuff from the python. :-(
Related
I'm new to Kivy and I stumbled upon an issue when using the RecycleViw. Basically, I have a switch for every entry in my recycle view to activate or deactivate the text input. This said, when I activate the "profilename_1", it also activates the "profilename_7" (see images below).
Example when OFF:
profilename_1
profilename_7
Example when ON:
profilename_1
profilename_7
If I enable the "profilename_2" it will also enable "profilename_8" and so on. How can I make each entry its own individual element/object, where it's completely independent of one another?
Here's my code:
PYTHON MAIN FILE:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import ObjectProperty, StringProperty, NumericProperty
from models import Profile
import json
class ProfileWidget(BoxLayout):
profilename = StringProperty()
phonenumber = NumericProperty()
alias = StringProperty()
class MainWidget(FloatLayout):
recycleView = ObjectProperty(None)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.profiles = fetch_profiles()
def on_parent(self, widget, parent):
self.recycleView.data = [profile.get_dictionary() for profile in self.profiles]
def fetch_profiles():
file = open("data.json")
result = json.load(file)
profile_list = []
for x in result['profiles']:
profile = Profile(x["profilename"], x["phonenumber"], x["alias"])
profile_list.append(profile)
return profile_list
class SwitchTestApp(App):
pass
SwitchTestApp().run()
MODELS.PY FILE (UPDATE)
class Profile:
profilename = ""
phonenumber = 0
alias = ""
def __init__(self, profilename, phonenumber, alias):
self.profilename = profilename
self.phonenumber = phonenumber
self.alias = alias
def get_dictionary(self):
return {"profilename": self.profilename, "phonenumber": self.phonenumber, "alias": self.alias}
KIVY FILE (named SwitchTest.kv in this case):
#:import utils kivy.utils
#:set color1 "#DD7835"
MainWidget:
<MainWidget>:
recycleView: recycleView
BoxLayout:
orientation: "vertical"
FitLabel:
text: "PROFILES LIST"
color: 1, 1, 1, 1
font_size: dp(22)
canvas.before:
Color:
rgb: utils.get_color_from_hex(color1)
Rectangle:
pos: self.pos
size: self.size
BoxLayout:
padding: dp(100), dp(20)
RecycleView:
id: recycleView
viewclass: "ProfileWidget"
RecycleBoxLayout:
default_size: None, dp(115)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(16)
<ProfileWidget>:
BoxLayout:
orientation: "vertical"
Label:
text: root.profilename
color: 1, 1, 1, 1
bold: True
font_size: dp(22)
text_size: self.size
halign: "left"
valign: "center"
BoxLayout:
FmtLabel:
text: "Profile Name: "
TextInput:
text: str(root.profilename).upper()
disabled: not switchID.active
on_text_validate: root.on_text_validate(self)
BoxLayout:
FmtLabel:
text: "Phone Number: "
TextInput:
text: str(int(root.phonenumber))
disabled: not switchID.active
BoxLayout:
FmtLabel:
text: "Alias: "
TextInput:
text: str(root.alias)
disabled: not switchID.active
BoxLayout:
orientation: "vertical"
padding: dp(20), dp(15)
Switch:
id: switchID
size_hint: None, 1
width: "100dp"
active: False
# on_active: root.switch_click(self, self.active)
Button:
text: "SAVE"
size_hint: None, None
width: "100dp"
height: "25dp"
opacity: 0 if not switchID.active else 1
bold: True
background_color: 0,1,0,1
disabled: not switchID.active
BoxLayout:
size_hint_y: 0.8
<FmtLabel#Label>:
color: 1, 1, 1, 1
text_size: self.size
halign: "left"
valign: "center"
<FitLabel#Label>:
color: 1, 0, 0, 1
size_hint: None, None
size: self.texture_size
I've tried assigning IDs to each element and playing with that, but no success either.
Also, this is my first post on StackOverflow, if I didn't do it right, please let me know! :)
Thank you!
I have the following code where I would like to be able to click on the navigation drawer, select Status under Users, input the name and then be sent to a given screen with the current_user now populated.
main.py
from kivy.uix.behaviors import CoverBehavior
from kivy import utils
from kivy.properties import ObjectProperty, ListProperty, NumericProperty, StringProperty, BooleanProperty
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import ScreenManager
from kivymd.app import MDApp
from kivy.lang import Builder
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
from kivymd.theming import ThemableBehavior
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.uix.list import MDList
#from models import ServerInfo
from navigation_drawer import navigation_helper
Window.fullscreen = 'auto'
class ItemWidget(BoxLayout):
sid = NumericProperty()
name = StringProperty()
work = NumericProperty()
is_disabled = BooleanProperty()
description = StringProperty()
has_issue = BooleanProperty()
class MainWidget(FloatLayout):
recycleView = ObjectProperty()
items = ListProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.items = [
#ServerInfo(1, "server1", 95, "desc", is_disabled=False, has_issue=False),
]
# Put the servers with issues on top, otherwise sort by name
self.items = sorted((s for s in self.items), key=lambda x: (not(x.has_issue),x.name))
# Load the server information when the recycleView is called
def on_recycleView(self, widget, parent):
self.recycleView.data = [
m.get_dictionary() for m in self.items
]
class Content(BoxLayout):
pass
class DemoApp(MDApp):
class ContentNavigationDrawer(BoxLayout):
pass
class DrawerList(ThemableBehavior, MDList):
pass
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dialog = None
# set main interface
self.manager = ScreenManager()
self.current_user = ""
# return main interface
#return self.manager
def build(self):
screen = Builder.load_string(navigation_helper)
return screen
def on_start(self):
pass
def dialog_close(self, *args):
self.dialog.dismiss(force=True)
def set_current_user(self,user,sm):
self.current_user = user
# Send over to given screen
# ???? = sm
def show_user_input_dialog(self,user,screen):
#print(sm.current)
if not self.dialog:
self.dialog = MDDialog(
title="Input Username:",
type="custom",
content_cls=Content(),
buttons=[
MDFlatButton(
text="CANCEL",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
on_release=self.dialog_close
),
MDFlatButton(
text="OK",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
#on_release=self.set_current_user(user,screen)
on_release=self.dialog_close
),
],
)
# Send to user management page with current user now populated
# Fails as sm is a str instead of ScreenManager()
#sm.current = 'user_management'
self.dialog.open()
DemoApp().run()
navigation_drawer.py
navigation_helper = """
Screen:
MDNavigationLayout:
ScreenManager:
id: screenManager
Screen:
name: "monitor"
BoxLayout:
orientation: 'vertical'
MDToolbar:
title: 'Administration'
left_action_items: [["menu", lambda x: nav_drawer.set_state('toggle')]]
elevation:5
MainWidget:
Screen:
name: "user_management"
BoxLayout:
orientation: 'vertical'
MDToolbar:
title: 'Administration'
left_action_items: [["menu", lambda x: nav_drawer.set_state('toggle')]]
elevation:5
UserManagement:
MDNavigationDrawer:
id: nav_drawer
ContentNavigationDrawer:
orientation: 'vertical'
padding: "2dp"
spacing: "2dp"
ScrollView:
DrawerList:
id: md_list
MDList:
TwoLineIconListItem:
text: "Monitor"
secondary_text: "Monitor servers"
on_release: screenManager.current = 'monitor'
IconLeftWidget:
icon: "monitor"
MDLabel:
padding: dp(10), dp(0)
text: "Users"
bold: True
font_size: dp(22)
size_hint_y: None
OneLineIconListItem:
text: "Status"
on_release: app.show_user_input_dialog('abc999','user_management')
<Content>
orientation: "vertical"
spacing: "12dp"
size_hint_y: None
height: "60dp"
MDTextField:
id: username_input
hint_text: "Username"
<UserManagement#BoxLayout>:
Label:
text: "Users"
<MainWidget>:
id: mainWidgetId2
recycleView: recycleView
CoverImage:
source: 'images/monitor.png'
# Darken the photo
canvas:
Color:
rgba: 0, 0, 0, .6
Rectangle:
pos: self.pos
size: self.size
BoxLayout:
padding: dp(20)
spacing: dp(10)
BoxLayout:
orientation: "horizontal"
BoxLayout:
canvas.before:
Color:
rgba: 1,1,1,.1
# Use a float layout to round the corners
RoundedRectangle:
pos: self.pos
size: self.size
RecycleView:
id: recycleView
viewclass: 'ItemWidget'
RecycleGridLayout:
cols: 1
default_size: self.parent.width / 5, dp(60)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
width: self.minimum_width
spacing: dp(155), dp(0)
BoxLayout:
orientation: "vertical"
spacing: dp(10)
BoxLayout:
spacing: dp(10)
BoxLayout:
canvas.before:
Color:
rgba: 1,1,1,.1
# Use a float layout to round the corners
RoundedRectangle:
pos: self.pos
size: self.size
Label:
text: "APPS"
BoxLayout:
canvas.before:
Color:
rgba: 1,1,1,.1
# Use a float layout to round the corners
RoundedRectangle:
pos: self.pos
size: self.size
Label:
text: "OTHER"
BoxLayout:
canvas.before:
Color:
rgba: 1,1,1,.1
# Use a float layout to round the corners
RoundedRectangle:
pos: self.pos
size: self.size
Label:
text: "MOUNTS"
BoxLayout:
canvas.before:
Color:
rgba: 1,1,1,.1
# Use a float layout to round the corners
RoundedRectangle:
pos: self.pos
size: self.size
Label:
text: "RECENT ACTIVITY"
<CoverImage#CoverBehavior+Image>:
reference_size: self.texture_size
<ItemWidget>:
BoxLayout:
#size_hint_max_x: dp(360)
size_hint_min_x: dp(150)
orientation: "horizontal"
BoxLayout:
orientation: "horizontal"
Button:
# Use the on_press to print troubleshooting info, otherwise comment out
on_press: print(self.background_color)
padding: dp(10), dp(10)
text_size: self.size
font_size: dp(22) if root.has_issue else dp(18)
background_color: .5,1,1,.6 #utils.get_random_color(alpha=.6)
halign: "left"
valign: "top"
size: 1,1
# Show last 4 of server name
text: str(root.name).upper()[-4:]
bold: True
color: (1,0,0) if root.has_issue else (1,1,1)
Button:
background_color: .5,1,1,.6
text_size: self.size
valign: "center"
halign: "center"
text: str(root.work)
padding: dp(8), dp(8)
bold: True if root.work > 90 else False
color: (1,0,0) if root.work > 90 else (1,1,1) # Red if greater than 90
"""
Questions:
How do I get the value from this so that I can pass the value instead of the hardcoded username of abc999
MDTextField:
id: username_input
How do I send to the given screen (user_management here) from
def set_current_user(self,user,sm):
self.current_user = user
# Send over to given screen
# ???? = sm
You can toggle commenting on the two on_release below to see the issue
MDFlatButton(
text="OK",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
#on_release=self.set_current_user(user,screen)
on_release=self.dialog_close
You can do what you want by modifying set_current_user() to:
def set_current_user(self, button):
self.current_user = self.dialog.content_cls.ids.username_input.text
self.dialog_close()
self.root.ids.users.add_widget(Label(text=self.current_user))
sm = self.root.ids.screenManager
sm.current = 'user_management'
The above code requires the addition of an id to the UserManagerment in the kv:
UserManagement:
id: users
This just adds a new Label with the name of the user.
A sample Kivy app populates Recycleview upon a button click.
However, when I copy the code from the class functions into the App.build to populate the Recycleview upon app loading, it doesn't do it. Is it not referencing the Recycleview correctly? Why is this not working?
I saw some answers here that used "self.ids." to find the Recycleview data, and also "App.get_running_app()." but it didn't work.
(Note: requires Kivy 2.0.0rc4 to compile)
from random import sample, randint
from string import ascii_lowercase
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
kv = """
<Row#RecycleKVIDsDataViewBehavior+BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Label:
id: name
Label:
text: root.value
<Test>:
canvas:
Color:
rgba: 0.3, 0.3, 0.3, 1
Rectangle:
size: self.size
pos: self.pos
rv: rv
orientation: 'vertical'
GridLayout:
cols: 3
rows: 2
size_hint_y: None
height: dp(108)
padding: dp(8)
spacing: dp(16)
Button:
text: 'Populate list'
on_press: root.populate()
Button:
text: 'Sort list'
on_press: root.sort()
Button:
text: 'Clear list'
on_press: root.clear()
BoxLayout:
spacing: dp(8)
Button:
text: 'Insert new item'
on_press: root.insert(new_item_input.text)
TextInput:
id: new_item_input
size_hint_x: 0.6
hint_text: 'value'
padding: dp(10), dp(10), 0, 0
BoxLayout:
spacing: dp(8)
Button:
text: 'Update first item'
on_press: root.update(update_item_input.text)
TextInput:
id: update_item_input
size_hint_x: 0.6
hint_text: 'new value'
padding: dp(10), dp(10), 0, 0
Button:
text: 'Remove first item'
on_press: root.remove()
RecycleView:
id: rv
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'Row'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
"""
Builder.load_string(kv)
class Test(BoxLayout):
def populate(self):
self.rv.data = [
{'name.text': ''.join(sample(ascii_lowercase, 6)),
'value': str(randint(0, 2000))}
for x in range(50)]
def sort(self):
self.rv.data = sorted(self.rv.data, key=lambda x: x['name.text'])
def clear(self):
self.rv.data = []
def insert(self, value):
self.rv.data.insert(0, {
'name.text': value or 'default value', 'value': 'unknown'})
def update(self, value):
if self.rv.data:
self.rv.data[0]['name.text'] = value or 'default new value'
self.rv.refresh_from_data()
def remove(self):
if self.rv.data:
self.rv.data.pop(0)
class TestApp(App):
def build(self):
t = Test()
t.rv.data = [
{'name.text': ''.join(sample(ascii_lowercase, 6)),
'value': str(randint(0, 2000))}
for x in range(50)]
t.rv.refresh_from_data()
return Test()
if __name__ == '__main__':
TestApp().run()
One solution is to perform the Recycleview update from the Test class.
from random import sample, randint
from string import ascii_lowercase
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
kv = """
<Row#RecycleKVIDsDataViewBehavior+BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Label:
id: name
Label:
text: root.value
<Test>:
canvas:
Color:
rgba: 0.3, 0.3, 0.3, 1
Rectangle:
size: self.size
pos: self.pos
rv: rv
orientation: 'vertical'
GridLayout:
cols: 3
rows: 2
size_hint_y: None
height: dp(108)
padding: dp(8)
spacing: dp(16)
Button:
text: 'Populate list'
on_press: root.populate()
Button:
text: 'Sort list'
on_press: root.sort()
Button:
text: 'Clear list'
on_press: root.clear()
BoxLayout:
spacing: dp(8)
Button:
text: 'Insert new item'
on_press: root.insert(new_item_input.text)
TextInput:
id: new_item_input
size_hint_x: 0.6
hint_text: 'value'
padding: dp(10), dp(10), 0, 0
BoxLayout:
spacing: dp(8)
Button:
text: 'Update first item'
on_press: root.update(update_item_input.text)
TextInput:
id: update_item_input
size_hint_x: 0.6
hint_text: 'new value'
padding: dp(10), dp(10), 0, 0
Button:
text: 'Remove first item'
on_press: root.remove()
RecycleView:
id: rv
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'Row'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
"""
Builder.load_string(kv)
class Test(BoxLayout):
def __init__(self,**kwargs):
super().__init__(**kwargs)
self.populate()
def populate(self):
self.rv.data = [
{'name.text': ''.join(sample(ascii_lowercase, 6)),
'value': str(randint(0, 2000))}
for x in range(50)]
def sort(self):
self.rv.data = sorted(self.rv.data, key=lambda x: x['name.text'])
def clear(self):
self.rv.data = []
def insert(self, value):
self.rv.data.insert(0, {
'name.text': value or 'default value', 'value': 'unknown'})
def update(self, value):
if self.rv.data:
self.rv.data[0]['name.text'] = value or 'default new value'
self.rv.refresh_from_data()
def remove(self):
if self.rv.data:
self.rv.data.pop(0)
class TestApp(App):
def build(self):
return Test()
if __name__ == '__main__':
TestApp().run()
It seems to be a question of the hierarchy and Kivy design. There's no instance of the RecycleView getting passed to the top object App.
good day
what im trying to accomplish is to input text on one screen then have it create a button on another screen within a recycleview where if I keep adding buttons the recycleview keeps getting populated. I would assume that the button1 function would update the rvs.rv.data and that the recycleview would use the it update itself. could you point me in the right direction please?
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import Screen, ScreenManager
kv = """
<custombutton#BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Button:
text: root.value
<Root>:
RVScreen:
name: 'Rv'
InputScreen:
name: 'input_screen'
<InputScreen>:
name: "input_screen"
orientation: "vertical"
BoxLayout:
orientation: "vertical"
TextInput:
id: textinput
Button:
text: "hi"
on_press: print(app.rvs.rv.data)
on_press: app.rvs.button1('some_value')
Button:
text: 'rvscreen'
on_press: root.manager.current = 'Rv'
<RVScreen>:
name: 'Rv'
rv: rv
orientation: "vertical"
BoxLayout:
orientation: "vertical"
Button:
text: 'refresh'
on_press: root.rv.refresh_from_data()
on_press: print(app.rvs.rv.data)
Button:
text: "input page"
on_press: root.manager.current = 'input_screen'
RecycleView:
id: rv
viewclass: 'custombutton'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
"""
Builder.load_string(kv)
class Root(ScreenManager):
pass
class RVScreen(Screen):
def __init__(self, **kwargs):
super(RVScreen, self).__init__(**kwargs)
def button1(self, value):
self.rv.data.insert(0, {'value': value or 'default value'})
class InputScreen(Screen):
pass
class TestApp(App):
rvs = RVScreen()
def build(self):
return Root()
if __name__ == '__main__':
TestApp().run()
The problem is that you are populating the data of the app.rvs, but that is created by the line:
rvs = RVScreen()
which creates a new RVScreen instance that is entirely unrelated to the RVScreen that is displayed in your GUI. In order to update the RVScreen in your GUI, you need to get a reference to that RVScreen.
I like to that that through a method in the App, just because it makes the code simpler. So here is a modified version of your code that does it my way:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen, ScreenManager
kv = """
<custombutton#BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Button:
text: root.value
<Root>:
RVScreen:
name: 'Rv'
InputScreen:
name: 'input_screen'
<InputScreen>:
name: "input_screen"
orientation: "vertical"
BoxLayout:
orientation: "vertical"
TextInput:
id: textinput
Button:
text: "hi"
on_press: app.add_data(textinput.text)
Button:
text: 'rvscreen'
on_press: root.manager.current = 'Rv'
<RVScreen>:
name: 'Rv'
rv: rv
orientation: "vertical"
BoxLayout:
orientation: "vertical"
Button:
text: 'refresh'
on_press: rv.refresh_from_data()
Button:
text: "input page"
on_press: root.manager.current = 'input_screen'
RecycleView:
id: rv
viewclass: 'custombutton'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
"""
Builder.load_string(kv)
class Root(ScreenManager):
pass
class RVScreen(Screen):
pass
class InputScreen(Screen):
pass
class TestApp(App):
# rvs = RVScreen() # this code does nothing useful
def build(self):
return Root()
def add_data(self, value):
# get the `Screen` instance
rvs = self.root.get_screen('Rv')
# insert the new value into the data
rvs.ids.rv.data.insert(0, {'value': value or 'default value'})
if __name__ == '__main__':
TestApp().run()
I also made a small modification, so that the new value comes from the TextInput.
RecycleView isn't updating its table when data is changed from outside the RecycleView class. As a summary I'm trying to create a simple stock portfolio manager.
I have a custom RecycleView class which I call RecycleViewPortfolio that inherits from RecycleView. From my .kv file I have three buttons connected to functions populate_1, populate_2 and populate_3 within my RecycleViewPortfolio class. Whenever I press a button and call any of the populate functions, the RecycleView behaves as expected.
However whenever I change the RecycleView data from outside the RecycleViewPortfolio class the table doesn't update. For example I have setup a global variable which I have imported to both my .py file and .kv file. I would like to be able to update the table whenever this data in this global variable is changed.
I have tried looking at the documentation from Kivy which mentions different functions that are suppose to solve this issue. But I guess Im clueless to how to apply these methods.
import StackOverflow.globalvariables as GlobalVariables
from kivy.app import App
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview import RecycleView
from kivy.uix.recyclegridlayout import RecycleGridLayout
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.properties import BooleanProperty, ListProperty, ObjectProperty
class AddPopup(Popup):
"""Popup for adding asset"""
asset_name = ObjectProperty
asset_price = ObjectProperty
asset_amount = ObjectProperty
currency = ObjectProperty
asset_class = ObjectProperty
wrapped_button = ObjectProperty()
def __init__(self, *args, **kwargs):
super(AddPopup, self).__init__(*args, **kwargs)
def open(self, correct=True):
super(AddPopup, self).open(correct)
def save_asset(self):
# Make sure no input is empty
if self.asset_name.text.strip() and self.asset_price.text.strip()\
and self.asset_amount.text.strip() and self.currency.text.strip()\
and self.asset_class.text.strip():
GlobalVariables.rv_data_global = [{'text': self.asset_name.text.strip()},
{'text': self.asset_amount.text.strip()},
{'text': self.asset_price.text.strip()}]
self.dismiss()
class RecycleViewPortfolio(RecycleView):
def __init__(self, **kwargs):
super(RecycleViewPortfolio, self).__init__(**kwargs)
self.populate_2()
def populate_1(self):
root = App.get_running_app().root
root.add_popup.open(True)
self.data = GlobalVariables.rv_data_global
def populate_2(self):
self.data = [{'text': str(x)} for x in range(0, 6)]
def populate_3(self):
self.data = [{'text': str(x)} for x in range(6, 12)]
class PortfolioRoot(GridLayout):
"""root to all screens"""
add_popup = ObjectProperty(None)
list = ListProperty([])
def __init__(self, **kwargs):
super(PortfolioRoot, self).__init__(**kwargs)
self.add_popup = AddPopup()
def test_set_data(self):
GlobalVariables.rv_data_global = [{'text': str(x)} for x in range(12, 18)]
class SelectableRecycleGridLayout(FocusBehavior, LayoutSelectionBehavior, # View Behavior
RecycleGridLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableButton(RecycleDataViewBehavior, Button): # Data Behavior
''' Add selection support to the Label '''
index = None
selected = BooleanProperty(True)
selectable = BooleanProperty(True)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
return super(SelectableButton, self).refresh_view_attrs(
rv, index, data)
def on_touch_down(self, touch):
''' Add selection on touch down '''
if super(SelectableButton, self).on_touch_down(touch):
return True
if self.collide_point(*touch.pos) and self.selectable:
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, rv, index, is_selected):
''' Respond to the selection of items in the view. '''
self.selected = is_selected
class PortfolioApp(App):
"""App object"""
def __init__(self, **kwargs):
super(PortfolioApp, self).__init__(**kwargs)
def build(self):
return PortfolioRoot()
PortfolioApp().run()
.kv file
#:kivy 1.10.0
#:import GlobalVariables StackOverflow.globalvariables
<SelectableButton>:
# Draw a background to indicate selection
canvas.before:
Color:
rgba: (0, 0.517, 0.705, 1) if self.selected else (0, 0.517, 0.705, 1)
Rectangle:
pos: self.pos
size: self.size
background_color: [1, 0, 0, 1] if self.selected else [1, 1, 1, 1] # dark red else dark grey
on_release:
print("Pressed")
<WrappedLabel#Label>:
size_hint_y: None
height: self.texture_size[1] + (self.texture_size[1]/2)
markup: True
<RecycleViewPortfolio#RecycleView>:
viewclass: 'SelectableButton'
target_id: None
# id: rv_data_list
data: GlobalVariables.rv_data_global
SelectableRecycleGridLayout:
cols: 3
key_selection: 'selectable'
default_size: None, dp(26)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
multiselect: True
touch_multiselect: True
<PortfolioRoot>:
BoxLayout:
list: rv_data_list
size: root.size
orientation: 'vertical'
WrappedLabel:
text: "[b] Portfolio Manager [/b]"
font_size: min(root.height, root.width) / 10
GridLayout:
size_hint_y: None
height: root.height * 0.1
cols: 4
rows: 1
# Settings
padding: root.width * 0.001, root.height * 0.001
spacing: min(root.width, root.height) * 0.001
Button:
text: "Add"
background_color: [1, 1, 1, 1]
on_release:
rv_data_list.populate_1()
print("Add")
Button:
text: "Change"
background_color: [1, 1, 1, 1]
on_release:
rv_data_list.populate_2()
print("Change")
Button:
text: "Remove"
background_color: [1, 1, 1, 1]
on_release:
rv_data_list.populate_3()
print("Remove")
Button:
text: "Test"
background_color: [1, 1, 1, 1]
on_release:
root.test_set_data()
print("Test set data")
RecycleViewPortfolio:
id: rv_data_list
<AddPopup>:
size_hint: 0.8, 0.8
title: "Add Asset"
title_size: root.height * 0.05
auto_dismiss: False
asset_name: asset_name
asset_price: asset_price
asset_amount: asset_amount
currency: currency
asset_class:asset_class
wrapped_button: wrapped_button
BoxLayout:
orientation: 'vertical'
GridLayout:
rows: 5
cols: 2
padding: root.width * 0.02, root.height * 0.02
spacing: min(root.width, root.height) * 0.02
Label:
id: asset_name_label
text: "Asset name"
halign: "center"
font_size: root.height/25
text_size: self.width, None
center_y: .5
TextInput:
id: asset_name
text: "Asset name"
halign: "center"
font_size: root.height/25
text_size: self.width, None
center_y: .5
Label:
id: asset_price_label
text: "Asset price"
halign: "center"
font_size: root.height/25
text_size: self.width, None
center_y: .5
TextInput:
id: asset_price
text: "asset"
halign: "center"
font_size: root.height/25
text_size: self.width, None
center_y: .5
Label:
id: asset_amount_label
text: "Asset amount"
halign: "center"
font_size: root.height/25
text_size: self.width, None
center_y: .5
TextInput:
id: asset_amount
text: "Asset amount"
halign: "center"
font_size: root.height/25
text_size: self.width, None
center_y: .5
Label:
id: currency_label
text: "Asset currency"
halign: "center"
font_size: root.height/25
text_size: self.width, None
center_y: .5
TextInput:
id: currency
text: "currency"
halign: "center"
font_size: root.height/25
text_size: self.width, None
center_y: .5
Label:
id: asset_class_label
text: "Asset class"
halign: "center"
font_size: root.height/25
text_size: self.width, None
center_y: .5
TextInput:
id: asset_class
text: "Asset class"
halign: "center"
font_size: root.height/25
text_size: self.width, None
center_y: .5
Button:
id: wrapped_button
text: "Save"
size_hint: 1, None
height: root.height / 8
on_release: root.save_asset()
Button:
id: wrapped_button
text: "close"
size_hint: 1, None
height: root.height / 8
on_release: root.dismiss()
globalvariables.py
# global variables
rv_data_global = []
I expect to be able to create a popup window where I add information which is stored in a global variable and after changes are made I call for the RecycleView to be updated.
Edit: Added a working example
This example shows how Im able to use the buttons "Change" and "Remove" in order to populate the RecycleView as expected. However when the add button is pressed and the popup window appears and the save button is pressed the RecycleView doesn't update. If the add button is pressed again and directly closed the RecyleView gets updated and shows the correct information.
The same goes for the "Test" buttons where I call a function which changes the global variable. From there I have no idea of how to update the view since Im no longer working underneath the RecycleView class.
TLDR;
Method for manually updating the RecycleView after data has been changed.
I found the answers to one of my questions. By adding:
self.ids.rv_data_list.data = GlobalVariables.rv_data_global
self.ids.rv_data_list.refresh_from_data()
to the test_set_data function, I am now able to refresh the data as I requested. Hence the magic was the refresh_from_data() method.
Through the App.get_running_app() I was able to access the refresh_from_data() command from the popup class.
root = App.get_running_app().root
root.ids.rv_data_list.data = GlobalVariables.rv_data_global
root.ids.rv_data_list.refresh_from_data()
I seem to have solved my own issues here. But if anyone has a better or cleaner solution, please let me know.