Tutoriel : Graphical User Interfaces. PyQt pour vous.

Introduction

Mme Françoise Lambert envisage en cours une introduction à Tkinter. Son avantage est sa disponibilité d'office sur toutes les plates-formes de Python [et donc, pas mal d'exemples et de références Web], et une certaine simplicité de structure. Pour les débutants ceci peut aller, et si cela vous convient... Concernant les GUIs en général, lisez ce texte, et aussi Wiki. La documentation de Tkinter, ainsi que quelques exercices seront sans doute proposés par FL.

Mais Tkinter à long terme peut ne pas être une bonne idée, en tout cas, c'est mon opinion personnelle. Il n'est pas évolutif, figé avec son noyau Tcl/Tk assez vieux (et Tk est de toute façon un "organisme étrange" dans le monde Python, adopté il y a longtemps, faute d'autre solution). Le monde professionnel abandonne Tkinter. Mal adapté au multi-threading, ce qui fait que parfois les programmes graphiques (comme Turtle) échouent. Le lancement simultané de plusieurs applications Tk, par ex. Turtle et IDLE, peut engendrer des erreurs difficiles à diagnostiquer. Le jeu de widgets est pauvre, et TIX évolue très lentement. La plupart de vous ne se mettra (probablement...) pas dans des situations comme ça pendant plusieurs mois. Mais en L2 on aura besoin de threads, vous ferez des exercices en animation, et certains peuvent avoir des ennuis... Tkinter a quand même évolué, les entrées/sorties ont été modernisées un peu, le paquetage de widgets ttk évolue, mais tout ceci est un peu bricolé, d'autres solutions sont plus rationnelles. Cependant : pas de solutions-miracles, il faudra apprendre plusieurs choses. D'habitude les contraintes et protocoles sont là pour faciliter la vie des programmeurs, mais plutôt ceux avec de l'expérience.

En deux mots : je suis loin d'être sectaire, si vous voulez faire des simples exercices dans le cadre de ce cours, et rien d'autre, prenez Tkinter et oubliez le reste de ce texte. Si vous voulez vous investir dans le domaine d'interfaçage visuel, focalisez votre attention sur des solutions plus modernes.

Alors, où aller?...

Au moins TROIS alternatives existent, évoluent, sont populaires, maintenues, et relativement universelles (ne sont pas restreintes à la couche Python). En fait, "trois" est un raccourci, le nombre est considérablement plus grand, mais d'autres sont ciblées sur des projets plus concrets, comme les jeux, ou la communication. Ces projets sont souvent basés sur ces "trois" ; parfois offrent des passerelles Web.


PyQt, les premiers programmes

J'utiliserai PyQt4 (la version récente c'est 5, mais - sauf erreur de ma part, Matplotlib CHEZ NOUS utilise la version 4). PyQt est un paquetage considérablement plus évolué que Tkinter, et sa description ne peut être que superficielle. Je recommande le livre de Mark Summerfield, "Rapid GUI Programming with Python and Qt; The Definitive Guide to PyQt Programming", qui (sauf erreur de ma part) est disponible sur le Web en format PDF.

Voici un premier programme commenté, afin de vérifier si le paquetage marche. Importons (partiellement) le paquetage (dont les noms des composants commencent par "Q"). Plus tard nous importerons QtCore, et autres choses. Il vous faudra lire la documentation de référence, et aussi cela. Ensuite, créons l'objet "application", qui (plus tard) lancera l'interface, après avoir installé ses composants.

from PyQt4 import *
from PyQt4.Qt import *
 
