Display data from QAbstractTableModel in QTreeView

2020-02-14 16:47发布

问题:

Short version

I am trying to display data from a QAbstracctTableModel in a QTreeView. When I do, I end up with the entire table displayed as the child of every node (and grandchild, and so-on). How can I display a treeview of an abstract table model?

Details

I am trying to display some data in a QAbstractTableModel in a QTreeView. In the Model-View Tutorial, after presenting an example QAbstractTableModel, it makes it seem it's as simple as replacing QTableView with QTreeView:

You can convert the example above into an application with a tree view. Simply replace QTableView with QTreeView, which results in a read/write tree. No changes have to be made to the model.

When I make this replacement, I do end up with a tree showing, but if I click on any of the icons to expand it (which should do nothing, as there is no hierarchy built in), Python crashes with Python.exe has stopped working. This has been brought up before ], but without a workable solution.

To try to fix this behavior, I reimplemented the index function in my QAbstractTableModel subclass (see full working example below). This leads to a very different type of error. Namely, every node in the tree now contains the entire table as data. No matter how many times I click, the entire table shows up. Like this:

I seem to be in some kind of recursive nightmare, and do not know how to escape. The related question below suggests I might have to go to QAbstractItemModel, but the tutorial quote above suggests otherwise (which states, No changes have to be made to the model).

Related question

QTreeView always displaying the same data

Full working example

from PySide import QtGui, QtCore

class Food(object):
    def __init__(self, name, shortDescription, note, parent = None):
        self.data = (name, shortDescription, note);
        self.parentIndex = parent

class FavoritesTableModel(QtCore.QAbstractTableModel):
    def __init__(self):
        QtCore.QAbstractTableModel.__init__(self)
        self.foods = []  
        self.loadData() 

    def data(self, index, role = QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            return self.foods[index.row()].data[index.column()]
        return None

    def rowCount(self, index=QtCore.QModelIndex()):
        return len(self.foods)

    def columnCount(self, index=QtCore.QModelIndex()):
        return 3

    def index(self, row, column, parent = QtCore.QModelIndex()):  
        return self.createIndex(row, column, parent)

    def loadData(self):   
        allFoods=("Apples", "Pears", "Grapes", "Cookies", "Stinkberries")
        allDescs = ("Red", "Green", "Purple", "Yummy", "Huh?")
        allNotes = ("Bought recently", "Kind of delicious", "Weird wine grapes",
                    "So good...eat with milk", "Don't put in your nose")
        for name, shortDescription, note in zip(allFoods, allDescs, allNotes):
            food = Food(name, shortDescription, note)                                      
            self.foods.append(food) 

def main():
    import sys
    app = QtGui.QApplication(sys.argv)

    model = FavoritesTableModel() 

    #Table view
    view1 = QtGui.QTableView()
    view1.setModel(model)
    view1.show()

    #Tree view
    view2 = QtGui.QTreeView()
    view2.setModel(model)
    view2.show()

    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

回答1:

From the official documentation:

When implementing a table based model, rowCount() should return 0 when the parent is valid.

The same goes for columnCount(). And for completeness, data() should return None if the parent is valid.


What happened is this:

  1. You click on the '+' sign next to "Stinkberries".
  2. The QTreeView thinks, "I need to expand the view. I wonder how many rows exist under 'Stinkberries'?" To find out, the QTreeView calls rowCount(), and passes the index of the "Stinkberries" cell as the parent.
  3. FavoritesTableModel::rowCount() returns 5, so the QTreeView thinks, "Ah, there are 5 rows under 'Stinkberries'."
  4. The same process happens for columns.
  5. The QTreeView decides to retrieve the first item under "Stinkberries". It calls data(), passing Row 0, Column 0, and the index of the "Stinkberries" cell as the parent.
  6. FavoritesTableModel::data() returns "Apples", so the QTreeView thinks, "Ah, the first item under 'Stinkberries' is 'Apples'."
  7. etc.

To get correct behaviour, your code must return 0 for Steps #3 and #4.


Finally, to make sure that the '+' signs don't appear at all, make hasChildren() return false for every cell.



回答2:

Solution

In theory, a nice feature of the Model-View framework is that you can have multiple views of the same Model. In practice, though, QAbstractTableModel is really meant to help you view tables, not trees. The documentation for QAbstractTableModel says:

Since the model provides a more specialized interface than QAbstractItemModel, it is not suitable for use with tree views

However, even given that caveat, there is a way to get this to work. First, as pointed out by JKSH, you have to fix rowCount (and note that its second parameter is a parent index):

def rowCount(self, parent=QtCore.QModelIndex()):
    if parent.isValid():
        return 0
    return len(self.foods)

Second, remove the quixotic reimplementation of index, which was making the selection behavior act really weird for reasons I frankly don't understand.

In general, though, if you want a generic model then play it safe and subclass from QAbstractItemModel instead of one of the pre-made models.

Discussion

Ignoring the parent within rowCount is acceptable in table models. In the official Qt book, they follow the standard procedure of having rowCount returning solely the number of rows to be displayed in the table. Blanchette and Summerfield note:

The parent parameter has no meaning for a table model; it is there because rowCount() and columnCount() are inherited from the more generic QAbstractItemModel base class, which supports heirarchies. (p 255)

In his PyQt book, Summerfield notes:

[T]he parent QModelIndex p matters only to tree models (p 434)

Basically, rowCount tells you how many rows to display underneath a parent item. Because in tables all items have the same parent, the parent item is not used in QTableViews. But for reasons pointed out very nicely by JKSH in his answer, this strategy won't work with trees.

Hence, the claim that the parent parameter "has no meaning for a table model" should be modified with the qualification that this is only true if the data will exclusively be displayed by a QTableView (which is usually a pretty good assumption).



标签: qt pyqt pyside