Search code examples
pythonpyside2qsortfilterproxymodelqabstractlistmodel

QSortFilterProxyModel creates blank items


What I'm trying to do: Take items from a model and sort them using a sorting proxy by a different role: Expected output:

What I want to happen:

Real output contains blank lines which shouldn't be there:

You can see the empty lines expand the ListView and can even be selected by cursor

You can see the empty lines expand the ListView and can even be selected by cursor.

Here's the code that produces this incorrect behaviour:

from PySide2.QtCore import *
from PySide2.QtWidgets import *
import sys
import string
import random

class MyItem:
    def __init__(self, name, value):
        self.name = name
        self.value = value

    def __str__(self):
        return self.name +" "+ str(self.value)

class MyCustomModel(QAbstractListModel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.list = []

    def rowCount(self, parent=None):
        return len(self.list)

    def data(self, index, role):
        row = index.row()
        if row < 0 or row >= len(self.list):
            return None

        item = self.list[row]
        if role == Qt.DisplayRole:
            return str(item)
        if role == Qt.UserRole:
            return item.value
        else:
            return None

    def add(self, item):
        rc = self.rowCount()
        self.beginInsertRows(QModelIndex(), rc, rc+1)
        self.list.append(item)
        self.endInsertRows()

class MyWidget(QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.model = MyCustomModel()
        self.listView = QListView(self)

        self.sortingProxy = QSortFilterProxyModel()
        self.sortingProxy.setSourceModel(self.model)
        self.sortingProxy.setSortRole(Qt.UserRole)
        self.sortingProxy.sort(0, Qt.AscendingOrder)

        self.listView.setModel(self.sortingProxy)

        self.layout = QVBoxLayout(self)
        self.layout.addWidget(self.listView)

        self.setLayout(self.layout)
        self.show()

        # create some random data for the model
        for i in range(10):
            randomName = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(8)])
            self.model.add(MyItem(randomName, random.randint(0, 30)))

app = QApplication(sys.argv)
widget = MyWidget()
app.exec_()

I've tracked down the issue to QSortFilterProxyModel because when it's removed the problem goes away, but the program no longer sorts the data:

from PySide2.QtCore import *
from PySide2.QtWidgets import *
import sys
import string
import random

class MyItem:
    def __init__(self, name, value):
        self.name = name
        self.value = value

    def __str__(self):
        return self.name +" "+ str(self.value)

class MyCustomModel(QAbstractListModel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.list = []

    def rowCount(self, parent=None):
        return len(self.list)

    def data(self, index, role):
        row = index.row()
        if row < 0 or row >= len(self.list):
            return None

        item = self.list[row]
        if role == Qt.DisplayRole:
            return str(item)
        if role == Qt.UserRole:
            return item.value
        else:
            return None

    def add(self, item):
        rc = self.rowCount()
        self.beginInsertRows(QModelIndex(), rc, rc+1)
        self.list.append(item)
        self.endInsertRows()

class MyWidget(QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.model = MyCustomModel()
        self.listView = QListView(self)

        self.listView.setModel(self.model)

        self.layout = QVBoxLayout(self)
        self.layout.addWidget(self.listView)

        self.setLayout(self.layout)
        self.show()

        # create some random data for the model
        for i in range(10):
            randomName = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(8)])
            self.model.add(MyItem(randomName, random.randint(0, 30)))

app = QApplication(sys.argv)
widget = MyWidget()
app.exec_()

Because the problem seems to be caused by Pyside2/Qt5 code it seems I have no idea how to counter it.


Solution

  • The problem is not the proxy, the problem is caused by the method you use to add items, if you review the docs you must pass the row number from and to where it is added, in this case as only 1 is added then both match , in the general case if n-elements are added, the solution is:

    rc = self.rowCount()
    self.beginInsertRows(QModelIndex(), rc, rc + n - 1)
    

    So in your case the solution is:

    def add(self, item):
        rc = self.rowCount()
        self.beginInsertRows(QModelIndex(), rc, rc)
        self.list.append(item)
        self.endInsertRows()