Search code examples
pythonpyqtpyqt5qpushbuttonqlistview

QListView Selected Index not updated when embedding a QWidget


I have embedded a QPushbutton in a Qwidget in a QListView: QPushbutton >> QWidget >> QListView

list_widget = QWidget()
list_widget.layout(QHBoxLayout())
btn = QPushButton()
btn.pressed.connect(clicked)
list_widget.layout().addWidget(QPushButton())
list_view.setIndexWidget(self.list_model.index(row, 0), list_widget)

def clicked():
    row = list_view.selectedIndexes()

The problem is now list_view.selectedIndexes() does not return the row of the pressed button, when pressed. This seems to work only when the QPushbutton is embedded in the QListView directly: QPushbutton >> QListView.

Does anyone have an idea how to delegate the focus of the pushbutton to the QListView?


Solution

  • When you click on the button it is not transmitted to the QListView because the button consumes it and does not transmit it to other widgets so if you want to obtain the row it must be obtained indirectly, a possible solution is to use the geometry for it you must obtain the sender , in this case the button, and then for its topleft to global positions, then convert it to a local position with respect to the viewport of QListView, using that position with the method indexAt() you get the QModelIndex.

    from PyQt5 import QtCore, QtGui, QtWidgets
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self, parent=None):
            super(MainWindow, self).__init__(parent)
            self.list_model = QtGui.QStandardItemModel(200, 1)
            self.list_view = QtWidgets.QListView()
            self.list_view.setModel(self.list_model)
            self.setCentralWidget(self.list_view)
    
            for row in range(self.list_model.rowCount()):
                list_widget = QtWidgets.QWidget()
                hlay = QtWidgets.QHBoxLayout(list_widget)
                btn = QtWidgets.QPushButton(str(row))
                btn.pressed.connect(self.clicked)
                hlay.addWidget(btn)
                hlay.setContentsMargins(0, 0, 0, 0)
                self.list_view.setIndexWidget(self.list_model.index(row, 0), list_widget)
    
        @QtCore.pyqtSlot()
        def clicked(self):
            btn = self.sender()
            gp = btn.mapToGlobal(QtCore.QPoint())
            lp = self.list_view.viewport().mapFromGlobal(gp)
            ix = self.list_view.indexAt(lp)
            print("row", ix.row())
    
    if __name__ == '__main__':
        import sys
        app = QtWidgets.QApplication(sys.argv)
        w = MainWindow()
        w.show()
        sys.exit(app.exec_())
    

    Another much simpler way is to pass the row as an argument using functools.partial():

    from functools import partial
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self, parent=None):
            super(MainWindow, self).__init__(parent)
            self.list_model = QtGui.QStandardItemModel(200, 1)
            self.list_view = QtWidgets.QListView()
            self.list_view.setModel(self.list_model)
            self.setCentralWidget(self.list_view)
    
            for row in range(self.list_model.rowCount()):
                list_widget = QtWidgets.QWidget()
                hlay = QtWidgets.QHBoxLayout(list_widget)
                btn = QtWidgets.QPushButton(str(row))
                btn.pressed.connect(partial(self.clicked, row))
                hlay.addWidget(btn)
                hlay.setContentsMargins(0, 0, 0, 0)
                self.list_view.setIndexWidget(self.list_model.index(row, 0), list_widget)
    
        @QtCore.pyqtSlot(int)
        def clicked(self, row):
            print(row)
    
    if __name__ == '__main__':
        import sys
        app = QtWidgets.QApplication(sys.argv)
        w = MainWindow()
        w.show()
        sys.exit(app.exec_())
    

    Or using a lambda method:

    from PyQt5 import QtCore, QtGui, QtWidgets
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self, parent=None):
            super(MainWindow, self).__init__(parent)
            self.list_model = QtGui.QStandardItemModel(200, 1)
            self.list_view = QtWidgets.QListView()
            self.list_view.setModel(self.list_model)
            self.setCentralWidget(self.list_view)
    
            for row in range(self.list_model.rowCount()):
                list_widget = QtWidgets.QWidget()
                hlay = QtWidgets.QHBoxLayout(list_widget)
                btn = QtWidgets.QPushButton(str(row))
                btn.pressed.connect(lambda *args, row=row: self.clicked(row))
                hlay.addWidget(btn)
                hlay.setContentsMargins(0, 0, 0, 0)
                self.list_view.setIndexWidget(self.list_model.index(row, 0), list_widget)
    
        @QtCore.pyqtSlot(int)
        def clicked(self, row):
            print(row)
    
    if __name__ == '__main__':
        import sys
        app = QtWidgets.QApplication(sys.argv)
        w = MainWindow()
        w.show()
        sys.exit(app.exec_())
    

    In my case I prefer to use partials since you do not need to write a lot of logic and it's thread-safe.