python kivy app stops functioning after adding widget (on PC) - python

I removed everything superfluous, leaving only what was necessary to reproduce the same behavior.
There is an MD Text Field in which, when entering text, if there are matches, MDDropdownMenu appears with options to choose from. The options are stored in the P_LIST list. If you don't enter text into this Mytextfield, everything works. As soon as you enter the text, the function is triggered, a menu appears, you select. After that, the application does not function.
I determined that this is happening because of the line: self.add_widget(list drop down) # <----------- marked in the code
The menu appears without add_widget, but if you enter more than one letter, a new instance of the ListDropdownValue class is created each time and the menus overlap.
#kivymd 0.104.2
#kivy 2.0.0
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import ObjectProperty
from kivymd.app import MDApp
from kivy.uix.boxlayout import BoxLayout
from kivymd.uix.menu import MDDropdownMenu
kv_str = """
<StartScreen>:
startscreen_textfield_1: textfield_id
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint: 1, 0.5
BoxLayout:
size_hint: 1, 0.5
orientation: "vertical"
BoxLayout:
MDTextField:
id: textfield_id
on_text:
root.open_listdropdown(textfield_id)#
BoxLayout:
MDTextField:
BoxLayout:
MDTextField:
"""
P_LIST = ["ASD", "SDF", "AASD"]
def search_product(prefix):
filtered_list = []
filtered_list = list(filter(lambda l: l.startswith(prefix), P_LIST))
return filtered_list
class MyListDropdownValue(MDDropdownMenu):
def __init__(self, dropdown_list, **kwargs):
super().__init__(**kwargs)
self.dropdown_list_id = dropdown_list
def list_dropdown(self):
if len(self.dropdown_list_id.text) != 0:
prefix = self.dropdown_list_id.text.upper()
filtered_list = search_product(prefix)
menu_items = [{'text':f'{value}',
"height": dp(56),
"viewclass": "OneLineListItem",
"on_release": lambda x= f"{value}": self.set_item(x)}
for value in filtered_list]
self.menu = MDDropdownMenu(
caller=self.dropdown_list_id,
items=menu_items,
width_mult=5,
)
self.menu.open()
def set_item(self, value):
def set_item(interval):
self.dropdown_list_id.text = value
self.menu.dismiss()
Clock.schedule_once(set_item, 0.1)
class StartScreen(BoxLayout):
startscreen_textfield_1 = ObjectProperty()
def open_listdropdown(self, id):
if len(self.children) == 1:
listdropdown = MyListDropdownProduct(id)
self.add_widget(listdropdown)
self.children[0].list_dropdown()
else:
self.children[0].menu.dismiss()
self.children[0].list_dropdown()
kv = Builder.load_string(kv_str)
class Program(MDApp):
def build(self):
self.screen = StartScreen()
return self.screen
def main():
app = Program()
app.run()
if __name__ == "__main__":
main()

Your MyListDropdownValue(MDDropdownMenu) class inherits from a MDDropdownMenu.
Then you make dropdown instance with
openlistdrop_down = MyDropdownValue(id)
Then you add that instance every time you "on_text" with
self.add_widget(listdropdown)
So you are adding multiple dropdowns.
In Kv try changing
on_text:
root.open_listdropdown(textfield_id)
To
on_validate:
root.open_listdropdown(textfield_id)
Then the user will need to hit enter before the list is made instead of with every letter added.

Related

Kivy MD DropDownItem: how to set the item of a dropdownitem into another dropdownitem

