来自其他 class 属性的 kivy 绑定属性(变量)(导入 class 属性)

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

我正在尝试将属性从实例化导入的外部 class 绑定到 kivy class,但它不起作用,我无法在 kivy 文档或其他地方找到我的具体案例场景。

我制作了一个 kivy gui 示例,左侧的标签绑定到 kivy class 属性,当计数器启动时,左侧的标签相应更新,但右侧的标签绑定到来自其他 class 的属性在 kivy "on_start" 方法中实例化,当右侧的计数器启动时,您可以在控制台中看到右侧实例化 class 的属性正在改变但不是右边的标签。

这里的代码很简单,"Start counter"按钮调用了一个方法,该方法又使用线程调用计数器方法以避免冻结gui,计数器方法使用while循环递增一个数字,作为数字左侧的标签也会发生变化。带"external"关键字的方法引用右边导入的class计数器,所以和左边一样,右边的"start counter"按钮调用"start_external_counter"方法并在实例化的导入class中启动计数器,导入class中的数字属性递增,但kivy中的绑定不适用于导入的class属性,并且是我希望解决的问题,How to bind an attribute from an external class or imported module to kivy environment.

注意:我可以使用带有轮询循环的时钟更新右侧的标签,每个时间间隔调用外部 class 属性,但我认为这不是正确的方法。

提前感谢您的帮助。

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()

我找到了解决方案,诀窍是使用 kivy“EventDispatcher”。

包含你想在kivy中绑定的属性的模块或class需要有kivy绑定功能,所以导入的模块或你想绑定的外部class是来自的属性,必须继承自 kivy 的“EventDispatcher”class,但由于我们不想以任何方式更改外部 class 或模块,因此最好的解决方案是覆盖外部 class使用继承自外部 class 和 kivy“EventDispatcher”class 的自定义 class,这样我们就可以在 kivy 中使用新的自定义 class 并绑定其中的任何一个属性。

这里我做了一个小例子。

注意: 如果有人知道更好的方法,请评论并 post 一个简短的例子,谢谢。

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()