Le poste de contrôle (PC) / Les onglets /
Création d'un graphe

Base visuelle de la programmation graphique


Avant-propos

Réviser (ou apprendre) → Python : les f-strings


Description

Création de la boîte de dialogue :

 


Libellé des boutons :

Si votre Qt Designer est en français, vous remarquerez une différence dans le libellé des boutons, ci-dessus : 

A faireutils.py et main.py → implémenter les codes suivants :
from PyQt5.QtCore import QSettings, QTimer, QRect, QTranslator, QLocale, QLibraryInfo
import subprocess
import inspect
import os
import sys


class DateTime:
    """
    Classe dédiée à la gestion du temps :
    - Dates, heures, différents formats (Unix, Windows, ...)
    - Temporisateurs, horloges, ...
    """
    def __init__(self):
        self.d_tempo = dict()     # Dictionnaire de temporisateurs.

    def delay(self, client_function, delay=20, params=None):
        """ Asynchrone (ne prend pas la main).
        :param client_function: Cette fonction sera exécutée après un délai.
        :param delay: Délai en ms (20ms par défaut).
        :param params: Optionnel. Arguments dans l'appel de la fonction.
        :return: NA
        """
        key = client_function.__name__
        if key in self.d_tempo:
            if self.d_tempo[key].isActive():
                self.d_tempo[key].disconnect()
        else:
            self.d_tempo[key] = QTimer()
            self.d_tempo[key].setSingleShot(True)

        if params is None:
            self.d_tempo[key].timeout.connect(client_function)
        else:
            self.d_tempo[key].timeout.connect(lambda: client_function(params))

        self.d_tempo[key].start(delay)


class Utils:
    def __init__(self):
        self.o_dt = DateTime()
        self.caller_dir = os.getcwd()  # dossier du code appelant.
        self.settings = QSettings(f"{self.caller_dir}{os.sep}params.conf", QSettings.IniFormat)

    def save_state(self, win):
        def memo_state():
            self.settings.setValue('Geometrie', win.geometry())
            self.settings.setValue('Etat', win.saveState())
        self.o_dt.delay(memo_state, 100)

    def restore_state(self, win):
        """ Lecture et application de la géométrie à la fenêtre principale. """
        win.setGeometry(self.settings.value('Geometrie', QRect(200, 200, 400, 200)))
        """ Lecture et application de la géométrie aux dockables. """
        q_state = self.settings.value('Etat')
        if q_state is not None:
            win.restoreState(q_state)

    @staticmethod
    def translate(app):
        """ Exemple d'utilisation : ut.translate(app)
        :param app: objet QApplication
        :return: NA
        """
        translator = QTranslator(app)
        locale = QLocale.system().name().split('_')[0]  # Paramètre de Windows. Exemple : 'fr'
        path = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
        translator.load(f'{path}/qtbase_{locale}')
        app.installTranslator(translator)

    @staticmethod
    def ui2py(action=None):
        """
        Cette méthode est chargée de compiler tous les fichiers .ui et .qrc du dossier /design du code appelant.
        - Le code appelant manipule des fenêtres ou des boîtes de dialogue créées par Qt Designer.
        - Le code appelant est dans un dossier : /pc, /affichage, /tests, ... et d'autres à venir.
        :param action: False, True ou None
            - Si False, ne compile pas.
            - Si True, compile systématiquement.
            - si None (par défaut), compile si nécessaire (automatique).
                |_ automatique = seuls les fichiers-source qui ont été modifiés.
        :return: True si réussite, False si échec.
        """
        if action is False:
            return True

        """ Création de listes des fichiers concernés. """
        # client_dir = os.getcwd()  # Le test_main.py ne passe plus : sa classe Main (qui hérite de Ui_Main)
        client_dir = os.path.abspath(os.path.dirname(inspect.stack()[1][1]))  # dossier du code client.
        design_dir = os.path.abspath(f"{client_dir}{os.sep}design")
        l_files_ui = list()     # Liste des fichiers-source '.ui' (interfaces utilisateur).
        l_files_rc = list()     # Liste des fichiers-source '.qrc' (ressources).
        l_targets = list()      # Liste des fichiers-cible à obtenir '.py'.
        for file_name in os.listdir(design_dir):
            if file_name.lower().endswith('.ui'):
                l_files_ui.append(file_name[:-3])   # Nom du fichier sans l'extension '.ui'
                l_targets.append(f"{client_dir}{os.sep}{file_name[:-3]}.py")  # chemin complet, avec l'extension '.py'
            elif file_name.lower().endswith('.qrc'):
                l_files_rc.append(file_name[:-4])   # Nom du fichier sans l'extension '.qrc'
                l_targets.append(f"{client_dir}{os.sep}{file_name[:-4]}_rc.py")      # complet, avec l'extension '.py'

        settings = QSettings(f"{client_dir}{os.sep}params.conf", QSettings.IniFormat)
        if action is True:
            """ action == True => Compilation forcée (pas de filtrage). """
            filtered_files_ui = l_files_ui
            filtered_files_rc = l_files_rc
        else:
            """ Compilation automatique => Filtrage (seulement les fichiers modifiés).
            On compare les date-heure (de modification) de chaque fichier à compiler avec celles qui ont été mémorisées.
            Si égalité => compilation inutile.
            """
            filtered_files_ui = list()
            for file_ui in l_files_ui:
                file_source = f"{design_dir}{os.sep}{file_ui}.ui"
                file_target = f"{client_dir}{os.sep}{file_ui}.py"
                dh_now = os.path.getmtime(file_source)
                dh_memo = settings.value(f"dh_{file_source}", 0.)
                if not os.path.isfile(file_target) or float(dh_now) != float(dh_memo):
                    filtered_files_ui.append(file_ui)

            filtered_files_rc = list()
            for file_rc in l_files_rc:
                file_source = f"{design_dir}{os.sep}{file_rc}.qrc"
                file_target = f"{client_dir}{os.sep}{file_rc}.py"
                dh_now = os.path.getmtime(file_source)
                dh_memo = settings.value(f"dh_{file_source}", 0.)
                if not os.path.isfile(file_target) or float(dh_now) != float(dh_memo):
                    filtered_files_rc.append(file_rc)

        """ Compilation """
        scripts_path = f"{os.path.dirname(sys.executable)}{os.sep}Scripts{os.sep}"
        pyuic_exe = f"{scripts_path}pyuic5.exe"
        for file_ui in filtered_files_ui:
            f_source = f"{design_dir}{os.sep}{file_ui}.ui"
            f_target = f"{client_dir}{os.sep}{file_ui}.py"
            if subprocess.run([pyuic_exe, f_source, '-o', f_target, '-x']).returncode != 0:
                return False
            settings.setValue(f"dh_{f_source}", os.path.getmtime(f_source))

        pyrcc_exe = f"{scripts_path}pyrcc5.exe"
        for file_rc in filtered_files_rc:
            f_source = f"{design_dir}{os.sep}{file_rc}.qrc"
            f_target = f"{client_dir}{os.sep}{file_rc}_rc.py"
            if subprocess.run([pyrcc_exe, f_source, '-o', f_target]).returncode != 0:
                return False
            settings.setValue(f"dh_{f_source}", os.path.getmtime(f_source))

        return True

