Enes
Enes

Reputation: 47

QtQuick/QML Create Expandable SubMenu from PyQt5

I want to create ListView which includes nested ListModel for expanding and collapsing submenu. (The topic that I use while I creating nested expandable listview in here.)

My problem is expandable menu is not responding when I create it's ListModel from Python/PyQt5. But if I create it in QML side, expanding operation is working smoothly. (But I don't want create it on QML side cause I have to manipulate it on backend.)

Here is the main.py

import sys
from PyQt5.QtQml    import QQmlApplicationEngine, qmlRegisterType
from PyQt5.QtGui    import QGuiApplication
from PyQt5.QtCore   import QTimer, QObject, pyqtSignal, pyqtSlot, QAbstractListModel, QModelIndex, Qt, pyqtProperty

class Backend(QObject):
    modelChanged = pyqtSignal()

    def __init__(self):
        super().__init__()
        self._model = MyListModel()                                   

    ##~~Expose model as a property of our backend~~##
    @pyqtProperty(QObject, constant=False, notify=modelChanged)         
    def model(self):                                                   
        return self._model                                              

class MyListModel(QAbstractListModel):
    ##~~My Custom UserRoles~~##
    NameRole = Qt.UserRole + 1000
    CollapsedRole = Qt.UserRole + 1001
    SubItemsRole = Qt.UserRole + 1002

    def __init__(self, parent=None):
        super().__init__()
        self.itemNames = []

    def rowCount(self, parent=None, *args, **kwargs):
        return len(self.itemNames)

    def data(self, index, role=Qt.DisplayRole):
        if 0 <= index.row() < self.rowCount() and index.isValid():
            item = self.itemNames[index.row()]

            if role == MyListModel.NameRole:
                return item['assetName']
            elif role == MyListModel.SubItemsRole:
                return item['subItems']
            elif role == MyListModel.CollapsedRole:
                return item['isCollapsed']

    def roleNames(self):
        roles = dict()
        roles[MyListModel.NameRole] = b'assetName'
        roles[MyListModel.SubItemsRole] = b'subItems'
        roles[MyListModel.CollapsedRole] = b'isCollapsed'
        return roles

    @pyqtSlot(str, bool)
    def appendRow(self, name, isCollapsed):
        self.subItem = MySubListModel()
        self.subItem.addRow()
        self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
        self.itemNames.append({'assetName': name, 'subItems': self.subItem, 'isCollapsed': isCollapsed})
        self.endInsertRows()
        print(self.itemNames)
        print(self.subItem.subItemParams)

    @pyqtSlot(int, str)
    def collapseEditInputsMenu(self, index, modelIndexName):
        self.itemNames[index][modelIndexName] = not self.itemNames[index][modelIndexName]
        print(f"From Backend: {self.itemNames}")

class MySubListModel(QAbstractListModel):
    ##~~My Custom UserRole For SubItem ListModel~~##
    CellSizeRole = Qt.UserRole + 1004

    def __init__(self, parent=None):
        super().__init__()
        self.subItemParams = []

    def rowCount(self, parent=None, *args, **kwargs):
        return len(self.subItemParams)

    def data(self, index, role=Qt.DisplayRole):
        if 0 <= index.row() < self.rowCount() and index.isValid():
            item = self.subItemParams[index.row()]

            if role == MySubListModel.CellSizeRole:
                return item['cellSize']

    def roleNames(self):
        roles = dict()
        roles[MySubListModel.CellSizeRole] = b'cellSize'
        return roles

    def addRow(self):
        self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
        self.subItemParams.append({'cellSize': "888"})
        self.endInsertRows() 



class MainWindow():
    def __init__(self):
        app = QGuiApplication(sys.argv)
        self.engine = QQmlApplicationEngine()
        self.engine.quit.connect(app.quit)
        self.engine.load("main.qml")

        app_backend = Backend()
        self.engine.rootObjects()[0].setProperty("backendObjectInQML", app_backend)

        sys.exit(app.exec())




