我一直在尋找Kivy作爲一種動態繪製從同一應用程序中託管的Web API驅動的小部件的方式。我對這個話題仍然很陌生,並且遇到了Kivy框架生命週期的問題。 總而言之,我試圖實現的是使用Flask設置的API調用發送kv字符串。收到新的kv字符串後,我嘗試卸載舊視圖並加載新視圖。這適用於任何像按鈕和簡單佈局一樣簡單的工作,但我有一個倒數計時器小部件,它在每次通話時都會重複其標籤,並且永遠不會正確清除視圖。這幾乎就像每次kv字符串被加載時都會複製widget對象。在嘗試加載新視圖之前,我顯然沒有正確清除視圖,但我無法弄清楚我要出錯的地方。Kivy應用程序生命週期(小部件在每次重繪時都會自我複製)
我將首先發送完整代碼蟒應用程式:
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對象的destroy方法,則其父節點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
哇!我無法相信我沒有注意到這一點。謝謝一堆!現在這更有意義了。 – user5763563