Kivy binding to on_property doesn't seem to work - python

Essentially, I have a grid with squares, and I keep track of which squares are occupied with a BooleanProperty on each square. Here's a simplified version of all the places in my code I declare the "occupied" property:
class Board(GridLayout):
def __init__(self):
super().__init__()
self.cols = 4
self.grid = []
self.create_slots()
def create_slots(self):
for i in range(10):
self.grid.append([])
for j in range(4):
temp = Square(i,j, "sideboard")
self.grid[i].append(temp)
self.add_widget(temp)
temp.bind(on_occupied = self.do_a_thing)
def do_a_thing(self):
for square in self.children:
#do a thing
class Square(Button):
def __init__(self, row, col, type):
self.row = row
self.col = col
self.id = str(self.row) + "," + str(self.col)
self.type = type
self.occupied = BooleanProperty(False)
super().__init__()
My goal is to bind the "do_a_thing" method to be called each time the value of the square's "occupied" property changes. Because the Square class is used elsewhere in my app, I don't want to set the callback for on_occupied in the kivy language, and I was hoping to avoid creating a Square sub-class just to change the one binding.
When I run my code, it throws no errors, and I've verified that the "occupied" property does actually change. But the "do_a_thing" method never gets fired. Can anyone tell me what I'm doing wrong?

Note that for a property my_property, the change event is called my_property as well. The callback receives two arguments: instance that fired the event, and new value of the property, as shown in the docs. Also, if the class has a method called on_propertyname, this will be called as well.
Here is a self-contained example that works for me:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.properties import BooleanProperty
class Board(GridLayout):
def __init__(self, **kwargs):
super(Board, self).__init__(**kwargs)
for i in range(10):
self.add_widget(Square())
for square in self.children:
print square
square.bind(occupied=self.do_a_thing)
def do_a_thing(self, *args):
print "hello from {}, new state: {}".format(*args)
for square in self.children:
pass
#do a thing
class Square(Button):
occupied = BooleanProperty(False)
def on_occupied(self, *args):
print "Callback defined in class: from {} state {}".format(*args)
class mApp(App):
def build(self):
return Builder.load_string("""
Board:
cols: 4
rows: 3
<Square>:
on_press: self.occupied = ~self.occupied
""")
mApp().run()

Related

How to observe a property inside a widget to change a readonly AliasProperty outside it?