def main():
    window = MainWindow()

if __name__ == '__main__':
    main()

... and main.qml

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtGraphicalEffects 1.15

ApplicationWindow {
    id: myApplicationWindow
    title: "Expandable ListView App"
    visible: true
    height: 400
    width: 400

    property QtObject backendObjectInQML

    Rectangle {
        id: rootRectangle
        color: "grey"
        anchors.fill: parent

        Item {
            id: solutionFileListViewRoot
            anchors.fill: parent
            // ListModel {
            //     id: myNestedListModel
            //     ListElement {
            //         assetName: "Dummy Item"
            //         isCollapsed: true
            //         subItems: [ListElement {cellSize: "888"}]
            //     }
            // }
            ListView {
                id: myNestedListView
                anchors {
                    top: parent.top
                    left: parent.left
                    right: parent.right
                    bottom: parent.bottom
                    bottomMargin: 50
                }
                model: myApplicationWindow.backendObjectInQML.model
                // model: myNestedListModel
                delegate: myAppListElementDelegate
                spacing: 6
                clip: true
                ScrollBar.vertical: ScrollBar {
                    active: true
                }
            }
        }

        Component {
            id: myAppListElementDelegate
            Column {
                id: listElementColumn
                width: myNestedListView.width
                Rectangle {
                    id: listElementRectangle
                    height: 30
                    anchors {
                        left: parent.left
                        right: parent.right
                        rightMargin: 15
                        leftMargin: 15
                    }
                    color: "yellow"
                    radius: 3
                    Text {
                        height: 24
                        width: 100
                        text: assetName
                        anchors {
                            verticalCenter: parent.verticalCenter
                            left: parent.left
                        }
                        horizontalAlignment: Text.AlignLeft
                        verticalAlignment: Text.AlignVCenter
                        color: "black"
                    }
                    Button {
                        id: expandButton
                        width: 70
                        height: 24
                        text: "Expand"
                        anchors {
                            right: parent.right
                            rightMargin: 20
                            verticalCenter: parent.verticalCenter
                        }
                        onClicked: {
                            myNestedListView.currentIndex = index
                            myApplicationWindow.backendObjectInQML.model.collapseEditInputsMenu(index, "isCollapsed")
                            // myNestedListModel.setProperty(index, "isCollapsed", !isCollapsed)
                            console.log("From QML isCollapsed:")
                            console.log(isCollapsed)
                        }
                    }
                }
                Loader {
                    id: subSolutionEditItemLoader
                    visible: !isCollapsed
                    property variant subEditItemModel: subItems
                    sourceComponent: isCollapsed ? null : subItemEditInputsDelegate
                    onStatusChanged: {
                        // console.log(subItems)
                        if(status == Loader.Ready) item.model = subEditItemModel
                    }
                }
            }
        }

        Component {
            id: subItemEditInputsDelegate

            Column {
                property alias model: subItemRepeater.model
                id: nestedListElementColumn
                width: myNestedListView.width
                anchors {
                    top: parent.top
                    topMargin: 3
                }
                spacing: 3

                Repeater {
                    id: subItemRepeater
                    width: parent.width
                    delegate: Rectangle {
                        id: nestedListElementRectangle
                        color: "blue"
                        height: 40
                        anchors {
                            left: parent.left
                            leftMargin: 30
                            right: parent.right
                            rightMargin: 30
                        }
                        radius: 5

                        Rectangle {
                            id: cellSizeBackground
                            height: 20
                            width: cellSizeLabel.implicitWidth
                            color: "#00000000"
                            anchors {
                                left: parent.left
                                leftMargin: 25
                                top: parent.top
                                topMargin: 10
                            }
                            Label {
                                id: cellSizeLabel
                                text: "Cell Size: "
                                anchors.fill: parent
                                verticalAlignment: Text.AlignVCenter
                                color: "#6e95bc"
                            }
                        }

                        Rectangle {
                            id: cellSizeTextInputBorder
                            height: 24
                            width: 120
                            color: "#00000000"
                            radius: 5
                            anchors {
                                left: cellSizeBackground.right
                                leftMargin: 10
                                verticalCenter: cellSizeBackground.verticalCenter
                            }
                            border.width: 1
                            border.color: "#12C56A"

                            TextInput {
                                id: cellSizeTextInput
                                text: cellSize
                                verticalAlignment: Text.AlignVCenter
                                anchors.fill: parent
                                color: "#6e95bc"
                                selectByMouse: true
                                leftPadding: 5
                                rightPadding: 5
                                clip: true

                                onEditingFinished: {
                                    console.log("cellSizeTextInput edited...")
                                }
                            }
                        }
                    }
                }
            }
        }

        Button {
            id: addListElementButton
            height: 24
            width: 70
            text: "Add"
            anchors {
                bottom: parent.bottom
                right: parent.right
            }
            onClicked: {
                myApplicationWindow.backendObjectInQML.model.appendRow("Dummy Item", false)
            }
        }
    }
}

