Python Kivy: Properly start a background process that updates GUI elements
Asked Answered
B

3

9

I have a Python script that performs some intensive processing of user's files and can take some time. I've build a user interface to it using Kivy, that allows the user to select the file, processing mode and shows them some messages as the process goes on.

My problem is that when the main Kivy loop passes calls the underlying user interface, the window freezes.

From what I've understood, the proper way of resolving this is to create a separate process to which the script would be off-loaded and from which it would send the updates to the user interface.

However, I was not able to find an example of how to do this or any specification on how to send messages from a separate thread back into application.

Could someone please give an example of how to do this properly or point me to the documentation pertaining to the subject?

Update:

For the sake of keeping the program maintainable I would like to avoid calling the elements of loops of processor from the main thread and instead call one long process that comes back to updated elements of the GUI, such as the progress bar or a text field. It looks like those elements can be modified only from the main kivy thread. How do I gain access to them from the outside?

Brabant answered 10/10, 2014 at 15:11 Comment(0)
O
5

Use publisher/consumer model as described here. Here's an example from that link modified to use separate threads:

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()
Omphalos answered 10/10, 2014 at 17:48 Comment(2)
This is a nice example and I've read their doc. The problem is that I have to fragment the calls to the underlying scripts from the main application. This is specifically what I want to avoid. I have an observer that monitors the messages and sends them back to the GUI, but it doesn't add elements GUI, it just modifies the properties of the elements, such as the advancement bar value or text field text. It seems that in this case the signal from thread cannot get back to main thread. How do I circumvent this?Brabant
You can try to schedule the call of your callback in the mainthread using kivy.clock.mainthread decorator. Some example: github.com/kivy/kivy/wiki/…Omphalos
F
4

I think it's worth providing a 2022 update. Kivy apps can now be run via Python's builtin asyncio library and utilities. Previously, the problem was there was no way to return control to the main Kivy event loop when an async function call finished, hence you could not update the GUI. Now, Kivy runs in the same event loop as any other asyncio awaitables (relevant docs).

To run the app asynchronously, replace the YourAppClass().run() at the bottom of your main.py with this:

import asyncio

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

And that's about it. With regards to the docs:

It is fully safe to interact with any kivy object from other coroutines running within the same async event loop. This is because they are all running from the same thread and the other coroutines are only executed when Kivy is idling.

Similarly, the kivy callbacks may safely interact with objects from other coroutines running in the same event loop. Normal single threaded rules apply to both case.

If explicitly need to create a new thread, @Nykakin 's approach is what you want. I'd recommend using Queues to pass data between threads, instead, because they're simpler to implement and more robust, being specifically designed for this purpose. If you just want asynchronicity, async_run() is your best friend.

Fabrianne answered 8/4, 2022 at 16:22 Comment(1)
This is very helpful thank you. This shows how to start a Kivy app with asyncio, however I cannot find any examples of how to actually call an async method within the Kivy app. For example, how can I bind a button widget to start a long running background task using asyncio.create_task?Roxannroxanna
S
2

BE WARNED: While modifying a kivy property from another thread nominally works, there is every indication that this is not a thread safe operation. (Use a debugger and step through the append function in the background thread.) Altering a kivy property from another thread states that you should not modify a property in this way.

Swedenborgian answered 15/2, 2018 at 18:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.