Fabian
Fabian

Reputation: 5650

Use a QSortFilterProxyModel from QML with PyQt5

I try to combine a QML view with a QSortFilterProxyModel within PyQt5. Unfortunately I can't get it to work in any way. My main problem right now seems to be to pass the items back from QML. But even if this would be possible it seems that it does not work, I get TypeError: QSortFilterProxyModel.setSourceModel(QAbstractItemModel): argument 1 has unexpected type 'PyCapsule' if I set the model directly in python.

Currently I have:

class SortFilterProxyModel(QSortFilterProxyModel):

    @pyqtProperty(QQmlListReference)
    def source (self):
        return self._source

    @source.setter
    def source (self, source):
        setSourceModel(source)
        self._source = source


class MyItem(QObject):

    nameChanged = pyqtSignal()

    def __init__(self, name, parent=None):
        QObject.__init__(self, parent)
        self._name = name

    @pyqtProperty('QString', notify=nameChanged)
    def name(self):
        return self._name


class MyModel(QObject):

    itemsChanged = pyqtSignal()

    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self._items = [MyItem('one'), MyItem('two'), MyItem('three')]

    @pyqtProperty(QQmlListProperty, notify=itemsChanged)
    def items(self):
        print('Query for items')
        return QQmlListProperty(MyItem, self, self._items)

    @pyqtSlot()
    def new_item(self):
        print('Append new item')
        self._items.append(MyItem('new'))
        self.itemsChanged.emit()

and

import QtQuick 2.2
import QtQuick.Layouts 1.1
import QtQuick.Controls 1.2
import QtQuick.Dialogs 1.2
import MyModel 1.0
import MyItem 1.0
import SortFilterProxyModel 1.0

ApplicationWindow {

    function getCurrentIndex(list, element) {
        console.log('getCurrentIndex')
        if (list && element) {
            for (var i = 0; i < list.length; i++) {
                if (list[i].name === element.name) {
                    console.log('Found item at pos: ' + i)
                    return i
                }
            }
        }
        return -1
    }

    id: mainWindow
    width: 800; height: 600
    color: "gray"

    MyModel {
        id: mymodel
    }

    SortFilterProxyModel {
        id: proxyModel
        source: mymodel.items
    }

    TableView {
        anchors.fill: parent
        //model: mymodel.items
        model: proxyModel
        TableViewColumn {
            role: "name"
            title: "Name"
        }
    }

}

full source here:

https://github.com/sturmf/python_samples/tree/master/pyqt_qsortfilterproxymodel

Upvotes: 1

Views: 1658

Answers (2)

Fabian
Fabian

Reputation: 5650

I now also implemented another way as described here: http://blog.qt.io/blog/2014/04/16/qt-weekly-6-sorting-and-filtering-a-tableview/

The implementation in the SortFilterProxyModel is a bit longer but the QML source gets imho nicer. This version also includes a filter implementation which makes it longer too.

class MyItem(QObject):

    nameChanged = pyqtSignal()

    def __init__(self, name, parent=None):
        QObject.__init__(self, parent)
        self._name = name

    @pyqtProperty('QString', notify=nameChanged)
    def name(self):
        return self._name


class MyModel(QAbstractListModel):
    NameRole = Qt.UserRole + 1
    _roles = {NameRole: "name"}

    def __init__(self, parent=None):
        super().__init__(parent)
        self._items = [MyItem('one'), MyItem('two'), MyItem('three')]
        self._column_count = 1

    def roleNames(self):
        return self._roles

    def rowCount(self, parent=QModelIndex()):
        return len(self._items)

    def data(self, index, role=Qt.DisplayRole):
        try:
            item = self._items[index.row()]
        except IndexError:
            return QVariant()

        if role == self.NameRole:
            return item.name

        return QVariant()

and a SortFilterProxyModel which I can use from QML

