Confort dans l'édition des paramètres
Avant-propos
Voir exemple ParameterTree
avec son code dans D:\anaconda\envs\ia\Lib\site-packages\pyqtgraph\examples
Description
QVBoxLayout
dans le dockable des paramètres. Il permettra l'affichage des "Tree parameters
" (images ci-dessus).fen_mère.ui
avec Qt Designer (Rappel : Alt + D)Vertical Layout
" dans le dockable des paramètres.params_layout
".dockParams
.Éléments concernés par l'édition de paramètres depuis le dockable :
Pour leur gestion, chacun de ces éléments (objets) doit posséder :
s_id
(str) identifiant. C'est le nom de sa grappe. Ex: "CtrlScene"
, "Node3"
, etc.get_default()
, qui fournit ses paramètres par défaut.refresh()
, qui raffraîchit l'affichage.La classe /pc/parameters.py > Parameters
:
parameters.py
from PyQt5.QtGui import QColor
from functions.utils import Dictionary
from pyqtgraph.parametertree import Parameter, ParameterTree
import pyqtgraph as pg
import copy
class Parameters:
"""
La scène (CtrlScene) et chaque node du graphe ont une instance de cette classe.
"""
def __init__(self, o_parent):
self.o_parent = o_parent
self.o_scene = None
self.od_real = None # Valeurs réelles, mémorisées sur disque dur dans le fichier params.pkl.
self.od_default = None # Valeurs par défaut, codées en dur dans o_parent.get_default().
self.od_params = None # Dictionnaire des vraies valeurs.
self.layout = None
self.setup()
def setup(self):
self.o_scene = self.o_parent if self.o_parent.s_id == 'CtrlScene' else self.o_parent.o_scene
self.od_real = self.o_scene.o_pkl.od_pkl
self.od_default = Dictionary(self.o_parent.get_default())
self.layout = self.o_scene.o_main.params_layout # Nom donné dans Qt Designer.
self.od_params = Dictionary()
self.set_params()
def set_params(self):
ll_keys = self.od_default.key_list()
""" self.od_params n'est instancié qu'une fois (setup()), mais affecté plusieurs fois (ici). """
self.od_params.clear() # self.od_params conserve son adresse-mémoire.
for l_keys in ll_keys:
v_default = self.od_default.read(l_keys)
if isinstance(v_default, list):
v_default = v_default[0]
l_k = [self.o_parent.s_id] + l_keys
self.od_params.write(l_keys, self.od_real.read(l_k, v_default))
def clear_params(self):
""" Effacement du dockable. """
for i in reversed(range(self.layout.count())):
self.layout.itemAt(i).widget().deleteLater() # <- Nettoyage du layout
def show_params(self):
"""
Code appelant : CtrlScene.show_params() - Affichage dans le dockable 'Params'.
:return: NA.
"""
""" 1 - Liste des paramètres 'l_params' créée à partir de od_real et od_default. """
l_params = self.dic2params()
try:
p = Parameter.create(name='params', type='group', children=l_params) # GroupParameter
except (Exception,):
# raise SystemExit("Parameters.show_params() :\nLa liste 'l_params' n'est pas conforme.")
print("Parameters.show_params() :\nLa liste 'l_params' n'est pas conforme.")
return
""" 2 - Mise en place de la liste des paramètres dans un 'ParameterTree'. """
pt = ParameterTree(showHeader=False) # ParameterTree
pt.setParameters(p, showTop=False) # Titre 'params' masqué.
""" 3 - Nettoyage de l'arbre des paramètres dans le dockable 'params'. """
self.clear_params()
""" 4 - Affichage à l'écran, en plaçant l'arbre de paramètres dans un layout propre. """
self.layout.addWidget(pt)
""" 5 - Événement : change() est appelée à chaque modification. """
p.sigTreeStateChanged.connect(self.change)
def dic2params(self):
r""" La valeur de retour est une liste formatée pour satisfaire aux conditions imposées par pyqtgraph.Parameter.
Cette liste contient un ou plusieurs dictionnaires, contenant à leur tour :
- Des paramètres (nom, type, valeur par défaut, valeur réelle, paramètres optionnels)
- Des 'children' de type 'group' qui contiennent à leur tour une liste au même format. <-- list()
L'ensemble des paramètres constitue donc une arborescence (donc hiérarchique) assez complexe.
Voir exemple : D:\anaconda\envs\robot\Lib\site-packages\pyqtgraph\examples\parametertree.py
"""
l_params = list()
# à coder ...
return l_params
def change(self, _, l_params):
""" 1 - Mise à jour du dictionnaire od_real. """
# value = à coder ...
# l_keys = à coder ...
self.od_real.write([self.o_parent.s_id] + l_keys, value)
""" 2 - Enregistrement. """
if self.o_scene.o_pkl.backup():
self.set_params()
self.o_parent.refresh(l_keys)
CtrlScene
:
__init__()
:ctrl_scene.py > CtrlScene.__init__()
def __init__(self, o_main, dir_name):
super().__init__()
self.o_main = o_main
self.graph_name = dir_name # On donne au graphe le nom de son dossier.
self.friendly_name = '' # Titre du dockable des paramètres.
self.s_id = 'CtrlScene' # Nom de la grappe dans les paramètres.
self.o_gr_scene = None # Instance d'un GraphicsScene dérivé : UiScene.
self.o_gr_view = None # Instance d'un GraphicsView dérivé : UiView.
self.o_params = None
self.o_pkl = None # Instance de Pkl (Une seule instance par graphe)
self.main_key = f"Paramètres du graphe '{self.graph_name}'"
self.setup()
o_main
a été ajouté, ainsi que l'attribut friendly_name
! Modifier le code appelant en conséquence :Dans main.py
, modifier l'instanciation de CtrlScene
, en ajoutant l'argument self
.
Retirer également l'affectation provisoire de friendly_name
.
Le code de main.py > UiMain.open_graph()
devient :
def open_graph(self, dir_name):
""" Ajoute un onglet dans le panneau central.
:param dir_name: Type booléen si le code appelant est 'actionOpen.triggered', str sinon.
:return: NA
"""
backups_folder = os.path.abspath(f"{os.getcwd()}{os.sep}..{os.sep}backups")
graph_dir = f"{backups_folder}{os.sep}{dir_name}"
""" Si graph_dir est invalide, on ouvre le sélecteur de dossier. """
if not os.path.isdir(graph_dir):
graph_dir = QFileDialog.getExistingDirectory(parent=None, caption="Choisissez un graphe",
directory='../backups', options=QFileDialog.ShowDirsOnly)
if graph_dir == '': # Clic sur 'Annuler'
return
graph_dir = os.path.abspath(graph_dir)
if graph_dir == backups_folder or not os.path.isdir(graph_dir): # Non conforme.
return
graph_name = os.path.abspath(graph_dir).split(os.sep)[-1]
o_scene = CtrlScene(self, graph_name)
o_scene.graph_name = graph_name
""" Liste des onglets existants. """
l_tabs = list()
for i in range(self.tabGraphs.count()):
l_tabs.append(self.tabGraphs.widget(i).graph_name)
""" Sélection du graphe choisi. """
try:
""" Ce graphe existe déjà dans les onglets => on note son index. """
indx = l_tabs.index(graph_name)
except ValueError:
""" Ce graphe n'existe pas dans les onglets => on l'ajoute. """
indx = self.tabGraphs.count() # Le dernier.
self.tabGraphs.addTab(o_scene, o_scene.friendly_name) # Ajoute l'onglet.
self.tabGraphs.setCurrentIndex(indx) # On active l'onglet.
ctrl_scene.py > CtrlScene.setup()
""" Paramètres. """
self.o_pkl = Pkl(self)
self.o_params = Parameters(self)
self.friendly_name = self.o_params.od_params.read([self.main_key, "Nom de l'onglet"], 'Choisir un nom')
Parameters
!ctrl_scene.py > CtrlScene.get_default()
def get_default(self):
return {
self.main_key: {
"Nom de l'onglet": 'Choisir un nom',
'Couleurs': {
'Fond': '#393939', # <- Si alpha, le placer à la fin -> ex : #39393944 (alpha = 44).
'Traits': '#484848', # Helper utilisé : pg.mkColor
'Repères': '#2e2e2e',
},
'Pas de la grille': 16,
'Grille magnétique': False,
'Traits de repère': [5, {'step': 1, 'limits': (1, 20)}],
'Épaisseurs': {
'Traits': [1.1, {'step': .1, 'limits': (.1, 3.)}],
'Repères': [2., {'step': .2, 'limits': (.2, 6.)}],
}
}
}
Nous n'avons pas encore traité les nodes. Par conséquent, nous allons simplement éditer les paramètres de la scène pour l'instant. Dans l'ordre :
CtrlScene.show_params()
.Parameters.show_params()
.Parameters.change()
← appelée automatiquement à chaque modification (événement).CtrlScene.refresh()
.UiScene.set_params()
← Mise à jour des paramètres d'affichage puis actualisation de l'affichage.Il a déjà été utilisé, et se trouve dans UiMain.set_events()
:
self.tabGraphs.currentChanged.connect(lambda: ut.save_tabs(self))
tab_changed()
:main.py > UiMain.set_events()
self.tabGraphs.currentChanged.connect(self.tab_changed) # Ouverture et déplacement d'onglet.
main.py > UiMain.tab_changed()
:
def tab_changed(self, index=-1):
""" Hook.
:param index: Automatiquement renseigné par le connect de 'currentChanged'.
:return: NA.
"""
indx = index if index >= 0 else self.tabGraphs.currentIndex()
if indx < 0:
return # Si aucun onglet affiché.
o_scene = self.tabGraphs.widget(indx)
o_scene.show_params()
ut.save_tabs(self) # On restitue la fonction première de self.tabGraphs.currentChanged
indx
.show_params()
de cette scène.save_tabs()
.UiMain.setup()
:main.py > UiMain.setup()
self.tab_changed()
main.py > UiMain.close_graph()
devient :
def close_graph(self, index):
""" 1 - Effacement du dockable des paramètres. """
self.tabGraphs.widget(index).o_params.clear_params()
""" 2 - Suppression de l'onglet. """
self.tabGraphs.removeTab(index)
ut.save_tabs(self) # Backup
ctrl_scene.py > CtrlScene.show_params()
:
def show_params(self):
"""
Si aucun élément du graphe n'est sélectionné, affiche les paramètres de la vue.
Si un élémént est sélectionné, affiche les paramètres de cet élément.
Si plusieurs éléménts sont sélectionnés, affiche une image 'multi-sélection'.
:return: NA
"""
l_selected = self.o_gr_scene.selectedItems()
if len(l_selected) == 0:
self.o_params.show_params()
parameters.py > Parameters.show_params()
: Voir code dans parameters.py
Remarquez, à la fin, l'événement sigTreeStateChanged
. Il appelle la méthode self.change()
, qui est chargée :
parameters.py > Parameters.change()
:
def change(self, _, l_params):
""" 1 - Mise à jour du dictionnaire od_real. """
# value = ... à coder ...
# l_keys = ... à coder ...
self.od_real.write([self.o_parent.s_id] + l_keys, value)
""" 2 - Enregistrement. """
if self.o_scene.o_pkl.backup():
self.set_params()
self.o_parent.refresh(l_keys) # [1:] => Nom de la grappe retiré.
Cette fonctionnalité est confiée à sa méthode refresh()
.
Code de ctrl_scene.py > CtrlScene.refresh()
:
def refresh(self, l_keys):
""" Actualisation de l'affichage, délégué à UiScene. """
self.o_gr_scene.set_params()
""" Titre de l'onglet. """
if l_keys[-1] == "Nom de l'onglet":
titre = self.o_params.od_params.read(l_keys)
indx = self.o_main.tabGraphs.currentIndex()
self.o_main.tabGraphs.setTabText(indx, titre)
Quels sont les paramètres concernés ? ceux de la grappe de la scène. Depuis UiScene
, nous avons :
self.d_params = self.o_scene.o_params.od_params[self.o_scene.main_key]
self.d_params['Couleurs']['Fond']
self.d_params['Couleurs']['Traits']
self.d_params['Couleurs']['Repères']
self.d_params['Pas de la grille']
self.d_params['Traits de repère']
self.d_params['Épaisseurs']['Traits']
self.d_params['Épaisseurs']['Repères']
Par conséquent, dans UiScene
:
d_params
dans __init__()
:ui_scene.py > UiScene.__init__()
self.d_params = None
init_ui()
par ces 2 méthodes :ui_scene.py > UiScene
def init_ui(self):
self.setSceneRect(-32000, -32000, 64000, 64000) # x, y, w, h
self.set_params()
def set_params(self):
self.d_params = self.o_scene.o_params.od_params[self.o_scene.main_key]
self.setBackgroundBrush(pg.mkColor(self.d_params['Couleurs']['Fond']))
self.update()
Dans la méthode drawBackground()
, remplacez ces lignes :
ui_scene.py > UiScene.drawBackground()
grid_mesh = self.d_params['Pas de la grille'] # (16) - Pas de la grille.
guide_lines = self.d_params['Traits de repère'] # (5) - traits_repere
line_color = self.d_params['Couleurs']['Traits'] # (#484848) - couleur_traits
line_thickness = self.d_params['Épaisseurs']['Traits'] # (1.1) - epaisseur_traits
marker_color = self.d_params['Couleurs']['Repères'] # (#2e2e2e) - couleur_reperes
marker_thickness = self.d_params['Épaisseurs']['Repères'] # (2.) - epaisseur_reperes
dic2params()
. Elle utilise les super-dictionnaires od_default
et od_real
pour obtenir l_params
. CtrlSene
, le résultat attendu pour l_params
:
l_params = [{
'name': "Paramètres du graphe 'Prédictions par IA'",
'type': 'group',
'children': [{
'name': "Nom de l'onglet",
'default': 'Choisir un nom',
'value': 'Londres',
'l_keys': ["Paramètres du graphe 'Prédictions par IA'", "Nom de l'onglet"],
'type': 'str'
}, {
'name': 'Couleurs',
'type': 'group',
'children': [{
'name': 'Fond',
'default': <PyQt5.QtGui.QColor object at 0x00000232F5462200>,
'value': <PyQt5.QtGui.QColor object at 0x00000232F5462430>,
'l_keys': ["Paramètres du graphe 'Prédictions par IA'", 'Couleurs', 'Fond'],
'type': 'color'
}, {
'name': 'Traits',
'default': <PyQt5.QtGui.QColor object at 0x00000232F54623C0>,
'value': <PyQt5.QtGui.QColor object at 0x00000232F5462350>,
'l_keys': ["Paramètres du graphe 'Prédictions par IA'", 'Couleurs', 'Traits'],
'type': 'color'
}, {
'name': 'Repères',
'default': <PyQt5.QtGui.QColor object at 0x00000232F54624A0>,
'value': <PyQt5.QtGui.QColor object at 0x00000232F5462510>,
'l_keys': ["Paramètres du graphe 'Prédictions par IA'", 'Couleurs', 'Repères'],
'type': 'color'
}
]
}, {
'name': 'Pas de la grille',
'default': 16,
'value': 10,
'l_keys': ["Paramètres du graphe 'Prédictions par IA'", 'Pas de la grille'],
'type': 'int'
}, {
'name': 'Grille magnétique',
'default': False,
'value': True,
'l_keys': ["Paramètres du graphe 'Prédictions par IA'", 'Grille magnétique'],
'type': 'bool'
}, {
'step': 1,
'limits': (1, 20),
'name': 'Traits de repère',
'default': 5,
'value': 8,
'l_keys': ["Paramètres du graphe 'Prédictions par IA'", 'Traits de repère'],
'type': 'int'
}, {
'name': 'Épaisseurs',
'type': 'group',
'children': [{
'step': 0.1,
'limits': (0.1, 3.0),
'name': 'Traits',
'default': 1.1,
'value': 1.0,
'l_keys': ["Paramètres du graphe 'Prédictions par IA'", 'Épaisseurs', 'Traits'],
'type': 'float'
}, {
'step': 0.2,
'limits': (0.2, 6.0),
'name': 'Repères',
'default': 2.0,
'value': 2.0,
'l_keys': ["Paramètres du graphe 'Prédictions par IA'", 'Épaisseurs', 'Repères'],
'type': 'float'
}]
}
]
}]
l_keys
qui ne servent pas ... pour l'affichage ! Elles serviront lors de modifications, dans la méthode change()
.default
proviennent de od_default
, celles dont la clé est value
proviennent de od_real
si elle y existe sinon de od_default
.UiMain.tab_changed()
est automatiquement appelée par l'événement self.tabGraphs.currentChanged
.
clear_params()
:
main.py
, remplacer la méthode UiMain.close_graph()
par celle-ci :main.py > UiMain.close_graph()
def close_graph(self, index):
""" 1 - Effacement du dockable des paramètres. """
self.tabGraphs.widget(index).o_params.clear_params()
""" 2 - Suppression de l'onglet. """
self.tabGraphs.removeTab(index)
ut.save_tabs(self) # Backup
Vérification
/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 -p no:warnings
Parameters
, les dictionnaires od_default
(valeurs par défaut) et od_real
(valeurs réelles), sont opérationnels.dic2params()
:
pyqtgraph.Parameter
.Parameter
, créez un fichier de test : test_parameters.py
, collez-y le code suivant :/tests/test_parameters.py
:
from PyQt5.QtWidgets import QApplication
from pc.main import UiMain
import pytest
import sys
import os
@pytest.fixture(scope='session') # scope = classes, modules, packages or session
def setup():
_ = QApplication(sys.argv)
win = UiMain()
assert os.path.isdir('../backups') is True, "Le dossier /backups/' n'existe pas."
list_graphs = os.listdir('../backups')
assert len(list_graphs) > 0, "Le dossier 'backups' ne contient aucun sous-dossier."
graph = list_graphs[0] # On choisit le premier sous-dossier (le premier graphe).
win.open_graph(graph) # Affichage d'un graphe pour avoir un 'CtrlScene()'.
o_scene = win.tabGraphs.widget(0) # Objet CtrlScene pour avoir un 'Parameters()'.
o_params = o_scene.o_params # Objet Parameters() pour accéder aux méthodes à tester.
o_params.od_real.clear()
return {
'scene': o_scene,
'params': o_params,
'real': o_params.od_real
}
def test_empty_dic(setup):
""" Test sur un dictionnaire vide. """
o_params = setup['params']
o_params.od_default.clear()
o_params.od_default.write("Paramètres du graphe 'Aragon'", {})
l_params = o_params.dic2params()
assert l_params == [{'name': "Paramètres du graphe 'Aragon'", 'type': 'group', 'children': []}]
def test_str(setup):
""" Test sur un paramètre de type str. """
o_params = setup['params']
o_params.od_default.clear()
l_keys = ["Paramètres du graphe 'Aragon'", "Nom de l'onglet"]
o_params.od_default.write(l_keys, 'Nom par défaut')
o_params.od_real.write(['CtrlScene'] + l_keys, 'test')
l_params = o_params.dic2params()
assert l_params == [{'name': "Paramètres du graphe 'Aragon'", 'type': 'group', 'children': [{'name': "Nom de l'onglet", 'default': 'Nom par défaut', 'value': 'test', 'l_keys': ["Paramètres du graphe 'Aragon'", "Nom de l'onglet"], 'type': 'str'}]}]
def test_color(setup):
""" Test sur un paramètre de type color. """
o_params = setup['params']
o_params.od_default.clear()
o_params.od_default.write(["Paramètres du graphe 'Aragon'", 'Couleurs', 'Fond'], '#393939')
l_params = o_params.dic2params()
""" Les valeurs étant des objets, on ne vérifie que le type (QColor). """
assert l_params[0]['children'][0]['children'][0]['default'].__class__.__name__ == 'QColor'
def test_int(setup):
""" Test sur un paramètre de type int. """
o_params = setup['params']
o_params.od_default.clear()
l_keys = ["Paramètres du graphe 'Aragon'", 'Pas de la grille']
o_params.od_default.write(l_keys, 16)
o_params.od_real.write(['CtrlScene'] + l_keys, 12345)
l_params = o_params.dic2params()
assert l_params == [{'name': "Paramètres du graphe 'Aragon'", 'type': 'group', 'children': [{'name': 'Pas de la grille', 'default': 16, 'value': 12345, 'l_keys': ["Paramètres du graphe 'Aragon'", 'Pas de la grille'], 'type': 'int'}]}]
def test_bool(setup):
""" Test sur un paramètre de type bool. """
o_params = setup['params']
o_params.od_default.clear()
l_keys = ["Paramètres du graphe 'Aragon'", 'Grille magnétique']
o_params.od_default.write(l_keys, False)
o_params.od_real.write(['CtrlScene'] + l_keys, True)
l_params = o_params.dic2params()
assert l_params == [{'name': "Paramètres du graphe 'Aragon'", 'type': 'group', 'children': [{'name': 'Grille magnétique', 'default': False, 'value': True, 'l_keys': ["Paramètres du graphe 'Aragon'", 'Grille magnétique'], 'type': 'bool'}]}]
def test_int_spin(setup):
""" Test sur un paramètre de type int dans un spin (clés supplémentaires : pas et limites). """
o_params = setup['params']
o_params.od_default.clear()
l_keys = ["Paramètres du graphe 'Aragon'", 'Traits de repère']
o_params.od_default.write(l_keys, [5, {'step': 1, 'limits': (1, 20)}])
o_params.od_real.write(['CtrlScene'] + l_keys, 54321)
l_params = o_params.dic2params()
assert l_params == [{'name': "Paramètres du graphe 'Aragon'", 'type': 'group', 'children': [{'step': 1, 'limits': (1, 20), 'name': 'Traits de repère', 'default': 5, 'value': 54321, 'l_keys': ["Paramètres du graphe 'Aragon'", 'Traits de repère'], 'type': 'int'}]}]
def test_float_spin(setup):
""" Test sur un paramètre de type float dans un spin (clés supplémentaires : pas et limites). """
o_params = setup['params']
o_params.od_default.clear()
l_keys = ["Paramètres du graphe 'Aragon'", 'Épaisseurs', 'Traits']
o_params.od_default.write(l_keys, [1.1, {'step': 0.1, 'limits': (0.1, 3.0)}])
o_params.od_real.write(['CtrlScene'] + l_keys, 8.888)
l_params = o_params.dic2params()
assert l_params == [{'name': "Paramètres du graphe 'Aragon'", 'type': 'group', 'children': [{'name': 'Épaisseurs', 'type': 'group', 'children': [{'step': 0.1, 'limits': (0.1, 3.0), 'name': 'Traits', 'default': 1.1, 'value': 8.888, 'l_keys': ["Paramètres du graphe 'Aragon'", 'Épaisseurs', 'Traits'], 'type': 'float'}]}]}]
o_params.od_real.print()
l_keys
et value
dans la méthode change()
.Bon courage et bon coding !
Snippets
Bonjour les codeurs !