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

时间:2016-05-31 15:53:18

标签: python python-2.7 kivy kivy-language

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

我将首先发布python app的完整代码:

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”将遍历小部件并检查它们是否已定义了destroy方法(这样我们就可以阻止任何计时事件继续)。 之后它调用“clear_widgets()”,如果我们传入布局,它将尝试使用load_string加载新视图。然后使用“add_widget”将视图添加到根BoxLayout。

这适用于第一次通话。如果我在第二次调用时调试CountdownTimer有2个TimerTicks对象。后续调用每次都会增加TimerTicks的数量,直到应用程序爆炸为止。奇怪的是,如果我在“self.parent.clear_widgets()”之后查看了TimerTicks对象的destroy方法,那么它的父级CountdownTimer总是没有孩子,这表明小部件在此时被清除,但每当“self.view”时= Builder.load_string(layout)“被调用它很奇怪,最终会复制TimerTicks。

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

PS:如果每次通话都会略微移动计时器的位置,那就更明显了。然后你实际上可以看到重复堆栈在彼此之上。

e.g:

<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

1 个答案:

答案 0 :(得分:0)

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

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