Reputation: 47
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:
Upvotes: 1
Views: 128
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()
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