Écrire du code robuste, de qualité et maintenable.
Avant-propos
Consultez au moins un article et une vidéo avant de poursuivre.
Mots clés pour Google ou Youtube : TDD, test driven development
Filtrez vos résultats pour avoir les plus récents.
Par ailleurs, nous aurons besoin pour cette fonctionnalité, de savoir mémoriser des paramètres : ici, la géométrie de la fenêtre (position à l'écran et taille)
Cherchez les informations avec les mots-clés : PyQt5 QSettings
Description
La première fonctionnalité à coder et la suivante :
Au lancement du programme, la fenêtre qui apparaît doit avoir la même taille et la même position à l'écran que lors de sa dernière fermeture.
Vous n'aurez pas à coder cette fonctionnalité, nous allons la traiter ensemble, à titre d'exemple.
A vous de jouer pour les suivantes.
A faire :
Settings (Ctrl + Alt + S) > Tools > Python Integrated Tools
Testing > Default test runner
, choisir pytest
No pytest runner found...
pytest.ini
dans le dossier tests
./tests/pytest.ini
:
;https://docs.pytest.org/en/stable/usage.html
;https://docs.pytest.org/en/stable/example/parametrize.html
[pytest]
addopts = -p no:faulthandler
main.py
devient./pc/main.py
:
from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow, QPushButton, QVBoxLayout
import sys
class UiMain(QMainWindow):
def __init__(self):
super().__init__()
# Code provisoire ************************************************
self.centralwidget = QWidget(self)
self.setCentralWidget(self.centralwidget)
self.button = QPushButton("Fermeture normale")
self.button2 = QPushButton("Fermeture sauvage\nVeuillez relancer le test")
self.layout = QVBoxLayout(self.centralwidget)
self.layout.addWidget(self.button)
self.layout.addWidget(self.button2)
self.setLayout(self.layout)
self.button.clicked.connect(self.close)
self.button2.clicked.connect(lambda: sys.exit(-1))
# Fin de code provisoire *****************************************
if __name__ == '__main__':
def start():
app = QApplication(sys.argv)
fen = UiMain()
fen.show()
sys.exit(app.exec())
""" Affichage de la fenêtre. """
start()
Tester : Déplacer, modifier puis fermer la fenêtre avec le bouton Fermeture normale
.
Ouvrez-la à nouveau : Vous voyez le problème ? Elle ne s'affiche pas au même endroit que lors de sa fermeture.
Il en est de même pour sa taille.
Nous voyons dans ce code que la classe utilisée est
et qu'elle hérite de la classe UiMain
QMainWindow
est une classe du module QMainWindow
QtWidgets
, contenu dans le package PyQt5
.
Nous verrons ses propriétés et ses méthodes au fur et à mesure des besoins.
Ici nous utiliserons geometry()
, setGeometry()
, closeEvent()
, moveEvent()
et resizeEvent()
Créez le fichier de test comme ceci :
Clic-droit dans le code de la classe UiMain() > Go To > Test > Create New Test
La fenêtre Create test
s'ouvre, laisser toutes les valeurs par défaut, valider.
Le fichier test_main.py
a été créé dans le dossier tests
.
Voici le code du test. Ouvrez ce fichier test_main.py
, remplacez son code par celui-ci.
/tests/test_main.py
:
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QSettings, QRect, QTimer
from pc.main import UiMain
import sys
import os
from random import randint
g_appli = QApplication(sys.argv)
class Main(UiMain):
""" Classe dérivée de la fenêtre à tester. """
def __init__(self, normal):
super().__init__()
self.settings_tests = QSettings(f"{os.getcwd()}{os.sep}tests.conf", QSettings.IniFormat)
self.geometrie_ante = self.settings_tests.value('Geometrie', QRect(200, 200, 400, 200))
self.geometrie_now = self.geometry() # Lecture de la géométrie à l'ouverture.
self.normal = normal
def closeEvent(self, ev):
self.settings_tests.setValue('Geometrie', self.geometry())
if self.normal is not False:
super().closeEvent(ev)
def fenetre(normal=None):
def open_window(normal):
win = Main(normal)
win.show()
return win
def geometrie(qrect):
return f"x = {qrect.x()}, y = {qrect.y()}, largeur = {qrect.width()}, hauteur = {qrect.height()}"
def move_window():
nb[0] += 1
if nb[0] > 2 or normal is None:
g_t.stop()
fen.close()
else:
fen.setGeometry(QRect(randint(50, 800), randint(50, 800), randint(300, 800), randint(300, 800), ))
g_t.start(200)
fen = open_window(normal)
g_t = QTimer()
g_t.setSingleShot(True)
g_t.timeout.connect(move_window)
nb = [0]
g_t.start(200)
""" Boucle d'exécution. """
g_appli.exec()
""" Fermeture fenêtre => Sortie de boucle d'exécution. """
if normal is None:
print(
"\n- La fenêtre doit apparaître avec la même géométrie (taille et position) "
"que lors de sa dernière fermeture."
f"\n- Géométrie à la dernière fermeture : {geometrie(fen.geometrie_ante)}."
f"\n- Géométrie à l'ouverture actuelle : {geometrie(fen.geometrie_now)}.")
assert fen.geometrie_now == fen.geometrie_ante
def test_ferme_normal():
"""
Affichage de la fenêtre, plusieurs déplacements et changements de taille, aléatoires.
Fermeture de la fenêtre, mémorisation de sa géométrie.
"""
fenetre(normal=True) # Fermeture normale.
""" Réouverture de la fenêtre, lecture de sa géométrie, comparaison. """
fenetre()
def test_ferme_sauvage():
fenetre(normal=False) # Fermeture sauvage.
fenetre()
Chaque fonction contenant test
dans son nom est un test.
Il y en a deux : test_ferme_normal
et test_ferme_sauvage
.
La fermeture 'sauvage' intervient lors de situations anormales : crash, bug, plantage système, fermeture par un élément extérieur, etc.
Lancer les tests : Clic-droit > Run...
Pour chacun des tests, le programme procède en 2 temps :
Il affiche la fenêtre à l'écran, modifie aléatoirement sa géométrie plusieurs fois, puis la ferme. La géométrie est mémorisée dans un fichier.
Il l'ouvre à nouveau, lit sa géométrie réelle et la compare avec celle mémorisée.
Erreur : Le test échoue, c'est normal car le code qui va permettre de corriger cela n'est pas encore écrit.
Comment corriger cela ? L'idée est la suivante :
Au moment de la fermeture de la fenêtre, on mémorise sa géométrie dans un fichier.
Lors de la prochaine ouverture, on lira ce fichier et on affectera la géométrie à la fenêtre.
Test à la fermeture :
Utilisons une méthode appelée automatiquement par l'événement de 'fermeture-fenêtre' : closeEvent()
. Copier-coller ce code dans la classe UiMain()
.
UiMain.closeEvent()
:
def closeEvent(self, ev):
""" Mémorisation de la géométrie. """
self.settings.setValue('Geometrie', self.geometry())
self.settings
qui n'existe pas ! Créons-le, dans la méthode UiMain.__init__()
:
self.settings = QSettings(f"{os.getcwd()}{os.sep}params.conf", QSettings.IniFormat)
QSettings
et os
. Ajoutons-les au début du fichier, dans les imports.main.py
:
from PyQt5.QtCore import QSettings
import os
run main.py
params.conf
[General]
Geometrie=@Rect(1062 133 510 417)
params.conf
et à affecter la géométrie à la fenêtre.__init__()
:
# """ Lecture et application de la géométrie. """
self.setGeometry(self.settings.value('Geometrie', QRect(200, 200, 400, 200)))
QRect
est nécessaire pour valider cette ligne de code. Elle appartient au module QtCore
. Modifions les imports de QtCore
:
from PyQt5.QtCore import QSettings, QRect
Tester : run main-py
test_main.py > Run 'pytest in test_main.py'
closeEvent()
n'est pas exécuté !closeEvent()
, nous ferons un enregistrement chaque fois que la fenêtre aura bougé ou aura été redimensionnée : respectivement les méthodes moveEvent()
et resizeEvent()
.UiMain
:
def moveEvent(self, ev):
""" Mémorisation de la géométrie. """
self.settings.setValue('Geometrie', self.geometry())
def resizeEvent(self, ev):
""" Mémorisation de la géométrie. """
self.settings.setValue('Geometrie', self.geometry())
On supprime closeEvent()
qui devient inutile.
Relancez les tests :
Tout fonctionne !
closeEvent()
.Code de main.py
:
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import QSettings, QRect
import os
import sys
class UiMain(QMainWindow):
def __init__(self):
super().__init__()
self.settings = QSettings(f"{os.getcwd()}{os.sep}params.conf", QSettings.IniFormat)
self.setGeometry(self.settings.value('Geometrie', QRect(200, 200, 400, 200)))
def moveEvent(self, ev):
""" Mémorisation de la géométrie. """
self.settings.setValue('Geometrie', self.geometry())
def resizeEvent(self, ev):
""" Mémorisation de la géométrie. """
self.settings.setValue('Geometrie', self.geometry())
if __name__ == '__main__':
def start():
app = QApplication(sys.argv)
fen = UiMain()
fen.show()
sys.exit(app.exec())
""" Affichage de la fenêtre. """
start()
Bonjour les codeurs !