Getting started using GTK+3 with Python.

Recently (Early part of 2017) I have taken a long overdue step and started to migrate some of my C programmes from GTK+2 to GTK+3. Some time ago I took a look at using GTK+2 with Python so I have also looked at using GTK+3 with Python, and quite a lot has changed.

NOTE: This is NOT a Python tutorial, it assumes you are already familiar with programming in Python.

The interface between GTK+3 and other languages is now provided by "GObject Introspection" and in the case of Python by PyGObject

Installing PyGObject

On Debian based systems like Raspbian the installation is quite simple.
sudo apt install python-gi python-gi-cairo python3-gi python3-gi-cairo gir1.2-gtk-3.0

Instructions for other Linux distributions and platforms can be found on the PyGObject web site

Don't just import Gtk !

Using GObject Introspection involves a slightly more complex process to import the required modules into your Python programme. Just three lines of code are needed at the start of every Python programme that uses GTK+3, but they will always be the same three lines !

import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk

"Hello World"

Here is the traditional "Your first programme" which server as a test that all the required packages are installed and working correctly. It simply creates a small window and waits for you to close it.

#!/usr/bin/python3
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk

window = Gtk.Window(title="Hello World")
window.show()
window.connect("delete-event", Gtk.main_quit)
Gtk.main()

Note that along with the blank area (which is the actual GtkWindow) the window manger has has added the familiar title bar and hide,expand and close buttons. Adding these items to a window is called "adding window decoration".

GTK+ Documentation

The main GTK+3 documentation describes the C language library interface, but the information about the individual widget types is applicable to the Python language interface. So for example the page describing GtkButtons says..

"The GtkButton widget is generally used to trigger a callback function that is called when the button is pressed. The various signals and how to use them are outlined below.
The GtkButton widget can hold any valid child widget. That is, it can hold almost any other standard GtkWidget. The most commonly used child is the GtkLabel."

This is also true when using Python.

Documentation on the interface to GTK+3 provided by PyGObject can be found here

Let's dive into an example...

This example is going to cover quite a lot of stuff, but by the end you'll understand the basics and be in a good place to create your own GTK+ applications.

The demo is visually quite simple.

The large red dot is either OFF (Dull Red, shown in the window on the left) or ON (Bright Red, shown in the window on the right). Pressing the button labelled "ON" turns the dot on, and pressing the button labelled "OFF" turns it off ! Pressing the button labelled "Quit" has a similarly predictable effect.

Using the Glade GUI design tool.

The window design was created using Glade. From the Glade web site .....

"What is Glade?
Glade is a RAD tool to enable quick & easy development of user interfaces for the GTK+ toolkit and the GNOME desktop environment.
The user interfaces designed in Glade are saved as XML, and by using the GtkBuilder GTK+ object these can be loaded by applications dynamically as needed."

Glade is itself a GTK+ application, and it allows application windows to be designed by dragging and dropping widgets from a pallet onto a version of the application's window. The window layout shown in Glade may not be exactly the same as that created when the application is run, but it will be the same in all the important details.

Part of the Glade display is a "widget tree" which shows how the various widgets are related to each other.

You can see that at the root of the tree (shown at the top) is a GtkWindow (unimaginatively called "window1"). While it might sound like a major limitation, a GtkWindow itself can only contain one other widget, in this case a GtkBox called "box1". A GtkBox is a type of container (more about the GTK widget types later) and can contain any number of other widgets.

At the leaves of the widget tree are a GtkDrawingArea and three GtkButtons. You can see that the buttons are contained in a GtkButtonBox which manages the layout of the buttons.

The GtkDrawingArea and GtkButtonBox are contained within their own GtkFrames which allow them to be easily labelled.

The xml file (demo1.glade) produced by Glade is shown below. Don't worry, there is no need to understand it all, but you shouldbe able to see how the widget tree hierarchy has been translated into objects with the appropriate class names and properties.

