Le poste de contrôle (PC) / Les paramètres /
Paramètres : 3 - Édition dans le dockable

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

 

A faire : Modifier la fenêtre avec Qt Designer. Ajouter un QVBoxLayout dans le dockable des paramètres. Il permettra l'affichage des "Tree parameters" (images ci-dessus).

É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 :

Certains paramètres ne sont pas éditables depuis le dockable, tels que : position, zoom, edges, ...

La classe /pc/parameters.py > Parameters :



Quand et comment afficher les paramètres ?

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 :

  1. Événement 'changement d'onglet'.
  2. Méthode CtrlScene.show_params().
  3. Méthode Parameters.show_params().
  4. Méthode Parameters.change()   ← appelée automatiquement à chaque modification (événement).
  5. Méthode CtrlScene.refresh().
  6. Méthode UiScene.set_params()   ← Mise à jour des paramètres d'affichage puis actualisation de l'affichage.

1 - Événement 'Changement d'onglet' :

Il a déjà été utilisé, et se trouve dans UiMain.set_events() :
self.tabGraphs.currentChanged.connect(lambda: ut.save_tabs(self))

Évitons un bug : Chaque onglet peut être fermé par son petit bouton de fermeture. Qu'advient-il lorsqu'on ferme le dernier onglet ?
    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

2 - Méthode 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()

3 - Méthode 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 :


4 - Méthode 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é.

5 - Actualisation de l'affichage et du titre de l'onglet.

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)

6 - Mise à jour des paramètres d'affichage puis rafraîchissement de la vue.

Quels sont les paramètres concernés ? ceux de la grappe de la scène. Depuis UiScene, nous avons :

 

Par conséquent, dans 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

A faire : Coder la méthode dic2params(). Elle utilise les super-dictionnaires od_default et od_real pour obtenir l_params
Tester.

Correction de bug :

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

Avec TDD :
  • Pour éviter certains warnings dûs au module pyqtgraph, nous allons modifier le fichier /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
    
  • Dans la classe Parameters, les dictionnaires od_default (valeurs par défaut) et od_real (valeurs réelles), sont opérationnels.
  • Vous devez coder la méthode dic2params() :
    • À partir de ces 2 dictionnaires, elle doit fournir une liste assez complexe, au format imposé par pyqtgraph.Parameter.
  • Les tests ci-après vous permettront une mise au point pas à pas.
  • À partir de la classe 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()
Sans TDD : Calculez les variables l_keys et value dans la méthode change().

Bon courage et bon coding !


Snippets

Essayez de résoudre cette fonctionnalité par vous-même.
Consultez les réponses (snippets) seulement si vous n'avez pas trop de temps.

Bonjour les codeurs !