Kivy 应用程序生命周期(小部件在每次重绘时复制自身)

Kivy application lifecycle (widget duplicating itself on each redraw)

我一直在将 Kivy 视为一种动态绘制从同一应用程序中托管的 Web API 驱动的小部件的方法。我对这个话题还很陌生,我 运行 遇到了 Kivy 框架生命周期的问题。 总之,我想要实现的是使用 API 调用发送一个 kv 字符串,该调用是使用 Flask 设置的。收到新的 kv 字符串后,我尝试卸载旧视图并加载新视图。这适用于任何琐碎的事情,比如按钮和简单的布局,但我有一个倒数计时器小部件,它在每次调用时都会复制它的标签,并且永远不会正确地清除视图。几乎就像每次加载 kv 字符串时它都会复制小部件对象。在尝试加载新视图之前,我显然没有正确清除视图,但我不知道哪里出错了。

我会先 post python 应用程序的完整代码:

import threading

import datetime

from kivy.app import App

from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import Property, ObjectProperty, BooleanProperty, StringProperty
from kivy.graphics import Color, SmoothLine
from kivy.clock import Clock

from app_shell import AppShell
from _functools import partial
from kivy.uix.widget import Widget
from math import cos, sin, pi
from kivy.uix.layout import Layout

class CountdownTimer(BoxLayout):
    pass

class TimerTicks(Widget):
    time = StringProperty()    
    running = BooleanProperty(False)
    countdown = 4520

    def __init__(self, **kwargs):
        super(TimerTicks, self).__init__(**kwargs) 

        self.time = '{:02d}:{:02d}:{:02d}'.format(0, 0, 0)

        self.update()

        self.start()

    def start(self):
        if not self.running:
            self.running = True
            Clock.schedule_interval(self.update, 1)

    def stop(self):
        if self.running:
            self.running = False
            print("timer stopped")
            Clock.unschedule(self.update)

    def destroy(self):
        print('TimerTicks destroy called')
        self.stop()

        parent = self.parent

        if parent is not None:
            self.parent.clear_widgets()

        print("i'm here")

    def update(self, *kwargs):
        print('update called')
        hours, mins_m = divmod(self.countdown, 3600)

        mins, secs = divmod(mins_m, 60)

        timeformat = '{:02d}:{:02d}:{:02d}'.format(hours, mins, secs)
        self.time = timeformat        

        if self.countdown == 0:
            self.stop()
        else:
            self.countdown -= 1        

        '''print('update called')
        mins, secs = divmod(self.countdown, 60)
        timeformat = '{:02d}:{:02d}'.format(mins, secs)
        self.time = timeformat        

        if self.countdown == 0:
            self.stop()
        else:
            self.countdown -= 1'''

    def reset(self, value):
        self.stop()

        print("reset with value {0}".format(value))

        self.time = '{:02d}:{:02d}:{:02d}'.format(0, 0, 0)

        self.countdown = value

        self.update()

        self.start()

