Python Qt6 基于视图编辑更新模型

问题描述 投票:0回答:1

我正在尝试创建一个 mvc 应用程序,它可以获取 JSON 文件,从中创建 TreeView,允许用户进行编辑,然后将编辑保存到 JSON 文件。我无法理解如何根据相应视图所做的可视化编辑来更新底层模型,因此我可以将这些编辑保存到 JSON 文件中。

我从 https://doc.qt.io/qtforpython-6/examples/example_widgets_itemviews_jsonmodel.html复制了大部分代码,并且更改很少(仅 setData() 和 flags() 方法)。

下面是我正在使用的代码:

import json
import sys
from typing import Any, List, Dict, Union

from PySide6.QtWidgets import QTreeView, QApplication, QHeaderView
from PySide6.QtCore import QAbstractItemModel, QModelIndex, QObject, Qt, QFileInfo
import PySide6.QtGui as QtGui

class TreeItem:
    """A Json item corresponding to a line in QTreeView"""

    def __init__(self, parent: "TreeItem" = None):
        self._parent = parent
        self._key = ""
        self._value = ""
        self._value_type = None
        self._children = []

    def appendChild(self, item: "TreeItem"):
        """Add item as a child"""
        self._children.append(item)

    def child(self, row: int) -> "TreeItem":
        """Return the child of the current item from the given row"""
        return self._children[row]

    def parent(self) -> "TreeItem":
        """Return the parent of the current item"""
        return self._parent

    def childCount(self) -> int:
        """Return the number of children of the current item"""
        return len(self._children)

    def row(self) -> int:
        """Return the row where the current item occupies in the parent"""
        return self._parent._children.index(self) if self._parent else 0

    @property
    def key(self) -> str:
        """Return the key name"""
        return self._key

    @key.setter
    def key(self, key: str):
        """Set key name of the current item"""
        self._key = key

    @property
    def value(self) -> str:
        """Return the value name of the current item"""
        return self._value

    @value.setter
    def value(self, value: str):
        """Set value name of the current item"""
        self._value = value

    @property
    def value_type(self):
        """Return the python type of the item's value."""
        return self._value_type

    @value_type.setter
    def value_type(self, value):
        """Set the python type of the item's value."""
        self._value_type = value

    @classmethod
    def load(
        cls, value: Union[List, Dict], parent: "TreeItem" = None, sort=True
    ) -> "TreeItem":
        """Create a 'root' TreeItem from a nested list or a nested dictonary

        Examples:
            with open("file.json") as file:
                data = json.dump(file)
                root = TreeItem.load(data)

        This method is a recursive function that calls itself.

        Returns:
            TreeItem: TreeItem
        """
        rootItem = TreeItem(parent)
        rootItem.key = "root"

        if isinstance(value, dict):
            items = sorted(value.items()) if sort else value.items()

            for key, value in items:
                child = cls.load(value, rootItem)
                child.key = key
                child.value_type = type(value)
                rootItem.appendChild(child)

        elif isinstance(value, list):
            for index, value in enumerate(value):
                child = cls.load(value, rootItem)
                child.key = index
                child.value_type = type(value)
                rootItem.appendChild(child)

        else:
            rootItem.value = value
            rootItem.value_type = type(value)

        return rootItem


class JsonModel(QAbstractItemModel):
    """ An editable model of Json data """

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

        self._rootItem = TreeItem()
        self._headers = ("key", "value")

    def clear(self):
        """ Clear data from the model """
        self.load({})

    def load(self, document: dict):
        """Load model from a nested dictionary returned by json.loads()

        Arguments:
            document (dict): JSON-compatible dictionary
        """

        assert isinstance(
            document, (dict, list, tuple)
        ), "`document` must be of dict, list or tuple, " f"not {type(document)}"

        self.beginResetModel()

        self._rootItem = TreeItem.load(document)
        self._rootItem.value_type = type(document)

        self.endResetModel()

        return True

    def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> Any:
        """Override from QAbstractItemModel

        Return data from a json item according index and role

        """
        if not index.isValid():
            return None

        item = index.internalPointer()

        if role == Qt.DisplayRole:
            if index.column() == 0:
                return item.key

            if index.column() == 1:
                return item.value

        elif role == Qt.EditRole:
            if index.column() == 1:
                return item.value

    def setData(self, index: QModelIndex, value: Any, role: Qt.ItemDataRole):
        """Override from QAbstractItemModel

        Set json item according index and role

        Args:
            index (QModelIndex)
            value (Any)
            role (Qt.ItemDataRole)

        """
        item = index.internalPointer()
        item.value = str(value)

        self.dataChanged.emit(index, index, [Qt.EditRole])

        return True

    def headerData(
        self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole
    ):
        """Override from QAbstractItemModel

        For the JsonModel, it returns only data for columns (orientation = Horizontal)

        """
        if role != Qt.DisplayRole:
            return None

        if orientation == Qt.Horizontal:
            return self._headers[section]

    def index(self, row: int, column: int, parent=QModelIndex()) -> QModelIndex:
        """Override from QAbstractItemModel

        Return index according row, column and parent

        """
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        if not parent.isValid():
            parentItem = self._rootItem
        else:
            parentItem = parent.internalPointer()

        childItem = parentItem.child(row)
        if childItem:
            return self.createIndex(row, column, childItem)
        else:
            return QModelIndex()

    def parent(self, index: QModelIndex) -> QModelIndex:
        """Override from QAbstractItemModel

        Return parent index of index

        """

        if not index.isValid():
            return QModelIndex()

        childItem = index.internalPointer()
        parentItem = childItem.parent()

        if parentItem == self._rootItem:
            return QModelIndex()

        return self.createIndex(parentItem.row(), 0, parentItem)

    def rowCount(self, parent=QModelIndex()):
        """Override from QAbstractItemModel

        Return row count from parent index
        """
        if parent.column() > 0:
            return 0

        if not parent.isValid():
            parentItem = self._rootItem
        else:
            parentItem = parent.internalPointer()

        return parentItem.childCount()

    def columnCount(self, parent=QModelIndex()):
        """Override from QAbstractItemModel

        Return column number. For the model, it always return 2 columns
        """
        return 2

    def flags(self, index: QModelIndex) -> Qt.ItemFlags:
        """Override from QAbstractItemModel

        Return flags of index
        """
        flags = super(JsonModel, self).flags(index)

        #if index.column() == 1:
        return Qt.ItemIsEditable | flags
        #else:
        #    return flags

    def to_json(self, item=None):

        if item is None:
            item = self._rootItem

        nchild = item.childCount()

        if item.value_type is dict:
            document = {}
            for i in range(nchild):
                ch = item.child(i)
                document[ch.key] = self.to_json(ch)
            return document

        elif item.value_type == list:
            document = []
            for i in range(nchild):
                ch = item.child(i)
                document.append(self.to_json(ch))
            return document

        else:
            return item.value


    def save(self):
        print(model.to_json())
        with open("example.json", 'w') as f:
            json.dump(model.to_json(), f)
        
