Search code examples
pythonqmlpyside2

PySide2/QML populate and animate Gridview model/delegate


I'm newbie to QML and looking for help on below points

  • How to filter QAbstractListModel data (Title) in Gridview model via PySide2 based on TextField input (as Regex ).

  • How to animate delegate of Gridview on mouse hover (as showen in below image.)

enter image description here

Here is the test code

qmlHoverView.py

from PySide2 import QtCore, QtQuick, QtGui, QtWidgets, QtQml
import os
import sys

class inventoryModel(QtCore.QAbstractListModel):
    def __init__(self, entries, parent=None):
        super(inventoryModel, self).__init__(parent)
        self.titleRole = QtCore.Qt.UserRole + 1000
        self.thumbnailRole = QtCore.Qt.UserRole + 1001
        self._entries = entries

    def rowCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid(): return 0
        return len(self._entries)

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if 0 <= index.row() < self.rowCount() and index.isValid():
            item = self._entries[index.row()]
            if role == self.titleRole:
                return item["title"]
            elif role == self.thumbnailRole:
                return item["thumbnail"]

    def roleNames(self):
        roles = dict()
        roles[self.titleRole] = b"title"
        roles[self.thumbnailRole] = b"thumbnail"
        return roles

    def appendRow(self, n, t):
        self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
        self._entries.append(dict(name=n, type=t))
        self.endInsertRows()



