3
  1. J'ai prêt de nombreux messages sur la façon de connecter plusieurs signaux au même gestionnaire d'événements en python et PyQt. Par exemple, connecter plusieurs boutons ou combobox à la même fonction.pyqt: Une bonne façon de connecter plusieurs signaux à la même fonction dans pyqt (QSignalMapper non applicable)

  2. De nombreux exemples montrent comment faire cela avec QSignalMapper, mais il est pas applicable lorsque le signal comporte un paramètre, comme combobox.currentIndexChanged

  3. Beaucoup de gens suggèrent qu'il peut être fait avec lambda. C'est une solution propre et jolie, je suis d'accord, mais personne ne mentionne que lambda crée une fermeture, qui contient une référence - ainsi l'objet référencé ne peut pas être supprimé. Bonjour la fuite de mémoire!

Preuve:

from PyQt4 import QtGui, QtCore 

class Widget(QtGui.QWidget): 
    def __init__(self): 
     super(Widget, self).__init__() 

     # create and set the layout 
     lay_main = QtGui.QHBoxLayout() 
     self.setLayout(lay_main) 

     # create two comboboxes and connect them to a single handler with lambda 

     combobox = QtGui.QComboBox() 
     combobox.addItems('Nol Adyn Dwa Tri'.split()) 
     combobox.currentIndexChanged.connect(lambda ind: self.on_selected('1', ind)) 
     lay_main.addWidget(combobox) 

     combobox = QtGui.QComboBox() 
     combobox.addItems('Nol Adyn Dwa Tri'.split()) 
     combobox.currentIndexChanged.connect(lambda ind: self.on_selected('2', ind)) 
     lay_main.addWidget(combobox) 

    # let the handler show which combobox was selected with which value 
    def on_selected(self, cb, index): 
     print '! combobox ', cb, ' index ', index 

    def __del__(self): 
     print 'deleted' 

if __name__ == '__main__': 

    import sys 
    app = QtGui.QApplication(sys.argv) 

    wdg = Widget() 
    wdg.show() 

    wdg = None 

    sys.exit(app.exec_()) 

Le widget n'est pas supprimé si nous dégageons la référence. Supprimer la connexion à lambda - il est supprimé correctement. Donc, la question est: quelle est la bonne façon de connecter plusieurs signaux avec des paramètres à un seul gestionnaire sans fuite de mémoire?

Répondre

2

Il est simplement faux qu'un objet ne puisse pas être supprimé car une connexion de signal contient une référence dans une fermeture. Qt supprimera automatiquement toutes les connexions de signaux lorsqu'il supprimera un objet, ce qui supprimera à son tour la référence au lambda du côté de Python.

Mais cela implique que vous ne pouvez pas toujours compter sur Python seul pour supprimer des objets. Chaque objet PyQt comporte deux parties: la partie Qt C++ et la partie wrapper Python. Les deux parties doivent être supprimées - et parfois dans un ordre spécifique (selon que Qt ou Python a actuellement la propriété de l'objet). En plus de cela, il y a aussi les caprices du garbage-collector Python à prendre en compte (surtout pendant la courte période où l'interpréteur est en train de s'arrêter).

Quoi qu'il en soit, dans votre exemple précis, la solution facile est de simplement faire:

# wdg = None 
    wdg.deleteLater() 

l'objet Ce horaires pour la suppression, donc une boucle d'événements en cours d'exécution est nécessaire pour elle d'effet. Dans votre exemple, cela quittera automatiquement l'application (car l'objet est la dernière fenêtre fermée).

Pour voir plus clairement ce qui se passe, vous pouvez aussi essayer ceci:

#wdg = None 
    wdg.deleteLater() 

    app.exec_() 

    # Python part is still alive here... 
    print(wdg) 
    # but the Qt part has already gone 
    print(wdg.objectName()) 

Sortie:

<__main__.Widget object at 0x7fa953688510> 
Traceback (most recent call last): 
    File "test.py", line 45, in <module> 
    print(wdg.objectName()) 
RuntimeError: wrapped C/C++ object of type Widget has been deleted 
deleted 

EDIT:

Voici un autre exemple de débogage qui fait l'espérons encore clearer:

wdg = Widget() 
    wdg.show() 

    wdg.deleteLater() 
    print 'wdg.deleteLater called' 

    del wdg 
    print 'del widget executed' 

    wd2 = Widget() 
    wd2.show() 

    print 'starting event-loop' 
    app.exec_() 

sortie:

$ python2 test.py 
wdg.deleteLater called 
del widget executed 
starting event-loop 
deleted 
+0

deleteLater() semble cacher le widget, mais le destructor est pas appelé pas moins. L'ajout d'un second widget après la programmation de la suppression du premier montre le second, mais aucun signe de suppression du premier. Comme précédemment, la suppression des connexions résout le problème. wdg.deleteLater() = wdg2 Widget() wdg2.move (300100) wdg2.show() –

+0

@GrigoryMakeev. Non, ce n'est pas du tout ce qui se passe. Évidemment, l'encapsuleur Python n'est pas supprimé immédiatement, puisque vous y avez toujours une référence globale. Mais tout ce que vous avez à faire est 'del wdg', et' __del__' sera appelé une fois que Qt aura supprimé la partie C++. J'ai ajouté un autre exemple de débogage à ma réponse qui devrait montrer encore plus clairement ce qui se passe réellement. – ekhumoro

+0

En effet cela fonctionne maintenant, merci! Une seule chose ne me reste pas claire: si j'ajoute gc.collect() juste après del wdg, le destructeur n'est pas encore appelé. Une idée pourquoi? –

1

dans de nombreux cas, le paramètre porté par signal peut être attrapé dans une autre façon, par exemple, si un objectName est défini pour l'objet, si QSignalMapper peut être utilisé envoyer:

self.signalMapper = QtCore.QSignalMapper(self) 
    self.signalMapper.mapped[str].connect(myFunction) 

    self.combo.currentIndexChanged.connect(self.signalMapper.map) 
    self.signalMapper.setMapping(self.combo, self.combo.objectName()) 

    def myFunction(self, identifier): 
     combo = self.findChild(QtGui.QComboBox,identifier) 
     index = combo.currentIndex() 
     text = combo.currentText() 
     data = combo.currentData() 
+0

Oui, merci, c'est une solution de contournement que nous utilisons actuellement. Fondamentalement, il ne fait que souligner le fait que nous ne pouvons pas capturer le signal de paramètre dans ce cas et nous devrions donc essayer de le déduire d'une autre manière, dans ce cas avec combo.currentIndex(). Nous utilisons seulement le formulaire self.signalMapper.mapped [QtCore.QWidget], donc nous n'avons pas besoin d'utiliser findChild. –