001: <?xml version="1.0" encoding="UTF-8"?>
002: <!-- Generated with glade 3.16.1 -->
003: <interface>
004:   <requires lib="gtk+" version="3.10"/>
005:   <object class="GtkWindow" id="window1">
006:     <property name="visible">True</property>
007:     <property name="can_focus">False</property>
008:     <property name="gravity">center</property>
009:     <signal name="delete-event" handler="on_window1_delete_event" swapped="no"/>
010:     <child>
011:       <object class="GtkBox" id="box1">
012:         <property name="visible">True</property>
013:         <property name="can_focus">False</property>
014:         <property name="orientation">vertical</property>
015:         <child>
016:           <object class="GtkFrame" id="frame1">
017:             <property name="visible">True</property>
018:             <property name="can_focus">False</property>
019:             <property name="label_xalign">0</property>
020:             <property name="shadow_type">out</property>
021:             <child>
022:               <object class="GtkAlignment" id="alignment1">
023:                 <property name="visible">True</property>
024:                 <property name="can_focus">False</property>
025:                 <property name="left_padding">12</property>
026:                 <child>
027:                   <object class="GtkDrawingArea" id="drawingarea1">
028:                     <property name="width_request">200</property>
029:                     <property name="height_request">200</property>
030:                     <property name="visible">True</property>
031:                     <property name="can_focus">False</property>
032:                     <signal name="draw" handler="on_drawingarea1_draw" swapped="no"/>
033:                   </object>
034:                 </child>
035:               </object>
036:             </child>
037:             <child type="label">
038:               <object class="GtkLabel" id="label1">
039:                 <property name="visible">True</property>
040:                 <property name="can_focus">False</property>
041:                 <property name="label" translatable="yes">GtkDrawingArea</property>
042:               </object>
043:             </child>
044:           </object>
045:           <packing>
046:             <property name="expand">True</property>
047:             <property name="fill">True</property>
048:             <property name="position">0</property>
049:           </packing>
050:         </child>
051:         <child>
052:           <object class="GtkFrame" id="frame2">
053:             <property name="visible">True</property>
054:             <property name="can_focus">False</property>
055:             <property name="label_xalign">0</property>
056:             <property name="shadow_type">out</property>
057:             <child>
058:               <object class="GtkAlignment" id="alignment2">
059:                 <property name="visible">True</property>
060:                 <property name="can_focus">False</property>
061:                 <property name="left_padding">12</property>
062:                 <child>
063:                   <object class="GtkButtonBox" id="buttonbox1">
064:                     <property name="visible">True</property>
065:                     <property name="can_focus">False</property>
066:                     <property name="layout_style">start</property>
067:                     <child>
068:                       <object class="GtkButton" id="onButton">
069:                         <property name="label" translatable="yes">ON</property>
070:                         <property name="visible">True</property>
071:                         <property name="can_focus">True</property>
072:                         <property name="receives_default">True</property>
073:                         <signal name="clicked" handler="on_onButton_clicked" swapped="no"/>
074:                       </object>
075:                       <packing>
076:                         <property name="expand">True</property>
077:                         <property name="fill">True</property>
078:                         <property name="position">0</property>
079:                       </packing>
080:                     </child>
081:                     <child>
082:                       <object class="GtkButton" id="offButton">
083:                         <property name="label" translatable="yes">OFF</property>
084:                         <property name="visible">True</property>
085:                         <property name="can_focus">True</property>
086:                         <property name="receives_default">True</property>
087:                         <signal name="clicked" handler="on_offButton_clicked" swapped="no"/>
088:                       </object>
089:                       <packing>
090:                         <property name="expand">True</property>
091:                         <property name="fill">True</property>
092:                         <property name="position">1</property>
093:                       </packing>
094:                     </child>
095:                     <child>
096:                       <object class="GtkButton" id="quitButton">
097:                         <property name="label">gtk-quit</property>
098:                         <property name="visible">True</property>
099:                         <property name="can_focus">True</property>
100:                         <property name="receives_default">True</property>
101:                         <property name="use_stock">True</property>
102:                         <property name="always_show_image">True</property>
103:                         <signal name="clicked" handler="on_quitButton_clicked" swapped="no"/>
104:                       </object>
105:                       <packing>
106:                         <property name="expand">True</property>
107:                         <property name="fill">True</property>
108:                         <property name="position">2</property>
109:                       </packing>
110:                     </child>
111:                   </object>
112:                 </child>
113:               </object>
114:             </child>
115:             <child type="label">
116:               <object class="GtkLabel" id="label2">
117:                 <property name="visible">True</property>
118:                 <property name="can_focus">False</property>
119:                 <property name="label" translatable="yes">GtkButtonBox</property>
120:               </object>
121:             </child>
122:           </object>
123:           <packing>
124:             <property name="expand">False</property>
125:             <property name="fill">True</property>
126:             <property name="position">1</property>
127:           </packing>
128:         </child>
129:       </object>
130:     </child>
131:   </object>
132: </interface>
133: 

