Search code examples
pygtkgtk3pygobject

Proper way force refresh of window in GTK+ 3 using PyGObject?


I'm writing my first GTK+ 3 application in Python using PyGObject.

I have a task where I want user to click on a Start button and then I want update a text field with a count down that displays "3", "2", "1", "Go!" in succession with a pause in between.

If within the button callback method I change the text in between pauses it only shows the final change which I learned is because the window is only redrawn when it gets back to the main loop. My thought was then that I needed to force it to redraw.

I found some old PyGTK examples to do this using:

while Gtk.events_pending():
    Gtk.main_iteration()

and now it kind of works but it dosen't reddraw the unpressed button until the 2 is displayed and in a different version it always misses drawing the 3. Maybe the events are not yet pending? I'm also not sure if this would be the "proper" way to do what I'm trying to do. I actually have a bunch of linear tasks that need to be conducted after the button is pushed that require screen updates, this is just a simple example.

Here is the code as it is now:

from gi.repository import Gtk, GdkPixbuf, Gdk, GLib
import Image
import os, sys
import time

class GUI:
    def __init__(self):
        self.window=Gtk.Window()
        self.window.set_title("Countdown")
        self.window.set_border_width(10)
        self.window.connect_after('destroy', self.destroy)

        # main container of projct
        self.main_box=Gtk.VBox()
        self.main_box.set_spacing(5)

        # countdown label
        self.countdown_label = Gtk.Label()

        # start button
        self.start_button=Gtk.Button("Start!")

        # add the elements to the window
        self.window.add(self.main_box)
        self.main_box.pack_start(self.countdown_label, False, False, 0)
        self.main_box.pack_start(self.start_button, False, False, 0)

        # connect buttons
        self.start_button.connect_after('clicked', self.start_clicked)

        self.window.show_all()

    def start_clicked(self, button):
        # start the countdown
        count=3
        while count > 0:
            self.countdown(count)
            count = count - 1
            while Gtk.events_pending():
                Gtk.main_iteration()
            time.sleep(2)

        self.countdown_label.set_text("Go!")

    def countdown(self, count):
        self.countdown_label.set_text(str(count))
        print count
        return

    def destroy(window, self):
        Gtk.main_quit()

def main():
    app=GUI()
    Gtk.main()

if __name__ == "__main__":
    sys.exit(main())

Solution

  • Needing to use Gtk.events_pending() and Gtk.main_iteration() is generally a red flag in terms of GTK+ programming. Fixing this particular example, an asynchronous timeout which does not block the main loop can be used [1]:

    def start_clicked(self, button):
        self.countdown(3)
    
    def countdown(self, count):
        if count > 0:
            self.countdown_label.set_text(str(count))
            # Manually re-add this callback with a decremented count.
            GLib.timeout_add_seconds(2, self.countdown, count - 1)
        else:
            self.countdown_label.set_text("Go!")
    
        # Return False so the callback is not called repeatedly
        # as we manually re-added the callback ourselves above.
        return False
    

    In a real world situation, and as mentioned "a bunch of linear tasks that need to be conducted", the answer depends on the type of task. IO bound tasks which may block the UI can use the Gio library to maintain UI responsiveness [2]. For CPU bound tasks which block the UI, a Python thread [3] can be used in combination with UI update callbacks scheduled with Gdk.threads_add_idle or Gdk.threads_add_timeout [4].

    See also:

    1. http://lazka.github.io/pgi-docs/api/GLib-2.0/functions.html#GLib.timeout_add_seconds
    2. http://lazka.github.io/pgi-docs/api/Gio-2.0/
    3. https://wiki.gnome.org/Projects/PyGObject/Threading
    4. http://lazka.github.io/pgi-docs/api/Gdk-3.0/functions.html#Gdk.threads_add_idle