Search code examples
pythonsignals-slotspyqt5qlistwidgetitem

Slot on list widget item data object never called (in PyQt 5.7)


In PyQt 5.5 the following code worked, but not so in PyQt 5.7 (the list shows 'example' rather than 'new example', and indeed debugging shows that the slot is never hit). Does anyone know what is wrong with it:

from PyQt5.QtWidgets import QListWidgetItem, QListWidget, QApplication
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, Qt


class MyListItemData(QObject):
    def __init__(self, list_widget_item, obj):
        super().__init__()
        self.list_widget_item = list_widget_item
        obj.sig_name_changed.connect(self.__on_list_item_name_changed)

    # @pyqtSlot(str)
    def __on_list_item_name_changed(self, new_name: str):
        self.list_widget_item.setText(new_name)


class Data(QObject):
    sig_name_changed = pyqtSignal(str)


class SearchPanel2(QListWidget):
    def __init__(self, parent=None):
        QListWidget.__init__(self, parent)
        obj = Data()
        hit_item = QListWidgetItem('example')
        hit_item.setData(Qt.UserRole, MyListItemData(hit_item, obj))
        self.addItem(hit_item)
        obj.sig_name_changed.emit('new_example')


app = QApplication([])
search = SearchPanel2()
search.show()
app.exec

Although probably not the way this was supposed to be done, in PyQt 5.5 it was an acceptable workaround for a PyQt 5.5 bug (that prevented us from simply deriving from QListWidgetItem so the item could be directly connect to signals).

Post-answer edit

After Ekhumoro answered, I was confronted with a harsh reality: this fixed the example code posted, but not my app, because my app was doing exactly what the solution said to do. So I revisited: in the real app, the items are created later, and the signal for name change is emitted later. Therefore a better minimal example to reproduce my problem would have had the following:

class SearchPanel2(QListWidget):
    def __init__(self, obj, parent=None):
        QListWidget.__init__(self, parent)

        hit_item = QListWidgetItem('example')
        data = MyListItemData(hit_item, obj)
        hit_item.setData(Qt.UserRole, data)  # slot not called

        self.addItem(hit_item)
        # self.data = data

    def emit(self):
        obj.sig_name_changed.emit('new_example')


app = QApplication([])
obj = Data()
search = SearchPanel2(obj)
search.show()
QTimer.singleShot(2000, search.emit)
app.exec()

assert search.item(0).text() == 'new_example'

This fails assertion. The assertion passes if data is kept by strong reference (uncomment last line of init). So it is likely that setData() keeps only a weak reference to its second argument, causing data to get deleted at end of init unless it is stored somewhere.


Solution

  • There seems to be some kind of garbage-collection issue. Try this instead:

        hit_item = QListWidgetItem('example')
        data = MyListItemData(hit_item, obj)
        hit_item.setData(Qt.UserRole, data)