0

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

我将首先发布 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 对象。随后的调用每次都会增加 TimerTicks 的数量,直到应用程序崩溃。奇怪的是,如果我在“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
4

1 回答 1

0

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

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

于 2016-05-31T16:03:43.750 回答