You can get this glade file from here.

Let's take a closer look at just one of the widget descriptions, the "ON" button.


067:                     <child>
068:                       <object class="GtkButton" id="onButton">
069:                         <property name="label" translatable="yes">ON</property>
070:                         <property name="visible">True</property>
071:                         <property name="can_focus">True</property>
072:                         <property name="receives_default">True</property>
073:                         <signal name="clicked" handler="on_onButton_clicked" swapped="no"/>
074:                       </object>
075:                       <packing>
076:                         <property name="expand">True</property>
077:                         <property name="fill">True</property>
078:                         <property name="position">0</property>
079:                       </packing>
080:                     </child>
 

For this demo I have provided the complete glade file ready to be used. I'll cover actually using Glade later on.

Building the widgets using the .glade file

GTK+ includes a type called a GtkBuilder. The manual page describes it like this ..

"A GtkBuilder is an auxiliary object that reads textual descriptions of a user interface and instantiates the described objects. To create a GtkBuilder from a user interface description, call gtk_builder_new_from_file(), gtk_builder_new_from_resource() or gtk_builder_new_from_string()."

In C the code to use our glade file would be

01: #include <gtk/gtk.h>
02: 
03: GtkBuilder *builder = NULL;
04: 
05: int main(int argc,char **argv)
06: {
07:     GtkWidget *window;
08: 
09:     gtk_init(&argc,&argv);
10: 
11:     builder = gtk_builder_new_from_file ("demo1.glade");
12: 
13:     window  = GTK_WIDGET (gtk_builder_get_object (builder,"window1"));
14: 
15:     g_signal_connect(window, "delete_event", G_CALLBACK(gtk_main_quit), NULL);
16: 
17:     gtk_main();
18: }
19: 

In Python this becomes..

01: #!/usr/bin/python3
02: 
03: import gi
04: gi.require_version('Gtk', '3.0')
05: from gi.repository import Gtk
06: 
07: builder = Gtk.Builder.new_from_file("demo1.glade")
08: 
09: window = builder.get_object("window1")
10: window.connect("delete-event", Gtk.main_quit)
11: 
12: Gtk.main()
13: 

An important thing to note is that

builder = gtk_builder_new_from_file ("demo1.glade");
has become
builder = Gtk.Builder.new_from_file("demo1.glade")

That pattern of changes will come up again and again as we look at more of feature provided by GTK+3.

So far there is only the code to create the interface and deal correctly with the window being closed, so pressing the buttons will not yet do anything.

An aside about "Look and Feel"

The image below show the result of running the same Python code with the same glade file on two different machines.
The left hand window is from a PC running Mint 17.3 with the Mate window manger.
The right hand window comes from a Raspberry Pi running Jessie with the PIXEL desktop configuration.

The differences are really only "cosmetic". They are a consequence of the particular "theme" that is being use to control the overall look of the desktop. Functionally the application behaves the same on both platforms.

