简体   繁体   English

QTreeView仅在第一列编辑?

[英]QTreeView only edits in first column?

I am trying to make a simple property editor, where the property list is a nested dict and the data is displayed and edited in a QTreeView. 我正在尝试创建一个简单的属性编辑器,其中属性列表是嵌套的dict,数据在QTreeView中显示和编辑。 (Before I get to my question -- if anyone already has a working implementation of this in Python 3 I'd love to be pointed at it). (在我提出我的问题之前 - 如果有人已经在Python 3中实现了这个功能,我很乐意指出它)。

Anyway, after much work I have my QAbstractItemModel and I can open a QTreeView with this model and it shows the data. 无论如何,经过大量的工作,我有了我的QAbstractItemModel,我可以用这个模型打开一个QTreeView,它显示了数据。 If I click on a label in the first column (the key) then it opens up an editor, either a text editor or a spinbox etc depending on the datatype. 如果我单击第一列(键)中的标签,则会根据数据类型打开编辑器,文本编辑器或旋转框等。 When I finish editing it calls my "model.setData" where I reject it because I don't want to allow editable keys. 当我完成编辑它调用我的“model.setData”,我拒绝它,因为我不想允许可编辑的键。 I can disable the editing of this by using flags and that works fine. 我可以通过使用标志禁用编辑此功能,并且工作正常。 I just wanted to check that everything works the way that I'd expect it to. 我只想检查一切是否符合我的预期。

Here is what doesn't happen: if I click on a cell in the second column (the value that I actually want to edit) then it bypasses the loading of an editor and simply calls model.setData with the current value. 以下是不会发生的事情:如果我单击第二列中的单元格(我实际想要编辑的值),则它会绕过编辑器的加载,只需使用当前值调用model.setData。 I am baffled. 我很困惑。 I've tried changing the tree selectionBehavior and selectionMode but no dice. 我已经尝试更改树选择行为和选择模式,但没有骰子。 I'm returning Qt.ItemIsEnabled | 我正在返回Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsSelectable | Qt.ItemIsEditable in flags. Qt.ItemIsEditable in flags。 It seems to display fine. 它看起来很好。 It just won't open up an editor. 它只是不会打开一个编辑器。

Any thoughts about what stupid mistake I must be making? 关于我必须犯下什么愚蠢错误的任何想法? I'll include the code below, with some print statements that I'm using to try to debug the thing. 我将包含下面的代码,其中包含一些我正在尝试调试该事物的打印语句。

Thanks 谢谢

PS One thing that hung me up for a long time was that my QModelIndex members would just disappear, so the indices that I got back were garbage. PS让我长时间困扰的一件事是我的QModelIndex成员会消失,所以我得到的索引是垃圾。 I found that by keeping a reference to them (throwing them in a list) that they worked. 我发现通过保持对它们的引用(将它们放入列表中)它们起作用。 This seems to be a problem that springs up a lot in Qt work (I had the same problem with menus disappearing -- I guess that means that I should think about it sooner). 这似乎是Qt工作中出现的一个问题(菜单消失时我遇到了同样的问题 - 我想这意味着我应该早点考虑一下)。 Is there a "best practices" way of dealing with this? 是否有“最佳实践”方式来处理这个问题?

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

@strubbly was real close but forgot to unpack the tuple in his index method. @strubbly真的很接近但忘了在他的index方法中解压缩元组。

Here's the working code for Qt5. 这是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_())

You keep a list of indexes to prevent them being garbage collected. 保留索引列表以防止它们被垃圾回收。 This is needed because, as the documentation explains, the Python object referenced by the internalPointer of a QModelIndex is not protected from garbage collection by that reference. 这是必需的,因为正如文档所解释的那样, QModelIndexinternalPointer引用的Python对象不受该引用的垃圾回收保护。 However, your list is added to every time your Model is asked for an index so a new internalPointer is created even for the same Item in the Model. 但是,每次要求模型索引时,都会添加列表,因此即使对于模型中的同一项,也会创建新的internalPointer。 Whereas Qt expects the index and therefore the internalPointer to be the same. 而Qt期望索引,因此internalPointer是相同的。 This is also problematic since it means the indexes list just keeps growing (as you can see if you add a debug print printing out the contents of self.myIndexes ). 这也是有问题的,因为它意味着索引列表不断增长(如您可以看到,如果您添加调试打印打印出self.myIndexes的内容)。

This is not trivial to fix in your case. 在您的情况下,这不是一件容易的事情。 In most models, the internalPointer just stores a pointer to the parent item which is therefore never duplicated. 在大多数模型中,internalPointer只存储指向父项的指针,因此永远不会重复。 But that won't work in your case because items in the PropertyList don't know their parent. 但是这在你的情况下不起作用,因为PropertyList中的项目不知道它们的父项。 The easiest solution might be to change that, but the PropertyList shouldn't really be affected by its use in the Qt model. 最简单的解决方案可能是改变它,但PropertyList不应该真正受它在Qt模型中使用的影响。

Instead, I have built a dict which is used to find an "original" key list for any key list you build. 相反,我已经构建了一个dict,用于查找您构建的任何键列表的“原始”键列表。 This looks a bit odd but it works and fixes your code with the fewest changes. 这看起来有点奇怪,但它可以使用最少的更改来修复代码。 I have mentioned some alternative approaches at the bottom. 我在底部提到了一些替代方法。

So these are my changes (really just the lines changing self.myIndexes but also changing the key list to be a tuple rather than a list so it can be hashed): 所以这些是我的更改(实际上只是更改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()

This seems to work, though I've not done too much testing. 这似乎有效,但我没有做太多测试。

Alternatively you could use the internalPointer to store the parent model item (dictionary) and keep a mapping from model item to key list. 或者,您可以使用internalPointer来存储父模型项(字典)并保持从模型项到键列表的映射。 Or a mapping from model item to parent item. 或者从模型项到父项的映射。 Both of these need a little fiddling (not least because dictionaries are not immediately hashable) but both are possible. 这两个都需要一点点摆弄(尤其是因为字典不能立即清洗)但两者都是可能的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM