Plot dans GTK

Comment créer une interface graphique (GUI) et afficher y un graphique ?

Nous allons créer un gui en utilisant GTK+, un toolkit (boite à outil ?) pour créer des interface graphique. Ce toolkit est disponible sur GNU/Linux (et autres unices), Windows et OSX.

En premier lieu, vous avez besoin d'un utilitaire pour créer l'interface. Ici, glade est fait pour vous ! Glade est disponible sur votre dépot linux ou sur darwinport pour les utilisateur mac. Pour les utilisateurs windows, je ne sais pas. Ça peut vous faire une bonne occasion d'explorer les richesses de linux ;-

Dans ce premier tutoriel, nous allons créer un simple programme qui génèrera une grille de taille voulue et nous premetra de créer un nombre donné de points dans une position aléatoire. Votre intérface aura l'air de ça:

screenshot of the result of the tutorial

Je décrirais dans un autre tutoriel comment créer cette interface avec glade. Mais pour tout de suite, vous pouvez télécharger le fichier glade ici.

Le préambule de votre code ressemble à ça :

#!/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
Les deux premières lignes sont là pour déclarer ce fichier comme exécutable python et le codage du texte, ici en UTF-8.

Ensuite suivent les importation des librairies.

pygtk, gtk et gtk.glade sont nécéssaire pour construire notre interface graphique.

numpy est un module qui permet de manipuler les grilles de multiple dimentions. Ce module est très utile dans un contexte scientifique.

 

Finalement, matplotlib est utilisé pour dessiner les graphiques. Matplotlib a été initialement écris pour émuler le comportement de python, mais peut aussi être utilisé d'une façon pythonique, signifiant que tout est objet. La bonne nouvelle est que les utilisateur de Matlab ne seront pas dépaysés. À l'instar de matlab, matplotlib contient des figures dans lesquelles nous pouvons traces des axes.

Après le préambule, nous pouvons déclarer notre objet "plot" avec sa fonction d'initialisation :

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)
Dans cette partie, les quelques données que nous allons utiliser sont initialisées, et nous nous occupons aussi de l'interface graphique.

 

Glade 3 introduit un grand changement en supportant le format GtkBuilder, qui remplace le vieux format libGlade. Si vous voulez des explications plus précises au sujet de ce nouveau format, et comment l'utiliser avec C++ et Python, suivez ce lien.
La variable builder contient la hiérarchier de l'interface graphique. Pour trouver un widget, c'est dans cette variable que nous devons chercher. Vous pouvez faire quelque chose du genre :

>>> this_widget = builder.get_objet('name_of_the_widget_I_search')

C'est ce que nous avons dans l'avant-dernière ligne.

Dans l'interface de glade, nous avions défini quelques actions relatives aux évènements sur les widgets. Par exemple, pour le gangement de la taille de la grille, nous avons utilisé un widget du type GtkSpinButton que nous avons nommé spin_size. Ce widget est fait de deux flèches, pointant vers le haut et vers le bas, ainsi qu'un label avec un nombre. Quand l'utilisateur clique sur une des flèches (haut ou bas), cela appelle le signal "change-value". Nous avons alors lié ce signal à une fonction appellée "on_size_value_changed". TouT cela a été fait dans glade. Maintenant, nous devons créer cette fonction dans notre scripte de façon à ce que cette fonction soit appelée à chaque fois que l'utilisateur clique sur une de ces flèche.

    def on_size_value_changed(self, widget):
        self.array_size = int(widget.value)
        self.generate_seed_array()
        self.plot_gride()

Comme vous pouvez le voir, cette fonction change la valeur de self.array_size vers la valeur contenue dans le SpinButton (widget.value) et appelle deux autres fonctions appeléer generate_seed_array() et plot_gride().

    def on_nbr_pix_value_changed(self, widget):
        self.nbr_dots = int(widget.value)
        self.generate_seed_array()
        self.plot_gride()

Cette autre fonctions sont très similaire à la précédente mais prends en compte le SpinButton qui controle le nombre de pixels marqués.

Maintenant, regardons comment générer la grille, et, le plus important, comment nous l'afficherons :

    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)

Nous voulons générer une grille 2D de la taille self.array_size X self.array_size avec des points aléatoiresment marqués. Pour le faire, nous générons d'abord une grille 1D (ou vecteur) avec la taille self.array_size2 et des valeurs aléatoires comprises entre -1 et 1.

