QTreeView only edits in the first column?

I am trying to create a simple property editor where the property list is a nested dict and the data is displayed and edited in a QTreeView. (Before I get to my question - if anyone already has a working implementation of this in Python 3 I would love to be pointed out).

Anyway, after a lot of work, I have a QAbstractItemModel and I can open a QTreeView with that model and display the data. If I click on the label in the first column (key) then it opens an editor, text editor or spinbox, etc. Depending on the data type. When I finish editing it calls my "model.setData" where I dismiss it because I don't want to allow editable keys. I can disable editing of this using flags and it works great. I just wanted to check that everything is working as I expected.

Here's what's going wrong: if I click on a cell in the second column (the value I really want to edit), then it bypasses the editor load and just calls model.setData with the current value. I'm confused. I tried changing treeBehavior and selectionMode but not cubes. I am returning Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable in flags. It seems to look great. It just won't open the editor.

Any thoughts on what kind of stupid mistake I should be making? I'll provide the code below, with some of the print statements that I use to try and debug this thing.

thank

PS One thing that hung me up a long time ago was that my QModelIndex members just disappeared, so the indices I got were garbage. I found that by keeping a link to them (dropping them into the list) they worked. This seems to be a problem that comes up very often with Qt working (I had the same problem with the menu disappearing - I think this means I should have thought of this before). Is there a "better way" to handle 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_())

      

+3


source to share


2 answers


@strubbly was really close, but forgot to unpack the tuple in his method index

.

Here's the working code for Qt5. There are probably a couple of imported and other materials that will need to be fixed. It only cost me a couple of 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_())

      

+1


source


You keep a list of indices to prevent them from being garbage collected. This is necessary because, as the documentation explains, the Python object being referenced internalPointer

QModelIndex

is not protected from garbage collection by that reference. However, your list is added every time your model is requested for an index, so a new innerPointer is created even for the same item in the model. Whereas Qt expects an index and so the internalPointer will be the same. This is also problematic as it means that the list of indexes keeps growing (as you can see if you've added debug printing by printing the contents self.myIndexes

).

This is not the way to fix it in your case. In most models, internalPointer simply stores a pointer to the parent, which is therefore not duplicated. But that won't work in your case, because the items in the PropertyList don't know their parent. The simplest solution is to change it, but the PropertyList should not be affected by its use in the Qt model.

Instead, I built a dict that is used to find the "original" list of keys for any list of keys you create. It looks a little odd, but it works and fixes your code with minimal changes. I have mentioned some of the alternative approaches below.

So these are my changes (actually, these are just string changes self.myIndexes

, but also changing the list of keys as a tuple, not a list so that it can be hashed):



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, although I haven't tested too much.

Alternatively, you can use innerPointer to store the parent of the model (dictionary) and store the mapping from the model to a list of keys. Or a mapping from a model element to a parent element. They both need a little play (not least because dictionaries are not immediately hashed), but both are possible.

0


source







All Articles