6

I have a GUI with a progressbar. It should show the progress of the work a second thread does. I would like to have something like an event the thread can send to the GUIs progressbar immediatly on each step of the work. But I don't see how this could be done.

Python itself offers a Event class for threading situations. But it would block the GUI main thread because of the Event.wait() methode.

How does it change the situaton and possible solutions if the second thread is a process?

My example here is based on PyGObject (Pythons Gtk) but is related to all other GUI libraries, too. The current solution works but it is IMO just a workaround. The GUI (as main thread) and the second (worker) thread sharing data via a threadsafe queue.Queue. There is a timer event in the GUI thread checking the qeueu in **fixed intervalls* for new data from the thread and updates the progressbar.

#!/usr/bin/env python3
import time
import threading
import queue
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib


class MyThread(threading.Thread):
    def __init__(self, queue, n_tasks):
        threading.Thread.__init__(self)
        self._queue = queue
        self._max = n_tasks

    def run(self):
        for i in range(self._max):
            # simulate a task 
            time.sleep(1)
            # put something in the data queue
            self._queue.put(1)


class MyWindow(Gtk.Window):
    def __init__(self, n_tasks):
        Gtk.Window.__init__(self)

        # max and current number of tasks
        self._max = n_tasks
        self._curr = 0

        # queue to share data between threads
        self._queue = queue.Queue()

        # gui: progressbar
        self._bar = Gtk.ProgressBar(show_text=True)
        self.add(self._bar)
        self.connect('destroy', Gtk.main_quit)

        # install timer event to check the queue for new data from the thread
        GLib.timeout_add(interval=250, function=self._on_timer)
        # start the thread
        self._thread = MyThread(self._queue, self._max)
        self._thread.start()

    def _on_timer(self):
        # if the thread is dead and no more data available...
        if not self._thread.is_alive() and self._queue.empty():
            # ...end the timer
            return False

        # if data available
        while not self._queue.empty():
            # read data from the thread
            self._curr += self._queue.get()
            # update the progressbar
            self._bar.set_fraction(self._curr / self._max)

        # keep the timer alive
        return True

if __name__ == '__main__':
    win = MyWindow(30)
    win.show_all()
    Gtk.main()
buhtz
  • 10,774
  • 18
  • 76
  • 149
  • 1
    for me it is correct solution, not workaround. – furas Apr 26 '19 at 13:45
  • 1
    Try `MyThread(self._bar, self._max)`, and in `def run(self): ... self.bar.set_fraction(...`. – stovfl Apr 26 '19 at 14:57
  • @stovfl But isn't this thread-**un**save? – buhtz Apr 27 '19 at 01:30
  • 1
    @buhtz: *"thread-unsave?"*: Define *thread-unsave*? As long as your `Thread` is the **only one** that calls `self.bar.set_fraction(...` there is no conflict. Try it, you get either a error or it works. – stovfl Apr 27 '19 at 09:40
  • @stovfl He means thread-unsafe. And yes, `threading` may not update the progressbar. The Gtk main loop needs to update the progressbar (via the `GLib.timeout_add()` ). – theGtknerd Apr 28 '19 at 01:43
  • 1
    The bad part about updating a widget via a thread is that it will randomly cause segmentation faults and other nightmares. Read up on GUI main loops. – theGtknerd Apr 28 '19 at 01:45
  • Read [wiki.gnome.org](https://wiki.gnome.org/Projects/PyGObject/Threading). This example do it **without** `Queue` and `_on_timer(...`. – stovfl Apr 28 '19 at 10:44
  • I am confused. I don't get the essence of the wiki-example you linked, too. Doesn't it udate the progress bar from the thread, too? It would help my and future readers understanding if you could adapt my example to your solution. – buhtz Apr 29 '19 at 04:34
  • 1
    @buhtz: *" don't get the essence"*: The usage of `GLib.idle_add(update_progress, i)` which worked like your `self._queue.put(1)`. Function `def example_target()` is your `def run(self):`, running in a `Thread`. – stovfl Apr 29 '19 at 07:19
  • @stovlf It woul improve the outcome of this question and fit better to he stackoverflow qualitiy if you would adapt my example and explain it a bit more. – buhtz Apr 29 '19 at 20:01

2 Answers2

3

Based on the comments of my question I modified my example. Please use this with caution because it is still unclear for me if the solution is thread safe or not.

I tried the GLib.idle_add() and removed my own timer and the queue. Attention: The docu is no correct about the signature/parameters of the methode. Originaly it is

idle_add(function, *user_data, **kwargs)

Possible solution for Threads

#!/usr/bin/env python3
import time
import threading
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib


class MyThread(threading.Thread):
    def __init__(self, callback, n_tasks):
        threading.Thread.__init__(self)
        self._callback = callback
        self._max = n_tasks

    def run(self):
        for i in range(self._max):
            # simulate a task 
            time.sleep(1)
            # increment/update progress
            GLib.idle_add(self._callback)


class MyWindow(Gtk.Window):
    def __init__(self, n_tasks):
        Gtk.Window.__init__(self)

        # max and current number of tasks
        self._max = n_tasks
        self._curr = 0

        # gui: progressbar
        self._bar = Gtk.ProgressBar(show_text=True)
        self.add(self._bar)
        self.connect('destroy', Gtk.main_quit)

        # start the thread
        self._thread = MyThread(self._update_progress, self._max)
        self._thread.start()

    def _update_progress(self):
        # increment
        self._curr += 1
        # update the progressbar
        self._bar.set_fraction(self._curr / self._max)

        # end this event handler
        return False

if __name__ == '__main__':
    win = MyWindow(30)
    win.show_all()
    Gtk.main()

What GLib.idle_add() does?

I am not an expert or core developer of Gtk. In my understanding I would say you can _install__ event handler metodes into the Gtk main loop. In other words the second thread tells the Gtk main loop (which is the first thread) to call the givin methode when nothing else is todo (which is quit often in a GUI loop).

There is no solution for Process because they run in a separate Python interpreter instance. There is no way to call GLib.idle_add() between two process.

buhtz
  • 10,774
  • 18
  • 76
  • 149
  • 1
    *"The docu is no correct about the signature/parameters"*: You can do `GLib.idle_add(.set_fraction, fraction)` which makes `def _update_progress(self):` unneeded. – stovfl May 05 '19 at 09:30
2

Your implementation is correct. You are processing the threaded commands, sharing the feedback in a Queue, and updating the progressbar from the main loop via the GLib.timeout_add().

Initially, this may seem like a complex way to do a simple progressbar update, but it is one of the only ways to spawn a child process and track the progress, all the while respecting the Gtk main loop.

theGtknerd
  • 3,647
  • 1
  • 13
  • 34