3

我正在尝试制作一个简单的属性编辑器,其中属性列表是一个嵌套的字典,并且数据在 QTreeView 中显示和编辑。(在我回答我的问题之前——如果有人已经在 Python 3 中有一个可行的实现,我很乐意指出它)。

无论如何,经过大量工作,我有了我的 QAbstractItemModel,我可以用这个模型打开一个 QTreeView 并显示数据。如果我单击第一列(键)中的标签,则会打开一个编辑器,根据数据类型,可以是文本编辑器,也可以是旋转框等。当我完成编辑时,它会调用我的“model.setData”,我拒绝它,因为我不想允许可编辑的键。我可以通过使用标志来禁用对此的编辑,并且效果很好。我只是想检查一切是否按我期望的方式工作。

以下是不会发生的情况:如果我单击第二列中的一个单元格(我实际想要编辑的值),那么它会绕过编辑器的加载并简单地使用当前值调用 model.setData。我很困惑。我试过改变树 selectionBehavior 和 selectionMode 但没有骰子。我要返回 Qt.ItemIsEnabled | Qt.ItemIsSelectable | 标志中的 Qt.ItemIsEditable。它似乎显示正常。它只是不会打开编辑器。

关于我必须犯什么愚蠢的错误有什么想法吗?我将包含下面的代码,以及一些我用来尝试调试的打印语句。

谢谢

PS 困扰我很久的一件事是我的 QModelIndex 成员会消失,所以我找回的索引是垃圾。我发现通过保留对它们的引用(将它们放入列表中)它们可以工作。这似乎是 Qt 工作中经常出现的一个问题(我也遇到了菜单消失的问题——我想这意味着我应该早点考虑一下)。是否有处理此问题的“最佳实践”方式?

# -*- coding: utf-8 -*-

from collections import OrderedDict
from PyQt4.QtCore import QAbstractItemModel, QModelIndex, Qt
from PyQt4.QtGui import QAbstractItemView

class PropertyList(OrderedDict):
    def __init__(self, *args, **kwargs):
        OrderedDict.__init__(self, *args, **kwargs)
        self.myModel = PropertyListModel(self)

    def __getitem__(self,index):
        if issubclass(type(index), list):
            item = self
            for key in index:
                item = item[key]
            return item
        else:
            return OrderedDict.__getitem__(self, index)


class PropertyListModel(QAbstractItemModel):

    def __init__(self, propList, *args, **kwargs):
        QAbstractItemModel.__init__(self, *args, **kwargs)
        self.propertyList = propList
        self.myIndexes = []   # Needed to stop garbage collection

    def index(self, row, column, parent):
        """Returns QModelIndex to row, column in parent (QModelIndex)"""
        if not self.hasIndex(row, column, parent):
            return QModelIndex()        
        if parent.isValid():
            indexPtr = parent.internalPointer()
            parentDict = self.propertyList[indexPtr]
        else:
            parentDict = self.propertyList
            indexPtr = []
        rowKey = list(parentDict.keys())[row]
        childPtr = indexPtr+[rowKey]
        newIndex = self.createIndex(row, column, childPtr)
        self.myIndexes.append(childPtr)
        return newIndex

    def get_row(self, key):
        """Returns the row of the given key (list of keys) in its parent"""
        if key:
            parent = key[:-1]
            return list(self.propertyList[parent].keys()).index(key[-1])
        else:
            return 0

    def parent(self, index):
        """
        Returns the parent (QModelIndex) of the given item (QModelIndex)
        Top level returns QModelIndex()
        """
        if not index.isValid():
            return QModelIndex()
        childKeylist = index.internalPointer()
        if childKeylist:
            parentKeylist = childKeylist[:-1]
            self.myIndexes.append(parentKeylist)
            return self.createIndex(self.get_row(parentKeylist), 0,
                                    parentKeylist)
        else:
            return QModelIndex()

    def rowCount(self, parent):
        """Returns number of rows in parent (QModelIndex)"""
        if parent.column() > 0:
            return 0    # only keys have children, not values
        if parent.isValid():
            indexPtr = parent.internalPointer()
            try:
                parentValue = self.propertyList[indexPtr]
            except:
                return 0
            if issubclass(type(parentValue), dict):
                return len(self.propertyList[indexPtr])
            else:
                return 0
        else:
            return len(self.propertyList)

    def columnCount(self, parent):
        return 2  # Key & value

    def data(self, index, role):
        """Returns data for given role for given index (QModelIndex)"""
       # print('Looking for data in role {}'.format(role))
        if not index.isValid():
            return None
        if role in (Qt.DisplayRole, Qt.EditRole):
            indexPtr = index.internalPointer()
            if index.column() == 1:    # Column 1, send the value
                return self.propertyList[indexPtr]
            else:                   # Column 0, send the key
                if indexPtr:
                    return indexPtr[-1]
                else:
                    return ""
        else:  # Not display or Edit
            return None

    def setData(self, index, value, role):
        """Sets the value of index in a given role"""
        print('In SetData')
        if not index.isValid():
            return False
        print('Trying to set {} to {}'.format(index,value))
        print('That is column {}'.format(index.column()))
        if not index.column():  # Only change column 1
            return False
        try:
            ptr = index.internalPointer()
            self.propertyList[ptr[:-1]][ptr[-1]] = value
            self.emit(self.dataChanged(index, index))
            return True
        except:
            return False

    def flags(self, index):
        """Indicates what can be done with the data"""
        if not index.isValid():
            return Qt.NoItemFlags
        if index.column():  # only enable editing of values, not keys
            return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
        else:
            return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable #Qt.NoItemFlags