ap = QApplication([]) #Une seule ! C'est la "machine événementielle.
(On peut passer des paramètres à l'application, mais ce sera pour plus tard). Visuellement, il ne se passe rien. Voici la création d'un composant visuel.
def greet(): print("Bonjour Monde Cruel !") # C'est à nous !
hello = QPushButton("Cliquez SVP",None)
# ap.connect(hello, SIGNAL("clicked()"), greet)
hello.show()
La variable ap est un "avatar" du programme utilisateur, qui contient les mécanismes qui lancent des différentes fonctions. La variable hello référence un widget - bouton.
Les widgets ont d'habitude des "parents" où ils sont placés.
Ici, None signifie que le bouton est placé dans la fenêtre principale. Il n'est dynamiquement attaché (déclaré) à rien, et il ne "fait" rien. L'application (le "driver" événementiel) doit le rendre sensible aux événements, en activant l'instruction commentée, le bouton "reçoit le signal" quand on clique dessus, et il exécute la procédure réponse, qui dans ce contexte s'appelle le "slot". Ceci est l'exemple le plus simple du système "slots / signals", nous en verrons d'autres.

Ce programme marche, mais il est défaillant, normalement, après avoir affiché les composants, on rend le contrôle au pilote événementiel, afin que l'interaction avec l'utilisateur s'engage correctement.

ap.exec_()
La vision ci-dessus est simplifiée. Le bouton ne peut "exister seul", Qt créé spécialement la fenêtre principale pour lui : n'importe quel widget peut se comporter comme la fenêtre principale, mais ceci n'est pas commode. On peut modifier le code, et après avoir créé l'Application, on exécute :
mwi = QMainWindow()
hello = QPushButton("Cliquez SVP",mwi) # mwi est le parent du bouton.
...
mwi.show()
Ainsi le bouton est attaché à cette fenêtre, et il est affiché avec. On peut facilement modifier la taille et la position de la fenêtre, et de ses composants, par ex. mwi.resize(150, 250), et/ou hello.move(QPoint(40,70)).

En lisant la documentation il faut tenir compte d'un principe fondamental de la technologie objet : l'héritage. Dans la description de la classe d'objets QApplication vous ne trouverez pas la méthode connect. Ces objets sont des variantes (instances d'une sous-classe) de QCoreApplication, qui est une sous-classe d'une catégorie générale : QObject. C'est ici où on trouvera la description de cette méthode ; certaines de ses propriétés sont héritées et partagées par les sous-classes.


Un peu de graphisme