Création du graphe :

from PyQt5.QtWidgets import QApplication, QMainWindow, QDialog
from PyQt5.QtCore import QEvent
import sys
import os

from functions.utils import Utils
ut = Utils()
if ut.ui2py():
    """ Si la compilation a réussi. """
    from fen_mere import Ui_MainWindow
    from dialog_new_graph import Ui_Dialog_new
else:
    """ Si la compilation a échoué. """
    raise SystemExit("La compilation des fichiers de Qt Designer a échoué.")


class UiMain(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)

        ut.restore_state(self)

        """ Événements. """
        self.dockNodes.installEventFilter(self)
        self.dockParams.installEventFilter(self)
        self.actionNodes.triggered.connect(self.visibility_nodes)
        self.actionParams.triggered.connect(self.visibility_params)
        self.actionNew.triggered.connect(self.new_graph)

    """ Nouveau graphe. """
    def new_graph(self):
        """ - Affichage de la boîte de dialogue, centrée dans la fenêtre-parent.
            - Saisie du nom du nouveau graphe.
            - Clic sur 'Annuler' -> fermeture.
            - Clic sur 'Ok' :
                - Création d'un dossier
        """
        NewGraph(self).exec()

    """ Visibilité dockable des noeuds. """
    def visibility_nodes(self):
        """ Si visible => devient invisible et vice-versa. """
        self.dockNodes.setVisible(not self.dockNodes.isVisible())

    """ Visibilité dockable des paramètres. """
    def visibility_params(self):
        self.dockParams.setVisible(not self.dockParams.isVisible())

    def eventFilter(self, obj, ev):
        if ev.type() in [QEvent.Move, QEvent.Resize, QEvent.Hide, QEvent.Show]:
            ut.save_state(self)
        return super().eventFilter(obj, ev)

    def moveEvent(self, ev):
        """ Mémorisation de la géométrie. """
        ut.save_state(self)

    def resizeEvent(self, ev):
        """ Mémorisation de la géométrie. """
        ut.save_state(self)


class NewGraph(QDialog, Ui_Dialog_new):
    """ Clic sur le bouton 'Annuler' : sortie sans exécuter de code.
        Clic sur le bouton 'Ok' :
            - Si le nom du nouveau graphe existe déjà, on ne crée rien, on ouvre le graphe existant.
            - Sinon, on crée le graphe puis on l'ouvre.
    """
    def __init__(self, o_main):
        super().__init__(o_main)    # Attribut o_main pour centrer la boîte de dialogue dans la fenêtre-parent.
        self.o_main = o_main
        self.setupUi(self)


if __name__ == '__main__':
    def start():
        app = QApplication(sys.argv)
        ut.translate(app)
        fen = UiMain()
        fen.show()
        sys.exit(app.exec())

    """ Affichage de la fenêtre. """
    start()


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 !