I am using Kivy in an application where several widgets are used to modify/edit a set of data (the model). When some data is changed in one widget others need to be informed that something has changed so they can update their view.
To do this I use Kivy Properties to mirror the model data, and this works fine.
However the first time the Kivy Properties are set, when reading in the model data, this generates alot of unnecessary updates in the UI. I would like there to be a way to update a Kivy Property without generating update events.
I have looked in the documentation (https://kivy.org/docs/api-kivy.properties.html) and in the code (https://github.com/kivy/kivy/blob/master/kivy/properties.pyx) but I have not been able to find anything to do this.
How can this be solved?
I have done a very simple example app to show how the code is organized and where the problem occurs.
#! /usr/bin/env python
import kivy
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.listview import ListView
from kivy.adapters.listadapter import ListAdapter
from kivy.uix.listview import ListItemButton
from kivy.properties import StringProperty
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
model_data = ["Data1","Data2","Data3"]
class TestApp(App):
def build(self):
tl = TestLayout()
tl.read_data(model_data)
return tl
class TestLayout(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.test_list = TestList()
self.test_edit = TestEdit()
self.add_widget(self.test_list)
self.add_widget(self.test_edit)
self.test_list.adapter = ListAdapter(data=[],
cls=ListItemButton,
selection_mode='single')
self.test_list.adapter.bind(selection=self.select_data)
self.test_edit.bind(data=self.update_list)
def read_data(self, data_list):
self.test_list.init_list(data_list)
def select_data(self, list_adapter, selection):
if len(selection) > 0:
data = selection[0].text
self.test_edit.init_data(data)
def update_list(self, test_edit, data):
self.test_list.adapter.selection[0].text = data
class TestList(ListView):
def init_list(self, data_list):
self.adapter.data = [str(data) for data in data_list]
class TestEdit(BoxLayout):
data = StringProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.orientation = "vertical"
self.text_input=TextInput()
self.add_widget(self.text_input)
btn_update = Button(text="Update data")
self.add_widget(btn_update)
btn_update.bind(on_press=self.update_data)
def init_data(self, data):
# Setting the data property the first time triggers an unnecessary update.
# How can this be prevented?
self.data = data
self.text_input.text = data
def update_data(self, btn):
self.data = self.text_input.text
if __name__ == "__main__":
app = TestApp()
app.run()
Also as you can see in the example code, an unnecessary update is dispatch whenever you select a new item in the list view.
Have I overlooked something obvious? Or is this not supported by Kivy?
Thanks!
Related
I'm new to Python, and especially new to Kivy.
I'm sure that whatever I'm doing is a simple fix, but I just cannot figure it out for the life of me.
I've been doing this all in a Python file, with no my.kv.
What I'm trying to do is call a function upon entering the first screen of my app, but when I do this it just gives me a blank screen.
Sorry if my code is an absolute mess.
This is my code:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.lang import Builder
from kivy.uix.gridlayout import GridLayout
import requests
import json
Builder.load_string("""
<Manager>:
BuildScreen:
SubmitScreen:
<BuildScreen>:
name: 'page1'
on_enter: app.BuildAnswer()
<SubmitScreen>:
name: 'page2'
on enter: app.SubmitAnswer()
GridLayout:
cols:1
row_force_default:True
row_default_height:40
Button:
text:"Return"
on_release: root.manager.current = 'page1'
""")
class MainWidget(Widget):
pass
class Manager(ScreenManager):
pass
class BuildScreen(Screen):
pass
class SubmitScreen(Screen):
pass
class TheLabApp(App):
def __init__(self,**kwargs):
super(TheLabApp, self).__init__(**kwargs)
def BuildAnswer(self):
layout = GridLayout(cols=1, row_force_default=True, row_default_height=40)
self.spell = TextInput(hint_text = "Enter Spell", multiline=False)
button = Button(text="Get Spell", on_release=self.SubmitAnswer)
layout.add_widget(self.spell)
layout.add_widget(button)
return layout
def SubmitAnswer(self):
user_input = self.spell.text
#making input into url ready thingy
making_string = ''.join(str(x) for x in user_input)
x = '-'.join(making_string.split())
url = requests.get('https://www.dnd5eapi.co/api/spells/' + x)
#making it look pretty
pretty_spells = json.dumps(url.json(), indent=2)
#making it so I can get values from json
resp = json.loads(pretty_spells)
print(resp['name'])
print(resp['range'])
#the rest is just printing more of the spell's information
def build(self):
sm=ScreenManager()
sm.add_widget(BuildScreen(name="page1"))
sm.add_widget(SubmitScreen(name="page2"))
return sm
Any and all help would be incredibly appreciated, as I've been trying to find a solution for a couple days now.
A couple problems with your code.
First, the BuildAnswer() method creates and returns a layout, but returning something from an on_enter method has no effect. If you want that layout to appear in the BuildScreen, you must explicitly add that layout to the BuildScreen instance.
Second, defining the BuildAnswer() method in the TheLabApp class makes it difficult to access the BuildScreen instance. This is because the on_enter method is triggered very early in the process (before the root of the App is assigned).
I suggest moving the BuildAnswer() method to the BuildScreen class, and calling add_widget() to actually add the created layout to the BuildScreen instance. To do that, start by modifying your kv:
<BuildScreen>:
name: 'page1'
on_enter: self.BuildAnswer() # reflects new location of BuildAnswer()
And modifying your python code:
class BuildScreen(Screen):
def BuildAnswer(self):
layout = GridLayout(cols=1, row_force_default=True, row_default_height=40)
self.spell = TextInput(hint_text="Enter Spell", multiline=False)
button = Button(text="Get Spell", on_release=App.get_running_app().SubmitAnswer) # to access the SubmitAnswer method
layout.add_widget(self.spell)
layout.add_widget(button)
self.add_widget(layout) # add layout to the GUI
# return layout
And remove the BuildAnswer() method from the TheLabApp class.
I could really really need some help with my actually quite simple Python Kivy Problem! I wrote a program that first announces counting to 5 and then should start counting from 1 to 5. The info should be shown in a scrollview-Label. The code roughly does its job but does not update the scrollview step-by-step but all at once after time is elapsed...can anybody please help? Thank you in advance!
import kivy
from kivy.config import Config
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.core.window import Window
from kivy.uix.scrollview import ScrollView
import time
kivy.require("2.0.0")
Config.set('kivy', 'keyboard_mode', 'systemandmulti')
class MainMenu(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.cols = 1
self.rows = 2
self.infowindow = ScrollableInfo(height=Window.size[1]*0.8, size_hint_y=None)
self.add_widget(self.infowindow)
self.ButtonCheckConnection = Button(text="Start Counting to 5")
self.ButtonCheckConnection.bind(on_press=self.countingtofive)
self.add_widget(self.ButtonCheckConnection)
def countingtofive(self, *_):
self.infowindow.update_scrollview(f"Counting to 5 is going to start in 3 seconds")
time.sleep(3)
countingmaximum = 5
for i in range(countingmaximum):
currentnumber = i+1
self.infowindow.update_scrollview(str(currentnumber))
time.sleep(1)
class ScrollableInfo(ScrollView):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.layout = GridLayout(cols=1, size_hint_y=None)
self.add_widget(self.layout)
self.connectioninfo_history = Label(size_hint_y=None, markup=True)
self.layout.add_widget(self.connectioninfo_history)
def update_scrollview(self, newinfo):
self.connectioninfo_history.text += '\n' + newinfo
self.layout.height = self.connectioninfo_history.texture_size[1]+15
self.connectioninfo_history.height = self.connectioninfo_history.texture_size[1]
self.connectioninfo_history.text_size = (self.connectioninfo_history.width*0.98, None)
class Counting(App):
def build(self):
self.screen_manager = ScreenManager()
self.mainmenu_page = MainMenu()
screen = Screen(name="MainMenu")
screen.add_widget(self.mainmenu_page)
self.screen_manager.add_widget(screen)
return self.screen_manager
if __name__ == "__main__":
counting_app = Counting()
counting_app.run()
The problem is that you are running your countingtofive() method on the main thread. Since Kivy uses the main thread to update the GUI, it cannot do that until you release the main thread (by returning from the countingtofive() method). That is why you never see anything until that method completes.
To fix that, run the countingtofive() method in another thread, like this:
def start_counting_thread(self, *args):
Thread(target=self.countingtofive, daemon=True).start()
And change the Button to bind to the start_counting_thread() method:
self.ButtonCheckConnection.bind(on_press=self.start_counting_thread)
And one minor change to the update_scrollview() method (add the #mainthread decorator):
#mainthread
def update_scrollview(self, newinfo):
The #mainthread decorator forces the decorated method to be run on the main thread. The same can be accomplished by using Clock.schedule_once(), but the decorator is easier. Just the piece of the code that actually updates the GUI must be run on the main thread. Generally, you should try to avoid long running methods on the main thread.
I am trying to create a simple programe that animates my label widget to desired position upon clicking on the kivy window. Just trying to experiment with inheritance to access all the elements in the parent element.
ani is the parent class and transit is the child class. I am trying to access the label attribute in transit via inheritance. But this throws error.
from kivy.app import App
from kivy.uix.label import Label
from kivy.animation import Animation
from kivy.uix.widget import Widget
class transit(ani,Widget):
def __init__(self,**kwargs):
ani.__init__(self,**kwargs)
def on_touch_down(self,touch):
val = 5
print(touch.x,touch.y)
self.val +=10
animation = Animation(x = touch.x,y =touch.y,font_size=self.val,d=2,t='in_out_quad')
animation.start(self.parent.label)
class ani(App):
def __init__(self,**kwargs):
self.label = Label(text='increase')
def build(self):
return transit()
root = ani()
root.run()
I believe you are trying to use inheritance with the wrong class. If you want to move a Label, you need to put the Label in a container and make sure the size of the Label does not fill the container. Since the position of the Label is controlled by its container, you need to use inheritance with the container. Here is a simple code, similar to yours, that uses a FloatLayout as the container:
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.animation import Animation
class MyFloatLayout(FloatLayout):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
val = 5
label = App.get_running_app().label
animation = Animation(x = touch.x,y =touch.y,font_size=val,d=2,t='in_out_quad')
animation.start(label)
return super(MyFloatLayout, self).on_touch_down(touch)
class ani(App):
def build(self):
root = MyFloatLayout()
self.label = Label(text='increase', size_hint=(None, None), size=(100,40))
root.add_widget(self.label)
return root
if __name__ == '__main__':
ani().run()
This code uses inheritance to define a new MyFloatLayout class that adds a new on_touch_down() method that does the animation. Note that the new on_touch_down() also calls super(MyFloatLayout, self).on_touch_down(touch). Unless there is a specific reason for not doing that, you normally want to call the super method in an inherited method.
I am trying to implement the graphical part of a game that I wrote using kivy. Since I am new to kivy I went through ts documentation which I found some programming samples that I studied and used. In one of the samples, I get:
TypeError: object.__init__() takes no parameters
Here is the code:
from kivy.app import App;
from kivy.uix.label import Label;
from kivy.uix.gridlayout import GridLayout;
from kivy.uix.textinput import TextInput;
class LoginScreen(GridLayout):
def __init__(self, **kwargs):
#super(LoginScreen, self).__new__(**kwargs) # == super(LoginScreen, self).__init__(**kwagrs)
#GridLayout.__init__()
super().__init__(**kwargs);
self.cols = 2 # The colors
# Creating the Object for username and then adding it into Canvans
self.add_widget(Label(text="Username: "))
self.username = TextInput(multiline=False)
self.add_widget(self.username)
# Creating the Object for password and then adding it into Canvans
self.add_widget(None,Label(Text="password:"))
self.password = TextInput(password=True,multiline=False)
self.add_widget(self.password)
class SimpleKivy(App):
def build(self):
return LoginScreen();
if __name__ == "__main__":
SimpleKivy().run();
The error is on this line:
self.add_widget(None,Label(Text="password:"))
You don't need to use None, and change Text= to text= because kivy's keyword args are all lowercase. So change it to:
self.add_widget(Label(text="password:"))
Also, have a look at Kv language it is useful for building apps with kivy.
I am trying to get the textinput widget to pass text into the callback function that makes a label with the text when called by the printbutton, should be fairly simple when you think about it. But I have a habit of not seeing the wood for the trees. Anyhoo, if anyone can figure this out then code it up :P
import kivy
kivy.require('1.5.1')
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
class kivyentrywidget(GridLayout):
def __init__(self, **kwargs):
super(kivyentrywidget, self).__init__(**kwargs)
self.cols = 2
self.add_widget(Label(text='What do you want to print?'))
self.text_input = TextInput(multiline=False)
self.add_widget(self.text_input)
self.printbutton = Button(text='Print')
self.printbutton.bind(on_press=callback)
self.add_widget(self.printbutton)
def callback(self):
return Label(text=self.text_input.text)
class Firstapp(App):
def build(self):
return kivyentrywidget()
if __name__ == '__main__':
Firstapp().run()
def callback(self,evt=None): #not sure if kivy sends event info so added optional arg just in case
return self.add_widget(Label(text=self.text_input.text))
maybe ... not overly familiar with kivy but i think that would do it ..
also
self.printbutton.bind(on_press=self.callback)
should fix your other problem