2016-05-31 78 views
0

我一直在尋找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 

回答

0

你千伏文件包括根小部件和正常千伏規則,所以每次加載它添加了另一個相同的規則給CountdownTimer。當所有這些相同的規則被實例化時,它們一個接一個地被應用。

取而代之,將要加載的窗口小部件的kv放入它自己的文件(或只是python文件中的一個字符串)。

+0

哇!我無法相信我沒有注意到這一點。謝謝一堆!現在這更有意義了。 – user5763563

相關問題