PySide2 / QML Фильтрация иерархических данных по заданному корневому индексу - PullRequest
1 голос
/ 08 октября 2019

Я пытаюсь отфильтровать QStandardItemModel (с иерархическими данными), используя QSortFilterProxyModel (с setRecursiveFilteringEnabled в True), который обновляет событие QML TextField: onTextChanged.

Результирующая модель прокси кажется пустой и возвращает предупреждения как 100* QSortFilterProxyModel: index from wrong model passed to mapToSource

Так скажите, пожалуйста, что не так в моем коде? Как я могу начать фильтрацию из исходной модели текущего rootIndex?

crumbsProxy.py

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

crumbs_data = {"books":{
    "web":{
      "front-end":{
        "html":["the missing manual", "core html5 canvas"],
        "css":["css pocket reference", "css in depth"],
        "js":["you don't know js", "eloquent javascript"]
      },
      "back-end":{
        "php":["modern php", "php web services"],
        "python":["dive into python", "python for everybody", 
        "Think Python", "Effective Python", "Fluent Python"]
      }
    },
    "database":{
      "sql":{
        "mysql":["mysql in a nutshell", "mysql cookbook"],
        "postgresql":["postgresql up and running", "practical postgresql"]
      },
      "nosql":{
        "mongodb":["mongodb in action", "scaling mongodb"],
        "cassandra":["practical cassandra", "mastering cassandra"]
}}}}


def dict_to_model(item, d):
    if isinstance(d, dict):
        for k, v in d.items():
            it = QtGui.QStandardItem(k)
            item.appendRow(it)
            dict_to_model(it, v)
    elif isinstance(d, list):
        for v in d:
            dict_to_model(item, v)
    else:
        item.appendRow(QtGui.QStandardItem(str(d)))

class crumbsProxyModel(QtCore.QSortFilterProxyModel):
    def __init__(self, parent=None):
        super(crumbsProxyModel,self).__init__(parent)
        self.setRecursiveFilteringEnabled(True)

    def mapFromSource(self, index):
        return self.createIndex(index.column(), index.row())      


class NavigationManager(QtCore.QObject):
    headersChanged = QtCore.Signal()
    rootIndexChanged = QtCore.Signal("QModelIndex")

    def __init__(self, json_data, parent=None):
        super().__init__(parent)

        self.m_model = QtGui.QStandardItemModel(self)
        dict_to_model(self.m_model.invisibleRootItem(), json_data)

        self.m_headers = []
        self.m_rootindex = QtCore.QModelIndex()
        self.rootIndexChanged.connect(self._update_headers)

        self.rootIndex = self.m_model.index(0, 0)

        self.proxy_model = crumbsProxyModel()
        self.proxy_model.setSourceModel(self.m_model)

    def _update_headers(self, ix):
        self.m_headers = []
        while ix.isValid():
            self.m_headers.insert(0, [ix, ix.data()])
            ix = ix.parent()
        self.headersChanged.emit()

    @QtCore.Property(QtCore.QObject, constant=True)
    def model(self):
        #return self.m_model
        return self.proxy_model

    @QtCore.Property("QVariantList", notify=headersChanged)
    def headers(self):
        return self.m_headers

    def get_root_index(self):
        return self.m_rootindex

    def set_root_index(self, ix):
        if self.m_rootindex != ix:
            self.m_rootindex = ix
            self.rootIndexChanged.emit(ix)

    rootIndex = QtCore.Property("QModelIndex", fget=get_root_index, fset=set_root_index, notify=rootIndexChanged)


if __name__ == "__main__":
    import os
    import sys

    navigation_manager = NavigationManager(crumbs_data)
    model = QtGui.QStandardItemModel()
    app = QtWidgets.QApplication(sys.argv)
    engine = QtQml.QQmlApplicationEngine()
    engine.rootContext().setContextProperty("navigation_manager", navigation_manager)
    current_dir = os.path.dirname(os.path.realpath(__file__))
    filename = os.path.join(current_dir, "CrumbsMain.qml")
    engine.load(QtCore.QUrl.fromLocalFile(filename))
    if not engine.rootObjects():
        sys.exit(-1)
    engine.quit.connect(app.quit)
    sys.exit(app.exec_())

CrumbsMain.qml

import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtQml.Models 2.13

