'Python Kivy: Properly start a background process that updates GUI elements
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?
Solution 1:[1]
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()
Solution 2:[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.
Solution 3:[3]
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Nykakin |
| Solution 2 | mpwhsv |
| Solution 3 | PyUnchained |
