Géométrie de la fenêtre principale et des dockables
Avant-propos
QDockWidget
dans PyQt5
.installEventFilter
sur QDockWidget
eventFilter
dans QMainWindow
Exécution retardée de fonctions avec QTimer
Alt+D
dans Pycharm, pour éditer fen_mere.ui
dans Qt Designer. Voir ci-après :
Ctrl+Alt+S
pour entrer dans les settings.- Tools > External tools
- Ajoutez un 'tool' (un outil) en cliquant sur le signe plus +, compléter les champs :
- Ajoutez un raccourci-clavier :
- Toujours dans les settings >
Keymap > External Tools > External Tools
- Clic droit sur Designer >
Add Keyboard Shortcut
- Tapez
Alt+D
- Validez
- Utilisation :
- Cliquez sur le fichier fen_mere.ui pour le sélectionner.
- Tapez
Alt+D
=> Qt Designer s'ouvre, vous pouvez éditer.
Description
Nous avons traité la persistance de la géométrie de la fenêtre principale.
Oui mais... elle contient 2 dockables qui ont également leur géométrie.
De plus, les dockables ont 2 paramètres supplémentaires :
Avant de poursuivre, nous allons coder les 2 boutons et
En effet il faut pouvoir rouvrir un dockable qui a été fermé par sa petite croix.
main.py
: Ajouter ces 2 méthodes à la classe UiMain, qui permettent cela :
""" Visibilité dockable des noeuds. """
def visibilite_noeuds(self):
""" Si visible => devient invisible et vice-versa. """
self.dockNodes.setVisible(not self.dockNodes.isVisible())
""" Visibilité dockable des paramètres. """
def visibilite_params(self):
self.dockParams.setVisible(not self.dockParams.isVisible())
""" Événements. """
self.actionNoeuds.triggered.connect(self.visibilite_noeuds)
self.actionParams.triggered.connect(self.visibilite_params)
UiMain
pour satisfaire aux tests.
- Le but est le suivant :
- Modifiez manuellement l'état des dockables : visible ou invisible, attaché ou détaché.
- Modifiez leur géométrie :
- position, largeur, hauteur lorsqu'ils sont détachés.
- Largeur et empilement seulement lorsqu'ils sont attachés à gauche ou à droite.
- Provoquez une fermeture 'normale' ou 'sauvage'.
- Relancez la fenêtre :
- Vous devez tout retrouver dans le même état que lors de la fermeture : fenêtre + dockables.
TestMain
).Remplacez tout le code de test_main.py
par celui-ci :
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QSettings, QRect, QTimer
from pc.main import UiMain
import pytest
import sys
import os
import random
class Main(UiMain):
""" Classe dérivée de la fenêtre à tester. """
def __init__(self, normal, dockable):
super().__init__()
self.premier_passage = normal is not None
self.dockable = dockable
self.sauvage = normal is False
self.settings_ante = QSettings(f"{os.getcwd()}{os.sep}ante.conf", QSettings.IniFormat)
def moveEvent(self, ev):
if not self.dockable:
super().moveEvent(ev)
def resizeEvent(self, ev):
if not self.dockable:
super().resizeEvent(ev)
def closeEvent(self, ev):
if self.premier_passage:
""" Premier passage : Mémorisation des états. """
self.settings_ante.setValue('Geometrie', self.geometry())
self.settings_ante.setValue('dockNodes visible', self.dockNodes.isVisible())
self.settings_ante.setValue('dockNodes détaché', self.dockNodes.isFloating())
self.settings_ante.setValue('dockNodes géométrie', self.dockNodes.geometry())
self.settings_ante.setValue('dockParams visible', self.dockParams.isVisible())
self.settings_ante.setValue('dockParams détaché', self.dockParams.isFloating())
self.settings_ante.setValue('dockParams géométrie', self.dockParams.geometry())
if not self.sauvage:
""" Non exécuté en mode 'sauvage'. """
super().closeEvent(ev)
# @pytest.mark.skip() # Commenter pour tester
class TestMain:
g_appli = QApplication(sys.argv)
tempo = QTimer()
tempo.setSingleShot(True)
@staticmethod
@pytest.fixture(scope="session", autouse=True)
def cleanup(request):
""" Appel final pour effacer les fichiers temporaires créés par les tests. """
def remove_files():
test_folder = os.getcwd() + os.sep
if os.path.isfile(f"{test_folder}ante.conf"):
os.remove(f"{test_folder}ante.conf")
if os.path.isfile(f"{test_folder}params.conf"):
os.remove(f"{test_folder}params.conf")
request.addfinalizer(remove_files)
@staticmethod
def get_rect(t_x, t_y, t_w, t_h):
""" Retourne un QRect aléatoire. """
return QRect(random.randint(*t_x), random.randint(*t_y), random.randint(*t_w), random.randint(*t_h))
@staticmethod
def open_window(normal=None, dockable=False):
win = Main(normal, dockable)
if normal is not None:
win.settings_ante.clear()
win.show()
return win
def connect(self, function):
try:
self.tempo.timeout.disconnect()
except TypeError:
pass
self.tempo.timeout.connect(function)
def fenetre_modif(self, normal):
win = self.open_window(normal)
win.setGeometry(self.get_rect((50, 800), (50, 800), (300, 800), (300, 800)))
self.connect(win.close)
self.tempo.start(300) # {x}ms : On laisse le temps à la fenêtre main de mémoriser ses états.
""" Boucle d'exécution. """
self.g_appli.exec()
def fenetre_assert(self):
def geometry(qrect):
return f"x = {qrect.x()}, y = {qrect.y()}, largeur = {qrect.width()}, hauteur = {qrect.height()}"
win = self.open_window()
self.connect(win.close)
self.tempo.start(50)
geometry_ante = win.settings_ante.value('Geometrie', QRect(200, 200, 800, 600))
geometry_now = win.geometry()
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 : {geometry(geometry_ante)}."
f"\n- Géométrie à l'ouverture actuelle : {geometry(geometry_now)}."
)
assert geometry_now == geometry_ante
""" Boucle d'exécution. """
self.g_appli.exec()
""" *************************************************************************************** """
""" LES TESTS """
""" *************************************************************************************** """
# @pytest.mark.skip() # Commenter pour tester
def test_ferme_normal(self):
""" Phase 1 : 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. """
self.fenetre_modif(normal=True) # True = Fermeture normale.
""" Phase 2: Réouverture de la fenêtre, lecture de sa géométrie, comparaison. """
self.fenetre_assert()
# @pytest.mark.skip() # Commenter pour tester
def test_ferme_sauvage(self):
self.fenetre_modif(normal=False) # False = Fermeture sauvage.
self.fenetre_assert()
# @pytest.mark.skip() # Commenter pour tester
def test_dock_state(self):
def update_dock():
self.connect(win.close)
self.tempo.start(500) # {x} ms : On laisse le temps à la fenêtre main de mémoriser ses états.
""" Modification des états des dockables, de façon aléatoire. """
for o_dock in [win.dockNodes, win.dockParams]:
x, y = win.x() + win.width() // 2, win.y() + win.height() // 2
o_dock.setFloating(random.choice([False, True]))
if o_dock.isFloating():
o_dock.setGeometry(self.get_rect((x - 50, x + 50), (y - 50, y + 50), (50, 400), (50, 400)))
else:
o_dock.setFixedWidth(random.randint(82, 200))
def state_dock():
self.connect(win.close) # Obligatoirement avant les asserts.
self.tempo.start(50)
""" Vérification des résultats. """
msg = 'DockNodes : Flottant (détaché).'
assert win.dockNodes.isFloating() == win.settings_ante.value('dockNodes détaché'), msg
res, qrect_now, qrect_ante = 0, win.dockNodes.geometry().getRect(), win.settings_ante.value('dockNodes géométrie').getRect()
if win.dockNodes.isFloating():
msg = 'DockNodes : Géométrie (x, y, w, h).'
for i in range(4):
""" Tolérence : on accepte 3 bonnes valeurs sur 4. """
res += qrect_now[i] == qrect_ante[i]
assert res > 2, msg
else:
msg = 'DockNodes : Largeur.' # 0=x, 1=y, 2=width, 3=height
assert qrect_now[2] == qrect_ante[2], msg # [2]=width
msg = 'DockParams : Flottant (détaché).'
assert win.dockParams.isFloating() == win.settings_ante.value('dockParams détaché'), msg
res, qrect_now, qrect_ante = 0, win.dockParams.geometry().getRect(), win.settings_ante.value('dockParams géométrie').getRect()
if win.dockParams.isFloating():
msg = 'DockParams : Géométrie (x, y, w, h).'
for i in range(4):
res += qrect_now[i] == qrect_ante[i]
assert res > 2, msg
else:
msg = 'DockParams : Largeur.'
assert qrect_now[2] == qrect_ante[2], msg
""" Phase 1 : Modification aléatoire des dockables (détaché ou attaché, position, taille, visibilité). """
win = self.open_window(normal=True)
update_dock()
""" Boucle d'exécution. Sortie lorsque la fenêtre se ferme. """
self.g_appli.exec()
""" Phase 2 : Ouverture fenêtre, lecture de l'état des dockables, assertions et fermeture. """
win = self.open_window()
state_dock()
""" Boucle d'exécution. Sortie lorsque la fenêtre se ferme. """
self.g_appli.exec()
@pytest.mark.skip() # Commenter pour tester
def test_dock_visible(self):
def update_dock():
self.connect(win.close)
self.tempo.start(300) # {x} ms : On laisse le temps à la fenêtre main de mémoriser ses états.
""" Modification de tous les états, de façon aléatoire. """
for o_dock in [win.dockNodes, win.dockParams]:
o_dock.setVisible(random.choice([False, True]))
def state_dock():
self.connect(win.close)
self.tempo.start(50) # Obligatoirement avant les asserts.
""" Vérification des résultats. """
try:
msg = 'DockNodes : Visibilité.'
assert win.dockNodes.isVisible() == win.settings_ante.value('dockNodes visible'), msg
msg = 'DockParams : Visibilité.'
assert win.dockParams.isVisible() == win.settings_ante.value('dockParams visible'), msg
except (Exception,):
pass
for i in range(4):
""" Phase 1 : Modification aléatoire des dockables (détaché ou attaché, position, taille, visibilité). """
self.indx = 0
win = self.open_window(normal=True, dockable=True)
update_dock()
""" Boucle d'exécution. Sortie lorsque la fenêtre se ferme. """
self.g_appli.exec()
""" Phase 2 : Ouverture fenêtre, lecture de l'état des dockables, assertions et fermeture. """
win = self.open_window()
state_dock()
""" Boucle d'exécution. Sortie lorsque la fenêtre se ferme. """
self.g_appli.exec()
Snippets
Bonjour les codeurs !