Partage
  • Partager sur Facebook
  • Partager sur Twitter

Connexion de signals PyQt

    29 novembre 2021 à 21:43:44

    Bonjour à tous,

    Je suis en train de développer une application avec PyQt et je me retrouve dans une impasse. 

    Le contexte :

    Mon application dispose d'un interpréteur Python intégré, c'est à dire que l'utilisateur peux écrire du code Python et l'exécuter via l'application.

    J'ai en parallèle une API pour contrôler mon application et je souhaiterai que certaines classes de cette API modifient la GUI lorsque l'utilisateur créer des objets de ces classes. Pour ce faire ces classes héritent de QObject et peuvent donc envoyer des signaux. 

    Le problème étant que je ne peux pas connecter un Slot à ce signal étant donné que les objets sont créés dynamiquement.

    Mes questions sont les suivantes :

    • Est-il possible de connecter des slots à des signaux d'objets dynamiquement ?
    • Est-il possible d'avoir un slot qui serait en "écoute" et qui réagirait à un signal émit sans qu'on ai eu à explicitement écrire la connexion ?

    Je vous propose un petit exemple simplifié du problème :

    from PyQt5 import QtWidgets
    from PyQt5.QtCore import QObject, pyqtSignal
    
    class MonAPI(QObject):
    
        on_bonjour = pyqtSignal(str)
    
        def __init__(self):
            self.on_bonjour.emit("bonjour") 
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self):
            super().__init__()
    
            def réagit_au_signal_bonjour(self):
                print("j'ai réagit")
    
            # J'aimerai faire quelque chose comme ça mais j'obtiens ce message d'erreur :
            # 'PyQt5.QtCore.pyqtSignal' object has no attribute 'connect'
            MonAPI.on_bonjour.connect(réagit_au_signal_bonjour) 
    


    Je pense que je comprends bien le problème, Qt à besoin de connecter un slot au signal qui peut être émit par un objet, et étant donné que j'émets mon signal avant la création de mon objet (enfin plutôt avant l'initialisation) ça pose problème.

    Je suis ouvert à toute pistes qui pourrait solutionner ce problème, 

    Merci

    • Partager sur Facebook
    • Partager sur Twitter
      30 novembre 2021 à 17:38:14

      je pense qu'il faut que tu déclares une instance de MonAPI sur laquelle tu fait ensuite la connexion  (le signal n'est en fait pas partagé par les instances)

      from PyQt5 import QtWidgets
      from PyQt5.QtCore import QObject, pyqtSignal
       
      class MonAPI(QObject):
       
          on_bonjour = pyqtSignal(str)
       
          def __init__(self):
              self.on_bonjour.emit("bonjour")
       
      class MainWindow(QtWidgets.QMainWindow):
          def __init__(self):
              super().__init__()
       
              def réagit_au_signal_bonjour(self):
                  print("j'ai réagit")
       
               monAPI=MonAPI()        
               monAPI.on_bonjour.connect(réagit_au_signal_bonjour)
      


      https://stackoverflow.com/questions/15025686/why-does-pyside-implicitely-create-object-members-from-class-members-for-signals/15033578#15033578 

      -
      Edité par umfred 30 novembre 2021 à 17:38:43

      • Partager sur Facebook
      • Partager sur Twitter
        30 novembre 2021 à 20:18:07

        La est tout le problème, ce que j'aimerai c'est un signal qui serait partagé par les instances. Car comme les instances sont créer pendant le runtime de l'app, je ne peux pas statiquement les connecter. Il me faut un moyen de les connecter dynamiquement, c'est là que je sèche.

        EDIT : 

        J'ai un example en pseudocode un peu plus parlant de ce que je souhaite faire réellement, en gros j'aimerai trouver comment créer la fonction connect_automatically_all_WorkplaneWrapped_objects_when_they_are_created()

        from PyQt5 import QtWidgets
        from PyQt5.QtCore import QObject, pyqtSignal
        from cadquery import Workplane
        
        class WorkplaneWrapped(Workplane, QObject):
        
            on_method = pyqtSignal(data)
        
            def __init__(self):
                self.wrap_all_cq_method()
                super().__init__(self)
        
            def wrap_all_cq_method(self):
                for method in Workplane.methods:
                    def new_method(method):
                        self.emit.on_method(data)
                    WorkplaneWrapped.method = new_method
        
        
        
        class MainWindow(QtWidgets.QMainWindow):
            def __init__(self):
                super().__init__()
        
                connect_automatically_all_WorkplaneWrapped_objects_when_they_are_created()
        
            @pyqtSlot(data)
            def modify_GUI(self, data):
                with data:
                    do_something_on_GUI()



        Sachant que WorkplaneWrapped est censé wrapper la classe Workplane donc l'utilisateur doit pour créer des WorkplaneWrapped comme si il créait des Workplane

        -
        Edité par Jojuss 30 novembre 2021 à 22:53:49

        • Partager sur Facebook
        • Partager sur Twitter
          1 décembre 2021 à 13:25:42

          en le faisant dans le __init__ de ta classe avec un self.connect(ta_fonction) ? ou après le emit

          • Partager sur Facebook
          • Partager sur Twitter
            2 décembre 2021 à 4:10:24

            Salut,

            Peut-être en utilisant les métaclasses comme ici

            • Partager sur Facebook
            • Partager sur Twitter
              2 décembre 2021 à 23:34:24

              @Umfred du coup j'ai fait ça mais étant donné que j'avais besoin de l'instance de la MainWindow j'ai du magouiller un peu. Voici un exemple qui fonctionne :

              A savoir qu'en réalité les deux classes ci-dessous sont dans des fichiers différents

              class MainWindow(QtWidgets.QMainWindow):
                  instance = None
                  def __init__(self):
                      super().__init__()
                      MainWindow.instance = self
                
                      def réagit_au_signal_bonjour(self):
                          print("j'ai réagit")
              from PyQt5 import QtWidgets
              from PyQt5.QtCore import QObject, pyqtSignal
                
              class MonAPI(QObject):
                
                  on_bonjour = pyqtSignal(str)
                
                  def __init__(self):
                      from main_window import MainWindow #j'importe la classe à partir du fichier main_window.py
                      self.on_bonjour.connect(MainWindow.instance.réagit_au_signal_bonjour)
                      self.on_bonjour.emit("bonjour")
                
              

              Du coup je dois importer la mainwindow dans la classe pour éviter un import circulaire. La solution n'est pas très compliquée mais je ne suis pas super fan du fait qu'une "sous classe" import l'instance de la MainWindow. D'un point de vue architecture il me semble plus logique que la MainWindow supervise toutes les autres classes et qu'aucune "sous classe" n'ai besoin de récupérer l'instance de la MainWindow...

              @Diablo76, ton lien est intéressant, cependant ça ne solutionne pas vraiment mon problème (ou bien je n'y arrive pas) car j'ai besoin de connecter des signaux dynamiquement pas de les créer. 
              Après avoir passé quelques jours sur le problème, j'ai l'impression que je suis obligé à un moment ou à un autre de créer un objet statiquement pour lier mes signaux/slots, je ne peux connecter un slot à un signal à une classe sans avoir d'objet et c'est bien ça le problème ...

              EDIT:

              J'ai réussi à bricoler une deuxième méthode avec le lien de Diablo76, je ne suis pas sûr que ce soit mieux finalement...

              # fichier API
              
              class SignalHandler(QObject):
                  on_test = pyqtSignal(str)
                  def __init__(self, parent = None):
                      super().__init__(parent=parent)
              
                  def get_test_class(self, callable):     
                      instance = self   
                      class Test(QObject):
                          def __init__(self, parent = None):
                              super().__init__(parent=parent)
                              instance.on_test.connect(callable)
                              instance.on_test.emit("yes")
              
                          def emit_sgl(self):
                              instance.on_test.emit("yes")
              
                      return Test
              # fichier main_window
              from APIfile import SignalHandler
              
              class MainWindow(QtWidgets.QMainWindow):
                  def __init__(self):
                      super().__init__()
               
                      self.sh = SignalHandler()
                      cls = self.sh.get_test_class(lambda msg: StdErrorMsgBox(msg,self)) #StdErrorMsgBox à besoin de l'instance de MainWindow pour s'afficher
              
                      print(type(cls)) # Retourne <sip.wrappertype> ??????????? Pourquoi pas type Test ?


              Ca fonctionne aussi, ca me semble au final reprendre le même principe mais en un peu plus chaloupé ...

              Quitte à faire un truc sale autant que ce soit simple je présume. Donc utiliser la première solution. 

              Si quelqu'un d'autre à d'autres idées c'est toujours intéressant !

              -
              Edité par Jojuss 3 décembre 2021 à 0:10:08

              • Partager sur Facebook
              • Partager sur Twitter

              Connexion de signals PyQt

              × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
              • Editeur
              • Markdown