my problem is that I am doing recycle view of async Images that have a special url. I am loading them thanks to a recycle view. My problem is that I want to print the source of the async image thanks to an app function, on another screen. All I did, didn"t work. The idea is only to recup the source of the async image and to show it on UserInfo screen. Only that. Nb : (The images are taken on an API server.)
Thanks !
The code :
class ReviewinApp(MDApp):
new_dialog = None
notdialog = None
dialog =None
dialoge = None
asyncimage = None
doc = None
data = ListProperty()
id_ = ListProperty()
def build(self):
dialog = None
self.title = "ReviewinApp"
global sm
sm = ScreenManager()
sm.add_widget(Builder.load_file("splash.kv"))
sm.add_widget(Builder.load_file("accueil.kv"))
sm.add_widget(Builder.load_file('conditions.kv'))
sm.add_widget(Builder.load_file("register.kv"))
sm.add_widget(Builder.load_file("faq.kv"))
sm.add_widget(Builder.load_file("login.kv"))
sm.add_widget(Builder.load_file("User2.kv"))
sm.add_widget(Builder.load_file("rules.kv"))
sm.add_widget(Builder.load_file("commentinput.kv"))
sm.add_widget(Builder.load_file("UserInfo.kv"))
sm.add_widget(Builder.load_file('test.kv'))
return sm
def load_products(self):
token = self.token
json = {"token":token}
res = requests.post('http://127.0.0.1:2223/products/list', json=json)
self.doc = res.json()
self.data = [{'source': 'http://127.0.0.1:2223/products/' + str(self.doc[i].replace('.png', ''))} for i in range(len(self.doc))]
def en(self):
app = App.get_running_app()
print(app.data)
```
MDScreen:
name: 'test'
md_bg_color: 41/255, 34/255, 34/255, 1
on_enter: app.load_products()
image1: ""
RecycleView:
viewclass: "OwnWidget"
data: app.data
RecycleBoxLayout:
default_size: None, dp(300)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(20)
<OwnWidget#BoxLayout>:
id: recycle
orientation: 'vertical'
text: ''
source:''
MDIconButton:
icon: 'arrow-left'
icon_color: 'white'
theme_icon_color: 'Custom'
pos_hint: {'center_x':0.3, 'center_y': 0.1}
size_hint_x: 0.3
height: dp(40)
on_press:
app.reload_datas()
app.root.current = "UserInfo"
print(product.source)
AsyncImage:
id: product
source: root.source
```
MDScreen:
name: "UserInfo"
on_enter: app.en()
MDFloatLayout:
md_bg_color: 41/255, 34/255, 34/255, 1
MDLabel:
id: test_text
text: ""
```
All you need to access objects in a different screen is the .get screen() method, the name of the screen, object id, and object property.
This family of question comes up a lot with kivy, so I have put together a single-file runnable example that you can hopefully get some ideas from.
Regarding the specific question about linking an id; the ids in the .kv code are not available directly in Python but one can make a cross-reference at the top of the kivy object. Here are 4 kivy object ids being connected to Python objects of the same name
<MainScreen>:
# python names on the left: kivy id on the right
box_layout_a: box_layout_a
box_layout_b: box_layout_b
my_button: my_button
my_text_input: my_text_input
and here is the reference to the Python objects in the class definition. I like using the type-hints because the IDE can help with auto completion and type checking.
class MainScreen(BoxLayout):
# python names on the left: kivy id on the right
box_layout_a: BoxLayout
box_layout_b: BoxLayout
my_button: Button
my_string_property = StringProperty()
Runnable code:
from kivy.lang import Builder
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import StringProperty
from functools import partial
Builder.load_string('''
<MainScreen>:
# python names on the left: kivy id on the right
box_layout_a: box_layout_a
box_layout_b: box_layout_b
my_button: my_button
my_text_input: my_text_input
orientation: "vertical"
BoxLayout:
id: box_layout_a
orientation: "vertical"
Button:
text: "button connected to widget"
on_press: root.kv_button(1)
Button:
text: "button connected to app, increments a counter"
on_press: app.kv_app_button("method in the top level app")
BoxLayout:
id: box_layout_b
orientation: "horizontal"
Button:
id: my_button
Label:
id: my_text_input
text: root.my_string_property
Label:
text: app.string_property_bound_in_app
'''
)
class MainScreen(BoxLayout):
# python names on the left: kivy id on the right
box_layout_a: BoxLayout
box_layout_b: BoxLayout
my_button: Button
my_string_property = StringProperty()
def function_to_bind(self, my_argument, *args, **kwargs):
print(f"{self}bound function {my_argument} {args}")
def __init__(self, **kwargs):
super().__init__(**kwargs)
# often you can define the buttons and on_press method in the kivy file
# but sometimes there are a lot of buttons, and it could makse sense to create them dynamically
_late_button = Button()
_late_button.text = "added programatically"
_late_button.bind(on_press=partial(self.function_to_bind, "custom parameter sent"))
self.box_layout_a.add_widget(_late_button)
def kv_button(self, _input, *args) -> None:
print(f"{self} input {_input}, {args}")
# This is a little strange, but down here in the widget, we don't have a reference to the App object,
# however, it can be obtained
App.get_running_app().string_property_bound_in_app = "hi"
class KivyDemo(App):
string_property_bound_in_app = StringProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.count = 0
# keep a reference to the main screen object
self.main_box_layout: MainScreen = MainScreen()
def kv_app_button(self, _input, *args) -> None:
self.count += 1
self.main_box_layout.my_button.text = f"{self.count}"
self.main_box_layout.my_string_property = f"bound string property {self.count + 1}"
print(f"{self} input: {_input}, {args}")
def build(self) -> MainScreen:
return self.main_box_layout
def on_start(self):
self.main_box_layout.my_string_property = "initial text"
self.string_property_bound_in_app = "!"
test: KivyDemo = KivyDemo()
test.run()
Related
Trying to implement an application using ResponsiveView and having trouble both linking all the widgets and keeping all three view consistent. I began with a prototype using the backdrop view, which was fine for a mobile device, but fell short for a tablet or desktop. In the initial prototype I was able to identify the widget hierarchy of the status bar(s) and simply call them from a bound application level method. That is obviously not going to work in the ResponseView implementation, as each view may have different instances of the widgets. I expect that the best solution may be to create my own custom events, but verification or the suggestion of another better approach would be appreciated.
I would like to be able to update the right status bar to display properties about the selected items in list one. Since each of the three views may have its own instance of the status bar and lists, I need to update each for every event. Or figure out how to have a single widget for each item that can be used in each view. The implementation below will work for the tablet view, but not the mobile or desktop views. The mobile view is incomplete, as the full backdrop implementation is not present.
Below is a rather lengthy prototype that should display the problem(s):
from kivymd.app import MDApp
from kivymd.uix.list import MDList
from kivymd.uix.scrollview import ScrollView
from kivymd.uix.list import OneLineIconListItem
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.screen import MDScreen
from kivymd.uix.responsivelayout import MDResponsiveLayout
from kivymd.uix.toolbar import MDTopAppBar
from kivy.properties import StringProperty, BooleanProperty, ObjectProperty
Version = '0.1'
MainAppKV = '''
#:import NoTransition kivy.uix.screenmanager.NoTransition
#:import Window kivy.core.window.Window
#:import IconLeftWidget kivymd.uix.list.IconLeftWidget
<NotificationBar>:
id: top_bar
title: "Title"
anchor_title: "center"
<LeftStatusBar>:
id: left_status_bar
IconLeftWidget:
id: status_icon
icon: ''
<RightStatusBar>:
id: right_status_bar
IconLeftWidget:
id: status_icon
icon: ''
<TableOneHeader>:
id: "table_one_header"
size_hint: [1, .12]
orientation: "horizontal"
Button:
id: "column_1"
text: "Column 1"
on_press: root.button_press(self)
Button:
id: "column_2"
text: "Column 2"
Button:
id: "column_3"
text: "Column 3"
<TableTwoHeader>:
id: "table_two_header"
size_hint: [1, .12]
orientation: "horizontal"
Button:
id: "column_1"
text: "Column 1"
Button:
id: "column_2"
text: "Column 2"
<ScrollViewOne>:
id: "scrollview_one"
MDSelectionList:
id: "mdselection_list"
spacing: "12dp"
on_selected: app.on_selected(*args)
on_unselected: app.on_unselected(*args)
OneLineIconListItem:
text: "List One, Item One"
OneLineIconListItem:
text: "List One, Item Two"
<ScrollViewTwo>:
id: "scrollview_two"
ListTwo:
id: "list_two"
OneLineIconListItem:
text: "List Two, Item One"
OneLineIconListItem:
text: "List Two, Item Two"
<ListOneLayout>:
id: "list_one_layout"
orientation: "vertical"
TableOneHeader:
ScrollViewOne:
<ListTwoLayout>:
id: "list_two_layout"
orientation: "vertical"
TableTwoHeader:
ScrollViewTwo:
<BackDropLayout>:
id: "backdrop_layout"
orientation: "vertical"
NotificationBar:
BoxLayout:
orientation: "horizontal"
size_hint: (1, .1)
LeftStatusBar:
size_hint: (.6, 1)
RightStatusBar:
size_hint: (.4, 1)
ListOneLayout:
<MyBackdropBackLayer#ScreenManager>
transition: NoTransition()
MDScreen:
ListOneLayout:
<SideBySideLayout>
orientation: "vertical"
NotificationBar:
BoxLayout:
orientation: "horizontal"
BoxLayout:
orientation: "vertical"
size_hint: (.5, 1)
LeftStatusBar:
ListTwoLayout:
BoxLayout:
orientation: "vertical"
size_hint: (.5, 1)
RightStatusBar:
ListOneLayout:
<MobileView>
id: "mobile_view"
name: "mobile"
MDScreen:
name: "backdrop"
BackDropLayout:
id: "mobile_layout"
<TabletView>
id: "tablet_view"
name: "tablet"
SideBySideLayout:
id: "tablet_layout"
<DesktopView>
id: "desktop_view"
name: "desktop"
SideBySideLayout:
id: "desktop_layout"
ResponsiveView:
'''
class ScrollViewOne(ScrollView):
pass
class TableOneHeader(MDBoxLayout):
pass
class TableTwoHeader(MDBoxLayout):
pass
class BackDropLayout(MDBoxLayout):
pass
class SideBySideLayout(MDBoxLayout):
pass
class MobileView(MDScreen):
pass
class TabletView(MDScreen):
pass
class DesktopView(MDScreen):
pass
class ResponsiveView(MDResponsiveLayout, MDScreen):
def __init__(self, **kw):
super().__init__(**kw)
self.mobile_view = MobileView()
self.tablet_view = TabletView()
self.desktop_view = DesktopView()
class LeftStatusBar(OneLineIconListItem):
status_text = StringProperty("Left-Status")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.text = self.status_text
class RightStatusBar(OneLineIconListItem):
in_selection = BooleanProperty(False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.text = "None selected"
def update(self, item, remove=False):
if remove:
self.text = 'None selected'
return
self.text = "update selection"
class NotificationBar(MDTopAppBar):
pass
class ListOneLayout(MDBoxLayout):
pass
class ListTwoLayout(MDBoxLayout):
pass
class ScrollViewTwo(ScrollView):
pass
class ListTwo(MDList):
pass
class MainApp(MDApp):
status_left = ObjectProperty()
status_right = ObjectProperty()
def __init__(self):
super().__init__()
self.title = "Title (v{})".format(Version)
def build(self):
self.root = Builder.load_string(MainAppKV)
self.status_right = self.root.tablet_view.children[0].children[0].children[0].children[1]
self.status_left = self.root.tablet_view.children[0].children[0].children[1].children[1]
return self.root
def on_selected(self, instance_selection_list, instance_selection_item):
print("on_selected {}".format(instance_selection_item))
self.status_right.update(0)
def on_unselected(self, instance_selection_list, instance_selection_item):
print("on_unselected {}".format(instance_selection_item))
self.status_right.update(0, True)
if __name__ == "__main__":
MainApp().run()
If you define a Property in your App class, like this:
class MainApp(MDApp):
left_status_text = StringProperty('Left-Status from App')
Then, you can use that same property in every instance of LeftStatusBar by using it in the kv:
<LeftStatusBar>:
id: left_status_bar
text: app.left_status_text # refers to Property in App
IconLeftWidget:
id: status_icon
icon: ''
Looking further into the ResponseView, I found that it isn't necessarily intended to support synchronizing the distinct views, but rather allowing a single app to display one of 3 views. The other problem that I had was to update the subordinate widgets appropriate for the selected view. To accomplish this, I added Object properties to the ResponsiveView and set them in the on_change_screen_type().
class ResponsiveView(MDResponsiveLayout, MDScreen):
active_view = ObjectProperty()
status_left = ObjectProperty()
status_right = ObjectProperty()
def on_change_screen_type(self, *args):
# first child is the Active screen
self.active_view = self.children[0]
self.status_left = self.active_view.ids.view_layout.ids.left_status
self.status_right = self.active_view.ids.view_layout.ids.right_status
The Problem
I'm attempting to create dynamic buttons tied to "pages" in my program. Each button and page inherits a name from a database that I have tied to the program. In this case, I'm having trouble adding widgets to a screen and changing to that screen on button press. So far, I'm able to implement dynamic buttons that could change the screen and receive variables for text but the widgets aren't being added or aren't showing up.
The Code
Upon request, I recreated the code with the same problem my program was experiencing.
from kivy.lang import Builder
from kivymd.app import MDApp
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.properties import StringProperty
from kivymd.uix.button import MDRectangleFlatButton
from functools import partial
kv = '''
ScreenManager
ScreenOne:
ScreenTwo:
ScreenThree:
<ScreenOne>:
ScrollView:
MDGridLayout:
id: screen_one
cols: 1
adaptive_height: True
padding: "40dp"
MDFlatButton:
text: "Next Page"
theme_text_color: "Custom"
text_color: 0, 0, 1, 1
pos_hint: {"center_x": .5, "center_y": .5}
on_press: root.manager.current = 'Screen 2'
<ScreenTwo>:
name: 'Screen 2'
on_pre_enter: root.on_load()
ScrollView:
MDGridLayout:
id: screen_two
cols: 1
adaptive_height: True
padding: "40dp"
<ScreenThree>:
name: 'Screen 3'
ScrollView:
MDGridLayout:
id: screen_three
cols: 1
adaptive_height: True
padding: "40dp"
'''
sm = ScreenManager()
class ScreenOne(Screen):
pass
class ScreenTwo(Screen):
def on_load(self):
name_list = ['Page 1', 'Page 2', 'Page 3']
button_list = []
for x in name_list:
page_button = MDRectangleFlatButton(text = x, theme_text_color = "Custom")
button_list.append(page_button)
for x in button_list:
x.bind(on_press = partial(self.load_home, x.text))
self.ids.screen_two.add_widget(x)
def load_home(self, name, *largs):
self.manager.current = 'Screen 3'
Screen3 = ScreenThree()
Screen3.on_load(name)
def on_leave(self):
self.ids.screen_two.clear_widgets()
# Below is the particular code I'm having trouble with. It's supposed
# to display a page with a button with the same name as the button
# that sends you to Screen Three. However, the button is not showing up.
class ScreenThree(Screen):
def on_load(self, name, *kwargs):
test_button = MDRectangleFlatButton(text = name, theme_text_color = "Custom")
self.ids.screen_three.add_widget(test_button)
sm.add_widget(ScreenOne(name="Screen 1"))
sm.add_widget(ScreenTwo(name="Screen 2"))
sm.add_widget(ScreenThree(name="Screen 3"))
class TestApp(MDApp):
def build(self):
screen = Builder.load_string(kv)
return screen
TestApp().run()
All help, tips, and solutions would be greatly appreciated. Thank you in advanced.
I figured it out! Turns out, I can add the buttons from the same method as ScreenTwo's load_home() method by giving each screen ids via screenmanager in the kivy code:
ScreenManager
ScreenOne:
id: one
ScreenTwo:
id: two
ScreenThree:
id: three
And then use the add_widget function in the load_home() method:
def load_home(self, name, *largs):
test_button = MDRectangleFlatButton(text = name, theme_text_color = "Custom")
self.manager.ids.three.ids.screen_three.add_widget(test_button)
self.manager.current = 'Screen 3'
I need help.
I created a small mobile application with Kivy.
I have two screens: ScreenList and ScreenDetail.
However the screen(ScreenList) containing GridLayout does not refresh
ScreenList: contains a list of items
ScreenDetail: Contains the details of a single item.
How the app works:
When I click on the first item on button 1
I go to the details of the item.
I modify the second field. I replace the text: Firt element for First and update data
After recording, I redirect the application to the screens which contain (ScreenList) the list of elements.
But the list of elements remains unchanged then the data has been modified in the database.
6.And when I return to the screen (ScreenDetail) which contains the details, there I see that the data is updated.
How can I refresh the item list in ScreenList?
Here are the pictures as an example
List before update
before update
after update
List after update
Here is the python code:
import kivy
from kivy.app import App
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.scrollview import ScrollView
from kivy.properties import ObjectProperty, StringProperty
from kivy.lang import Builder
from kivymd.uix.picker import MDTimePicker
from kivymd.uix.picker import MDDatePicker
from kivymd.app import MDApp
import sqlite3
import os.path
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
db_path = os.path.join(BASE_DIR, "donnee/ProjetMaison.db")
def donnee_dic(cursor, row):
d = {}
for idx, col in enumerate(cursor.description):
d[col[0]] = row[idx]
return d
cur_id = None
class ScreenList(Screen):
data_grid = ObjectProperty()
def go_to_detail(self, instance):
global cur_id
cur_id = int(instance.text)
self.manager.current = 'screen_detail'
def __init__(self, **kwargs):
super(ScreenList, self).__init__(**kwargs)
con_list_course = sqlite3.connect(db_path)
con_list_course.row_factory = donnee_dic
curss_list_course = con_list_course.cursor()
data_list_course = curss_list_course.execute("select * FROM courses")
self.data_grid.bind(minimum_height=self.data_grid.setter('height'))
for row in data_list_course:
template_screen = GridLayout(cols=2, size_hint_y=None, height=40)
template_screen.add_widget(Label(text=row['nom']))
template_screen.add_widget(Button(text=str(row['id']), on_press=self.go_to_detail))
self.data_grid.add_widget(template_screen)
con_list_course.close()
def Maj_colonne(id, nom):
try:
sqliteConnection = sqlite3.connect(db_path)
cursor = sqliteConnection.cursor()
sqlite_update_query = """Update courses set nom = ? where id = ?"""
columnValues = (nom, id)
cursor.execute(sqlite_update_query, columnValues)
sqliteConnection.commit()
sqliteConnection.commit()
cursor.close()
except sqlite3.Error as error:
print("Erreur de connexion", error)
finally:
if sqliteConnection:
sqliteConnection.close()
class ScreenDetail(Screen):
label_id = ObjectProperty()
label_nom = ObjectProperty()
def __init__(self, **kwargs):
super(ScreenDetail, self).__init__(**kwargs)
def on_enter(self):
global cur_id
conn = sqlite3.connect(db_path)
cursor = conn.execute("select * FROM courses where id=?",str(cur_id))
for row in cursor:
self.label_id.text = str(row[0])
self.label_nom.text = str(row[1])
cursor.close()
def update_course(self):
id_pk = self.ids['label_id'].text
nom = self.ids['label_nom'].text
if id_pk:
Maj_colonne(str(id_pk), str(nom))
self.ids['label_id'].text = ''
self.ids['label_nom'].text = ''
self.manager.current = 'screen_list'
class Listapp(MDApp):
def build(self):
screenmanager = ScreenManager()
screenmanager.add_widget(ScreenList(name='screen_list'))
screenmanager.add_widget(ScreenDetail(name='screen_detail'))
return screenmanager
if __name__ == '__main__':
Listapp().run()
Here is the kv code:
#:import utils kivy.utils
<ScreenList>:
data_grid: data_grid
MDBoxLayout:
orientation: 'vertical'
md_bg_color: app.theme_cls.primary_color
radius: [25, 0, 0, 0]
ScrollView:
MDGridLayout:
id: data_grid
cols: 1
spacing:10
size_hint_y:None
<ScreenDetail>:
label_id: label_id
label_nom: label_nom
MDBoxLayout:
orientation: 'vertical'
md_bg_color: app.theme_cls.primary_color
ScrollView:
GridLayout:
id: detail_grid
cols:2
Label:
text: 'Numéro:'
bold: True
TextInput:
id: label_id
text: ''
Label:
text: 'Nom:'
bold: True
TextInput:
id: label_nom
text: ''
Button:
text: 'OK'
size_hint_y: None
on_press: root.update_course()
Thank you
Few notes to take, in general, when working with Kivy
When you're trying to share data in between screens, it's often useful to use app methods instead of specific methods of screens.
And when you need to create lots of buttons, maybe inside a loop, and bind methods on its events( on_press, on_release), it's often bad to create button instances on the fly and bind methods on its events because you'll need to do extra work to make sure that those bound methods are called with right parameters when events are fired. Rather create a custom class template and use that instead.
Working solution for your problem (only showing sections that has been added/updated
Created custom GridLayout:
class MyGrid(GridLayout):
pass
Updated __init__ method inside ScreenList:
def __init__(self, **kwargs):
#...
app = MDApp.get_running_app()
for row in data_list_course:
template_screen = MyGrid()
template_screen.ids.lbl.text = row['nom']
template_screen.ids.btn.text = str(row['id'])
self.data_grid.add_widget(template_screen)
Added methods inside app class:
class Listapp(MDApp):
def build(self):
self.screenmanager = ScreenManager()
self.screenmanager.add_widget(ScreenList(name='screen_list'))
self.screenmanager.add_widget(ScreenDetail(name='screen_detail'))
return self.screenmanager
def go_to_detail_(self, inst):
self.inst = inst
self.screenmanager.current = 'screen_detail'
def update_course_(self, label_id, label_nom):
c = self.inst.children[::-1]
c[0].text = label_nom.text
c[1].text = label_id.text
print(label_id.text, label_nom.text)
if label_id.text:
Maj_colonne(str(id_pk), str(nom))
label_nom.text = ''
label_id.text = ''
self.screenmanager.current = 'screen_list'
Here's the updated kv code:
#:import utils kivy.utils
<ScreenList>:
data_grid: data_grid
MDBoxLayout:
orientation: 'vertical'
md_bg_color: app.theme_cls.primary_color
radius: [25, 0, 0, 0]
ScrollView:
MDGridLayout:
id: data_grid
cols: 1
spacing:10
size_hint_y:None
<ScreenDetail>:
label_id: label_id
label_nom: label_nom
MDBoxLayout:
orientation: 'vertical'
md_bg_color: app.theme_cls.primary_color
ScrollView:
GridLayout:
id: detail_grid
cols:2
Label:
text: 'Numéro:'
bold: True
TextInput:
id: label_id
text: ''
Label:
text: 'Nom:'
bold: True
TextInput:
id: label_nom
text: ''
Button:
text: 'OK'
size_hint_y: None
on_press: app.update_course_(label_id, label_nom)
<MyGrid>:
cols:2
size_hint_y:None
height:40
Label:
id: lbl
text: ''
Button:
id: btn
text: ''
on_press:
app.go_to_detail_(root)
In a previous question, I asked how to have a row of text inputs which is dinamically added on press of a button, all from a py script.
I am now attempting at moving all my layout code to a kv file. While this was pretty straightforward for the widgets which appear on screen by default, I am not really sure of how to define the dinamically added text inputs from the kv file.
My solution, at present, is to create the 'default' widgets in the kv file, and to add the other ones from the py script through the addIngredient method. Below a minimal working version.
The kv file:
WMan:
AddWindow:
<AddWindow>:
name: 'add'
ingsGrid: ingsGrid
ing1: ing1
quant1: quant1
addIng: addIng
saveButton: saveButton
StackLayout:
id: ingsGrid
size_hint: .9, None
height: self.minimum_height
orientation: 'lr-tb'
spacing: '5sp', '5sp'
TextInput:
id: ing1
multiline: False
size_hint: .65, None
height: self.minimum_height
TextInput:
id: quant1
multiline: False
size_hint: .25, None
height: self.minimum_height
Button:
id: addIng
text: "+"
size_hint: .1, None
height: ing1.height
on_release: root.addIngredient(self)
Button:
id: saveButton
text: "Save"
size_hint: .3, None
on_release:
root.saveRec(self)
The py script reads:
from kivy.app import App
from kivy.properties import ObjectProperty
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.stacklayout import StackLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.lang import Builder
class AddWindow(Screen):
def __init__(self, **kwargs):
super(AddWindow, self).__init__(**kwargs)
recipeName = ObjectProperty(None)
ingsGrid = ObjectProperty(None)
ing1 = ObjectProperty(None)
quant1 = ObjectProperty(None)
self.i = 1
self.ingsList = {}
self.ingsList[0] = ing1
self.quants = {}
self.quants[0] = quant1
def addIngredient(self, instance):
tmp_children_list = self.ingsGrid.children[:]
self.ingsGrid.clear_widgets()
# range(start,stop[, step])
for index in range(len(tmp_children_list)-1, -1, -1):
# the last item, then last-1, etc
child = tmp_children_list[index]
# add the last first (new ones will be added on top)
self.ingsGrid.add_widget(child)
# if child is the pressed button
if child == instance:
self.ing = TextInput(
size_hint=(.65, None),
height='30sp')
self.ingsGrid.add_widget(self.ing)
self.ingsList[self.i] = self.ing
self.quant = TextInput(
size_hint=(0.25, None),
height='30sp')
self.ingsGrid.add_widget(self.quant)
self.quants[self.i] = self.quant
self.i += 1
self.addNext = Button(
text="+",
size_hint=(0.1, None),
height='30sp')
self.addNext.bind(on_press=self.addIngredient)
self.ingsGrid.add_widget(self.addNext)
def saveRec(self, instance): # grab all inputs and send to SQLite db
print(self.ingsList)
print(self.ingsList[0].text)
print(self.ingsList[1].text)
class WMan(ScreenManager):
pass
kv = Builder.load_file("test.kv")
class TestApp(App):
def build(self):
return kv
if __name__ == "__main__":
TestApp().run()
My problem here is twofold: first, while this way of dinamically adding rows works as it should, it is to me a bit messy to have half of the layout defined on the kv file, and the other half defined in the py script. So my first question is:
1. Is there a way to move the entire layout to the kv file?
Second questions is:
2. How do I access the content of textInput 'ing1' (the one created in the kv file)?
when I run print(self.ingsList), I get:
{0: <ObjectProperty name=>, 1: <kivy.uix.textinput.TextInput object at 0x000002077FB89C88>}
So while I can easily do print(self.ingsList[1].text), running print(self.ingsList[0].text) will give error:
AttributeError: 'kivy.properties.ObjectProperty' object has no attribute 'text'
Here is a modified version of your code that does what I think you want:
from kivy.app import App
from kivy.properties import ObjectProperty, NumericProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.lang import Builder
class AddWindow(Screen):
def __init__(self, **kwargs):
super(AddWindow, self).__init__(**kwargs)
recipeName = ObjectProperty(None)
ingsGrid = ObjectProperty(None)
ing1 = ObjectProperty(None)
quant1 = ObjectProperty(None)
self.i = 1
self.ingsList = {}
self.ingsList[0] = ing1
self.quants = {}
self.quants[0] = quant1
def saveRec(self, instance): # grab all inputs and send to SQLite db
for child in self.ids.ingsGrid.children:
if isinstance(child, OneRow):
print('ingedient name:', child.ids.ing1.text)
print('quantity:', child.ids.quant1.text)
print('')
class WMan(ScreenManager):
pass
class OneRow(BoxLayout):
inst_count = NumericProperty(0)
count = 0
def __init__(self, **kwargs):
OneRow.count += 1
self.inst_count = OneRow.count
super(OneRow, self).__init__(**kwargs)
def get_index(self):
par = self.parent
if par is None:
return None
index = 0
for index in range(len(par.children) - 1, -1, -1):
child = par.children[index]
if child == self:
return index
kv_str = '''
#:import Factory kivy.factory.Factory
WMan:
AddWindow:
id: add
#:set row_height 30
<OneRow>:
orientation: 'horizontal'
size_hint_y: None
height: row_height
TextInput:
id: ing1
multiline: False
size_hint: .65, None
height: row_height
TextInput:
id: quant1
multiline: False
text: str(root.inst_count)
size_hint: .25, None
height: row_height
Button:
id: addIng
text: "+"
size_hint: .1, None
height: row_height
on_release: app.root.ids.add.ids.ingsGrid.add_widget(Factory.OneRow(), index=root.get_index())
<AddWindow>:
name: 'add'
ingsGrid: ingsGrid
saveButton: saveButton
StackLayout:
id: ingsGrid
size_hint: .9, None
height: self.minimum_height
orientation: 'lr-tb'
spacing: '5sp', '5sp'
OneRow:
Button:
id: saveButton
text: "Save"
size_hint: .3, None
on_release:
root.saveRec(self)
'''
# kv = Builder.load_file("test.kv")
kv = Builder.load_string(kv_str)
class TestApp(App):
def build(self):
return kv
if __name__ == "__main__":
TestApp().run()
I used Builder.load_string() instead of load_file() just for my own convenience.
I have created a class named OneRow that holds a single row from the ingredients list, and the + Button now just adds an instance of that class. The get_index() method of that class is only used to position new ingredients below the row with the Button that was pressed. And the other code in that class is just to add some identifying info. If those things are not important to you, you can eliminate the OneRow class definition from the python, and just replace <OneRow>: in the kv with <OneRow#BoxLayout>: and where the OneRow is added you can just set index=1.
Question:
How do you use an On-Press event to change screen for dynamically created buttons in python, and without using KV language?
Goal:
Be able to navigate to new screen from clicking on dynamically created button,
[without needing to create button in Kivy, and still getting to use Screenmanager in both Python and Kivy (not sure if you have to stick with either Python or Kivy throughout entire program?]
Things I've already tried:
Using button_share.bind(on_press = self.changer), then this:
def changer(self,*args):
ScreenManager()
screenmanager.current = 'MainScreen'
But I get the error ScreenManagerException: No Screen with name "MainScreen".
Suspicion:
I think this is because I'm creating a new instance of ScreenManager, instead of referencing the existing one. To combat this issue, I considered instantiating Screenmanager() in the App class, then referencing that instantiation in the my button def changer(self, *args) method, but that's useless if it's not the same ScreenManager I'm actually using for all my screens. And those are all defined in KV language. I wouldn't be able to switch them all over without a substantial amount of effort.
Using:
button_share.bind(on_press=partial(app.sm.setter('current'), (app.sm, "MainScreen")))`
But the error I get here is ValueError: ScreenManager.current accept only str
Below is a fully runnable example:
Note: In this example, I want to click 'Continue Editing' button, then click on 'Test 1', 'Test 2' or 'Test 3' button and have it take me to another screen.
Python Code:
from kivy.app import App
# kivy.require("1.10.0")
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.clock import Clock
from kivy.uix.widget import Widget
#from kivy.base import runTouchApp
from kivy.properties import StringProperty, ObjectProperty, NumericProperty
from functools import partial
class ScrollableLabelDataEntryInstructions(BoxLayout):
pass
class NewGarageScreen(Screen):
pass
class ContinueEditingScreen(Screen):
pass
class GarageNameBoxLayout(BoxLayout):
box_share2 = ObjectProperty()
sm = ScreenManager()
def __init__(self, **kwargs):
super(GarageNameBoxLayout, self).__init__(**kwargs)
self.orientation = "vertical"
Clock.schedule_interval(self.create_button, 5)
def create_button(self, *args):
self.box_share2.clear_widgets()
app = App.get_running_app()
#put GarageNameStartList data into app class, then pull from it in this class
top_button_share = 1.1
color = (.4, .4, .4, 1)
for i in range(len(app.GarageNameStartList)):
top_button_share -= .4
id_ = app.GarageNameStartList[i]
button_share = Button(background_normal='',
background_color = color,
id = id_,
pos_hint = {"x": 0, "top": top_button_share},
size_hint_y = None,
height = 60,
font_size = 30,
text = app.GarageNameStartList[i])
button_share.bind(on_press = self.changer)
#button_share.bind(on_press=partial(app.sm.setter('current'), (app.sm, "MainScreen")))
self.box_share2.add_widget(button_share)
def changer(self,*args):
ScreenManager()
#app = App.get_running_app()
screenmanager.current = 'MainScreen'
class BackHomeWidget(Widget):
pass
class MainScreen(Screen):
pass
class AnotherScreen(Screen):
pass
class ScreenManagement(ScreenManager):
pass
presentation = Builder.load_file("example_on_press.kv")
class MainApp(App):
GarageNameStartList = ["Test1", "Test2", "Test3"]
def Update_GarageNameStartList(self, *args):
self.GarageNameStartList = ["Test1", "Test2", "Test3"]
def build(self):
return presentation
if __name__ == "__main__":
MainApp().run()
KV Code:
#: import FadeTransition kivy.uix.screenmanager.FadeTransition
ScreenManagement:
transition: FadeTransition()
MainScreen:
AnotherScreen:
NewGarageScreen:
ContinueEditingScreen:
<SmallNavButton#Button>:
font_size: 32
size: 125, 50
color: 0,1,0,1
<MedButton#Button>:
font_size: 30
size_hint: 0.25, 0.1
color: 0,1,0,1
<BackHomeWidget>:
SmallNavButton:
on_release: app.root.current = "main"
text: "Home"
pos: root.x, root.top - self.height
<MainScreen>:
name: "main"
FloatLayout:
MedButton:
on_release: app.root.current = "edit"
text: "Edit"
pos_hint: {"x":0.3728, "top": 0.4}
<AnotherScreen>:
name: "edit"
BackHomeWidget:
SmallNavButton:
on_release: app.root.current = "main"
text: "Back"
pos: root.x, root.top - (2.25*(self.height))
FloatLayout:
MedButton:
on_release: app.root.current = "continueediting"
text: "Continue Editing"
pos_hint: {"x":0.25, "top": 0.6}
MedButton:
on_release: app.root.current = "newgarage"
text: "Create New"
pos_hint: {"x":0.3728, "top": 0.4}
<NewGarageScreen>:
name: "newgarage"
BackHomeWidget:
SmallNavButton:
on_release: app.root.current = "edit"
text: "Back"
pos: root.x, root.top - (2.25*(self.height))
FloatLayout:
MedButton:
text: "1. Groundfloor"
pos_hint: {"x":0, "top": 0.6}
<GarageNameBoxLayout>:
box_share2: box_share2
ScrollView:
GridLayout:
id: box_share2
cols: 1
size_hint_y: None
size_hint_x: 0.5
spacing: 5
padding: 130
height: self.minimum_height
canvas:
Color:
rgb: 0, 0, 0
Rectangle:
pos: self.pos
size: self.size
<ContinueEditingScreen>:
name: "continueediting"
GarageNameBoxLayout:
BackHomeWidget:
SmallNavButton:
on_release: app.root.current = "edit"
text: "Back"
pos: root.x, root.top - (2.25*(self.height))
Your code can be improved in the following things:
You do not have to create box_share2 in the .py since you're creating it in the .kv
When you use sm = ScreenManager() you are creating another ScreenManager different from the original, that is not necessary.
It is not necessary to use range and len, make your code less readable, you just have to iterate.
If we look at the structure of the .kv we see that the presentation object is the ScreenManager so you can get it via app.root.
Using the above your code the solution is:
[...]
class GarageNameBoxLayout(BoxLayout):
def __init__(self, **kwargs):
super(GarageNameBoxLayout, self).__init__(**kwargs)
self.orientation = "vertical"
Clock.schedule_interval(self.create_button, 5)
def create_button(self, *args):
self.box_share2.clear_widgets()
app = App.get_running_app()
sm = app.root
#put GarageNameStartList data into app class, then pull from it in this class
top_button_share = 1.1
color = (.4, .4, .4, 1)
for text in app.GarageNameStartList:
top_button_share -= .4
id_ = text
button_share = Button(background_normal='',
background_color = color,
id = id_,
pos_hint = {"x": 0, "top": top_button_share},
size_hint_y = None,
height = 60,
font_size = 30,
text = text)
button_share.bind(on_press=lambda *args: setattr(sm, 'current', "main"))
self.box_share2.add_widget(button_share)
[...]