This sample code
from kivy.app import App
from kivy.properties import AliasProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
class SampleLayout(BoxLayout):
def get_something(self):
return self.sample_widget.width + 30
something = AliasProperty(get_something, bind=['sample_widget.width'], cache=True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.sample_widget = Button(on_release=self.sample_test)
self.add_widget(self.sample_widget)
def sample_test(self, *args):
print(self.something)
class MyApp(App):
def build(self):
return SampleLayout()
MyApp().run()
Returns this error
AttributeError: type object 'SampleLayout' has no attribute 'sample_widget.width'
bcs (I guess) bind is searching for an attribute of the instance and not an attribute of an attribute of the instance... Maybe with getattr() which also doesn't work if using dots in the variable name.
So, is there an API for such bind? or should I bind directly with self.sample_widget.bind(width=idontknow)?
If it is the last case, what is that function idontknow to call the getter of a (cached) readonly property?
Understand also that the code above is an example, I need this functionality for something more complex. I cannot just put print(self.sample_widget.width + 30) under sample_test to get it working.
Thanks a lot.
Answering myself :)
I don't know if this is the better approach, but works.
sample_width = NumericProperty() # an intermediate
...
something = AliasProperty(get_something, bind=['sample_width'], cache=True)
...
# sync the intermediate with the wanted var
self.sample_widget.bind(width=self.setter('sample_width'))
so the example code would be like this:
from kivy.app import App
from kivy.properties import AliasProperty, NumericProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
class SampleLayout(BoxLayout):
def get_something(self):
return self.sample_widget.width + 30
sample_width = NumericProperty() # an intermediate
something = AliasProperty(get_something, bind=['sample_width'], cache=True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.sample_widget = Button(on_release=self.sample_test)
self.add_widget(self.sample_widget)
# sync the intermediate with the wanted var
self.sample_widget.bind(width=self.setter('sample_width'))
def sample_test(self, *args):
print(self.something)
class MyApp(App):
def build(self):
return SampleLayout()
MyApp().run()

Kivy - Access instance created in other Screen

I created some instances of a class (Player class) in my first Screen, and would like to access these instances (and their data) in another Screen. The idea is this:
Player class: instance of this class contains players name and their points. (done)
WelcomeWindow(Screen) class: extract name of players via TextInput + create instances of Player class and assign the names. (done)
ThirdRound(Screen) class: Access names and points of the player instances, display the names on a label and change the players points when a +/- Button is pressed. (problem)
I marked in the code below where I tried to access the instances, but got the error:
AttributeError: 'NoneType' object has no attribute 'get_screen'
My question is: How can I access the player points in ThirdRound(Screen) class in order to display their points on a label and to change the points when +/- Button is pressed?
(I searched for similar cases but was not able to apply them to my case.)
.py file:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.textinput import TextInput
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.properties import ObjectProperty, NumericProperty
class Player:
def __init__(self, name):
self.name = name
self.points = 0
def reset_points(self):
self.points = 0
def add_point(self, *args):
self.points += 1
def subtract_point(self, *args):
self.points -= 1
class WelcomeWindow(Screen):
# Introduce names of the 4 players
def __init__(self, **kwargs):
super(WelcomeWindow, self).__init__(**kwargs)
self.name = "welcomewindow"
# Create global layout of HOME screen
global_layout = GridLayout(rows=3)
self.add_widget(global_layout)
# Some code with labels and buttons
# Create button to go to next screen
go_further_button = Button(text="Go to first round")
go_further_button.bind(on_release=self.go_further)
global_layout.add_widget(go_further_button)
def go_further(self, *args):
#Give names to players
self.player1 = Player("name1") # <--- 4 player instances are created here
self.player2 = Player("name2")
self.player3 = Player("name3")
self.player4 = Player("name4")
self.manager.current = "thirdround"
self.manager.transition.direction = "left"
class ThirdRound(Screen):
def __init__(self, **kwargs):
super(ThirdRound, self).__init__(**kwargs)
self.name = "thirdround"
welcome_window = self.manager.get_screen('welcomewindow') # <--- Trying to access player instances here but failed
self.player1_points = welcome_window.player1.points
def change_label(self, add_sub, *args): # <--- method which will change the points of the player instance
label = self.ids['testlabel']
if add_sub == 'add':
label.text = 'should add one point'
elif add_sub == 'sub':
label.text = 'should subtract one point'
kv = Builder.load_file("Kingen.kv")
WindowManager = ScreenManager()
WindowManager.add_widget(WelcomeWindow())
WindowManager.add_widget(ThirdRound())
class KingenApp(App):
def build(self):
return WindowManager
if __name__ == "__main__":
KingenApp().run()
.kv file:
<ThirdRound>:
GridLayout:
rows: 3
Label:
id: testlabel
text: 'shows number of points here'
Button:
text: "+"
on_release: root.change_label('add')
Button:
text: "-"
on_release: root.change_label('sub')
The __init__() method of ThirdRound is called at the line:
WindowManager.add_widget(ThirdRound())
Specifically, the ThirdRound() is executed before the WindowManager.add_widget, so the ThirdRound instance does not have its manager yet set when its __init__() is executed, so it is still None. Try moving the code that accesses the manager till it is needed. Something like this:
class ThirdRound(Screen):
def __init__(self, **kwargs):
super(ThirdRound, self).__init__(**kwargs)
self.name = "thirdround"
def change_label(self, add_sub, *args): # <--- method which will change the points of the player instance
welcome_window = self.manager.get_screen('welcomewindow')
label = self.ids['testlabel']
if add_sub == 'add':
welcome_window.player1.points += 1
label.text = str(welcome_window.player1.points)
elif add_sub == 'sub':
welcome_window.player1.points -= 1
label.text = str(welcome_window.player1.points)

Kivy update label dynamically on canvas

I am trying update Label on canvas , but it changed only if I call update() method from MyApp(APP).
And what is interesting that method obj Slider (on touch) works perfectly
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.uix.label import Label
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.graphics import (Color, Ellipse)
from kivy.uix.slider import Slider
import random
from functools import partial
class Canvas_w(Widget):
def __init__(self, **kwargs):
self.i = 0
super(Canvas_w, self).__init__(**kwargs)
self.label_text =Label(text= "0",color = (255,0,0))
def update_text(self, text):
self.label_text.text = text
def on_touch_down(self, touch):
with self.canvas:
d=30
self.i+=1
Color(1., 0, 0)
Ellipse(pos=(touch.x - d/2, touch.y - d/2), size=(20,20))
self.label_text=str(self.i)
class Label_l(GridLayout):
bones = 0
def __init__(self, **kwargs):
super(Label_l, self).__init__(**kwargs)
self.label_text =Label(text= "0")
self.add_widget(self.label_text)
#self.label_text.text=self.C.i
def update_text(self, text):
Label_l.bones=text
self.label_text.text = str(Label_l().bones)#
Label_l.text=str(Label_l().bones)
class WidgetContainer(GridLayout):
sld_value = 0.00001
def __init__(self, **kwargs):
super(WidgetContainer, self).__init__(**kwargs)
self.cols = 1
self.speedControl = Slider(min=0.00001, max=3, step=0.0001)
self.add_widget(Label(text="Speed Iteration"))
self.add_widget(self.speedControl)
self.add_widget(Label(text=''))
self.speedValue = Label(text="0.00001")
self.add_widget(self.speedValue)
self.speedControl.bind(value=self.on_value)
def on_value(self, instance, speed):
self.sld_value = speed
WidgetContainer.sld_value = self.sld_value
self.speedValue.text = str(round(speed,5))
class MyApp(App):
def build(self):
b3 = BoxLayout(orientation="horizontal")
b1 = FloatLayout()
self.painter = Canvas_w()
self.label_l= Label_l()
b1.add_widget(self.label_l)
b1.add_widget(self.painter)
b1.add_widget(WidgetContainer())
b3.add_widget(b1)
return(b3)
if __name__=="__main__":
MyApp().run()
I am trying update Label on canvas , but it changed only if I call update() method from MyApp(APP).
And what is interesting that method obj Slider (on touch) works perfectly
A few problems with your code:
The color attribute of a Label requires a list of four values with each value in the range 0 to 1. Change self.label_text =Label(text= "0",color = (255,0,0)) to self.label_text =Label(text= "0",color = (1,0,0,1)) in the Canvas_w class.
The above Label is never getting drawn. You need to add self.add_widget(self.label_text) to the __init__() method of Canvas_w.
You have another Label in the Label_l class that is being drawn on top of the Label in the Canvas_w.
After fixing the above problems, I believe your code will work as you expect.
If you call self.canvas.clear(), it appears as though the label_text child of Canvas_w is removed. Actually it is not removed, but is just not visible (perhaps overdrawn). One way to make it visible is to just make sure it is drawn last by removing it and then re-adding it (last added child is drawn last), like this:
self.canvas.clear()
self.remove_widget(self.label_text)
self.add_widget(self.label_text)
After a bit more research, I believe that the canvas.clear() is removing the drawing of the Label from the Canvas. You can, however, do your drawing on canvas.before by using:
with self.canvas.before:
in the on_touch_down() method. And rather than using:
self.canvas.clear()
use:
self.canvas.before.clear()
I believe that your Label will be displayed correctly after doing the above.

Kivy: is it possible to trigger events with class level (not instance) Properties?

Consider following code. I would like to update multiple widget instances when prefix changes. As it is the same for all the instances it seems efficient to store/update it only once on class level (so that when instance does not have its own self.prefix, it will automatically refer to class level prefix attribute)
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.properties import StringProperty
import random
kivy_lang = '''
<MainWidget>:
Button:
id: my_button
text: 'increase indice'
<MyLabel>:
on_prefix: self.text = self.prefix +':'+ self.indice
on_indice: self.text = self.prefix +':'+ self.indice
'''
class MyLabel(Label):
prefix = StringProperty('1')
indice = StringProperty('0')
pass
class MainWidget(BoxLayout):
def __init__(self, **kwargs):
super(MainWidget, self).__init__(**kwargs)
self.my_label1 = MyLabel()
self.my_label2 = MyLabel()
self.add_widget(self.my_label1)
self.add_widget(self.my_label2)
self.ids.my_button.bind(on_press=self.my_method)
def my_method(self,*args,**kwargs):
MyLabel.prefix = str(random.randint(0,9))
self.my_label1.indice = str(int(self.my_label1.indice) + 1)
# my_label2 would also be updated if its 'indice' got changed as below
# self.my_label2.indice = str(int(self.my_label2.indice) + 2)
class MyApp(App):
def build(self):
Builder.load_string(kivy_lang)
return MainWidget()
if __name__ == '__main__':
MyApp().run()
As from the python side this seems right, from Kivy side it looks like kivy has problem recognising when prefix got changed (my_label1 only gets updated because indice was also updated and on_indice is triggered).
Is there a way to get 'class level Property' prefix change to trigger on_prefix ?
I don't think this is possible directly, but you could mimic that functionality with AliasProperty and another property stored, say, on App. As long as the instance of MyLabel hasn't changed prefix, the value set for App is used (and automatically updated). Once prefix is set on an instance, _my_prefix is not None, and will be used to retrieve the value for prefix.
Change the <MyLabel> rule to
<MyLabel>:
_prefix: app.prefix
text: self.prefix +':'+ self.indice
And change the python code to
class MyLabel(Label):
indice = StringProperty('0')
_prefix = StringProperty('')
_my_prefix = StringProperty(None)
def get_prefix(self):
if self._my_prefix is None:
return self._prefix
else:
return self._my_prefix
def set_prefix(self, value):
self._my_prefix = value
prefix = AliasProperty(get_prefix, set_prefix, bind=('_prefix', '_my_prefix'))
[...]
def my_method(self,*args,**kwargs):
App.get_running_app().prefix = str(random.randint(0,9))
self.my_label1.indice = str(int(self.my_label1.indice) + 1)
if int(self.my_label1.indice) == 2:
self.my_label2.prefix = 'changed'
[...]
class MyApp(App):
prefix = StringProperty('1')

Inheritance in Variable Value - Python

Well, I'm having trouble in a specific part of my code. It uses kivy, however I'm pretty sure there is a python solution. Here is the thing: I'll have a button, when pressed will take me to another screen, when it's pressed, calls method vai, that changes, or should change the string variable value that was created in init method. Afterwards, when another screen shows up, its button receives that CHANGED variable at text parameter. But the real issue is that by the time second screen appears, the button text does not change, remaining the value I set up on init once, not the changed value on vai method.
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.button import Button
class Principal(App):
def build(self):
return SM()
class SM(ScreenManager):
def __init__(self):
super(SM, self).__init__()
self.add_widget(Um())
self.current = 'TelaUm'
class Um(Screen):
def __init__(self):
super(Um, self).__init__()
self.name = 'TelaUm'
self.add_widget(UmC())
class UmC(FloatLayout):
def __init__(self):
super(UmC, self).__init__()
self.btnSelecionado = 'qualquer_merda'
self.btn = Button(id = 'Bosta!', text = 'Bosta!', pos_hint = { 'center_x' : .5, 'center_y' : .5 }, size_hint = (None, None))
self.btn.bind(on_press = self.vai)
self.add_widget(self.btn)
def vai(self, instance):
self.parent.parent.add_widget(Dois())
self.parent.parent.current = 'TelaDois'
self.btnSelecionado = instance.id
class Dois(Screen):
def __init__(self):
super(Dois, self).__init__()
self.name = 'TelaDois'
self.add_widget(DoisC())
class DoisC(UmC, FloatLayout):
def __init__(self):
super(DoisC, self).__init__()
self.btn2 = Button(text = self.btnSelecionado, pos_hint = { 'center_x' : .5, 'center_y' : .5 }, size_hint = (None, None) )
self.add_widget(self.btn2)
Principal().run()
I don't completely understand what you are trying to do. But it looks to me like the second screen is a new instance of UmC and therefore has its own value of btnSelecionado. So inevitably this new instance has the value from init since it's only the old instance that has been changed.
def vai(self, instance):
self.parent.parent.add_widget(Dois())
self.parent.parent.current = 'TelaDois'
self.btnSelecionado = instance.id
Line 2 creates a new instance and line 4 sets the value in the old instance.

Categories