Accessing TextInput data in kivy without using ids - python

here i've made a grid full of inputboxes using for-loop and added them to a dictionary to access later since i couldn't set id's in the python file. Now i'm trying to make a function that will make basic calculations based on the inputbox's data but i do not know how i can pass particular inputboxes in that function without using id's. Also the on_text function isn't working as i expected. I'm sure i'm missing something big here. Any guidance is appreciated.
from kivy.uix.textinput import TextInput
from kivy.uix.label import Label
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.stacklayout import StackLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.button import Button
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import StringProperty
Builder.load_file("gridtable.kv")
class MyBox(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
pass
class MyGrid(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.cols=6
self.textinputs = {}
for i in range(48):
key = i+1
self.textinputs[key] = TextInput(multiline=False,font_size=dp(30),on_text=self.calc(key))
self.add_widget(self.textinputs[key])
def calc(self,key):
print(self.textinputs[key])
class MyApp(App):
def build(self):
return MyBox()
if __name__ == "__main__":
MyApp().run()
<MyBox>:
mygrid:my_grid
orientation: "vertical"
MyGrid:
id: my_grid
size_hint: 1,0.8
BoxLayout:
orientation: "horizontal"
size_hint: 1,0.2
BoxLayout:
orientation: "vertical"
Button:
text: "Expense Total:"
Button:
text: "Revenue Total:"
Button:
text: "Profit:"
font_size: 40
Traceback (most recent call last):
File "c:\Users\jaika\OneDrive\Desktop\python\lil_curry_project\gridtable.py", line 38, in <module>
MyApp().run()
print(self.textinputs[key])
KeyError: 1
#better version of MyGrid()
class MyGrid(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.cols=4
self.textinputs = [0]
self.expense = 0
self.revenue = 0
self.profit = 0
for i in range(16):
t = TextInput(multiline=False,text=str(i+1))
t.bind(on_text_validate=self.calc_profit)
self.textinputs.append(t)
self.add_widget(t)
def calc_profit(self,instance):
index = self.textinputs.index(instance)
if index == 1 or index == 2 or index == 5 or index == 6 or index == 9 or index == 10 or index == 13 or index == 14:
self.expense += int(instance.text)
else:
self.revenue += int(instance.text)
self.profit = self.revenue - self.expense
print(self.profit)

You can use bind to trigger a method call, or use events available in the TextInput (like on_text_validate). Here is a modified MyGrid class that uses both:
class MyGrid(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.cols = 6
self.textinputs = {}
for i in range(48):
key = i + 1
self.textinputs[key] = TextInput(multiline=False, font_size=dp(30), on_text_validate=self.abba)
self.textinputs[key].bind(text=partial(self.calc, key))
print('in init, key =', key)
self.add_widget(self.textinputs[key])
def abba(self, ti_instance):
print('abba, textinput instance =', ti_instance, ', text =', ti_instance.text)
def calc(self, key, ti_instance, text):
print('calc:')
print('key =', key, ', text =', text, ', textinput instance =', ti_instance)
Note that the bind to text gets triggered with every change to the text in a TextInput. The on_text_validate will only be triggered when the user hits Enter.

Related

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

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.

Python/Kivy passing variables

Struggling to pass a variable to kivy window. I have read similar threads all over the place but none of the fixes seem to work for me. Im sure this is simple to someone who knows their way around tiny, unfortunately I don't.
main.py
import kivy
from kivy.uix.togglebutton import ToggleButton
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.app import App
kivy.require('1.10.0')
from phue import Bridge
import nest
b = Bridge('xxx.xxx.x.xxx')
b.connect()
b.get_api()
lights = b.lights
class Controller(GridLayout):
print("launching")
def __init__(self):
super(Controller, self).__init__()
def KitchenSpot1(self,state):
lights[0].name
lights[0].on = state
def update(dt):
if b.get_light(1, 'on')== True:
#print("down") # When this line is commented out I get an continuous accurate update on the status of the light, showing that its working.
return 'down' # This is the part I want passed to the state criteria in the ivy window
else:
#print("up")# When this line is commented out I get an continuous accurate update on the status of the light, showing that its working.
return 'down' # This is the part I want passed to the state criteria in the ivy window
class ActionApp(App):
def build(self):
Clock.schedule_interval(Controller.update, 1.0 / 60.0)
return Controller()
myApp = ActionApp()
myApp.run()
action.kv
<Controller>:
cols: 4
rows: 3
spacing: 10
ToggleButton:
id: KitchenSpot1Toggle
text: "Kitchen Spot 1"
on_press: root.KitchenSpot1(True)
#on_release: root.KitchenSpot1(False)
#state1 = app.update.h
state: Controller.update # This is the part that is throwing up the error.
The error:
11: #on_release: root.KitchenSpot1(False)
12: #state1 = app.update.h
>> 13: state: Controller.update
14:
15:
...
NameError: name 'Controller' is not defined
Thanks in advance to anyone that can help me.
Make update an instance method and use a StringProperty to update state property in your kv:
main.py:
import kivy
kivy.require('1.10.0')
from kivy.app import App
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import StringProperty
from kivy.uix.gridlayout import GridLayout
from kivy.uix.togglebutton import ToggleButton
from phue import Bridge
import nest
b = Bridge('xxx.xxx.x.xxx')
b.connect()
b.get_api()
lights = b.lights
class Controller(GridLayout):
state = StringProperty('normal') # <<<<<<<<<<<<
def __init__(self, **kwargs):
super(Controller, self).__init__(**kwargs)
Clock.schedule_interval(self.update, 1.0 / 60.0)
def KitchenSpot1(self,state):
lights[0].name
lights[0].on = state
def update(self, dt):
if b.get_light(1, 'on'):
self.state = 'down' # <<<<<<<<<<<<
else:
self.state = 'normal' # <<<<<<<<<<<<
class ActionApp(App):
def build(self):
return Controller()
if __name__ == "__main__":
myApp = ActionApp()
myApp.run()
action.kv:
<Controller>:
cols: 4
rows: 3
spacing: 10
state: "normal" # <<<<<<<<<<<<
ToggleButton:
id: KitchenSpot1Toggle
text: "Kitchen Spot 1"
on_press: root.KitchenSpot1(True)
#on_release: root.KitchenSpot1(False)
#state1 = app.update.h
state: root.state # <<<<<<<<<<<<
Here is a more generic simplified answer from the kivy documentation, look for the section called "Keyword arguments and init()" because there are some other ways to do it as well.
The following code passes myvar to the build() method of MyApp. It does this by over-riding the init() of the Kivy App class by a new init() that calls App.init() and then continues with whatever extra initialisation you want. You can then store variables in the MyApp class instances and use them in build().
from kivy.app import App
from kivy.uix.label import Label
myvar = 'Hello Kivy'
class MyApp(App):
def __init__(self, myvar, **kwargs):
super(MyApp, self).__init__(**kwargs)
self.myvar = myvar
def build(self):
widget = Label(text=self.myvar)
return widget
if __name__ == '__main__':
MyApp(myvar).run()

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)

How to trigger an action once on overscroll in Kivy?

I have a ScrollView that's supposed to have an update feature when you overscroll to the top (like in many apps). I've found a way to trigger it when the overscroll exceeds a certain threshold, but it triggers it a lot of times, as the on_overscroll event is triggered on every movement. So is there a way to limit it?
My code looks like this:
from kivy.app import App
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.effects.dampedscroll import DampedScrollEffect
class Effect(DampedScrollEffect):
def on_overscroll(self, *args):
super().on_overscroll(*args)
if self.overscroll < -50:
print('hey')
class TestApp(App):
def build(self):
sv = ScrollView(effect_cls = Effect,
size_hint_y = 0.2)
gl = GridLayout(cols = 1,
size_hint_y = None)
gl.bind(minimum_height = gl.setter('height'))
for i in range(5):
gl.add_widget(Button(text = str(i),
size_hint = (None, None)))
sv.add_widget(gl)
return sv
TestApp().run()
So, as you can see, when the overscroll goes beyond 50, it prints a simple message. But when you actually try it, you'll see that it prints it many times. What I want for it is to trigger an event, stay untriggerable for some time (like a second) and update the content. I've tried messing with boolean flags and Clock, but it didn't work. What could be done here?
I would use a stateful decorator here:
class call_control:
def __init__(self, max_call_interval):
self._max_call_interval = max_call_interval
self._last_call = time()
def __call__(self, function):
def wrapped(*args, **kwargs):
now = time()
if now - self._last_call > self._max_call_interval:
self._last_call = now
function(*args, **kwargs)
return wrapped
class Effect(DampedScrollEffect):
def on_overscroll(self, *args):
super().on_overscroll(*args)
if self.overscroll < -50:
self.do_something()
#call_control(max_call_interval=1)
def do_something(self):
print('hey')
I know this an old question but someone might find it useful
This is a sample from tshirtman's github gist
from threading import Thread
from time import sleep
from kivy.app import App
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.clock import mainthread
from kivy.properties import ListProperty, BooleanProperty
KV = '''
FloatLayout:
Label:
opacity: 1 if app.refreshing or rv.scroll_y > 1 else 0
size_hint_y: None
pos_hint: {'top': 1}
text: 'Refreshing…' if app.refreshing else 'Pull down to refresh'
RecycleView:
id: rv
data: app.data
viewclass: 'Row'
do_scroll_y: True
do_scroll_x: False
on_scroll_y: app.check_pull_refresh(self, grid)
RecycleGridLayout:
id: grid
cols: 1
size_hint_y: None
height: self.minimum_height
default_size: 0, 36
default_size_hint: 1, None
<Row#Label>:
_id: 0
text: ''
canvas:
Line:
rectangle: self.pos + self.size
width: 0.6
'''
class Application(App):
data = ListProperty([])
refreshing = BooleanProperty()
def build(self):
self.refresh_data()
return Builder.load_string(KV)
def check_pull_refresh(self, view, grid):
max_pixel = 200
to_relative = max_pixel / (grid.height - view.height)
if view.scroll_y < 1.0 + to_relative or self.refreshing:
return
self.refresh_data()
def refresh_data(self):
Thread(target=self._refresh_data).start()
def _refresh_data(self):
self.refreshing = True
sleep(2)
self.append_data([
{'_id': i, 'text': 'hello {}'.format(i)}
for i in range(len(self.data), len(self.data) + 50)
])
self.refreshing = False
#mainthread
def append_data(self, data):
self.data = self.data + data
if __name__ == "__main__":
Application().run()

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