I want to have a Python (3) GTK (3) application working as follows:
Figure 1. Views of the application: A, Initial view. B, Progress view. C, Lunch ready view. D, Lunch burnt view.
-
User clicks on “Make lunch” (view A)
-
App displays Progress view (view B)
-
App starts a
make_lunch
[blocking] operation in the background -
Depending on
make_lunch
result:- App displays Lunch ready view (view C) or
- App displays Lunch burnt view (view D)
What I’ve tried
I started by writing a synchronous application (see App: synchronous version below). In this version, the GUI gets blocked in the Initial view (view A) when the user clicks the “Make lunch” button. The view is blocked for as long as the make_lunch
operation lasts (5 seconds). The Progress view (view B) is never displayed. Instead, it goes directly from view A to any of the result views, C or D, depending on the outcome of the blocking operation. All this, obviously, is what I want to avoid.
If I understand GNOME Developer documentation correctly (see Threads and Asynchronous programming), I need to use Gio.Task
(aka GTask
) to run the blocking operation in a worker thread.
So, I started translating the standard pattern for asynchronous calls given in the C examples on those documents into Python (see App: Asynchronous version below), but I didn’t get far because I’m not familiar with C and because I started getting errors like
ValueError: Pointer arguments are restricted to integers,
capsules, and None. See:
https://bugzilla.gnome.org/show_bug.cgi?id=683599
when calling different methods of the Gio.Task
object, like
task.set_source_tag(self.make_lunch_async)
task.set_task_data(task_data)
task.run_in_thread(self.make_lunch_async_callback)
I couldn’t get much from the bug report linked from the error messages, so I am stuck.
App: Synchronous version
Here is a blocking version of the application to use as a starting point, an app.py
file and an app-window.ui
file, which are to be saved in the same directory as follows:
my-blocking-app/
├── app.py
└── app-window.ui
Python code (save as app.py
):
import random
import time
import sys
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gio, Gtk
# VIEWS
class App(Gtk.Application):
def __init__(self):
Gtk.Application.__init__(
self,
application_id="org.example.app",
flags=Gio.ApplicationFlags.FLAGS_NONE
)
def do_activate(self):
# Show default application window.
window = AppWindow(self)
self.add_window(window)
window.show_all()
@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
__gtype_name__ = "AppWindow"
start_button = Gtk.Template.Child("start-button")
operation_box = Gtk.Template.Child("operation-box")
progress_box = Gtk.Template.Child("progress-box")
spinner = Gtk.Template.Child("spinner")
success_box = Gtk.Template.Child("success-box")
success_label = Gtk.Template.Child("success-label")
failure_box = Gtk.Template.Child("failure-box")
failure_label = Gtk.Template.Child("failure-label")
back_button = Gtk.Template.Child("back-button")
def __init__(self, app):
super().__init__()
self.set_application(app)
@Gtk.Template.Callback()
def on_start_button_clicked(self, button):
"""Handle the BUTTON's clicked signal."""
self.operation_box.set_visible(False)
self.progress_box.set_visible(True)
self.spinner.start()
# FIXME: Blocking operation. Run asynchronously.
cook = Cook()
result = cook.make_lunch("rice", "lentils", "carrots")
# Show result.
self.show_result(result)
@Gtk.Template.Callback()
def on_back_button_clicked(self, button):
"""Handle the BUTTON's clicked signal."""
self.operation_box.set_visible(True)
self.success_box.set_visible(False)
self.failure_box.set_visible(False)
button.set_visible(False)
def show_result(self, result):
"""Update application according to result."""
self.progress_box.set_visible(False)
self.back_button.set_visible(True)
if isinstance(result, Plate):
message = "Lunch is ready: {}".format(result)
self.success_label.set_text(message)
self.success_box.set_visible(True)
else:
message = result.get("Error")
self.failure_label.set_text(message)
self.failure_box.set_visible(True)
# MODELS
class Plate():
def __init__(self, ingredients):
self.ingredients = ingredients
def __str__(self):
return ", ".join(self.ingredients)
class Cook():
def make_lunch(self, *ingredients):
time.sleep(5)
outcomes = [
Plate(ingredients),
{"Error": "Lunch is burned!!"}
]
return random.choice(outcomes)
# RUN APP
if __name__ == "__main__":
app = App()
exit_status = app.run(sys.argv)
sys.exit(exit_status)
XML UI (save as app-window.ui
):
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<template class="AppWindow" parent="GtkApplicationWindow">
<property name="can-focus">False</property>
<property name="window-position">center</property>
<property name="default-width">300</property>
<property name="default-height">200</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="operation-box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">emoji-food-symbolic</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Making lunch takes 5 seconds.</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="start-button">
<property name="label" translatable="yes">Make lunch</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_start_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="progress-box">
<property name="can-focus">False</property>
<property name="no-show-all">True</property>
<property name="halign">center</property>
<property name="spacing">12</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">emoji-food-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Making lunch...</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSpinner" id="spinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="success-box">
<property name="can-focus">False</property>
<property name="no-show-all">True</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">emoji-nature-symbolic</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="success-label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Lunch is ready!!</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="failure-box">
<property name="can-focus">False</property>
<property name="no-show-all">True</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">dialog-error-symbolic</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="failure-label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Error: message.</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton" id="back-button">
<property name="label" translatable="yes">Back</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="no-show-all">True</property>
<property name="margin-top">12</property>
<signal name="clicked" handler="on_back_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<object class="GtkHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Async</property>
<property name="subtitle" translatable="yes">Async operation with GTask</property>
<property name="show-close-button">True</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
</template>
</interface>
Running the app requires the following software in the environment:
gobject-introspection
gtk3
python3
pygobject
App: Asynchronous version (not working)
This adds the methods AppWindow.make_lunch_async
, AppWindow.make_lunch_finish
and AppWindow.make_lunch_async_callback
, trying to mimic the standard pattern for async operation, but couldn’t even define a working make_lunch_async
because of the errors indicated in the comments. Don’t know how to properly translate the C into Python.
import random
import time
import sys
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gio, Gtk
# VIEWS
class App(Gtk.Application):
def __init__(self):
Gtk.Application.__init__(
self,
application_id="org.example.app",
flags=Gio.ApplicationFlags.FLAGS_NONE
)
def do_activate(self):
# Show default application window.
window = AppWindow(self)
self.add_window(window)
window.show_all()
@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
__gtype_name__ = "AppWindow"
start_button = Gtk.Template.Child("start-button")
operation_box = Gtk.Template.Child("operation-box")
progress_box = Gtk.Template.Child("progress-box")
spinner = Gtk.Template.Child("spinner")
success_box = Gtk.Template.Child("success-box")
success_label = Gtk.Template.Child("success-label")
failure_box = Gtk.Template.Child("failure-box")
failure_label = Gtk.Template.Child("failure-label")
back_button = Gtk.Template.Child("back-button")
def __init__(self, app):
super().__init__()
self.set_application(app)
@Gtk.Template.Callback()
def on_start_button_clicked(self, button):
"""Handle the BUTTON's clicked signal."""
self.operation_box.set_visible(False)
self.progress_box.set_visible(True)
self.spinner.start()
# Make lunch asynchronously.
self.make_lunch_async(
Cook(),
("rice", "lentils", "carrots"),
None, # Operation is not cancellable.
self.make_lunch_async_callback,
None # No aditional data for the callback
)
def make_lunch_async_callback(self, task, source_object, task_data, cancellable):
"""Handle the result of the async operation."""
cook = task_data.get("cook")
ingredients = task_data.get("ingredients")
result = cook.make_lunch(*ingredients)
# Show result (should I call this here?).
self.show_result(result)
def make_lunch_async(self, cook, ingredients, cancellable, callback, callback_data):
"""Schedule async operation and invoke callback when operation
is done."""
task = Gio.Task.new(
self,
cancellable,
callback,
callback_data
)
task.set_source_tag(self.make_lunch_async) # FIXME.
# Previous line fails with:
#
# ValueError: Pointer arguments are restricted to integers,
# capsules, and None. See:
# https://bugzilla.gnome.org/show_bug.cgi?id=683599
# Cancellation should be handled manually using mechanisms
# specific to the blocking function.
task.set_return_on_cancel(False)
# Set up a closure containing the call’s parameters. Copy them
# to avoid locking issues between the calling thread and the
# worker thread.
task_data = {"cook": cook, "ingredients": ingredients}
task.set_task_data(task_data) # FIXME.
# Previous line fails with:
#
# ValueError: Pointer arguments are restricted to integers,
# capsules, and None. See:
# https://bugzilla.gnome.org/show_bug.cgi?id=683599
# Run the task in a worker thread and return immediately while
# that continues in the background. When it’s done it will call
# @callback in the current thread default main context.
task.run_in_thread(self.make_lunch_async_callback)
def make_lunch_finish(self, result, error):
"""What's the purpose of this method."""
pass
@Gtk.Template.Callback()
def on_back_button_clicked(self, button):
"""Handle the BUTTON's clicked signal."""
self.operation_box.set_visible(True)
self.success_box.set_visible(False)
self.failure_box.set_visible(False)
button.set_visible(False)
def show_result(self, result):
"""Update application according to result."""
self.progress_box.set_visible(False)
self.back_button.set_visible(True)
if isinstance(result, Plate):
message = "Lunch is ready: {}".format(result)
self.success_label.set_text(message)
self.success_box.set_visible(True)
else:
message = result.get("Error")
self.failure_label.set_text(message)
self.failure_box.set_visible(True)
# MODELS
class Plate():
def __init__(self, ingredients):
self.ingredients = ingredients
def __str__(self):
return ", ".join(self.ingredients)
class Cook():
def make_lunch(self, *ingredients):
time.sleep(5)
outcomes = [
Plate(ingredients),
{"Error": "Lunch is burned!!"}
]
return random.choice(outcomes)
# RUN APP
if __name__ == "__main__":
app = App()
exit_status = app.run(sys.argv)
sys.exit(exit_status)