if __name__ == '__main__':
    p = PropertyList({'k1':'v1','k2':{'k3':'v3','k4':4}})

    import sys
    from PyQt4 import QtGui
    qApp = QtGui.QApplication(sys.argv)

    treeView = QtGui.QTreeView()

# I've played with all the settings on these to no avail
    treeView.setHeaderHidden(False)
    treeView.setAllColumnsShowFocus(True)
    treeView.setUniformRowHeights(True)
    treeView.setSelectionBehavior(QAbstractItemView.SelectRows)
    treeView.setSelectionMode(QAbstractItemView.SingleSelection)
    treeView.setAlternatingRowColors(True)
    treeView.setEditTriggers(QAbstractItemView.DoubleClicked | 
                             QAbstractItemView.SelectedClicked |
                             QAbstractItemView.EditKeyPressed |
                             QAbstractItemView.AnyKeyPressed)
    treeView.setTabKeyNavigation(True)                             
    treeView.setModel(p.myModel)
    treeView.show()

    sys.exit(qApp.exec_())
4

2 回答 2

1

@strubbly was real close but forgot to unpack the tuple in his index method.

Here's the working code for Qt5. There are probably a couple of imports and stuff that would need to be fixed. Only cost me a couple weeks of my life :)

import sys
from collections import OrderedDict
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import Qt

class TupleKeyedOrderedDict(OrderedDict):
    def __init__(self, *args, **kwargs):
        super().__init__(sorted(kwargs.items()))

    def __getitem__(self, key):
        if isinstance(key, tuple):
            item = self
            for k in key:
                if item != ():
                    item = item[k]
            return item
        else:
            return super().__getitem__(key)

    def __setitem__(self, key, value):
        if isinstance(key, tuple):
            item = self
            previous_item = None
            for k in key:
                if item != ():
                    previous_item = item
                    item = item[k]
            previous_item[key[-1]] = value
        else:
            return super().__setitem__(key, value)