ApplicationWindow {
    id: mainWindowId
    visible: true
    width: 960
    height: 540
    title: qsTr("Breadcrumbs Test")

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

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

            TextField {
                id: filterTextFieldId
                Layout.fillWidth: true
                Layout.preferredHeight: 40
                font {
                    family: "SF Pro Display"
                 pixelSize: 22
                }
                placeholderText: "Type Filter Expression"
                color: "dodgerblue"
                onTextChanged: 
                    {
                        if (text != '')
                        navigation_manager.model.setFilterRegExp(text)
                    }
            }

            ToolBar {
                background: Rectangle {
                    color: "transparent"
                }
                RowLayout {
                    anchors.fill: parent
                    spacing: 10
                    Repeater{
                        model: navigation_manager.headers
                        ToolButton {
                            Layout.preferredHeight: 20
                            contentItem: Text {
                                text: model.modelData[1]
                                color: "#FFFFFF"
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                            background: Rectangle {
                                radius: 12
                                color:  "#40e0d0"
                            }
                            onClicked: navigation_manager.rootIndex = model.modelData[0]
                        }
                    }
                }
            }
            Rectangle {
                Layout.fillWidth: true
                Layout.fillHeight: true
                color: "dodgerblue"

                ListView{
                    id: view
                    anchors.fill: parent
                    anchors.margins: 12
                    model: DelegateModel {
                        model: navigation_manager.model
                        rootIndex: navigation_manager.rootIndex
                        delegate: Rectangle {
                            height: 25
                            color:"transparent"
                            Text { 
                                text: model.display
                                color:"white"
                                MouseArea{
                                    anchors.fill: parent
                                    onClicked: {
                                        if (model.hasModelChildren)
                                            {navigation_manager.rootIndex = view.model.modelIndex(index)}
                                        else
                                            {console.log(navigation_manager.headers)}
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

1 Ответ

1 голос
/ 08 октября 2019

У вас есть 2 ошибки:

  • Нет необходимости реализовывать mapFromSource.

  • m_rootindex должен быть частью модели, которую выэкспорт в QML, то есть в прокси, но в вашем случае это не так.

Учитывая вышеизложенное, решение:

class CrumbsProxyModel(QtCore.QSortFilterProxyModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setRecursiveFilteringEnabled(True)


class NavigationManager(QtCore.QObject):
    headersChanged = QtCore.Signal()
    rootIndexChanged = QtCore.Signal("QModelIndex")

    def __init__(self, json_data, parent=None):
        super().__init__(parent)

        self.m_model = QtGui.QStandardItemModel(self)
        dict_to_model(self.m_model.invisibleRootItem(), json_data)

        self.m_headers = []
        self.m_rootindex = QtCore.QModelIndex()
        self.rootIndexChanged.connect(self._update_headers)

        self.proxy_model = CrumbsProxyModel()
        self.proxy_model.setSourceModel(self.m_model)
        self.m_rootindex = self.proxy_model.mapFromSource(self.m_model.index(0, 0))

    def _update_headers(self, ix)
        # ...

Обновление:

При активации свойства recursiveFilteringEnabled QSortFilterProxyModel вы только указываете, что хотите фильтровать по всем элементам модели, и если элемент соответствует, тогда ваши родители будут видны, но это не соответствуетчто вам нужно: фильтруйте только те элементы, которые являются дочерними для rootIndex.

Учитывая это, возможное решение может реализовать пользовательский фильтр с использованием QSortFilterProxyModel, но это может быть утомительным и неэффективным. Другой альтернативой является фильтрация с использованием DelegateModel через «group».

Учитывая вышеизложенное, решение:

├── CrumbsMain.qml
├── crumbsProxy.py
└── FilterModel.qml

crumbsProxy.py

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

crumbs_data = {
    "books": {
        "web": {
            "front-end": {
                "html": ["the missing manual", "core html5 canvas"],
                "css": ["css pocket reference", "css in depth"],
                "js": ["you don't know js", "eloquent javascript"],
            },
            "back-end": {
                "php": ["modern php", "php web services"],
                "python": [
                    "dive into python",
                    "python for everybody",
                    "Think Python",
                    "Effective Python",
                    "Fluent Python",
                ],
            },
        },
        "database": {
            "sql": {
                "mysql": ["mysql in a nutshell", "mysql cookbook"],
                "postgresql": ["postgresql up and running", "practical postgresql"],
            },
            "nosql": {
                "mongodb": ["mongodb in action", "scaling mongodb"],
                "cassandra": ["practical cassandra", "mastering cassandra"],
            },
        },
    }
}


def dict_to_model(item, d):
    if isinstance(d, dict):
        for k, v in d.items():
            it = QtGui.QStandardItem(k)
            item.appendRow(it)
            dict_to_model(it, v)
    elif isinstance(d, list):
        for v in d:
            dict_to_model(item, v)
    else:
        item.appendRow(QtGui.QStandardItem(str(d)))


class NavigationManager(QtCore.QObject):
    headersChanged = QtCore.Signal()
    rootIndexChanged = QtCore.Signal("QModelIndex")

    def __init__(self, json_data, parent=None):
        super().__init__(parent)

        self.m_model = QtGui.QStandardItemModel(self)
        dict_to_model(self.m_model.invisibleRootItem(), json_data)

        self.m_headers = []
        self.m_rootindex = QtCore.QModelIndex()
        self.rootIndexChanged.connect(self._update_headers)

        self.rootIndex = self.m_model.index(0, 0)

    def _update_headers(self, ix):
        self.m_headers = []
        while ix.isValid():
            self.m_headers.insert(0, [ix, ix.data()])
            ix = ix.parent()
        self.headersChanged.emit()

    @QtCore.Property(QtCore.QObject, constant=True)
    def model(self):
        return self.m_model

    @QtCore.Property("QVariantList", notify=headersChanged)
    def headers(self):
        return self.m_headers

    def get_root_index(self):
        return self.m_rootindex

    def set_root_index(self, ix):
        if self.m_rootindex != ix:
            self.m_rootindex = ix
            self.rootIndexChanged.emit(ix)

    rootIndex = QtCore.Property(
        "QModelIndex", fget=get_root_index, fset=set_root_index, notify=rootIndexChanged
    )

    @QtCore.Slot(str, str, result=bool)
    def filter(self, word, wilcard):
        rx = QtCore.QRegExp(wilcard)
        rx.setPatternSyntax(QtCore.QRegExp.Wildcard)
        return rx.indexIn(word) != -1


if __name__ == "__main__":
    import os
    import sys

    navigation_manager = NavigationManager(crumbs_data)

    model = QtGui.QStandardItemModel()
    app = QtWidgets.QApplication(sys.argv)
    engine = QtQml.QQmlApplicationEngine()
    engine.rootContext().setContextProperty("navigation_manager", navigation_manager)
    current_dir = os.path.dirname(os.path.realpath(__file__))
    filename = os.path.join(current_dir, "CrumbsMain.qml")
    engine.load(QtCore.QUrl.fromLocalFile(filename))
    if not engine.rootObjects():
        sys.exit(-1)
    engine.quit.connect(app.quit)
    sys.exit(app.exec_())

CrumbsMain.qml

import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtQml.Models 2.13

ApplicationWindow {
    id: mainWindowId
    visible: true
    width: 960
    height: 540
    title: qsTr("Breadcrumbs Test")

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

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

            TextField {
                id: filterTextFieldId
                Layout.fillWidth: true
                Layout.preferredHeight: 40
                font {
                    family: "SF Pro Display"
                 pixelSize: 22
                }
                placeholderText: "Type Filter Expression"
                color: "dodgerblue"
                onTextChanged: filtermodel.update()
            }

            ToolBar {
                background: Rectangle {
                    color: "transparent"
                }
                RowLayout {
                    anchors.fill: parent
                    spacing: 10
                    Repeater{
                        model: navigation_manager.headers
                        ToolButton {
                            Layout.preferredHeight: 20
                            contentItem: Text {
                                text: model.modelData[1]
                                color: "#FFFFFF"
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                            background: Rectangle {
                                radius: 12
                                color:  "#40e0d0"
                            }
                            onClicked: navigation_manager.rootIndex = model.modelData[0]
                        }
                    }
                }
            }
            Rectangle {
                Layout.fillWidth: true
                Layout.fillHeight: true
                color: "dodgerblue"

                ListView{
                    id: view
                    anchors.fill: parent
                    anchors.margins: 12
                    model: FilterModel {
                        id: filtermodel
                        filter: function(item) {
                            return navigation_manager.filter(item.display, filterTextFieldId.text)
                        }
                        model: navigation_manager.model
                        rootIndex: navigation_manager.rootIndex
                        delegate: Rectangle {
                            height: 25
                            color:"transparent"
                            Text { 
                                text: model.display
                                color:"white"
                                MouseArea{
                                    anchors.fill: parent
                                    onClicked: {
                                        if (model.hasModelChildren){
                                            navigation_manager.rootIndex = view.model.modelIndex(index)
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

FilterModel.qml

import QtQuick 2.13
import QtQml.Models 2.13

DelegateModel{
    property var filter: function(item) { return true; }
    function update() {
        if (items.count > 0) {
            items.setGroups(0, items.count, "items");
        }
        var visible = [];
        for (var i = 0; i < items.count; ++i) {
            var item = items.get(i);
            if (filter(item.model)) {
                visible.push(item);
            }
        }
        for(var i in visible){
            item = visible[i];
            item.inVisible = true;
        }
    }
    items.onChanged: update()
    onFilterChanged: update()
    groups: DelegateModelGroup {
        id: visibleItems
        name: "visible"
        includeByDefault: false
    }
    filterOnGroup: "visible"
}
...