I'm trying to developed a simple GUI in Kivy MD / Python. Originally, I modified the example code:
from kivy.lang import Builder
from kivy.metrics import dp
from kivymd.uix.list import OneLineIconListItem
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.dropdownitem import MDDropDownItem
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
MDScreen
MDDropDownItem:
id: drop_item_1
pos_hint: {'center_x': .5, 'center_y': .8}
text: 'FREQUENCY_1'
on_release: app.menu_sampling_rate_1.open()
MDDropDownItem:
id: drop_item_2
pos_hint: {'center_x': .5, 'center_y': .4}
text: 'FREQUENCY_2'
on_release: app.menu_sampling_rate_2.open()
'''
class MainApp(MDApp):
sampling_rate = ['300 Hz', '200 Hz', '100 Hz']
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
self.menu_sampling_rate_1, self.sampling_rate_items_1 = self.Create_DropDown_Widget(self.screen.ids.drop_item_1, self.sampling_rate)
self.menu_sampling_rate_2, self.sampling_rate_items_2 = self.Create_DropDown_Widget(self.screen.ids.drop_item_2, self.sampling_rate)
def Create_DropDown_Widget(self, drop_down_item, item_list):
items_collection = [
{
"viewclass": "OneLineListItem",
"text": item_list[i],
"height": dp(56),
"on_release": lambda x = item_list[i]: self.Set_DropDown_Item(drop_down_item, menu, x),
} for i in range(len(item_list))
]
menu = MDDropdownMenu(caller=drop_down_item, items=items_collection, width_mult=2)
menu.bind()
return menu, items_collection
def Set_DropDown_Item(self, dropDownItem, dropDownMenu, textItem):
dropDownItem.set_item(textItem)
dropDownMenu.dismiss()
def build(self):
return self.screen
if __name__ == '__main__':
MainApp().run()
I tried to slightly modify it using a class View in which all methods and properties related to the interface are included.
from kivy.lang import Builder
from kivy.metrics import dp
from kivymd.uix.list import OneLineIconListItem
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.dropdownitem import MDDropDownItem
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
<View>:
orientation: vertical
MDDropDownItem:
id: drop_item_1
pos_hint: {'center_x': .5, 'center_y': .8}
text: 'FREQUENCY_1'
on_release: root.menu_sampling_rate_1.open()
MDDropDownItem:
id: drop_item_2
pos_hint: {'center_x': .5, 'center_y': .4}
text: 'FREQUENCY_2'
on_release: root.menu_sampling_rate_2.open()
'''
class View(MDBoxLayout):
sampling_rate = ['300 Hz', '200 Hz', '100 Hz']
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.menu_sampling_rate_1, self.sampling_rate_items_1 = self.Create_DropDown_Widget(self.ids.drop_item_1, self.sampling_rate)
self.menu_sampling_rate_2, self.sampling_rate_items_2 = self.Create_DropDown_Widget(self.ids.drop_item_2, self.sampling_rate)
def Create_DropDown_Widget(self, drop_down_item, item_list):
items_collection = [
{
"viewclass": "OneLineListItem",
"text": item_list[i],
"height": dp(56),
"on_release": lambda x = item_list[i]: self.Set_DropDown_Item(drop_down_item, menu, x),
} for i in range(len(item_list))
]
menu = MDDropdownMenu(caller=drop_down_item, items=items_collection, width_mult=2)
menu.bind()
return menu, items_collection
def Set_DropDown_Item(self, dropDownItem, dropDownMenu, textItem):
dropDownItem.set_item(textItem)
dropDownMenu.dismiss()
class MainApp(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.view = View()
def build(self):
return self.view
if __name__ == '__main__':
MainApp().run()
My questions are:
In this second version, with the View class, why I get the AttributeError: 'super' object has no attribute 'getattr'?
How to set the item of a dropdownitem equal to the current item of the second dropdownitem and viceversa? In this way when the user selects a new item into a dropdownitem, this new selection appears also into the other dropdownitem.So the two dropdownitem show the same current item.
How to set the width of a dropdownitem equal to dp(80)? The approach based on size_hint_x and width seems to not work.
Is there a way to enable/disable a dropdownitem? The property active seems to not work.
Thank you in advance for any suggestions.
In this second version, with the View class, why I get the
AttributeError: 'super' object has no attribute 'getattr'?
Because at the moment of executing init of class View(MDBoxLayout) you dont have anything in self.ids
If you try it in debug you will see it:
Solution here to create widgets after Kivy created own objects, in you first example you do it in MainApp class and thats ok.
How to set the item of a dropdownitem equal to the current item of the
second dropdownitem and viceversa? In this way when the user selects a
new item into a dropdownitem, this new selection appears also into the
other dropdownitem.So the two dropdownitem show the same current item.
If you want to change another widget(s) - just use ids to change them after you changed first one. Example of possible implementation:
def Set_DropDown_Item(self, dropDownMenu, textItem):
wanted_dropdowns = ('drop_item_1', 'drop_item_2') # ids of widgets you want to modify
for dropdown in wanted_dropdowns:
self.screen.ids[dropdown].set_item(textItem)
dropDownMenu.dismiss()
How to set the width of a dropdownitem equal to dp(80)? The approach
based on size_hint_x and width seems to not work.
Widget size changes to fit text, so maybe font_size: "80dp" is what you want.
Is there a way to enable/disable a dropdownitem? The property active
seems to not work.
Use disabled property. In python:
def disable_widget(self, widget):
widget.disabled = True
In kv
disabled: 'True'

Instance of a custom class assigned to Kivy ObjectProperty doesn't update GUI

I am trying to use Kivy Properties to refresh GUI after property is changed. However, when I try to assign an instance of custom class to property it doesn't work. After modifying property GUI is not refreshed with new value.
MWE:
python file:
from typing import List
from kivy.app import App
from kivy.properties import ObjectProperty
from kivy.uix.screenmanager import Screen
class SomeStruct:
def __init__(self, atr1: List[int], atr2: str):
self.atr1 = atr1
self.atr2 = atr2
class Root(Screen):
prop1 = ObjectProperty(None)
prop2 = ObjectProperty(None)
def __init__(self, **kw):
super().__init__(**kw)
self.prop1 = SomeStruct(atr1=[1, 2, 3], atr2="test1")
self.prop2 = "control1"
def modify1(self):
self.prop1.atr1 = [3, 2, 1]
def modify2(self):
self.prop1.atr2 = "test2"
def modify3(self):
self.prop2 = "control2"
class Mwe(App):
pass
if __name__ == '__main__':
Mwe().run()
kv file:
Root:
BoxLayout:
orientation: 'vertical'
Button:
text: str(root.prop1.atr1)
on_press: root.modify1()
Button:
text: root.prop1.atr2
on_press: root.modify2()
Button:
text: root.prop2
on_press: root.modify3()
Button 1 and 2 change classe's varaibles but text on these buttons is not updated. Button 3 works as it should.
Any help would be aprreciated, I really want to avoid making every varaible from class a separate property.

Kivy - Update a label with sensor data?

New to kivy, and OOP.
I'm trying to update a label in kivy with data I pull from a temp sensor. The code that pulls in the sensor data is in labeltempmod. I created a function getTheTemp() that is called every second. In the function I try to assign the text of the label via Label(text=(format(thetemp)), font_size=80). The program ignores this. What am I doing wrong here?
#This is a test to see if I can write the temp to label
import labeltempmod
import kivy
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.label import Label
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.textinput import TextInput
from kivy.uix.boxlayout import BoxLayout
def getTheTemp(dt):
thetemp = labeltempmod.readtemp()
Label(text=(format(thetemp)), font_size=80)
print thetemp
class LabelWidget(BoxLayout):
pass
class labeltestApp(App):
def build(self):
# call get_temp 0.5 seconds
Clock.schedule_interval(getTheTemp, 1)
return LabelWidget()
if __name__ == "__main__":
labeltestApp().run()
Here is the kivy language file:
<LabelWidget>:
orientation: 'vertical'
TextInput:
id: my_textinput
font_size: 80
size_hint_y: None
height: 100
text: 'default'
FloatLayout:
Label:
id: TempLabel
font_size: 150
text: 'Temp Test'
Thanks.
Sorry but you never update something You are just creating another label
Try this:
class LabelWidget(BoxLayout):
def __init__(self, **kwargs):
super(LabelWidget, self).__init__(**kwargs)
Clock.schedule_interval(self.getTheTemp, 1)
def getTheTemp(self, dt):
thetemp = labeltempmod.readtemp()
self.ids.TempLabel.text = thetemp
print thetemp
class labeltestApp(App):
def build(self):
return LabelWidget()
if __name__ == "__main__":
labeltestApp().run()
Update : for your last request, I think the best way to do that is:
...
class LabelWidget(BoxLayout):
def __init__(self, **kwargs):
super(LabelWidget, self).__init__(**kwargs)
self.Thetemp = None
Clock.schedule_interval(self.getTheTemp, 1)
def getTheTemp(self, dt):
if self.Thetemp is None:
self.thetemp = labeltempmod.readtemp()
else:
self.thetemp = labeltempmod.readtemp(self.theTemp)
self.ids.TempLabel.text = str(self.thetemp)

Kivy references across screens

I want to access root widgets ids from other rootwidgets, but I can't seem to fully grasp how referencing works in Kivy and using a ScreenManager with different screens makes it even harder for me.
I want to achieve the following:
Edit: single file version
(This code assumes you're going to build a complex app, so I don't want to load all code at startup. Hence the kv_strings are loaded when switching screen, and not put into kv code of the ScreenManager. Code is based on the Kivy Showcase.)
Code main.py, Edit 2: working code (see answer why)
#!/usr/bin/kivy
# -*- coding: utf-8 -*-
from kivy.app import App
from kivy.uix.screenmanager import Screen
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.clock import Clock
from kivy.properties import StringProperty, ObjectProperty
kv_foo = '''
<FooScreen>:
id: fooscreen_id
BoxLayout:
id: content
orientation: 'vertical'
spacing: '20dp'
padding: '8dp'
size_hint: (1, 1)
BoxLayout:
orientation: 'vertical'
Label:
id: important_text
size_hint_y: 0.3
text: app.imp_text
Button:
id: magic_change
size_hint_y: 0.3
text: "Change text above to text below (after screen switch)"
on_press: app.change_text()
ScreenManager:
id: sm
on_current_screen:
idx = app.screen_names.index(args[1].name)
'''
class FooScreen(Screen):
# 'content' refers to the id of the BoxLayout in FooScreen in foo.kv
def add_widget(self, *args):
if 'content' in self.ids:
return self.ids.content.add_widget(*args)
return super(FooScreen, self).add_widget(*args)
class FooApp(App):
imp_text = StringProperty("Should change to text from id: magic_text")
screen_magic = ObjectProperty()
magic_layout = ObjectProperty()
def build(self):
self.title = 'Foo'
self.root = root = Builder.load_string(kv_foo)
# Trying stuff with References
self.sm = self.root.ids.sm # ScreenManager
# Setting up screens for screen manager
self.screens = {}
self.available_screens = [kv_mainmenu, kv_magic]
self.screen_names = ['MainMenu', 'Magic']
self.go_screen(0)
# Go to other screen
def go_screen(self, idx):
print("Change MainScreen to: {}".format(idx))
self.index = idx
# Go to not main menu
if idx == 0:
self.root.ids.sm.switch_to(self.load_screen(idx), direction='right')
# Go to main menu
else:
self.root.ids.sm.switch_to(self.load_screen(idx), direction='left')
# Load kv files
def load_screen(self, index):
if index in self.screens:
return self.screens[index]
screen = Builder.load_string(self.available_screens[index])
self.screens[index] = screen
# if index refers to 'Magic' (kv_magic), create reference
if index == 1:
Clock.schedule_once(lambda dt: self.create_reference())
return screen
# Trying to get id's
def create_reference(self):
print("\nrefs:")
# Get screen from ScreenManager
self.screen_magic = self.sm.get_screen(self.screen_names[1])
# screen.boxlayout.magiclayout
self.magic_layout = self.screen_magic.children[0].children[0]
def change_text(self):
# Get text from id: magic_text
if self.magic_layout:
self.imp_text = self.magic_layout.ids['magic_text'].text
kv_mainmenu = '''
FooScreen:
id: mainm
name: 'MainMenu'
Button:
text: 'Magic'
on_release: app.go_screen(1)
'''
kv_magic = '''
<MagicLayout>
id: magic_layout
orientation: 'vertical'
Label:
id: magic_text
text: root.m_text
FooScreen:
id: magic_screen
name: 'Magic'
MagicLayout:
id: testmagic
'''
class MagicLayout(BoxLayout):
m_text = StringProperty("Reference between widgets test")
if __name__ == '__main__':
FooApp().run()
Question
How can I set up proper references that the button "Change text above..." can retrieve magic_text.text ("Reference between widgets test") and change self.imp_text to magic_text.text?
I found a way to reference to Kivy widgets not loaded at the startup of the app without using globals. Thanks #inclement for ScreenManager.get_screen().
I had to add the following code:
class FooApp(App):
screen_magic = ObjectProperty()
magic_layout = ObjectProperty()
...
# Trying to get id's
def create_reference(self):
print("\nrefs:")
# Get screen from ScreenManager
self.screen_magic = self.sm.get_screen(self.screen_names[1])
# screen.boxlayout.magiclayout
self.magic_layout = self.screen_magic.children[0].children[0]
def change_text(self):
# Get text from id: magic_text
if self.magic_layout:
self.imp_text = self.magic_layout.ids['magic_text'].text
self.screen_magic is assigned the screen I need (<FooScreen>) and self.magic_layout is assigned the widget I need (<MagicLayout>). Then I can use the ids from <MagicLayout> to access the Label magic_text's text.
(For full code see updated question)

Kivy multiple selection with checkboxes

I'm trying to create a view using Kivy that has a list of options that are all selected by default, and the user can choose to deselect some entries (by clicking on the checkbox or anywhere on the row).
Clicking on the label part of the row item works, but I noticed that clicking on the checkbox doesn't change the selection which I can't work out how to solve (I tried a few different state bindings, I left them commented out in the example code)
Here is a quick example showing what I've tried.
from kivy.app import App
from kivy.properties import StringProperty, ListProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.selectableview import SelectableView
from kivy.uix.togglebutton import ToggleButtonBehavior
from kivy.adapters.models import SelectableDataItem
from kivy.lang import Builder
Builder.load_string("""
#: import ListAdapter kivy.adapters.listadapter.ListAdapter
#: import Factory kivy.factory.Factory
<MyListItem>:
height: 50
on_state: root.is_selected = args[1] == "down"
state: "down" if root.is_selected else "normal"
BoxLayout:
spacing: 10
CheckBox:
on_state: root.is_selected = args[1] == "down"
state: "down" if root.is_selected else "normal"
# on_state: root.state = args[1]
# state: root.state
Label:
text: root.name
<Page>:
orientation: "vertical"
ListView:
id: LV
adapter: ListAdapter(data=root.data, cls=Factory.MyListItem, args_converter=root.args_converter, selection_mode="multiple", propagate_selection_to_data=True)
Button:
size_hint_y: None
text: "print selection"
on_press: print(LV.adapter.selection)
""")
class MyListItem(ToggleButtonBehavior, SelectableView, BoxLayout):
name = StringProperty()
def __repr__(self):
return "%s(name=%r)" % (type(self).__name__, self.name)
def on_state(self, me, state):
print me, state
if state == "down":
self.select()
else:
self.deselect()
# self.is_selected = state == "down"
class DataItem(SelectableDataItem):
def __init__(self, name, **kwargs):
super(DataItem, self).__init__(**kwargs)
self.name = name
def __repr__(self):
return "%s(name=%r, is_selected=%r)" % (type(self).__name__, self.name, self.is_selected)
class Page(BoxLayout):
data = ListProperty()
def __init__(self, **kwargs):
super(Page, self).__init__(**kwargs)
self.data = [DataItem("Item {}".format(i), is_selected=True) for i in range(10)]
def args_converter(self, index, data_item):
return {
"index": index,
"name": data_item.name,
}
class ExampleApp(App):
def build(self):
return Page()
if __name__ == "__main__":
ExampleApp().run()
I'm using Kivy v1.9.1-dev
Edit: I worked out how to get all the entries pre-selected, I've updated the code and took that part of the question out.
Just in case someone else has the the question I point to the right url:
You should consider the new RecycleView, which has all the functionality you request. Look here for a sample: Kivy: alternative to deprecated features

Categories