26/09/2020
Python pour l’ingénieur
IHM avec PyQt
Module de formation
© Copyright 2020 Cyril Keime & Arnaud Bêche
Introduction
• Qt est une bibliothèque de classes offrant entre autres
des composants d’interface graphique appelés widgets.
• Qt est multi-plateformes (portable) et open-source
(licence GNU LGPL permettant son utilisation légale et
gratuite par des logiciels propriétaires).
• Qt est initialement écrit en langage C++.
• PyQt est un binding de Qt pour le langage Python.
– PyQt n’est pas gratuit pour une utilisation commerciale
– Autre binding Python de Qt : PySide
• Alternatives à Qt :
– Python : Tk (intégré au langage), wxWidgets (wxPython)
– C++ : Gtk, wxWidgets, MFC (Microsoft), etc.
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 2
Ressources
• Version de Qt utilisée pour le cours : 5.12 (6 Décembre 2018)
• Site internet de Pyqt :
– [Link]
– Documentation : [Link]
• Site internet de Qt :
– [Link]
– Documentation : [Link]
• La documentation de PyQt n’est pas aussi complète que celle de Qt → il faut
parfois se référer à celle de Qt (en C++, mais la transcription en Python est assez
facile).
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 3
Application minimale
import sys
from [Link] import *
from [Link] import *
from [Link] import *
class MaFenetre(QMainWindow):
def __init__(self):
super().__init__()
[Link]('Pyqt')
def main():
app = QApplication([Link])
fenetre = MaFenetre()
[Link]()
[Link]()
if __name__ == '__main__':
main()
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 4
Layout de QMainWindow
Menu Bar
Tool Bar
Central Widget
Status Bar
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 5
Central Widget
• Central Widget = un objet dérivant de QWidget
– Soit un widget prédéfini
– Soit un widget personnalisé, défini par une classe
dérivant de QWidget
class MaFenetre(QMainWindow):
def __init__(self):
super().__init__()
[Link]('Pyqt')
button = QPushButton('Hello !')
[Link](button)
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 6
Widgets prédéfinis
label = QLabel('Mon Titre', self)
[Link]([Link])
image = QLabel(self)
[Link](QPixmap('[Link]'))
bouton = QPushButton('OK', self)
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 7
Widget personnalisé
class MonWidget(QWidget):
def __init__(self, parent):
super().__init__(parent)
label = QLabel('Mon Titre', self)
[Link]([Link])
[Link](10, 10, 200, 20)
image = QLabel(self)
[Link](QPixmap('[Link]'))
[Link](10, 30, 200, 100)
bouton = QPushButton('OK', self)
[Link](10, 150, 200, 20)
class MaFenetre(QMainWindow):
def __init__(self):
super().__init__()
[Link]('Pyqt')
central_widget = MonWidget(self)
[Link](central_widget)
[Link](250, 250)
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 8
Placement avec Layout
class MonWidget(QWidget):
def __init__(self, parent):
super().__init__(parent)
layout = QVBoxLayout()
label = QLabel('Mon Titre')
[Link]([Link])
image = QLabel()
[Link](QPixmap('[Link]'))
bouton = QPushButton('OK')
[Link](label)
[Link](image)
[Link](bouton)
[Link](layout)
class MaFenetre(QMainWindow):
def __init__(self):
super().__init__()
[Link]('Pyqt')
central_widget = MonWidget(self)
[Link](central_widget)
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 9
Imbrication des Layouts
LayoutH
1 2
1
LayoutV
class MonWidget(QWidget): 2
def __init__(self, parent):
super().__init__(parent)
bouton1 = QPushButton('bouton haut gauche')
bouton2 = QPushButton('bouton haut droite')
layoutH = QHBoxLayout()
[Link](bouton1)
[Link](bouton2)
bouton3 = QPushButton('bouton bas')
layoutV = QVBoxLayout()
[Link](layoutH)
[Link](bouton3)
[Link](layoutV)
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 10
Types de Layout
• QHBoxLayout
• QVBoxLayout
• QGridLayout
• QFormLayout
(grid)
(form)
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 11
Slot et Signal
clic
class MonWidget(QWidget):
def __init__(self):
super().__init__()
[Link] = QPushButton('Cliquez', self)
[Link] = QLineEdit(self)
[Link](self.on_bouton)
…
signal slot (ou ‘callback’)
def on_bouton(self):
[Link]('clic sur le bouton !')
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 12
Slot et Signal
• Un signal peut être accompagné d’une information émise
Ex: valueChanged(int) pour QSpinBox, QDial
Ex: currentTextChanged(str) pour QCombBox
• Dans ce cas, la fonction callback doit avoir des arguments correspondant à ce qu’envoie
le signal, pour réceptionner l’information émise :
émet un str reçoit un str
dial = QDial(self)
spin = QSpinBox(self)
[Link]([Link])
[Link]([Link])
(QSpin)
émet un int reçoit un int (QDial)
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 13
Fenêtre plus complexe
class MonWidgetA(QWidget):
def __init__(self, parent):
super().__init__(parent)
layout = QVBoxLayout()
[Link](QPushButton('1'))
[Link](QPushButton('2'))
[Link](QPushButton('3')) A A B
[Link](layout)
class MaFenetre(QMainWindow):
class MonWidgetB(QWidget): def __init__(self):
def __init__(self, parent): super().__init__()
super().__init__(parent) widget1 = MonWidgetA(self)
layout = QVBoxLayout() widget2 = MonWidgetA(self)
[Link](QPushButton('OK')) widget3 = MonWidgetB(self)
[Link](QLineEdit('test')) layout = QHBoxLayout()
[Link](QSlider()) [Link](widget1)
[Link](layout) [Link](widget2)
[Link](widget3)
widget = QWidget()
[Link](layout)
[Link](widget)
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 14
Feuilles de style
class MonWidgetA(QWidget):
def __init__(self, parent):
super().__init__(parent)
[Link]('QPushButton { font-weight: bold; font-size: 16px; }')
layout = QVBoxLayout()
[Link](QPushButton('1'))
[Link](QPushButton('2'))
[Link](QPushButton('3'))
[Link](layout)
class MonWidgetB(QWidget):
def __init__(self, parent):
super().__init__(parent)
[Link](' \
QLineEdit { background-color: rgb(0,0,128); color: white; font-family: courier; } \
QPushButton { background-color: rgb(0,128,0); }')
layout = QVBoxLayout()
[Link](QPushButton('OK'))
[Link](QLineEdit('test'))
[Link](QSlider())
[Link](layout)
CSS3
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 15
Menu déroulant
class MaFenetre(QMainWindow):
def __init__(self):
super().__init__()
menu1 = [Link]().addMenu('Fichier')
action1 = QAction('Ouvrir', self)
[Link](self.on_ouvrir)
[Link](action1)
méthode de classe à définir
action2 = QAction('Sauvegarder', self)
[Link](self.on_sauver)
[Link](action2)
action3 = QAction('Quitter', self)
[Link](self.on_quitter)
[Link](action3)
menu2 = [Link]().addMenu('Editer')
Voir 10/10/2021 © Copyright 2020 Cyril
aussi : [Link](), [Link](), Keime & Arnaud Bêche
[Link](), sous-menus 16
Quelques autres widgets de Qt
• Boites de dialogues usuelles
– QMessageBox
– QInputDialog
– QFileDialog
• Widgets conteneurs
– QGroupBox
– QTabWidget
– QStackedLayout
• Toolbar et Statusbar
– QToolBar
– QStatusBar
• Widgets complexes
– QListView
– QTreeView
– QTableView
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 17
Dessiner
class MaScene(QGraphicsScene):
"""cette classe décrit la scène"""
def __init__(self, parent):
super().__init__(parent)
[Link](0, 0, 300, 300)
texte = [Link]("Hello, world!")
[Link](10, 10)
[Link](50, 50, 200, 200)
stylo = QPen([Link], 5, [Link])
[Link](200, 100, 20, 20, stylo)
brosse = QBrush(QColor(128, 0, 128), [Link])
[Link](100, 200, 50, 50, stylo, brosse)
class MaVueGraphique(QGraphicsView):
"""cette classe fait le rendu (= dessin) de la scène"""
def __init__(self, parent):
super().__init__(parent)
scene = MaScene(self)
[Link](scene)
class MaFenetre(QMainWindow):
def __init__(self):
super().__init__()
[Link]("Dessiner avec PyQt")
vue = MaVueGraphique(self)
[Link](vue)
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 18
Clavier et Souris
• Toute classe dérivée de QWidget peut redéfinir des
fonctions héritées pour réagir aux événements
clavier et souris, par exemple :
• keyPressEvent() def keyPressEvent(self, keyevent):
if [Link]() == Qt.Key_Q:
– appelée lorsque qu’une ...
touche du clavier est pressée.
• mousePressEvent() def mousePressEvent(self, mouseevent):
self.x = [Link]().x()
– appelée lorsqu’un bouton self.y = [Link]().y()
de la souris est cliqué.
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 19
Timer
def afficher():
• Un timer permet de print("bonjour")
déclencher l’appel d’une
app = QApplication([Link])
fonction à intervalles de timer = QTimer()
temps réguliers. [Link](afficher)
• La classe Qt pour créer # répétition toutes les 1000 millisecondes
[Link](1000)
un timer est QTimer [Link]()
class MonTimer(QTimer):
def __init__(self):
super().__init__()
[Link]([Link])
def ontimer(self):
print("bonjour")
app = QApplication([Link])
timer = MonTimer()
# répétition toutes les 1000 millisecondes [Link]() permet d’arrêter le timer
[Link](1000)
[Link]()
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 20
Exercice
• 1re partie
– Créer une classe (scène) affichant une image de carte.
addPixmap(QPixmap("[Link]"))
– La classe contient une fonction permettant de repositionner
cette carte à l’endroit où on clique.
– La classe contient une fonction permettant de créer une 2e carte
quand on appuie sur la touche ‘c’.
– La classe contient une fonction permettant de déplacer la 2e carte
quand on appuie sur les flèches du clavier.
• 2e partie
– La classe contient un timer permettant d’animer la première carte,
en modifiant sa position de quelques pixels à intervalles de temps
réguliers.
– La classe contient une fonction permettant de mettre l’animation en
pause quand on appuie sur la touche ‘p’.
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 21
Exercice : mini Pacman (3e partie)
• Partir des modules [Link], [Link] et [Link] fournis sur [Link]
• Module [Link]:
– coder le constructeur de la classe PacmanParams de manière à obtenir l'interface ci-dessous
• Utiliser QSpinBox pour saisir les dimensions du plateau de jeu et le nombre de fantômes
• Utiliser QFormLayout pour titrer et disposer les 3 options
• connecter les méthodes on_start et on_stop aux boutons start et stop. Ces méthodes ne font rien pour le moment.
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 22
Exercice : mini Pacman (3e partie)
• Module [Link]
– Coder le constructeur et la méthode start() de PacmanControler
• Attributs de la classe PacmanControler :
– [Link] : liste d'objets fantômes (Entity)
– [Link] : objet pacman (Pacman)
– [Link] : timer
– [Link] : largeur du plateau
– [Link] : hauteur du plateau
• Dans le constructeur :
– Créer l'attribut timer (classe QTimer) et le connecter à la méthode next()
• Dans start():
– Créer nb_ghosts objets fantômes et leur donner une position initiale aléatoire
– Créer un objet pacman et lui donner une position initiale aléatoire
– Démarrer le timer
• Dans stop()
– Arrêter le timer
• Appeler les méthodes start() et stop() du contrôleur au moment adéquat dans la classe PacmanParams
([Link]). Bien prendre en compte les paramètres saisis dans l'interface (largeur, hauteur, nombre de
fantômes). Vérifier que ces paramètres parviennent avec la bonne valeur à la méthode start().
• Vérifier que la méthode next() est bien appelée à intervalle de temps régulier lorsqu'on clique sur le bouton
Start (mettre un print dans la méthode next()). Vérifier que cliquer sur le bouton Stop arrête bien le timer.
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 23
Exercice : mini Pacman (3e partie)
• Module [Link]:
– Classe PacmanScene :
• Constructeur :
– Initialiser la scène à une dimension de 512 x 512 pixels
• Méthode refresh() :
– Dessiner le plateau de jeu en allouant 25x25 pixels à chaque case du plateau de jeu
» Dessiner le fond en gris et les contours du plateau en noir (rectangles)
» Dessiner chaque fantôme (disque de couleur rouge)
» Dessiner Pacman (disque de couleur jaune)
• Module [Link]
– Compléter la méthode next() :
• Déplacer les fantômes
• Déplacer Pacman
• Si un fantôme est "mangé", le supprimer
de la liste des fantômes
• S'il n'y a plus de fantôme, arrêter le timer
• Provoquer un rafraichissement de tous
les "clients" du contrôleur
• Tester
• Vérifier le bon déplacement des
entités
• Vérifier la disparition des fantômes mangés
• Vérifier que la fin de partie se passe bien
(Pacman s'arrête)
• Vérifier qu'on peut démarrer une nouvelle partie,
en modifiant ou pas les paramètres du jeu
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 24
Exercice : mini Pacman (3e partie)
• Module [Link]:
– Téléportation de Pacman à la souris :
• Ajouter une méthode move_pacman(x, y) au contrôleur qui téléporte instantanément
Pacman à la position (x, y). Ne pas permettre de téléporter Pacman en dehors du plateau
de jeu ou sur la bordure.
• Coder la méthode mousePressEvent pour que Pacman soit téléporté sur le curseur de
souris lorsqu'on clique sur le plateau de jeu.
– Mettre le jeu en pause au clavier (touche P)
• Implémenter la méthode keyPressEvent() (module [Link]) pour transmettre la touche du
clavier saisie à la méthode process_keypress() du contrôleur
• Implémenter la méthode process_keypress() (module [Link]) pour activer / désactiver
la pause par la touche P. La mise en pause consiste à stopper le timer. Redémarrer le timer
pour enlever la pause, sans réinitialiser le jeu.
• Vérifier le bon comportement si le jeu est relancé (bouton start) pendant la pause.
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 25
Exercice : mini Pacman (3e partie)
• Quelques améliorations :
– Adapter automatiquement la dimension en pixels d'une case de plateau (actuellement fixé à
25x25 pixels) pour que la plateau occupent toujours 512 pixels quel que soit la largeur et la
hauteur choisies
– Lorsqu'il n'y a plus de fantômes, afficher "Game Over" par-dessus le plateau de jeu (jusqu'au
démarrage d'une nouvelle partie avec start)
– Afficher le nombre de fantômes encore en vie dans la barre des paramètres
– Ajouter un paramètre permettant de modifier la vitesse du jeu pendant une partie (ajouter un
widget QSpinBox dans les paramètres, utiliser la méthode setInterval du timer).
10/10/2021 © Copyright 2020 Cyril Keime & Arnaud Bêche 26