首页 > 解决方案 > 为什么在添加使用 Builder.load_string() 创建的小部件后没有捕获到这个 kivy 错误?

问题描述

问题总结

我正在尝试将根小部件添加到现有的 .kv 应用程序,其中所述任意根小部件由 .kv 创建kivy.lang.Builder.load_string method。如果提供给 Builder 的 kivy 字符串代表有效且合法的 .kv 代码,这将正常工作。预计,否则它将失败。

为了解决这个问题,我添加了一个try-except块,希望能发现任何可能导致无法添加适当的 kivy 小部件的错误。然后在弹出消息中使用对应Exception的,之后最终不应该添加无效的小部件。

对于某些输入,这按预期工作(如果错误则显示弹出消息)。但是,对于特定的字符串输入,应用程序会崩溃而没有发现负责任的错误。现在我想知道为什么这些错误没有被捕获,以及如何正确地捕获它们。具体代码见下文。

我的应用程序

我的应用程序由一个.py和一个.kv文件1组成,如下(简化):

# main.kv
ScreenManager:
    Screen:
        name: 'string_screen'
        BoxLayout:
            orientation: 'vertical'
            TextInput:
                id: code_text
                text: app.text
            Button:
                text: 'call'
                on_release: app.call()

    Screen:
        name: 'called_screen'
        BoxLayout:
            id: render_layout

<Button>:
    size_hint: 0.5, None
    height: '1.2cm'

<MsgPopup>:
    size_hint: .75, .6
    title: "Attention"

    BoxLayout:
        orientation: 'vertical'
        padding: 10
        spacing: 20
        Label:
            id: message_label
            size_hint_y: 0.4
            text: "Label"
        Button:
            text: 'Dismiss'
            size_hint_y: 0.4
            on_press: root.dismiss()

和python文件:

# main.py
from kivy.app import App
from kivy.properties import StringProperty
from kivy.lang import Builder
from kivy.uix.popup import Popup


class MainApp(App):
    text = StringProperty()
    kv = None
    def call(self):
        kv_text = self.root.ids['code_text'].text
        try:
            self.kv = Builder.load_string(kv_text)
            print(self.kv)
            self.root.ids['render_layout'].clear_widgets()
            print('cleared')
            self.root.ids['render_layout'].add_widget(self.kv)
            print('added')
            self.root.current = 'called_screen'
            self.root.transition.direction = 'left'
            print('swiped')
        except Exception as e:
            popup = MsgPopup(e)
            popup.open()


class MsgPopup(Popup):
    def __init__(self, msg):
        super().__init__()
        self.ids.message_label.text = str(msg)


if __name__ == '__main__':
    MainApp().run()

1我的实际应用程序包含一些额外的和更详细的代码,但它的这个简化版本足以重现不希望的行为。

正如您在代码中看到的,该应用程序由两个屏幕组成。第一个中的主要元素TextInput是创建第二个的元素。下面的两张图片演示了没有错误的情况。

屏幕1 屏幕2

以下是正确行为的示例,当文本输入包含任何会产生错误的内容时:

在此处输入图像描述

意外行为

最后一张图片正确显示了弹出消息。但是,例如,当我在 TextInput 字段中输入以下输入时:

FloatLayout:
    Label:
        text: "Hello World"
        pos_hint: 0.5, 0.7

这是pos_hintvalue 参数中的错误。然后,一旦我按下呼叫按钮,应用程序就会崩溃。而不是预期的弹出消息,我得到一个实际的堆栈跟踪!

<kivy.uix.floatlayout.FloatLayout object at 0x0000016F7F0FC800>
cleared
   File "C:/Users/ajdin/.PyCharmCE2019.1/config/scratches/scratch_1.py", line 34, in <module>