class SettingsModel(QtCore.QAbstractItemModel):
    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.root = data
        self.my_index = {}   # Needed to stop garbage collection

    def index(self, row, column, parent):
        if not self.hasIndex(row, column, parent):
            return QtCore.QModelIndex()
        if parent.isValid():
            index_pointer = parent.internalPointer()
            parent_dict = self.root[index_pointer]
        else:
            parent_dict = self.root
            index_pointer = ()
        row_key = list(parent_dict.keys())[row]
        child_pointer = (*index_pointer, row_key)
        try:
            child_pointer = self.my_index[child_pointer]
        except KeyError:
            self.my_index[child_pointer] = child_pointer
        index = self.createIndex(row, column, child_pointer)
        return index

    def get_row(self, key):
        if key:
            parent = key[:-1]
            if not parent:
                return 0
            return list(self.root[parent].keys()).index(key[-1])
        else:
            return 0

    def parent(self, index):
        if not index.isValid():
            return QtCore.QModelIndex()
        child_key_list = index.internalPointer()
        if child_key_list:
            parent_key_list = child_key_list[:-1]
            try:
                parent_key_list = self.my_index[parent_key_list]
            except KeyError:
                self.my_index[parent_key_list] = parent_key_list
            return self.createIndex(self.get_row(parent_key_list), 0,
                                    parent_key_list)
        else:
            return QtCore.QModelIndex()

    def rowCount(self, parent):
        if parent.column() > 0:
            return 0    # only keys have children, not values
        if parent.isValid():
            indexPtr = parent.internalPointer()
            parentValue = self.root[indexPtr]
            if isinstance(parentValue, OrderedDict):
                return len(self.root[indexPtr])
            else:
                return 0
        else:
            return len(self.root)

    def columnCount(self, parent):
        return 2  # Key & value

    def data(self, index, role):
        if not index.isValid():
            return None
        if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
            indexPtr = index.internalPointer()
            if index.column() == 1:    # Column 1, send the value
                return self.root[indexPtr]
            else:                   # Column 0, send the key
                if indexPtr:
                    return indexPtr[-1]
                else:
                    return None
        else:  # Not display or Edit
            return None

    def setData(self, index, value, role):
        pointer = self.my_index[index.internalPointer()]
        self.root[pointer] = value
        self.dataChanged.emit(index, index)
        return True

    def flags(self, index):
        if not index.isValid():
            return 0
        return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    data = TupleKeyedOrderedDict(**{'1': OrderedDict({'sub': 'b'}), '2': OrderedDict({'subsub': '3'})})

    model = SettingsModel(data)
    tree_view = QtWidgets.QTreeView()
    tree_view.setModel(model)
    tree_view.show()
    sys.exit(app.exec_())
于 2015-12-16T13:31:26.933 回答
0

您保留索引列表以防止它们被垃圾收集。internalPointer这是必需的,因为正如文档所解释的,由a引用的 Python 对象QModelIndex不受该引用的垃圾收集保护。但是,每次要求您的模型提供索引时,都会添加您的列表,因此即使模型中的同一个项目也会创建一个新的 internalPointer。而 Qt 期望索引和内部指针是相同的。这也是有问题的,因为这意味着索引列表只会不断增长(正如您可以看到是否添加了打印出内容的调试打印self.myIndexes)。

在您的情况下,这并非易事。在大多数模型中,internalPointer 只存储一个指向父项的指针,因此永远不会重复。但这不适用于您的情况,因为 PropertyList 中的项目不知道它们的父项。最简单的解决方案可能是改变它,但 PropertyList 不应该真正受到它在 Qt 模型中的使用的影响。

相反,我构建了一个字典,用于为您构建的任何密钥列表查找“原始”密钥列表。这看起来有点奇怪,但它可以工作并以最少的更改修复您的代码。我在底部提到了一些替代方法。

所以这些是我的更改(实际上只是更改了行self.myIndexes,但也将键列表更改为元组而不是列表,因此可以对其进行哈希处理):

def __init__(self, propList, *args, **kwargs):
    QAbstractItemModel.__init__(self, *args, **kwargs)
    self.propertyList = propList
    self.myIndexes = {}   # Needed to stop garbage collection

def index(self, row, column, parent):
    """Returns QModelIndex to row, column in parent (QModelIndex)"""
    if not self.hasIndex(row, column, parent):
        return QModelIndex()        
    if parent.isValid():
        indexPtr = parent.internalPointer()
        parentDict = self.propertyList[indexPtr]
    else:
        parentDict = self.propertyList
        indexPtr = ()
    rowKey = list(parentDict.keys())[row]
    childPtr = indexPtr+(rowKey,)
    try:
        childPtr = self.myIndexes[childPtr]
    except KeyError:
        self.myIndexes[childPtr] = childPtr
    newIndex = self.createIndex(row, column, childPtr)
    return newIndex

def parent(self, index):
    """
    Returns the parent (QModelIndex) of the given item (QModelIndex)
    Top level returns QModelIndex()
    """
    if not index.isValid():
        return QModelIndex()
    childKeylist = index.internalPointer()
    if childKeylist:
        parentKeylist = childKeylist[:-1]
        try:
            parentKeylist = self.myIndexes[parentKeylist]
        except KeyError:
            self.myIndexes[parentKeylist] = parentKeylist
        return self.createIndex(self.get_row(parentKeylist), 0,
                                parentKeylist)
    else:
        return QModelIndex()

这似乎有效,虽然我没有做太多的测试。

或者,您可以使用 internalPointer 来存储父模型项(字典)并保留从模型项到键列表的映射。或者从模型项到父项的映射。这两个都需要一点点摆弄(尤其是因为字典不是立即可散列的),但两者都是可能的。

于 2015-08-09T17:45:29.183 回答