Search code examples
pythonattributeskivybind

kivy binding attributes (variables) from other class attribute (import class attribute)


I am trying to bind attributes from an instantiated imported external class to kivy class, but it is not working, I can't find my specific case scenario in kivy doc's or elsewhere.

I made an example kivy gui, the label on the left is binded to a kivy class attribute and when the counter starts the label on the left updates accordingly, but the label on the right is binded to an attribute from other class that was instantiate inside kivy "on_start" method, when the counter on the right is started you can see in the console the attribute for the instantiate class on the right is changing but not the label on the right.

The code here is simple, "Start counter" button call's a method which in turn calls the counter method using a thread to avoid freezing the gui, the counter method increments a number using a while loop, as the number changes so does the label on the left. The methods with the "external" keyword refer to the imported class counter on the right, so it is the same like the one on the left, the right "start counter" button calls the "start_external_counter" method and starts the counter in the instantiated imported class, the number attribute in the imported class is incremented, but the binding in kivy does not work for the imported class attribute, and that is my question that I hope to solve, How to bind an attribute from an external class or imported module to kivy environment.

Note: I can update the label on the right by using a clock with a polling loop calling the external class attribute every interval, but I don't think this is the correct way.

Thank you in advance for your help.

from kivy.app import App 
from kivy.lang import Builder
from kivy.properties import ObjectProperty, NumericProperty

import time
import threading

kv = '''
BoxLayout:
    padding: 40
    spacing: 50
    # -- INTERNAL CLASS COUNTER --#
    BoxLayout:
        orientation: 'vertical'
        spacing: 50
        Label:
            text: 'Kivy class attribute binding'
            font_size: 30
            size: self.texture_size
            size_hint_y: .2
        Label:
            id: counter_label
            text: '0'
            font_size: 200
            size: self.texture_size
        BoxLayout:
            size_hint_y: None
            height: 80
            Button:
                text: 'Start Counter'
                font_size: 30
                on_release: app.get_running_app().start_counter()
            Button:
                text: 'Stop Counter'
                font_size: 30
                on_release: app.get_running_app().stop_counter()

    # -- EXTERNAL CLASS COUNTER -- #
    BoxLayout:
        orientation: 'vertical'
        spacing: 50
        Label:
            text: 'External class attribute binding'
            font_size: 30
            size: self.texture_size
            size_hint_y: .2
        Label:
            id: external_counter_label
            text: '0'
            font_size: 200
            size: self.texture_size
        BoxLayout:
            size_hint_y: None
            height: 80
            Button:
                text: 'Start Counter'
                font_size: 30
                on_release: app.get_running_app().start_external_counter()
            Button:
                text: 'Stop Counter'
                font_size: 30
                on_release: app.get_running_app().stop_external_counter()
'''

class MyClass:
    number = 0
    stop = False

    def count(self):
        while not self.stop:
            self.number += 1
            print 'External counter: %s' % self.number 
            time.sleep(1)
        self.stop = False



class main(App):
    number = NumericProperty(0)
    external_number = NumericProperty(0)
    external_counter = ObjectProperty()
    stop = False



    def build(self, *args):
        layout = Builder.load_string(kv)
        return layout

    def on_start(self):
        root = self.root_window
        self.layout = root.children[0]
        self.counter_label = self.layout.ids['counter_label']
        self.bind(number=self.update_label)

        ## -- Trying to bind a property 
        ## -- from other non kivy class
        self.external_counter_label = self.layout.ids['external_counter_label'] 
        self.external_counter = MyClass()
        self.external_number = self.external_counter.number
        self.bind(external_number=self.update_external_label)

    def update_label(self, *args):
        self.counter_label.text = str(self.number)

    def start_counter(self):
        ''' using a thread to start counter
            without freezing gui
        '''
        t = threading.Thread(target=self.count)
        t.setDaemon(True)
        t.start()

    def count(self):
        while not self.stop:
            self.number += 1
            time.sleep(1)
        self.stop = False

    def stop_counter(self):
        self.stop = True


    ## --- CALLING THE EXTERNAL CLASS METHODS -- ##
    def update_external_label(self):
        self.external_counter_label.text = self.external_number

    def start_external_counter(self):
        ''' using a thread to start counter
            without freezing gui
        '''
        t = threading.Thread(target=self.external_count)
        t.setDaemon(True)
        t.start()

    def external_count(self):
        self.external_counter.count()

    def stop_external_counter(self):
        self.external_counter.stop = True



if __name__ == '__main__':
    main().run()

Solution

  • I found the solution, the trick is to use kivy "EventDispatcher".

    The module or class which contains the attributes that you want to bind to in kivy needs to have the kivy binding functionality, so the imported module or external class that you want to bind it's attributes from, must inherit from kivy's "EventDispatcher" class, but since we don't want to change the external class or module in any way the best solution is to override the external class with a custom class that inherits from both the external class and kivy "EventDispatcher" class, this way we can use the new custom class in kivy and bind any of it's attributes.

    Here I made a small working example.

    Note: If anybody knows a better way please comment and post a short example, Thank you.

    from kivy.app import App 
    from kivy.lang import Builder 
    from kivy.properties import NumericProperty, ObjectProperty
    from kivy.event import EventDispatcher
    
    import time 
    import threading 
    
    class Count:
        """ This is the external class we want to bind 
            attributes from, this could be an imported 
            module also.
        """
        number = 0
    
        def counter(self):
            while True:
                self.increase()
                time.sleep(1)
    
        def increase(self):
            self.number += 1
    
    
    
    
    class MyClass(Count, EventDispatcher):
        """ Custom override class.
            This class contains the kivy 
            bindings functionality
        """
        number = NumericProperty(0)  # <-- turns number attribute into a kivy object, so it can be binded in kivy
    
    
    
    kv = '''
    
    AnchorLayout:
        BoxLayout:
            spacing: 40
            size_hint: None, None
            size: 300, 200 
            orientation: 'vertical'
            Label:
                id: counter_label
                text: '0'
                font_size: 100
            BoxLayout:
                Button:
                    text: 'Add 1'
                    on_release: app.get_running_app().change()
                Button:
                    text: 'Start Counter'
                    on_release: app.get_running_app().start_counter()
    '''
    
    class main(App):
        ''' Updating a label through binding an attribute
            from an external class "MyClass".
        '''
    
        def build(self):
            return Builder.load_string(kv)
    
        def on_start(self):
            self.counter_label = self.root.ids['counter_label']
            self.my_class = MyClass()                        # <-- here we instanciate the custom class
            self.my_class.bind(number=self.update_label)     # <-- here we bind the number attribute from the custom class
    
        def update_label(self, *args):
            """ when the "number" attribute from the external
                class changes this method is called and updates
                the label accordingly.
            """ 
            self.counter_label.text = str(self.my_class.number)
    
        def change(self):
            """ Use a thread here to avoid locking the 
                gui's main loop
            """
            t = threading.Thread(target=self.increase)
            t.setDaemon(True)
            t.start()
            
        def increase(self):
            """ Calls the increase method in the custom
                class and increases the "number" attribute
            """
            self.my_class.increase()
    
        def start_counter(self):
            t = threading.Thread(target=self.count)
            t.setDaemon(True)
            t.start()
    
        def count(self):
            self.my_class.counter()
    
    
    if __name__ == '__main__':
        main().run()