Search code examples
python-3.xpyqtpyqt4pyqt5

How to pass an extra arguments to PyQt slot?



Hello everyone. I am making simple model/view application using python3.4 and PyQt5 in Windows 7.

First of all, here is my code.

import sys

from PyQt5.QtWidgets import QApplication, QWidget, QListView
from PyQt5.Qt import QStandardItemModel, QStandardItem

class Example(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.list = QListView(self)

        model = QStandardItemModel(self.list)

        carMaker = ['Ford', 'GM', 'Renault', 'VW', 'Toyota']

        for maker in carMaker:
            item = QStandardItem(maker)
            item.setCheckable(True)
            model.appendRow(item)
        self.list.setModel(model)

        model.itemChanged.connect(self.on_item_changed)
        #model.itemChanged.connect(functools.partial(self.on_item_changed, item, 1))
        #model.itemChanged.connect(lambda: self.on_item_changed(item, 2))

        self.list.setMinimumSize(300, 300)
        self.setWindowTitle("Simple modelView")
        self.show()

    def on_item_changed(self, item):
        print(item.text())


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

This works fine. But I want to add extra arguments with 'itemChanged' signal. So, I've used lambda and functools

  1. lambda

    • changed from 'def on_item_changed(self, item)' to 'def on_item_changed(self, item, num)'
    • changed from 'model.itemChanged.connect(self.on_item_changed)' to 'model.itemChanged.connect(lambda: self.on_item_changed(item, 1))'
    • It has no error. But 'item.text()' shows only 'Toyota'. (maybe last item model)
  2. functools.partial

    • changed from 'def on_item_changed(self, item)' to 'def on_item_changed(self, item, num)'
    • changed from 'model.itemChanged.connect(self.on_item_changed)' to 'model.itemChanged.connect(functools.partial(self.on_item_changed, item, 1))'
    • It has no error. But 'item.text()' shows only 'Toyota'. (maybe last item model) Same result as lambda.


The questions are...

  1. I don't know why lambda and functools shows wrong text.

  2. Is any effective way to pass an extra args with signal?

Thank you for read my question.


Solution

  • To answer your first question, the problem with functools.partial and lambda function usage lies in the fact the when you connect the signal to the slot, the variable item is set and it references the last QStandardItem you added in the QStandardItemModel. So basically you are in this situation:

    def initUI(self):
        self.list = QListView(self)
        model = QStandardItemModel(self.list)
        carMaker = ['Ford', 'GM', 'Renault', 'VW', 'Toyota']
    
        for maker in carMaker:
            item = QStandardItem(maker) # here you create a item variable that is overwritten
            item.setCheckable(True)     # on every iteration of the for loop
            model.appendRow(item)
    
        self.list.setModel(model)
    
        # here you use again the item variable (the same applies for lambda)
        model.itemChanged.connect(functools.partial(self.on_item_changed, item, 1))
    

    The reason why it prints "Toyota" is simply the fact that it's the last element in the carMaker list, so it's the last QStandardItem created, hence the item variable references this very object.

    To answer the second question instead, you can make use of the setData() method of QStandardItem to store some data you need in the item, and then retrieve them using data().

    import sys
    
    from PyQt5.QtCore import pyqtSlot, Qt
    from PyQt5.QtWidgets import QApplication, QWidget, QListView
    from PyQt5.QtGui import QStandardItemModel, QStandardItem
    
    class Example(QWidget):
    
        def __init__(self):
            super().__init__()
            self.initUI()
    
        def initUI(self):
    
            self.list = QListView(self)
            model = QStandardItemModel(self.list)
            carMaker = ['Ford', 'GM', 'Renault', 'VW', 'Toyota']
            index = 0 # just to store something different for each QStandardItem
    
            for maker in carMaker:
                item = QStandardItem(maker)
                item.setCheckable(True)
                item.setData(index, Qt.UserRole + 1)
                model.appendRow(item)
                index += 1
    
            self.list.setModel(model)
    
            model.itemChanged.connect(self.on_item_changed)
    
            self.list.setMinimumSize(300, 300)
            self.setWindowTitle("Simple modelView")
            self.show()
    
        @pyqtSlot('QStandardItem')
        def on_item_changed(self, item):
            print("%s - %s" % (item.text(), item.data(Qt.UserRole + 1)))
    
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        ex = Example()
        sys.exit(app.exec_())
    

    Alternatively you can make use of a signal mapper as described here.