Handling Signals (a.k.a. Making the buttons work).

When a user interacts with a widget on the screen, the widget emits a signal. Signals have descriptive names like "clicked" and "key-press-event". While using Glade to design a window's layout we can specify any handlers (a.k.a. callbacks functions) to be called when a widget emits a particular signal. The image below shows part of the Glade display where a handler has been specified for when the "clicked" signal is emitted by the onButton.

There is a naming convention for signal handlers. They start with "on" then the widget name then the signal name, all separated by "_". So the handler for the clicked signal from the onButton is called "on_onButton_clicked". If you go back and look at the xml in the glade file, you can see lines like this :

<signal name="clicked" handler="on_onButton_clicked" swapped="no"/>

But the signal name and handler name are just text strings, they don't actually refer to signals or functions in our Python code. Making the connections between callback function names and actual callback functions is called "connecting the signals" and the GtkBuilder class contains functions for doing this automatically.

In Python there are a number of ways to specify the functions to used as signal handlers. In the examples below all handler functions are put in a class called Handlers. The names of the functions must match the names given in the glade file. Let's start by making the quit button work.

01: #!/usr/bin/python3
02: 
03: import gi
04: gi.require_version('Gtk', '3.0')
05: from gi.repository import Gtk
06: 
07: 
08: class Handlers:
09:     def on_quitButton_clicked(self, *args):
10:         Gtk.main_quit(*args)
11: 
12:     def on_window1_delete_event(self, *args):
13:         return True
14: 
15: 
16: builder = Gtk.Builder.new_from_file("demo1.glade")
17: 
18: builder.connect_signals(Handlers())
19: 
20: 
21: Gtk.main()
22: 

You can see there are actually two signal handlers defined

If you run this code you will see a number of errors produced

~/PyGObject $ ./demo1.2.py 
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/gi/overrides/Gtk.py", line 403, in _full_callback
    handler, args = self._extract_handler_and_args(obj_or_map, handler_name)
  File "/usr/lib/python3/dist-packages/gi/overrides/Gtk.py", line 376, in _extract_handler_and_args
    raise AttributeError('Handler %s not found' % handler_name)
AttributeError: Handler on_offButton_clicked not found
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/gi/overrides/Gtk.py", line 403, in _full_callback
    handler, args = self._extract_handler_and_args(obj_or_map, handler_name)
  File "/usr/lib/python3/dist-packages/gi/overrides/Gtk.py", line 376, in _extract_handler_and_args
    raise AttributeError('Handler %s not found' % handler_name)
AttributeError: Handler on_onButton_clicked not found
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/gi/overrides/Gtk.py", line 403, in _full_callback
    handler, args = self._extract_handler_and_args(obj_or_map, handler_name)
  File "/usr/lib/python3/dist-packages/gi/overrides/Gtk.py", line 376, in _extract_handler_and_args
    raise AttributeError('Handler %s not found' % handler_name)
AttributeError: Handler on_drawingarea1_draw not found

These error messages are produced because there are handlers specified in the glade file that have not yet been implemented in the Handlers class.

Using Cairo (a.k.a. How to draw shapes in a GtkDrawingArea)

Cairo is a 2D drawing library which can be used to draw to a variety of output devices. When it is used with GTK+3 a GtkDrawingArea can act as an output device to render the picture drawn by Cairo.

When the contents of a GtkDrawingArea needs to drawn (or redrawn) the widget emits a "draw" signal. As before the name of handler for such a signal can be specified in the glade file. In the demo code this is called "on_drawingarea1_draw".

GTK+ manages a lot of the nitty-gritty parts of interacting with Cairo, and passes a "Cairo Context" to the draw event handler. Below is a simple draw handler that just fills the drawing area with a dark green colour.

1: class Handlers:
2:  
3:     def on_drawingarea1_draw(self,widget,cr):
4:         cr.set_source_rgb(0.0,0.2,0.0)
5:         cr.paint()
6: 
7: 
8: 