I suspect that when I update "isCollapsed" item of listModel from false to true, it is updated on listView but somehow it is not triggering the GUI. But I don't know why.

I also added Nested ListModel and relevant lines to QML as commented out for ones who want to try the code with ListModel created on QML side.

You can see application ScreenShot below:

App ScreenShot

Upvotes: 1

Views: 128

Answers (1)

user16776498
user16776498

Reputation:

You didn't emitted any signals while changing the collapsed state. Therefore there was no way for properties that relay on that role to know that they need to synchronize with it.

Change:

@pyqtSlot(int, str)
def collapseEditInputsMenu(self, index, modelIndexName):
    self.itemNames[index][modelIndexName] = not self.itemNames[index][modelIndexName]
    print(f"From Backend: {self.itemNames}")

To:


@pyqtSlot(int, str)
def collapseEditInputsMenu(self, index, modelIndexName):
    self.layoutAboutToBeChanged.emit()
    self.itemNames[index][modelIndexName] = not self.itemNames[index][modelIndexName]
    print(f"From Backend: {self.itemNames}")
    self.layoutChanged.emit()

Fully working example


P.S: This is very messy code, I'll list you here a few severe mistakes:

First:

self.engine.rootObjects()[0].setProperty("backendObjectInQML", app_backend)

property QtObject backendObjectInQML

This is redundant you should use instead:

self.engine.rootContext().setContextProperty("backendObjectInQML", app_backend)

Second:

myApplicationWindow.backendObjectInQML.model

If you will use contextProperty as I said above you don't need to access it threw myApplicationWindow

Third:
The isCollapsed role from your model is probably redundant and you should have preferred creating a property for your myAppListElementDelegate component. Something like this:

Component {
    id: myAppListElementDelegate
    Column {
        id: listElementColumn
        property bool isCollapsed: false  // <<<---
        width: myNestedListView.width
        Rectangle {
            id: listElementRectangle
            height: 30
            anchors {
                left: parent.left
                right: parent.right
                rightMargin: 15
                leftMargin: 15
            }
            color: "yellow"
            radius: 3
            Text {
                height: 24
                width: 100
                text: assetName
                anchors {
                    verticalCenter: parent.verticalCenter
                    left: parent.left
                }
                horizontalAlignment: Text.AlignLeft
                verticalAlignment: Text.AlignVCenter
                color: "black"
            }
            Button {
                id: expandButton
                width: 70
                height: 24
                text: "Expand"
                anchors {
                    right: parent.right
                    rightMargin: 20
                    verticalCenter: parent.verticalCenter
                }
                onClicked: isCollapsed = !isCollapsed
            }
        }
        Loader {
            id: subSolutionEditItemLoader
            visible: !isCollapsed
            property variant subEditItemModel: subItems
            sourceComponent: isCollapsed ? null: subItemEditInputsDelegate
            onStatusChanged: {
                // console.log(subItems)
                if(status == Loader.Ready) item.model = subEditItemModel
            }
        }
    }
}

Upvotes: 1

Related Questions