How to create a graphical user interface (GUI) and display a plot inside it ?
We will create a gui using GTK+, a toolkit for graphical interface available for GNU/Linux (and other unices), Windows and OSX.
First, you need a facility to build the GUI. Here, glade is made for you ! Glade is available in your linux repository or in darwinports for mac users. For windows users, I don't know. Anyway, if you want to develop in python, the most powerfull platform is Linux (and not only for development).
In this first tutorial, we will create a simple soft that generates an array of the specified size and creates a given number of dots in a random position. Your interface will look like that :
I will describe later how to create your interface with glade. But from now, you can download the resulting glade file here.
The preamble of your code will look like this :
#!/usr/bin/env python # -*- coding: UTF-8 -*- import pygtk import gtk import gtk.glade import numpy ############## ## Graphic library ## ############## import matplotlib matplotlib.use('Agg') from matplotlib.figure import Figure from matplotlib.axes import Subplot from matplotlib.backends.backend_gtkagg import FigureCanvasGTK from matplotlib import cm # colormap from matplotlib import pylab pylab.hold(False) # This will avoid memory leak
The two first lines are to declare the python executable and the coding of the text, here in UTF-8.
Then follows the importations.
pygtk, gtk and gtk.glade are needed to build our graphical user interface.
numpy is an extra module that allow us to manipulate multidimentional arrays. This module is very usefull for several things needed for scientific use.
Finally, matplotlib is used to plot the data. Matplotlib was first written to emulate matlab behavior in python, but can also be used in the pythonic way, meaning that all are objects. The good news is that matlab users are not completly lost with matplotlib. Similarly to matlab, matplotlib has figures in which we draw axes.
After the preamble, we declare our plot object with the initialization function :
class plot: def __init__(self): ### # Initialize the datas ### self.array_size = 1 self.nbr_dots = 0 ### # Initialize the gui ### builder = gtk.Builder() builder.add_from_file("Gui.glade") self.window = builder.get_object("gride") # connect signals builder.connect_signals(self) self.figure = Figure(figsize=(100, 100), dpi=75) self.axis = self.figure.add_subplot(111) self.canvas = FigureCanvasGTK(self.figure) # a gtk.DrawingArea self.canvas.show() self.graphview = builder.get_object("plot") self.graphview.pack_start(self.canvas, True, True)
Some data that we will use are initialized, and then we take care of the GUI.
With glade 3, there is a big change in that it supports the GtkBuilder format that is replacing the old libGlade format. If you want a more precise explanation of this new format, and how to use it with C++ and Python, go here.
The builder contains the hierarchy of the graphical interface. To find a widget, it's in this variable that you have to search. You can do it like this :
>>> this_widget = builder.get_objet('name_of_the_widget_I_search')
That's what we do in the second to last line.
In the glade interface, we defined some action related to events occuring on the widgets. For example, to change the gride size, we used a widget called GtkSpinButton that we named spin_size. This widget is made of two arrows, pointing up and down, and a label with a number. When the user click on one of the arrowa (up or down), it calls the signal "change-value". We then linked this signal to a function called "on_size_value_changed". All that was done on glade. Now, we have to create a funtion in our script, that is called "on_size_value_changed" so that each time the user click on one of these arrows, this specific function is called.
def on_size_value_changed(self, widget): self.array_size = int(widget.value) self.generate_seed_array() self.plot_gride()
As you can see, this function change the value of self.array_size to the one that is in the SpinButton (widget.value) and called two other functions named generate_seed_array() and plot_gride().
def on_nbr_pix_value_changed(self, widget): self.nbr_dots = int(widget.value) self.generate_seed_array() self.plot_gride()
This other function is very similar to the previous one but takes care on the SpinButton that control the number of labeled pixels.
Now lets explain how we generate the arrays, and, most important, how we display it :
def generate_seed_array(self): rand_pos = numpy.random.random(self.array_size ** 2) rand_pos = rand_pos.argsort() rand_pos = rand_pos < self.nbr_dots self.seed_array = rand_pos.reshape(self.array_size, self.array_size)
We want to generate a 2D array of size self.array_size X self.array_size with random dots inside. To do so, we generate first an 1D array with self.array_size2 random values between 0 and 1.
In the third line, we order this array. In that way, we have a new array with the order of each elements. For example :
>>> rand_array = numpy.random.randn(5) >>> rand_array array([-0.26513548, 0.27386872, -0.27171874, 0.18567913, 0.25008833]) >>>> rand_array.argsort() array([2, 0, 3, 4, 1])
The last line reshapes the array to be a 2D array.
def plot_gride(self): self.axis.pcolor(self.seed_array, cmap=cm.gray) self.axis.axis([0, self.seed_array.shape, 0, self.seed_array.shape]) self.refresh_plot()
Here, we plot the array on the chosen place.
To plot an array we can use or pcolor or imshow. For small arrays, pcolor is the best choice. In matplotlib, you can choose several colormap. For a gray scale map : cm.gray. But there exist a lot of others as you can see by accessing the documentation in python shell :
>>> from matplotlib import cm
>>> help cm
At the DATA section, you will see all the available colormaps.
In the second line, we reshape the axis so that it is not greater than the size of the array. And the last line calls a function named "refresh_plot()" that simply redraw the plot area :
def refresh_plot(self): self.canvas.draw_idle()
Some last functions needs to be declare in order to create and destroy properly the GUI :
def on_gride_destroy(self, widget, data=None): gtk.main_quit() def main(self):self.window.show() gtk.main()
The on_gride_destroy catches the window closing. This permits to exit in a safe way. We created a link to this function with glade. From the GtkWindow widget we labeled "gride", we defined a destroy signal (in GtkObject) we called "on_gride_destroy".
gtk.main_quit() close all things and stops the scripts.
The main funtion generates a loop where the scripts wait until an event occurs (some clicks on the GUI for example).
Our object plot() is now complete. We have to create an instance of it and display this instance.
The very final part of our script looks then like this :
if __name__=='__main__': app = plot() app.window.show()
This will create an instance of the class plot() and display it with the show() method.
What are the important portion of the code important to embed a plot in GTK ?
In the initialization function of the class, we had those lines :
self.figure = Figure(figsize=(100, 100), dpi=75) self.axis = self.figure.add_subplot(111) self.canvas = FigureCanvasGTK(self.figure) # a gtk.DrawingArea self.canvas.show() self.graphview = builder.get_object("plot") self.graphview.pack_start(self.canvas, True, True)
From the first line to the last, we :
* create the figure with the correct size and dpi,
* create the axis plot in the figure(111 means 1 row, 1 column and the first plot, 211 would mean a plot made of two axis, 2 line, 1 column and points to the first one)
* creation of the drawing area. It takes the figure as attribute.
* Make the drawing area visible ( self.canvas.show() )
* We get the widget where we will draw the plot. This widget, a Gtk.vBox was labeled "plot".
* We pack the drawing area in the widget (with pack_start).
The function plot_gride() makes the necessary to plot the new arrays in the drawing area.
An important step is to redraw the axis to make the changes visible. This is done with the refresh_plot() function :
def refresh_plot(self): self.canvas.draw_idle()
If you have any question about this tutorial (this can help to improve its clarity...) don't hesitate to charles [dot] roduit [at] gmail [dot] com (send me an e-mail.)