if __name__ == "__main__":

    app = QApplication(sys.argv)
    view = QTreeView()
    model = JsonModel()

    view.setModel(model)

    json_path = "example.json"

    with open(json_path) as file:
        document = json.load(file)
        model.load(document)

    view.show()
    view.header().setSectionResizeMode(0, QHeaderView.Stretch)
    view.setAlternatingRowColors(True)
    view.resize(500, 300)
    view.shortcut1 = QtGui.QShortcut(QtGui.QKeySequence('Shift+S'), view)
    view.shortcut1.activated.connect(lambda : model.save())
    app.exec()

上面的代码创建了树视图,我可以毫无问题地进行编辑。按 Shift+S(根据快捷键事件)后,它会尝试保存模型。但是,由于视觉上所做的更改是在视图上进行的,因此它保存的模型与从 JSON 加载的模型完全相同。当我关闭应用程序时,我在视图上所做的所有编辑都会丢失。

如何将我在视图上所做的可视化编辑(更新列、值等)发送到模型,然后将其存储在 JSON 中,以便下次加载 JSON 数据时会出现更改?

Edit1:我发现它确实像我预期的那样保存了对 JSON 的更改,但前提是它们是针对没有子项或父项的项目进行的。我有兴趣保存所有更改,无论他们是否有父母/孩子。

python json pyside6 qtreeview qabstractitemmodel
1个回答
0
投票

您使用了错误的数据结构,因为您只考虑每个键可能只有以下一个

  • 列表或字典(概念上相似,因为字典本质上是父母的列表);
  • 另一个值;

这种类型的结构与基本的 dict 数据结构不兼容,并且通常对于 mapping 来说,这与 json 使用的数据结构相同:因为映射只是一个键和值对,这意味着键(父项)的值只能是更远的父项或值。

虽然可以使用“自定义pickling”序列化这样的结构,但据我所知json不提供这种可能性,唯一的解决方案是使用不同的nested结构:与

TreeItem
对象类似,每个键字典的数据结构中必须具有不同的存储字段。

例如:

{
    "some key": {
        "value": "this is the first key value",
        "children": [
            "foo", "bar"
        ]
    {,
    "another key": {
        "value": "this is another key value",
        "children": {
            "grandChild1": ["hello", "world"],
            "grandChild2": ["stack", "overflow"]
        }
    }
}

这显然意味着源和目标 json 文件都必须遵循该结构,因此模型将相应地加载/保存数据。

在上面的情况下,如果值是字典,它将创建一个在其辅助字段上设置“名称”的项目,并根据相关的键/值对创建更多子项。如果它是一个列表,它将为每个项目创建子项。

另一种更准确的方法,实际上并且始终使用字典作为项目引用(包括顶级项目),名称从键中获取,每个父值设置有相关的“值”键,子级内容进一步使用相同递归结构的字典:

{
    "some key": {
        "value": "this is the first key value",
        "children": {
            "foo": {"value": "foo value"}, 
            "bar": {"value": "bar value"}
        }
    {,
    "another key": {
        "value": "this is another key value",
        "children": {
            "grandChild1": {
                "value": "grandChild1 parent"
                "children": {
                    {"hello": {}},
                    {"world": {}}
                }
            },
            "grandChild2": {
                "value": "grandChild2 parent"
                "children": {
                    "stack": {}},
                    "overflow": {"value": "what?"}
                }
            },
        }
    }
}

在上述情况下:

  • “某个键”将在其第二列中显示“这是第一个键值”,并且将有两个子项:
    • “foo”,第二列显示“foo value”;
    • “bar”,旁边显示“bar value”;
  • “另一个键”在其兄弟列中将具有“这是另一个键值”,并且将有两个子项:
    • “grandChild1”,旁边有“grandChild1 Parent”文本;然后是另外两个孙子:
      • “你好”
      • “世界”
    • “grandChild2”旁边显示“grandChild2 Parent”;然后是它自己的两个孩子:
      • “堆栈”;
      • “溢出”,与“什么?”显示在第二列;

这显然使实现和数据存储变得复杂,但这也是存储父项(而不是子项)更多数据的唯一方法。如果这就是您想要的,那么这就是您需要做的。

© www.soinside.com 2019 - 2024. All rights reserved.