added
swiped
     MainApp().run()
   File "C:\Users\ajdin\Anaconda3\lib\site-packages\kivy\app.py", line 826, in run
     runTouchApp()
   File "C:\Users\ajdin\Anaconda3\lib\site-packages\kivy\base.py", line 502, in runTouchApp
     EventLoop.window.mainloop()
   File "C:\Users\ajdin\Anaconda3\lib\site-packages\kivy\core\window\window_sdl2.py", line 727, in mainloop
     self._mainloop()
   File "C:\Users\ajdin\Anaconda3\lib\site-packages\kivy\core\window\window_sdl2.py", line 460, in _mainloop
     EventLoop.idle()
   File "C:\Users\ajdin\Anaconda3\lib\site-packages\kivy\base.py", line 346, in idle
     Clock.tick_draw()
   File "C:\Users\ajdin\Anaconda3\lib\site-packages\kivy\clock.py", line 588, in tick_draw
     self._process_events_before_frame()
   File "kivy\_clock.pyx", line 427, in kivy._clock.CyClockBase._process_events_before_frame
   File "kivy\_clock.pyx", line 467, in kivy._clock.CyClockBase._process_events_before_frame
   File "kivy\_clock.pyx", line 465, in kivy._clock.CyClockBase._process_events_before_frame
   File "kivy\_clock.pyx", line 167, in kivy._clock.ClockEvent.tick
   File "C:\Users\ajdin\Anaconda3\lib\site-packages\kivy\uix\floatlayout.py", line 116, in do_layout
     for key, value in c.pos_hint.items():
 AttributeError: 'tuple' object has no attribute 'items'

Process finished with exit code 1

我在这里的预期输出将与之前类似:上面显示的堆栈跟踪消息显示在弹出窗口中,而应用程序没有崩溃!. 我希望这是因为我在按钮回调中处理异常的方式:

kv_text = self.root.ids['code_text'].text
try:
    self.kv = Builder.load_string(kv_text)
    print(self.kv)
    self.root.ids['called_screen'].clear_widgets()
    print('cleared')
    self.root.ids['called_screen'].add_widget(self.kv)
    print('added')
    self.root.current = 'called_screen'
    self.root.transition.direction = 'left'
    print('swiped')
except Exception as e:
    popup = MsgPopup(e)
    popup.open()

因此,如果 load_string 方法中出现错误,我希望能抓住它。否则,如果它以某种方式通过,我希望在 add_widget 方法中捕获一个错误。但是,从上面的堆栈跟踪看来,它成功地传递了所有这些语句,并给出了错误的文本输入!。您可以从堆栈跟踪中的打印输出中看到这一点:

...
<kivy.uix.floatlayout.FloatLayout object at 0x0000016F7F0FC800>
cleared
   File "C:/Users/ajdin/.PyCharmCE2019.1/config/scratches/scratch_1.py", line 34, in <module>
added
swiped
...

它打印 try 块中的所有语句,表明它通过它没有任何错误,对吗?

问题

因此,如果未捕获上述抛出的错误,是什么原因造成的,以及我如何/在哪里正确捕获它,以便应用程序以预期的行为结束(最多弹出错误消息)?

标签: pythonruntime-errorkivy

解决方案


我相信在您的方法完成后Exception会被抛出。通常,GUI 更新只发生在主线程上,因此必须等到您的代码(在主线程上运行)完成。您仍然可以通过将以下代码添加到您的. 中来使用 s 捕获这些 s :mainloopcall()ExceptionKivy ExceptionHandlerPython

from kivy.base import ExceptionHandler, ExceptionManager

class E(ExceptionHandler):
    def handle_exception(self, inst):
        app = App.get_running_app()
        if app.scheduled_switch is not None:
            app.scheduled_switch.cancel()  # cancel the scheduled switch
            app.scheduled_switch = None
        if app.Exception_counter == 0:
            popup = MsgPopup(inst)
            popup.open()
        app.Exception_counter += 1
        return ExceptionManager.PASS

ExceptionManager.add_handler(E())

上面的代码还取消了可能安排的Screen切换。

然后,为了限制每次调用Popup只出现一次,修改你的包含一个. 此外,为了防止切换到,修改后的代码用于安排切换(可能会被 取消):call()AppException_countercalled ScreenClockExceptionHandler

class MainApp(App):
    def __init__(self, **kwargs):
        self.Exception_counter = 0
        self.scheduled_switch = None
        super(MainApp, self).__init__(**kwargs)

    def call(self):
        self.Exception_counter = 0
        kv_text = self.root.ids['code_text'].text
        try:
            self.kv = Builder.load_string(kv_text)
            print(self.kv)
            self.root.ids['render_layout'].clear_widgets()
            print('cleared')
            self.root.ids['render_layout'].add_widget(self.kv)
            print('added')

            # schedule switch to 'called' screen
            self.scheduled_switch = Clock.schedule_once(self.switch_to_called_screen, 0.25)
        except Exception as e:
            popup = MsgPopup(e)
            popup.open()

    def switch_to_called_screen(self, dt):
        self.root.current = 'called_screen'
        self.root.transition.direction = 'left'
        print('swiped')
        self.scheduled_switch = None

推荐阅读