我一直将 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