class SortFilterProxyModel(QSortFilterProxyModel):

    class FilterSyntax:
        RegExp, Wildcard, FixedString = range(3)

    Q_ENUMS(FilterSyntax)

    def __init__(self, parent):
        super().__init__(parent)

    @pyqtProperty(QAbstractItemModel)
    def source(self):
        return super().sourceModel()

    @source.setter
    def source(self, source):
        self.setSourceModel(source)

    @pyqtProperty(int)
    def sortOrder(self):
        return self._order

    @sortOrder.setter
    def sortOrder(self, order):
        self._order = order
        super().sort(0, order)

    @pyqtProperty(QByteArray)
    def sortRole(self):
        return self._roleNames().get(super().sortRole())

    @sortRole.setter
    def sortRole(self, role):
        super().setSortRole(self._roleKey(role))

    @pyqtProperty(QByteArray)
    def filterRole(self):
        return self._roleNames().get(super().filterRole())

    @filterRole.setter
    def filterRole(self, role):
        super().setFilterRole(self._roleKey(role))

    @pyqtProperty(str)
    def filterString(self):
        return super().filterRegExp().pattern()

    @filterString.setter
    def filterString(self, filter):
        super().setFilterRegExp(QRegExp(filter, super().filterCaseSensitivity(), self.filterSyntax))

    @pyqtProperty(int)
    def filterSyntax(self):
        return super().filterRegExp().patternSyntax()

    @filterSyntax.setter
    def filterSyntax(self, syntax):
        super().setFilterRegExp(QRegExp(self.filterString, super().filterCaseSensitivity(), syntax))

    def filterAcceptsRow(self, sourceRow, sourceParent):
        rx = super().filterRegExp()
        if not rx or rx.isEmpty():
            return True
        model = super().sourceModel()
        sourceIndex = model.index(sourceRow, 0, sourceParent)
        # skip invalid indexes
        if not sourceIndex.isValid():
            return True
        # If no filterRole is set, iterate through all keys
        if not self.filterRole or self.filterRole == "":
            roles = self._roleNames()
            for key, value in roles.items():
                data = model.data(sourceIndex, key)
                if rx.indexIn(data) != -1:
                    return True
            return False
        # Here we have a filterRole set so only search in that
        data = model.data(sourceIndex, self._roleKey(self.filterRole))
        return rx.indexIn(data) != -1

    def _roleKey(self, role):
        roles = self.roleNames()
        for key, value in roles.items():
            if value == role:
                return key
        return -1

    def _roleNames(self):
        source = super().sourceModel()
        if source:
            return source.roleNames()
        return {}

Now I can do the following in QML

MyModel {
    id: mymodel
}

SortFilterProxyModel {
    id: proxyModel
    source: mymodel

    sortOrder: tableView.sortIndicatorOrder
    sortCaseSensitivity: Qt.CaseInsensitive
    sortRole: tableView.getColumn(tableView.sortIndicatorColumn).role

    filterString: "*" + searchBox.text + "*"
    filterSyntax: SortFilterProxyModel.Wildcard
    filterCaseSensitivity: Qt.CaseInsensitive
    filterRole: tableView.getColumn(tableView.sortIndicatorColumn).role
}

TableView {
    id: tableView
    anchors.fill: parent
    model: proxyModel
    sortIndicatorVisible: true
    TableViewColumn {
        role: "name"
        title: "Name"
    }
}

Upvotes: 1

Fabian
Fabian

Reputation: 5650

The only way to get it working was to switch to a QAbstractListModel as was suggested by Frank. Here is the important part of the code:

class MyItem(QObject):

    nameChanged = pyqtSignal()

    def __init__(self, name, parent=None):
        QObject.__init__(self, parent)
        self._name = name

    @pyqtProperty('QString', notify=nameChanged)
    def name(self):
        return self._name

class MyModel(QAbstractListModel):
    NameRole = Qt.UserRole + 1
    _roles = {NameRole: "name"}

    def __init__(self, parent=None):
        print("constructing")
        super().__init__(parent)
        self._items = [MyItem('one'), MyItem('two'), MyItem('three')]
        self._column_count = 1

    def roleNames(self):
        print("roleNames")
        return self._roles

    def rowCount(self, parent=QModelIndex()):
        print("rowCount", len(self._items))
        return len(self._items)

    def data(self, index, role=Qt.DisplayRole):
        print("in data")
        try:
            item = self._items[index.row()]
        except IndexError:
            return QVariant()

        if role == self.NameRole:
            return item.name

        return QVariant()

and a SortFilterProxyModel which I can use from QML

class SortFilterProxyModel(QSortFilterProxyModel):

    def __init__(self, parent):
        super().__init__(parent)

    @pyqtProperty(QAbstractItemModel)
    def source (self):
        return self._source

    @source.setter
    def source (self, source):
        self.setSourceModel(source)
        self._source = source

    def roleKey(self, role):
        roles = self.roleNames()
        for key, value in roles.items():
            if value == role:
                return key
        return -1

    @pyqtSlot(str, int)
    def sort(self, role, order):
        self.setSortRole(self.roleKey(role));
        super().sort(0, order);

Now I can do the following in QML

MyModel {
    id: mymodel
}

SortFilterProxyModel {
    id: proxyModel
    source: mymodel
}

TableView {
    id: tableView
    anchors.fill: parent
    model: proxyModel
    sortIndicatorVisible: true
    onSortIndicatorOrderChanged: model.sort(getColumn(sortIndicatorColumn).role, sortIndicatorOrder)
    onSortIndicatorColumnChanged: model.sort(getColumn(sortIndicatorColumn).role, sortIndicatorOrder)
    TableViewColumn {
        title: "Name"
        role: "name"
    }
}

Upvotes: 0

Related Questions