【问题标题】:QTreeView only edits in first column?QTreeView 仅在第一列中编辑?
【发布时间】:2015-10-03 19:05:15
【问题描述】:

我正在尝试制作一个简单的属性编辑器,其中属性列表是一个嵌套的字典,数据在 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_())

【问题讨论】:

  • 一个澄清问题:我没有看到任何代码可以让编辑器出现在任何列中,所以我会惊讶地看到它适用于第一列。你让编辑器在哪里弹出?
  • 在堆栈溢出格式中很难回答您的“最佳实践”问题,因为我不应该在 cmets 中提供答案,但答案不会是您问题的答案。但是,简而言之,qmenus 应该有一个父级,而您没有正确构建模型索引。我将尝试在这里回答您的索引问题,但 qmenu 问题作为一个单独的问题可能会更好。
  • 我找到了您的 Qmenu 问题并在那里添加了第二个答案。希望这个答案能解决您对如何保护 Qt 对象免受垃圾回收的担忧。

标签: python-3.x qt4 qtreeview qabstractitemmodel propertyeditor


【解决方案1】:

您保留索引列表以防止它们被垃圾收集。这是必需的,因为正如文档所解释的那样,QModelIndexinternalPointer 引用的 Python 对象不受该引用的垃圾收集保护。但是,每次要求您的模型提供索引时,都会添加您的列表,因此即使模型中的同一个项目也会创建一个新的 internalPointer。而 Qt 期望索引和 internalPointer 相同。这也是有问题的,因为这意味着索引列表一直在增长(您可以查看是否添加了打印出 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 来存储父模型项(字典)并保留从模型项到键列表的映射。或者从模型项到父项的映射。这两个都需要一点点摆弄(尤其是因为字典不是立即可散列的)但两者都是可能的。

【讨论】:

    【解决方案2】:

    @strubbly 非常接近,但忘记在他的 index 方法中解包元组。

    这是 Qt5 的工作代码。可能有几个进口和需要修复的东西。只花了我几个星期的生命:)

    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_())
    

    【讨论】:

      猜你喜欢
      • 2023-03-16
      • 2016-03-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多