Python Kivy:正确启动更新GUI元素的后台进程

9
我有一个Python脚本,用于对用户的文件进行一些密集的处理,可能需要一些时间。我使用Kivy构建了一个用户界面,允许用户选择文件、处理模式,并在处理过程中显示一些消息。
我的问题是,当主Kivy循环调用底层用户界面时,窗口会冻结。
据我所知,解决这个问题的正确方法是创建一个单独的进程,将脚本卸载到其中,并从中向用户界面发送更新。
然而,我找不到如何做这件事的示例,也没有关于如何从单独的线程将消息发送回应用程序的规范。
请问是否能给出正确的示例或者指向相关文档?
更新:
为了保持程序的可维护性,我想避免从主线程调用循环或处理器的元素,而是调用一个长时间的进程,该进程会返回更新后的GUI元素,例如进度条或文本字段。看起来这些元素只能从主kivy线程修改。我如何从外部访问它们?
3个回答

5

请按照此处所述,使用发布者/消费者模型。以下是该链接中的示例,经过修改以使用单独的线程:

from kivy.app import App
from kivy.clock import Clock, _default_time as time  # ok, no better way to use the same clock as kivy, hmm
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.uix.button import Button
from kivy.properties import ListProperty

from threading import Thread
from time import sleep

MAX_TIME = 1/60.

kv = '''
BoxLayout:
    ScrollView:
        GridLayout:
            cols: 1
            id: target
            size_hint: 1, None
            height: self.minimum_height

    MyButton:
        text: 'run'

<MyLabel@Label>:
    size_hint_y: None
    height: self.texture_size[1]
'''

class MyButton(Button):
    def on_press(self, *args):
        Thread(target=self.worker).start()

    def worker(self):
        sleep(5) # blocking operation
        App.get_running_app().consommables.append("done")

class PubConApp(App):
    consommables = ListProperty([])

    def build(self):
        Clock.schedule_interval(self.consume, 0)
        return Builder.load_string(kv)

    def consume(self, *args):
        while self.consommables and time() < (Clock.get_time() + MAX_TIME):
            item = self.consommables.pop(0)  # i want the first one
            label = Factory.MyLabel(text=item)
            self.root.ids.target.add_widget(label)

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

这是一个不错的例子,我已经阅读了它们的文档。问题在于我必须从主应用程序中分离出对底层脚本的调用。这正是我想避免的。我有一个观察者来监视消息并将它们发送回GUI,但它不会添加GUI元素,它只修改元素的属性,例如进度条值或文本字段文本。在这种情况下,似乎线程的信号无法返回到主线程。我该如何规避这个问题? - chiffa
您可以尝试使用kivy.clock.mainthread装饰器在主线程中调度回调函数的调用。一些示例: https://github.com/kivy/kivy/wiki/Working-with-Python-threads-inside-a-Kivy-application - Nykakin

4

我认为提供一个2022年的更新是值得的。现在,可以通过Python的内置asyncio库和工具运行Kivy应用程序。以前的问题是,在异步函数调用完成时没有办法将控制返回到主Kivy事件循环,因此无法更新GUI。现在,Kivy在与任何其他asyncio可等待对象相同的事件循环中运行(相关文档)。

要异步运行应用程序,请将main.py底部的YourAppClass().run()替换为以下内容:

import asyncio

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(
        YourAppClass().async_run()
    )
    loop.close()

就这些内容。关于文档:

在相同的异步事件循环中运行的其他协程可以完全安全地与任何Kivy对象交互,因为它们都是从同一线程运行的,而且只有在Kivy处于空闲状态时才会执行其他协程。

类似地,Kivy回调可以安全地与在同一事件循环中运行的其他协程中的对象交互。对于这两种情况,都适用于普通的单线程规则。

如果需要显式地创建新线程,则@Nykakin的方法是您想要的。我建议使用队列来在线程之间传递数据,因为它们更简单易用,并且专门为此目的设计,更加稳健。如果您只是想要实现异步性,则async_run()是您的好朋友。


非常有帮助,谢谢。这展示了如何使用asyncio启动Kivy应用程序,但我找不到任何实际调用Kivy应用程序中异步方法的示例。例如,我如何绑定一个按钮小部件来使用asyncio.create_task启动长时间运行的后台任务? - mihow

2

注意:虽然在另一个线程中修改kivy属性通常可以工作,但有很多迹象表明这不是一个线程安全的操作。(使用调试器并在后台线程中逐步执行append函数。)修改kivy属性时不应以此方式进行修改


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接