Voici un autre exemple. Le programme ci-dessous ouvre une fenêtre graphique, où il trace une courbe fractale connue comme le "dragon" de Heighway-Harter. L'utilisateur spécifie interactivement la profondeur récursive de l'algorithme. PyQT peut dessiner des lignes (ou des textes) sur n'importe quel widget : panneau, bouton, zone-étiquette, etc., il n'y a pas de widget-canevas unique, même si certains sont mieux adaptés au graphisme que les autres. Un des fréquents est QGraphicsView. Mais, avant de tracer quoi que ce soit, attention !. Contrairement aux systèmes, où on trace une ligne, et on l'oublie (elle reste sur l'écran, mais le programme ne le sait plus), ici les lignes, les cercles, les rectangles, etc. sont [normalement] des objets qui font partie du programme. On peut les transformer et les effacer. Le widget QGraphicsView est associé avec QGraphicsScene, une "surface virtuelle", où on place (de manière invisible) les items, et le View affiche le contenu de la Scene. Ceci est assez intuitif, mais le prix à payer est que la mémoire peut devenir encombrée, si on construit de très nombreux objets (quelques milliers n'est pas un problème, millions : si ; on peut gérer cela, mais avec précaution).

Commençons par la procédure qui construit le dragon. Elle n'affiche rien, elle construit les listes x et y des coordonnées des points, selon l'algorithme récursif décrit dans la Wikipédia et ailleurs.

def rdragon(x0,y0,x1,y1,n,tr=False):
    if n==0: return ([x0,x1],[y0,y1])
    if tr: x,y = 0.5*(x0+x1+y0-y1),0.5*(y0+y1+x1-x0)
    else:  x,y = 0.5*(x0+x1-y0+y1),0.5*(y0+y1-x1+x0)
    xa,ya=rdragon(x0,y0,x,y,n-1,True)
    xb,yb=rdragon(x,y,x1,y1,n-1,False)
    x=xa+xb[1:]; y=ya+yb[1:] # Elim. les doublons internes
    return(x,y)
Le dessin consiste à ajouter les fragments de ligne à la scène. C'est une simple boucle, rendue moins simple par une astuce permettant de parcourir en parallèle les deux listes. Ceci est redondant, on peut le faire de manière plus élémentaire.
wid,hgt = 600,400
x0,x1,y0=100,wid-150,250

def dessin(scene,n):
    global x0,x1,y0
    l=rdragon(x0,y0,x1,y0,n)
    (xi,yi),*p = zip(*l)  # Séparation du premier point
    for (x,y) in p:       #  et de la liste restante
        scene.addLine(xi,yi,x,y); xi,yi=x,y
Les variables globales servent à transmettre les informations entre les fonctions. Dans le style objet on les peut éviter (ce qui est recommandé), mais ce sera pour plus tard.

Passons à l'interface. Voici la création de quelques objets : la scène le widget du display, les boutons, l'entrée textuelle, et les layouts.

from PyQt4 import QtGui, QtCore

def start():
    global sce,inn   # Partagées
    ap=QtGui.QApplication([])
    win=QtGui.QWidget()    # fenêtre principale
    layh = QtGui.QHBoxLayout() # les layouts
    layv=QtGui.QVBoxLayout()
    sce=QtGui.QGraphicsScene(
        QtCore.QRectF(0, 0, wid, hgt))
    view=QtGui.QGraphicsView(sce,win)
    inn=QtGui.QLineEdit("6",win)
    gbut = QtGui.QPushButton("Go",win)
    cbut = QtGui.QPushButton("Clear",win)
    view.setMinimumSize(wid+20,hgt+20)
    ap.connect(gbut, QtCore.SIGNAL("clicked()"), go)
    ap.connect(cbut, QtCore.SIGNAL("clicked()"), clear)
Précisons la taille minimale du view, et connectons les boutons aux procédures qui seront précisées tout à l'heure. La dernière opération vous est connue.

Le concept de layout demande une explication approfondie. ce sont des "boîtes virtuelles", invisibles, qui positionnent leurs composants à l'intérieur. Ici on exploitera un layout vertical, et l'autre horizontal, de catégorie "box", la plus simple. Une autre c'est "grid", pour faire des tableaux réguliers de widgets (par ex. les claviers d'une calculatrice). Ce sera pour plus tard.

    layh.addWidget(view)
    layv.addWidget(inn)
    layv.addWidget(gbut); layv.addWidget(cbut)
    layv.setGeometry(QtCore.QRect(0, 0, 20, hgt))
    layh.addLayout(layv)
    win.setLayout(layh)
    dessin(sce,6) # Pour initialiser
    win.show(); ap.exec_()
Si on ajoute des widgets à un box (layout) horizontal, ils sont placés en ligne, de gauche vers la droite. Pour le vertical : de haut vers le bas. Ici à gauche nous avons le View, et à droite – les trois widgets de contrôle.

C'est tout. Le programme effectue encore le dessin initial, et termine les opérations administratives.

Les boutons, comme leurs noms le suggèrent, lancent le dessin avec la profondeur réglable, ou nettoient le graphique (efface les objets de la Scène).

La zone textuelle est gérée de manière simple : le widget contient un texte, qui peut être récupéré par la méthode .text(), et ensuite convertie en entier.

Il nous reste de définir les fonctions "slots" qui réagissent aux boutons :

def go(): 
    t=inn.text()
    if t=="": t="0"
    dessin(sce,int(t))
    
def clear(): sce.clear()
Ce programme est complet, vous pouvez le copier, tester, et modifier à volonté.

Quelques exercices pour vous


Un programme plus interactif

Nou allons écrire un programme – éditeur graphique, permettant de tracer les courbes à l'aide de la souris.


Si vous avez besoin d'autres informations d'intérêt général, écrivez-moi, peut-être j'écrirai la suite de ce tutoriel.

Retour à l'index