class Foo(QtCore.QObject):
    def __init__(self):
        QtCore.QObject.__init__(self)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    entries = [
        {"title": "Zero", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/zero.png"},
        {"title": "One", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/one.png"},
        {"title": "Two", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/two.png"},
        {"title": "Three", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/three.png"},
        {"title": "Four", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/four.png"},
        {"title": "Five", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/five.png"},
        {"title": "Six", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/six.png"},
        {"title": "Seven", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/seven.png"},
        {"title": "Eight", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/eight.png"},
        {"title": "Nine", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/nine.png"},
    ]
    assetModel = inventoryModel(entries)
    foo = Foo()
    engine = QtQml.QQmlApplicationEngine()
    engine.rootContext().setContextProperty("foo", foo)
    engine.rootContext().setContextProperty("assetModel", assetModel)
    engine.load(QtCore.QUrl.fromLocalFile('E:/Tech/QML/projects/Test_005/main.qml'))
    if not engine.rootObjects():
        sys.exit(-1)
    engine.quit.connect(app.quit)
    sys.exit(app.exec_())

main.qml

import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13

ApplicationWindow {
    id: mainWindowId
    visible: true
    width: 1280
    height: 720
    title: qsTr("Image Hover Effect")

    Rectangle {
        width: parent.width
        height: parent.height

        ColumnLayout {
            width: parent.width
            height: parent.height
            spacing: 0

            TextField{
                id: filterTextFieldId
                Layout.fillWidth: true
                Layout.preferredHeight: 40
                font {
                    family: "SF Pro Display"
                    pixelSize: 22
                }

                color: "dodgerblue"
            }

            Rectangle {
                Layout.fillWidth: true
                Layout.fillHeight: true
                color: "gold"

                GridView {
                    id: thumbViewId
                    width: parent.width
                    height: parent.height
                    anchors.fill: parent
                    anchors.margins: 25
                    cellWidth: 260
                    cellHeight: 260
                    model: assetModel
                    delegate: ThumbDelegate {}
                    focus: true
                }
            }
        }
    }

    Connections {
        target: foo
    }
}

ThumbDelegate.qml

import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13

Component {
    Rectangle {
        width: 256
        height: 256
        color: 'green'

        Image {
            id: thumbImageId
            source: thumbnail
            asynchronous: true
        }

        Rectangle {
            width: parent.width
            height: 50
            anchors.bottom: parent.bottom
            color: 'grey'

            Label {
                anchors.verticalCenter: parent.verticalCenter
                anchors.left: parent.left
                anchors.leftMargin: 10
                text: title
                font.family: 'SF Pro Display'
                font.pixelSize: 22
                color: 'white'
            }
        }
    }
}

Output of above code

enter image description here


Solution

  • You have asked different questions that for this occasion I will answer all but for a next occasion you will have to create a post for each question as indicated in the SO guides.


    In your case 3 elements are required:

    • Load the image into the GridView: It is advisable to implement a model, in this case, implement it based on a QStandardItemModel with custom roles, and make connections with the delegate.

    • Filter: For this you can use a DelegateModel, or a QSortFilterProxyModel, in this case use the second option since it implements the filtering by role and through a regex.

    • Hover Animation: The first thing is to detect when the mouse enters or exits the item, and for this a MouseArea is used that triggers the enter and exit signals. Then we use a Behavior to set the animation when the "y" property changes. And then it is only necessary to set the respective final values when the signals are triggered. I have removed "anchors.bottom: parent.bottom" since an anchor does not allow modifying the property.

    On the other hand if you create a qml for a delegate it is not necessary to use Component since in itself it is a component, on the other hand you must enable the "clip" property so that the painting of the items is not outside its own area.

    Considering the above, the solution is:

    ├── images
    │   └── android.png
    ├── main.py
    └── qml
        ├── main.qml
        └── ThumbDelegate.qml
    

    main.py

    import os
    import sys
    
    from PySide2 import QtCore, QtGui, QtWidgets, QtQml
    
    
    class CustomModel(QtGui.QStandardItemModel):
        TitleRole = QtCore.Qt.UserRole + 1000
        UrlRole = QtCore.Qt.UserRole + 1001
    
        def __init__(self, parent=None):
            super().__init__(parent)
            self.setItemRoleNames(
                {CustomModel.TitleRole: b"title", CustomModel.UrlRole: b"thumbnail"}
            )
    
        @QtCore.Slot(str, QtCore.QUrl)
        def addItem(self, title, url):
            it = QtGui.QStandardItem()
            it.setData(title, CustomModel.TitleRole)
            it.setData(url, CustomModel.UrlRole)
            self.appendRow(it)
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
        current_dir = os.path.dirname(os.path.realpath(__file__))
    
        model = CustomModel()
    
        # add items
        for (
            text
        ) in "amputate arena accept architecture astonishing advertise abortion apple absolute advice".split():
            title = text
            image_path = os.path.join(current_dir, "images", "android.png")
            model.addItem(title, QtCore.QUrl.fromLocalFile(image_path))
    
        proxy_filter = QtCore.QSortFilterProxyModel()
        proxy_filter.setSourceModel(model)
        proxy_filter.setFilterRole(CustomModel.TitleRole)
    
        engine = QtQml.QQmlApplicationEngine()
    
        engine.rootContext().setContextProperty("proxy_filter", proxy_filter)
        filename = os.path.join(current_dir, "qml", "main.qml")
        engine.load(QtCore.QUrl.fromLocalFile(filename))
    
        if not engine.rootObjects():
            sys.exit(-1)
        engine.quit.connect(app.quit)
        sys.exit(app.exec_())
    

    main.qml

    import QtQuick 2.13
    import QtQuick.Controls 2.13
    import QtQuick.Layouts 1.13
    
    ApplicationWindow {
        id: mainWindowId
        visible: true
        width: 1280
        height: 720
        title: qsTr("Image Hover Effect")
    
        Rectangle {
            anchors.fill: parent
    
            ColumnLayout {
                anchors.fill: parent
                spacing: 0
                TextField{
                    id: filterTextFieldId
                    Layout.fillWidth: true
                    Layout.preferredHeight: 40
                    font {
                        family: "SF Pro Display"
                        pixelSize: 22
                    }
                    color: "dodgerblue"
                    onTextChanged: proxy_filter.setFilterRegExp(text)
                }
    
                Rectangle {
                    Layout.fillWidth: true
                    Layout.fillHeight: true
                    color: "gold"
                    GridView {
                        clip: true
                        id: thumbViewId
                        anchors.fill: parent
                        anchors.margins: 25
    
                        cellWidth: 260
                        cellHeight: 260
    
                        model: proxy_filter
                        delegate: ThumbDelegate {
                            source: model.thumbnail
                            title: model.title
                        }
                        focus: true
                    }
                }
            }
        }
    }
    

    ThumbDelegate.qml

    import QtQuick 2.13
    import QtQuick.Controls 2.13
    import QtQuick.Layouts 1.13
    
    
    Rectangle {
        id: root
        width: 256
        height: 256
        color: 'green'
        clip: true
    
        property alias source: thumbImageId.source
        property alias title: label.text
    
        Image {
            id: thumbImageId
            asynchronous: true
            anchors.fill: parent
        }
    
        Rectangle {
            id: base
            width: parent.width
            height: 50
            color: 'grey'
            y: root.height
            Behavior on y { NumberAnimation {duration: 500} }
            Label {
                id: label
                anchors.verticalCenter: parent.verticalCenter
                anchors.left: parent.left
                anchors.leftMargin: 10
                font.family: 'SF Pro Display'
                font.pointSize: 22
                color: 'white'
            }
        }
    
    
        MouseArea{
            anchors.fill: parent
            hoverEnabled: true
            onEntered: base.y = root.height - base.height
            onExited: base.y = root.height
        }
    }
    

    Update:

    Seeing that you have updated your question, it is only necessary to modify the python code, the qml code must be the same as the one I proposed in the previous part of my answer.

    *.py

    import os
    import sys
    
    from PySide2 import QtCore, QtGui, QtWidgets, QtQml
    
    
    class InventoryModel(QtCore.QAbstractListModel):
        TitleRole = QtCore.Qt.UserRole + 1000
        ThumbnailRole = QtCore.Qt.UserRole + 1001
    
        def __init__(self, entries, parent=None):
            super().__init__(parent)
            self._entries = entries
    
        def rowCount(self, parent=QtCore.QModelIndex()):
            return 0 if parent.isValid() else len(self._entries)
    
        def data(self, index, role=QtCore.Qt.DisplayRole):
            if 0 <= index.row() < self.rowCount() and index.isValid():
                item = self._entries[index.row()]
                if role == InventoryModel.TitleRole:
                    return item["title"]
                elif role == InventoryModel.ThumbnailRole:
                    return item["thumbnail"]
    
        def roleNames(self):
            roles = dict()
            roles[InventoryModel.TitleRole] = b"title"
            roles[InventoryModel.ThumbnailRole] = b"thumbnail"
            return roles
    
        def appendRow(self, n, t):
            self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
            self._entries.append(dict(title=n, thumbnail=t))
            self.endInsertRows()
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
    
        current_dir = os.path.dirname(os.path.realpath(__file__))
    
        entries = [
            {"title": "Zero", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/zero.png"},
            {"title": "One", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/one.png"},
            {"title": "Two", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/two.png"},
            {"title": "Three", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/three.png"},
            {"title": "Four", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/four.png"},
            {"title": "Five", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/five.png"},
            {"title": "Six", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/six.png"},
            {"title": "Seven", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/seven.png"},
            {"title": "Eight", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/eight.png"},
            {"title": "Nine", "thumbnail": "file:///E:/Tech/QML/projects/Test_005/nine.png"},
        ]
    
        assetModel = InventoryModel(entries)
        engine = QtQml.QQmlApplicationEngine()
    
        proxy_filter = QtCore.QSortFilterProxyModel()
        proxy_filter.setSourceModel(assetModel)
        proxy_filter.setFilterRole(InventoryModel.TitleRole)
    
        engine.rootContext().setContextProperty("proxy_filter", proxy_filter)
        engine.load(QtCore.QUrl.fromLocalFile('E:/Tech/QML/projects/Test_005/main.qml'))
    
        if not engine.rootObjects():
            sys.exit(-1)
    
        engine.quit.connect(app.quit)
        sys.exit(app.exec_())