class MainApp(App):
    temp_count = 0

    current_layout_name = "home.kv"
    welcome_message = "Not set"
    error_message = "Not set"
    current_layout = None

    def build(self):
        print('building app')

        self.address = ""
        self.port = 0        

        t = threading.Thread(target=self.run_app_shell, args=    (self.on_to_gui_status_change, self.on_to_gui_layout_change,     self.on_to_gui_redraw))
        t.daemon = True
        t.start()  # Starts the thread
        t.setName('appShellThread')  # Makes it easier to interact with the     thread later

        self.root = BoxLayout()

        self.view = Builder.load_file('layouts/home.kv')

        self.root.add_widget(self.view)

        return self.root

    def run_app_shell(self, on_to_gui_status_change,     on_to_gui_layout_change, on_to_gui_redraw):
        self.shell = AppShell(on_to_gui_status_change,     on_to_gui_layout_change, on_to_gui_redraw)

        self.address = self.shell.self_address
        self.port = self.shell.http_port

        self.welcome_message = "Welcome!\n------    ---------\n Get request to http://{0}:{1}/change_layout/name to change the     current layout".format(self.address, self.port)        

        self.shell.start()

    def on_stop(self):
        self.shell.close()

    def on_to_gui_layout_change(self, layout_name, layout):
        print('on_to_gui_layout_change called!')
        try:            
            cb = partial(self.change_kv, layout_name, layout)

            Clock.schedule_once(cb)            

        except Exception as exp:
            print ("exception {0}".format(exp)) 

    def change_kv(self, layout_name, layout, *args):
        try:                        
            for widget in self.root.walk(restrict=True):
                if hasattr(widget, 'destroy'):
                    widget.destroy()

            self.root.clear_widgets()

            self.current_layout_name = '{0}.kv'.format(layout_name)        

            if layout is not None:
                print('loading custom kv {0}'.format(layout))

                self.current_layout = layout
                del self.view
                self.view = Builder.load_string(layout)
            else:
                print('loading {0}.kv'.format(layout_name))

                self.current_layout = None

                self.view =     Builder.load_file('layouts/{0}.kv'.format(layout_name))            

            self.root.add_widget(self.view)            

            Builder.apply(self.root)

        except (SyntaxError) as e:
            print("exp 1 {0}".format(e))
            self.load_error_gui()
        except Exception as e:
            print("exp 2 {0}".format(e))
            self.load_error_gui()

    def load_error_gui(self):
        self.error_message = "Welcome!\n--------    -------\n Your previous layout could not be loaded!"

        for widget in self.root.walk(restrict=True):
            if hasattr(widget, 'destroy'):
                widget.destroy()

        self.root.clear_widgets()

        self.current_layout_name = '{0}.kv'.format("error")

        print('loading {0}.kv'.format("error"))

        self.view = Builder.load_file('layouts/{0}.kv'.format("error"))

        Builder.apply(self.root)

        self.root.add_widget(self.view)

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

作为 API 调用传递的示例 kv 动态字符串是:

<CountdownTimer>:
    face: face
    ticks: ticks

    BoxLayout:
        id: face
        size_hint: None, None

        Label:
            text: ticks.time
            font_size: root.height/8
            color: 1,1,1,1

    TimerTicks:
        id: ticks

FloatLayout:
    timer: timer_1    

    CountdownTimer:
        id: timer_1
        pos: root.width/1.42, root.height/2.2         

申请流程总结:

启动时,MainApp 在不同的线程中创建一个 AppShell 对象。 你不需要太担心这个。本质上,AppShell 是定义所有 Flask 调用的地方,如果我只是想更改为已在本地定义的布局,我可以使用 layout_name 将 http put 调用推送到 "on_to_gui_layout_change" 方法中,或者使用一个布局字符串,它是传入的动态 kv 字符串(请参阅上面的 kv 示例)。

在上面发送新的 KV 字符串后,应用程序将调用 "on_to_gui_layout_change",最终将调用 "change_kv"。 "change_kv" 将遍历小部件并检查它们是否定义了销毁方法(这样我们就可以停止任何计时事件继续进行)。 之后它会调用 "clear_widgets()",如果我们传入布局,它将尝试使用 load_string 加载新视图。然后使用 "add_widget".

将视图添加到根 BoxLayout

第一次调用时效果很好。如果我在第二次调用时调试 CountdownTimer 有 2 个 TimerTicks 对象。随后的调用每次都会增加 TimerTick 的数量,直到应用程序运行正常。奇怪的是,如果我在 "self.parent.clear_widgets()" 之后查看 TimerTicks 对象的销毁方法,它的父 CountdownTimer 始终没有子对象,这表明此时已清除小部件,但每当 "self.view = Builder.load_string(layout)" 被调用时奇怪的是最终复制了 TimerTicks。

我意识到我可能没有正确地放弃旧观点,但我不完全理解生命周期以及这样做的适当方式。 任何帮助将不胜感激!

PS:如果每次调用都稍微移动计时器的位置,效果会更明显。然后您实际上可以看到重复项堆叠在一起。

例如:

<CountdownTimer>:
    face: face
    ticks: ticks

    BoxLayout:
        id: face
        size_hint: None, None

        Label:
            text: ticks.time
            font_size: root.height/8
            color: 1,1,1,1

    TimerTicks:
        id: ticks

FloatLayout:
    timer: timer_1    

    CountdownTimer:
        id: timer_1
        pos: root.width/1.3, root.height/2.5

您的 kv 文件包括一个根小部件和一个普通的 kv 规则,因此每次加载它时您都会向 CountdownTimer 添加另一个相同的规则。当它被实例化时,所有这些相同的规则都会一个接一个地应用。

相反,将要加载的小部件的 kv 放入其自己的文件中(或者只是 python 文件中的一个字符串)。