The draw handler gets passed two useful parameter. If you add code to print the types of the objects passed from GTK+ you find that...

widget= <class 'gi.repository.Gtk.DrawingArea'>
cr= <class 'cairo.Context'>

The PyGObject documentation for a Gtk.DrawingArea shows that Gtk.DrawingArea in a subclass of Gtk.Widget, which means that all the methods defined for a Gtk.Widget are also available for a Gtk.DrawingArea. Well see this in use shortly.

With the draw handler code above added to the rest of the code we now get a green background in the drawing area.

Inorder to center the red dot , and to make it the right size, the code needs to discover the dimensions of the GtkDrawingArea. Using the "widget" parameter that is passed into the draw handler, the code can use get_allocated_width() and get_allocated_height() which are part of the widget class.

01: class Handlers:
02:  
03:     def __init__(self):
04:         self.LightOn = False
05: 
06:     def on_drawingarea1_draw(self,widget,cr):
07:         w = widget.get_allocated_width()
08:         h = widget.get_allocated_height()
09:         size = min(w,h)
10: 
11:         cr.set_source_rgb(0.0,0.2,0.0)
12:         cr.paint()
13: 
14:         if self.LightOn == True:
15:             cr.set_source_rgb(1.0,0.0,0.0)
16:         else:
17:             cr.set_source_rgb(0.2,0.0,0.0)
18:         
19:         cr.arc(0.5*w,0.5*h,0.5*size,0.0,6.3)
20:         cr.fill()
21: 
Here's a link to information on the cairo arc command.

Remember that the draw handler is called when ever the image needs to be redrawn. So when the window is resized the draw handler is called, but the allocated dimensions will have been changed and the circle will still be drawn in the middle of the area. Depending on how the window manger is configured, the draw handler might be called repeatedly while the window is being resized, or it might only be called once when the final size is know. My MINT PC has the first behaviour but Raspberry PI's have the second behaviour. The picture below was grabbed on a PI while the window was being expanded and you can see that draw handler has not been called since the expansion started. Infact the window manager has not yet informed the application that the window size is changing.

Handling More Signals (a.k.a. Making the ON and OFF buttons work).

The "clicked" handlers for the On and Off buttons are quite simple, they set the value of the global variable "LigthOn" to True or False.

However changing the variable's value will not cause the draw handler to be called. The program needs to inform GTK+ that a redraw is needed to update the image.

01: class Handlers:
02: 
03:     def __init__(self):
04:         self.LightOn = False
05: 
06:     def on_offButton_clicked(self, widget):
07:         self.LightOn = False
08:         da.queue_draw() 
09: 
10:     def on_onButton_clicked(self, widget):
11:         self.LightOn = True
12:         widget.queue_draw()
13:  
14: builder = Gtk.Builder.new_from_file("demo1.glade")
15: da    = builder.get_object("drawingarea1")
16: 

The code above shows two variations on how the clicked handler informs GTK+ that the drawing area needs to be redrawn.

The offButton handler uses a global variable "da" which holds the value retrieved from the builder with builder.get_object("drawingarea1").

The onButton handler uses a feature of Glade to pass the drawingarea as the parameter to the handler. It is set in the "User data" field for the clicked event in the Signals tab in Glade as shown below. The first parameter to a handler is normally the widget that sent the signal, but setting the Userdata field causes it to be replaced with the given value.

You will see in the final version of the code below I've added a comment before the clicked handlers to explain the different use of the widget parameter.