Dans la troisième ligne, nous ordonnons cette grille. De cette façons, nous avons une nouvelle grille, avec l'ordre le chaque élément. Comme un exemple est plus efficace qu'un long discours :

>>> 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])

Avec argsort, nous voyons que le second élément est le plus grand (noté 0) puis le dernier (noté 1), ...

Par cette méthode, nous pouvons aisément créer un nombre voulu de points aléatoire en déclarant que les n éléments les plus grands sont notés 1 et les autres 0. C'est ce que nous faisons à la 4ème ligne (vrai est équivalent à 1 et faux à 0.

La dernière ligne transforme la grille 1D en grille 2D.

Occupons-nous maintenant de l'affichage :

    def plot_gride(self):
        self.axis.pcolor(self.seed_array, cmap=cm.gray)
        self.axis.axis([0, self.seed_array.shape[0],
                        0, self.seed_array.shape[1]])
        self.refresh_plot()

Pour plotter une grille, nous pouvons utiliser pcolor ou imshow. Pour des petites grille, pcolor est le meilleur choix. Dans matplotlib, vous pouver aussi choisir plusieur colormap. Pour une échelle de gris : cm.gray. Mais il existe beaucoup d'autre que vous pouvez voir en accedant à la documentation dans le shell Python :

>>> from matplotlib import cm
>>> help cm

Dans la section DATA, vous trouverez tous les colormaps disponibles.

Dans la seconde ligne, nous précison la taille des axes pour qu'ils ne soient pas plus grand que la grille. Et dans la dernière ligne, nous appelons la fonction nommée "refresh_plot" qui redessine simplement le plot :

def refresh_plot(self):
    self.canvas.draw_idle()

Quelques dernières fonction doivent être déclarées pour créer et détruire l'interface proprement :

def on_gride_destroy(self, widget, data=None):
    gtk.main_quit()
def main(self):self.window.show()
    gtk.main()

Le on_gride_destroy s'active quand on ferme la fenêtre. Cela permet de quitter de façons propre. Nous créons un lien pour cette fonction dans glade. A partire du widget principal GtkWindow que nous avons appelé "gride", nous avons défini un signal de destruction (dans GtkObject) que nous avons appelé "on_gride_destroy".

gtk.main_quit() ferme tout et arrête le scripte.

La fonction main génère une boucle dans laquelle le script attend qu'un évènement arrive (un clique sur l'interface par exemple).

Notre objet plot() est maintenant prêt. Nous devons créer une instance de cet objet et afficher cette instance.

Cette partie finale de notre scripte est donc le suivant :

if __name__=='__main__':
    app = plot()
    app.window.show()

Cela créera une instance de la classe plot() et l'affichera avec la methode show()

Encapsuler un graphe dans GTK

Quelles sont les portions de code importante pour encapsuler un graphique dans GTK ?

Dans la fonction d'initialisation de la classe, nous avions ces lignes :

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)

Depuis la première ligne vers la dernière, nous :

* créons la figure avec la bonne taill et définition (dpi),

* créons les axes du plot dans la figure (111 signifie 1 ligne, 1 colonne et le primier axe, 211 signifierait un plot fait de deux axes, 2 lignes, deux colonne et pointe vers le premier des deux axes).

* création de la zone de dessin. Il prend la figure comme attribut.

* rendre la zone de dessin visible ( self.canvas.show() )

* trouver le widget dans lequel on vas dessiner le plot. Ce widget, un Gtk.vBox a le label "plot".

* packetage de la zone de dessin dans le widget (avec pack_start).

Modier le plot

La fonction plot_gride() fait tout le nécessaire pour afficher la nouvelle grille dans la zone de dessin.

Une étape importante est de redessiner les axes pour rendre les changements visibles. Cela est fait avec la fonction refresh_plot() :

def refresh_plot(self):
    self.canvas.draw_idle()

Télécharger les fichiers

Ce script et le fichier glade peuvent être téléchargé ici : tutorial_gtk-1.py tutorial_gtk-1.glade

Si vous avez une quelqonque question à propos de ce tutoriel (ça peut toujours aider pour améliorer sa clareté...) n'hésitez pas à m'envoyer un courriel.