3

As I am very well aware, this question has been asked several times already. But after trying the following solutions:

I have reached the conclusion that i need help in my specific problem. The solutions listed didn't seem to work in my specific case.

Following situation:

I am currently trying to develop an application for smartphones using kivy. Since i like my code pretty clean and clear structured i have split my Kivy code into several kv-files. The python code is supposed to have primarily the logic and nothing more. In order to get it working properly i need to reference the instances of the different objects in the different kv-files. In order to make my problem clear i have constructed a fairly simple example:

FILE: attempt.py

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.factory import Factory
from kivy.uix.label import Label
from kivy.lang import Builder

x= 1

class ComplexBox(Widget):
    def testit(self):
        self.ids.layout.add_widget(Label(text = "Requirement A met."))
    def addsome(self):
        global x
        self.ids.layout.add_widget(SomeWidget(id="XXX"+str(x)))
        x = x +1
    pass

class SomeWidget(Widget):
    def change(self):
        self.ids.REQB.text = "Requirement B met."
    pass

class RequirementC(Widget):
    def triggerC(self):
        self.ids.ERRORBUTTON.text = "Requirement C met"
    pass

class Attempt(App):
    def build(self):
        return presentation
    pass


presentation = Builder.load_file("attempt.kv")
Attempt().run()

FILE: attempt.kv

#:kivy 1.0
#:include attemptsupp.kv
#:include attemptsuppC.kv

# root
<ComplexBox>:
    BoxLayout:
        id: layout
        size: root.size
        Button:
            id: ERRORBUTTON
            text: "add"
            on_press: root.addsome()
            on_release: root.testit()
BoxLayout:
    orientation: 'vertical'
    ComplexBox:
    RequirementC:

FILE: attemptsupp.kv

#:kivy 1.0

# rules for the widget
<SomeWidget>:
    BoxLayout:
        pos: root.pos
        size: root.size
        orientation: "vertical"
        Label:
            id: REQB
            text: "hello"
        Button:
            text: "world"
            on_release: root.change()

FILE: attemptsuppC.kv

#:kivy 1.0

<RequirementC>:
    Button:
        id: REQC
        text: "Press"
        on_release: root.triggerC()

picture of the Running Program - press the "Press"- button to get the error

Running with kivy version 1.10 and Python version 3.7.2 the program first starts perfectly fine. But when I press on the Button labeled "press" with the id ERRORBUTTON I get this error:

...--default --nodebug --client --host localhost --port 57777...\attempt.py "
[INFO   ] [Logger      ] Record log in...\.kivy\logs\kivy_19-03-15_31.txt
[INFO   ] [Kivy        ] v1.10.1
[INFO   ] [Python      ] v3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 
...
[INFO   ] [Window      ] auto add sdl2 input provider
[INFO   ] [Window      ] virtual keyboard not allowed, single mode, not docked
[WARNING] [Lang        ] attemptsupp.kv has already been included!
[WARNING] [Lang        ] attemptsuppC.kv has already been included!
[INFO   ] [Base        ] Start application main loop
[INFO   ] [GL          ] NPOT texture support is available
[INFO   ] [Base        ] Leaving application in progress...
 Traceback (most recent call last):
   File "kivy\properties.pyx", line 838, in kivy.properties.ObservableDict.__getattr__
 KeyError: 'ERRORBUTTON'

 During handling of the above exception, another exception occurred:

 Traceback (most recent call last):
   File "...\ptvsd_launcher.py", line 45, in <module>
     main(ptvsdArgs)
   ...
   File "e:\Daten\Github_Projects\pc-clicker\attempt.py", line 35, in <module>
     Attempt().run()
   File "...\lib\site-packages\kivy\app.py", line 826, in run
     runTouchApp()
...
   File ...\lib\site-packages\kivy\lang\builder.py", line 64, in custom_callback
     exec(__kvlang__.co_value, idmap)
   File ...\attemptsuppC.kv", line 7, in <module>
     on_release: root.triggerC()
   File "...\attempt.py", line 25, in triggerC
     self.ids.ERRORBUTTON.text = "Requirement C met"
   File "kivy\properties.pyx", line 841, in kivy.properties.ObservableDict.__getattr__
 AttributeError: 'super' object has no attribute '__getattr__'

Even though I shortened the error message it should be clear what happens. The ERRORBUTTON id I am referencing in the RequirementC Class can't be found in the dictionary. Now to my question:

How can i make it work? What am I a missing?

Here in short a few things i have tried:

  • I have tried wrapping the BoxLayouts in Screen and accessing them over the screenmanager.
  • I have tried rearranging the order in the python code. (for example the loading of the main kv file first)
  • I have tried using the Builder Factory and registering the different Classes there.
  • I have tried changing the references. (For example self.ids.['ERRORBUTTON']...)

None of these attempts seem to have worked in my case.

So to sum it all up:

How can I get my kivy References across different classes to work properly and why is the ERRORBUTTON id not in the dict I am looking into?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459

2 Answers2

1

The problem is caused by a common error, the ids are relative to a widget, for example in your case let's analyze the expression:

self.ids.ERRORBUTTON