01: #!/usr/bin/python3
02: 
03: import gi
04: gi.require_version('Gtk', '3.0')
05: from gi.repository import Gtk
06: 
07: 
08: class Handlers:
09: 
10:     def __init__(self):
11:         self.LightOn = False
12: 
13:     def on_quitButton_clicked(self, *args):
14:         Gtk.main_quit(*args)
15:     
16:     def on_offButton_clicked(self, widget):
17:         self.LightOn = False
18:         da.queue_draw() 
19:     
20:     # drawingarea1 is set as the userdata in glade
21:     def on_onButton_clicked(self, widget):
22:         self.LightOn = True
23:         widget.queue_draw()
24:  
25:     def on_window1_delete_event(self, *args):
26:         return True
27: 
28:     def on_drawingarea1_draw(self,widget,cr):
29:         w = widget.get_allocated_width()
30:         h = widget.get_allocated_height()
31:         size = min(w,h)
32: 
33:         cr.set_source_rgb(0.0,0.2,0.0)
34:         cr.paint()
35: 
36:         if self.LightOn == True:
37:             cr.set_source_rgb(1.0,0.0,0.0)
38:         else:
39:             cr.set_source_rgb(0.2,0.0,0.0)
40:         cr.arc(0.5*w,0.5*h,0.5*size,0.0,6.3)
41:         cr.fill()
42: 
43: 
44: builder = Gtk.Builder.new_from_file("demo1.glade")
45: 
46: da    = builder.get_object("drawingarea1")
47: 
48: builder.connect_signals(Handlers())
49: 
50: Gtk.main()
51: 

You can get this code from here.

An alternative way to specify signal handlers.

Specifying signal handers in Glade and using a "handlers class" to implement them is a clean but slightly inflexible. Greater control of the parameters passed to handlers can be achieved by using a dictionary to define the binding between signal handler names and the functions to be called. This provides more control over the parameters that are passed to the functions.

The first parameter will still be the widget that sent the signal (unless Userdata is specified in the Glade file) but subsequent parameters can be specified by using a tuple not just a function name like this:

"on_onButton_clicked": (onClicked,da),
which adds the drawingarea as an additional parameter when onClicked is called.

01: #!/usr/bin/python3
02: 
03: import gi
04: gi.require_version('Gtk', '3.0')
05: from gi.repository import Gtk
06: 
07: LightOn = False
08: 
09: def deleted(window, *args):
10:         return True
11: 
12: def onClicked(button,drawingArea):
13:     global LightOn
14:     LightOn = True
15:     drawingArea.queue_draw()
16: 
17: 
18: def offClicked(button,drawingArea):
19:     global LightOn
20:     LightOn = False
21:     drawingArea.queue_draw()
22: 
23: 
24: def draw(drawingArea,cr):
25:     w = drawingArea.get_allocated_width()
26:     h = drawingArea.get_allocated_height()
27:     size = min(w,h)
28: 
29:     cr.set_source_rgb(0.0,0.2,0.0)
30:     cr.paint()
31: 
32:     if LightOn == True:
33:         cr.set_source_rgb(1.0,0.0,0.0)
34:     else:
35:         cr.set_source_rgb(0.2,0.0,0.0)
36:     cr.arc(0.5*w,0.5*h,0.5*size,0.0,6.3)
37:     cr.fill()
38: 
39: 
40: 
41: builder = Gtk.Builder.new_from_file("demo1.glade")
42: 
43: da    = builder.get_object("drawingarea1")
44: 
45: handlers = {
46:     "on_quitButton_clicked": Gtk.main_quit,
47:     "on_window1_delete_event" : deleted,
48:     "on_onButton_clicked": (onClicked,da),
49:     "on_offButton_clicked": (offClicked,da),
50:     "on_drawingarea1_draw": draw,
51: }
52: 
53: builder.connect_signals(handlers)
54: 
55: Gtk.main()
56: 

You can get this code from here.

Meta Stuff

Page Creation

These web pages were created with Bluefish "a powerful editor targeted towards programmers and webdevelopers"

Code hilighting

The sections of code were produced using GNU Source-highlight 3.1.8 See The command used produces .html files which were pasted into the page source.
source-highlight --src-lang python --out-format htmltable --line-number --input demo1.py > demo1.py.html

Comments and Corrections

Please send any comments or corrections to


Licence

Creative Commons Licence
This work is licensed under a Creative Commons Attribution-NonCommercial 2.0 UK: England & Wales License.