Who is self? self is the instance of RequirementC.

what instance do you have? then let's see the .kv where RequirementC is implemented:

<RequirementC>:
    Button:
        id: REQC
        text: "Press"
        on_release: root.triggerC()

If you notice the only id that has access is to REQC, so the id ERRORBUTTON does not exist for RequirementC.

So which class does the id ERRORBUTTON belong to? So let's review where ERRORBUTTON was created:

 # ...

<ComplexBox>:
    BoxLayout:
        id: layout
        size: root.size
        Button:
            id: ERRORBUTTON
            text: "add"
            on_press: root.addsome()
            on_release: root.testit()
 # ...

As you can see ERRORBUTTON is a id of ComplexBox.


With what was mentioned in the previous part, we already know the cause of the problem. Before giving a solution we first understand a basic principle of programming: A class is an abstraction of a behavior, it must clearly define that you want to expose to the outside (therefore if you check the docs of any library do not document all the methods or all the classes since the idea is to abstract the class, that is to say, that whoever uses that library does not want to know how it works internally with such precision), so it is good to design thinking what methods the classes will have. For example let's say that we create a Person class, this class has certain attributes such as size or weight, what if you think it is necessary to expose how much your heart or brain weighs? Well, no. The same happens in your case.

The solution is to expose the event on_release so that it is part of the RequirementC class, in addition to expose ERRORBUTTON as a property (in my case I do not like to use the ids since they make the code less readable) and then make the connection in a place with common scope.

*.py

# ...

class RequirementC(Widget):
    def __init__(self, **kwargs):
        self.register_event_type('on_release')
        super().__init__(**kwargs)

    def on_release(self):
        pass

# ...

attempt.kv

#:kivy 1.0
#:include attemptsupp.kv
#:include attemptsuppC.kv

# root
<ComplexBox>:
    error_button: ERRORBUTTON # <---
    BoxLayout:
        id: layout
        size: root.size
        Button:
            id: ERRORBUTTON
            text: "add"
            on_press: root.addsome()
            on_release: root.testit()
BoxLayout:
    orientation: 'vertical'
    ComplexBox:
        id: complex_box
    RequirementC:
        on_release: complex_box.error_button.text = "Requirement C met"

attemptsuppC.kv

#:kivy 1.0

<RequirementC>:
    Button:
        id: REQC
        text: "Press"
        on_release: root.dispatch('on_release')
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thanks for your fast answer. In my case, my application might be connected to a database. Thats why i need to access the Buttons and labels by id from the python code. I want to be capable of dynamically adding new widgets, giving them an id and then manipulating them in different ways from the python program. The kv-files are supposed to picture more or less empty templates which i insert at different points. If I understood you correctly, the on_release action is hard coded in the kv file. Is there a way to avoid this? – Franz Dahmann Mar 16 '19 at 00:58
  • @FranzDahmann The ids can not and should not be added dynamically, this makes an application unbearable, instead it creates a class for each functionality: Single responsibility principle https://en.wikipedia.org/wiki/Single_responsibility_principle, so I recommend that before writing code you design your classes. As indicated by the classes should not depend much on other classes, should use them but not depend as much as in your case it is. – eyllanesc Mar 16 '19 at 01:03
  • @FranzDahmann I could give you a better solution if you explain in detail what exactly you want to do, that is to say what function fulfills each class. I recommend changing your view of the classes, a class is like a black box where it only exposes certain properties and has functions to modify the state of the class, and the rest is hidden. To drive a car you must know how the motor was built? No, you just need to know how to drive a car. – eyllanesc Mar 16 '19 at 01:07
  • Ok thanks for your advice. Let me try to get to it in more detail: I have the Widget RequirementC in a seperate kv file. This leads to it having a seperate corresponding class in the python code. The Button in this widget is supposed to change the Text of the Button with the id ERRORBUTTON in the ComplexBox. The ComplexBox is defined in a different class in python. Now heres the tricky part... The text that is supposed to be shown in the ERRORBUTTON will be generated in the python code. So hard coding it in the kv-file wont help in this situation. – Franz Dahmann Mar 16 '19 at 12:14
  • I need to find a way to bring the text that is generated in python over to the ERRORBUTTON in the kv-file when i press the REQC Button. – Franz Dahmann Mar 16 '19 at 12:16
  • For example: What would i have to do to make the x value in the python code be shown as text in the ERRORBUTTON? It refreshes at every button press on the "Press" Button (REQC). – Franz Dahmann Mar 16 '19 at 12:19
  • @FranzDahmann It seems that you have not understood me, you are explaining your possible solution and as we see it has serious flaws. Instead I require that you explain the underlying problem, IMHO you have a [XY Problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) – eyllanesc Mar 16 '19 at 18:15
0

So after a bit of research i found the answer i was looking for. For those who might also stumble over my problem here it comes:

class RequirementC(Widget):
def triggerC(self):
    presentation.children[1].ids.ERRORBUTTON.text = "Requirement C met"
pass

By going over the children of the presentation it is possible to use the "ids" method.

But for those tempted to use it now i would recommend iterating over the children to find the correct id instead of giving a "hard" reference (children[1]).