Luke
Luke

Reputation: 105

Qt Model for both QTreeView and QTableView

I'm trying to create a model that can be used for both a QTableView and QTreeView. As an example, my data is something like:

ID Location Name
101 201 Apple
201 None Kitchen
102 201 Banana
301 None Cellar
302 301 Potatoes
202 302 Nail

So every entry has a location which is itself an entry in the model. For the QTableView, I'd like to simply display all entries under each other as shown above, while for the QTreeView I'd like something like

My problem however is that I can't figure out how to implement QAbstractProxyModel.maptoSource() or mapfromSource() as I lose information about the parent in the QTableView. Reading https://www.qtcentre.org/threads/26163-Map-table-to-tree-through-model-view-possible it seems that perhaps this is not possible at all. However the QAbstractProxyModel explicitly says that's it's meant for showing data in both views. Can anyone point me in the right direction or knows whether it's possible to implement a model like this? Especially in Python, I can't find any examples unfortunately.


I really like the idea of just using an unindented TreeView as a sort of TableView. Unfortunately I'm still having trouble creating the model. Currently, only the top entries are being shown.

class MyModel(qtg.QStandardItemModel):
    def __init__(
        self,
        engine
    ):
        self.engine = engine
        self.hierarchy_key = 'location_id'

        
        self.column_names = ['id', 'location_id', 'name', 'quantity']
        super().__init__(0, len(self.fields))

        self.setHorizontalHeaderLabels(self.column_names)
        self.root = self.invisibleRootItem()
        self.build_model()

    def build_model(self):
        def add_children_to_tree(entries, parent_item):
            for entry in entries:
                items = []
                for col in self.column_names:
                    text = getattr(entry, col)
                    item = qtg.QStandardItem(text)
                    items.append(qtg.QStandardItem(text))

                parent_item.appendRow(items)
                item = items[1] #the location_id item
                parent_item.setChild(item.index().row(), item.index().column(), item)

                with session_scope(self.engine) as session:
                    child_entries = (
                        session.query(self.entry_type)
                            .filter(
                            getattr(getattr(self.entry_type, self.hierarchy_key), "is_")(
                                entry.id
                            )
                        )
                            .all()
                    )
                    if child_entries:
                        add_children_to_tree(child_entries, item)

        self.removeRows(0, self.rowCount())
        with session_scope(self.engine) as session:
            root_entries = session.query(self.entry_type).filter(getattr(getattr(self.entry_type, self.hierarchy_key), "is_")(None)).all()
            if not isinstance(root_entries, list):
               root_entries = [root_entries]
            add_children_to_tree(root_entries, self.root)

The idea is that the session query results in a list of entries. Each entry is a record in the database with the attributes "id", "location_id", etc. Each attribute thus is an Item and the list of items creates a row in the model. I can't figure out how one makes the row of items a child of another row in the way it's shown here: Tree view I assume the setChild() function needs to be called differently?

Upvotes: 0

Views: 1728

Answers (2)

mike rodent
mike rodent

Reputation: 15632

I have been using QTreeView for several years (usually with QStandardItemModel) but in the non-ideal situation of knowing what works in practice but not really with much knowledge in depth.

Luke in his answer is correct about the lack of clear documentation. However, there is a superb example which deserves a lot of scrutiny here. This is direct from the Qt Group, and is a rewoking in Python (actually PySide, but easy enough to adapt to PyQt) of a previous example in C++.

What it basically does is construct something similar to a QStandardItemModel, and show how this works in conjunction with a QTreeView.

To get the full benefit of things, it's probably best to read the notes of the projects in this order:

... and if you then actually build your own Python model implementation, essentially copying that example, you should end up with a fair grasp of how things fit together. As I say, it is the people at Qt Group who produced these examples, so we can hopefully be fairly confident that this represents "best practice".

NB of particular interest is the file treeitem.py, containing the class TreeItem. As explained in the C++ project notes, this does not use any Qt package imports at all! It is a pure-Python implementation of an essential component element in the structure. Luke's implementation of his class MyItem in his answer is very similar.

Upvotes: 0

Luke
Luke

Reputation: 105

As there is a distinct lack of examples for python, I'll post my modified version of the simpletreemodel here, which is what ended up working for me. By then using a QTreeView instead of a QTableView as suggested, I got the table to behave as I wanted it too. Overall, this creates MyItem which is an item containing the entire row of information and I then use recursion to add children to parents if their value for the hierarchy_key (location_id) is equal to the id of the parent.


class MyItem(object):
    def __init__(self, data, parent=None):
        self.parentItem = parent
        self.itemData = data
        self.childItems = []

    def appendChild(self, item):
        self.childItems.append(item)

    def child(self, row):
        return self.childItems[row]

    def childCount(self):
        return len(self.childItems)

    def columnCount(self):
        return len(self.itemData)

    def data(self, column=None):
        try:
            if column == None:
                return [self.itemData[i] for i in range(self.columnCount())]
            return self.itemData[column]
        except IndexError:
            return None

    def parent(self):
        return self.parentItem

    def row(self):
        if self.parentItem:
            return self.parentItem.childItems.index(self)
        return 0


class MyModel(QtCore.QAbstractItemModel):
    def __init__(self, entry_type, engine, hierarchy_key, description_key, parent=None):
        super(ORMModel, self).__init__(parent)

        self.entry_type = entry_type
        if isinstance(self.entry_type, str):
            self.entry_type = getattr(ds, self.entry_type)
        self.engine = engine
        self.hierarchy_key = hierarchy_key
        
        self.column_names = ['id', 'location_id', 'name', 'quantity']

        self.rootItem = MyItem(self.column_names)
        self.setHeaderData(0, Qt.Horizontal, self.rootItem)
        self.initiateModel()

    def root(self):
        return self.rootItem

    def columnCount(self, parent):
        if parent.isValid():
            return parent.internalPointer().columnCount()
        else:
            return self.rootItem.columnCount()

    def data(self, index, role):
        if not index.isValid():
            return None
        item = index.internalPointer()

        if role == Qt.DisplayRole:
            return item.data(index.column())

        return None

    def flags(self, index):
        if not index.isValid():
            return QtCore.Qt.NoItemFlags

        return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable

    def headerData(self, section, orientation, role):
        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
            return self.rootItem.data(section)

        return None

    def index(self, row, column, parent):
        if not self.hasIndex(row, column, parent):
            return QtCore.QModelIndex()

        if not parent.isValid():
            parentItem = self.rootItem
        else:
            parentItem = parent.internalPointer()

        childItem = parentItem.child(row)
        if childItem:
            return self.createIndex(row, column, childItem)
        else:
            return QtCore.QModelIndex()

    def parent(self, index):
        if not index.isValid():
            return QtCore.QModelIndex()

        childItem = index.internalPointer()
        parentItem = childItem.parent()

        if parentItem == self.rootItem:
            return QtCore.QModelIndex()

        return self.createIndex(parentItem.row(), 0, parentItem)

    def rowCount(self, parent):
        if parent.column() > 0:
            return 0

        if not parent.isValid():
            parentItem = self.rootItem
        else:
            parentItem = parent.internalPointer()

        return parentItem.childCount()

    def initiateModel(self):
        def add_children_to_tree(entries, parent_item):
            for entry in entries:
                row = []
                for field in self.fields.keys():
                    val = getattr(entry, field)
                    if isinstance(val, list):
                        text = "; ".join(map(str, val))
                    else:
                        text = str(val)
                    row.append(text)
                    item = ORMItem(row, parent_item)
                parent_item.appendChild(item)

                with session_scope(self.engine) as session:
                    child_entries = (
                        session.query(self.entry_type)
                        .filter(
                            getattr(
                                getattr(self.entry_type, self.hierarchy_key), "is_"
                            )(entry.id)
                        )
                        .all()
                    )
                    if child_entries:
                        add_children_to_tree(child_entries, item)

        with session_scope(self.engine) as session:
            root_entries = (
                session.query(self.entry_type)
                .filter(
                    getattr(getattr(self.entry_type, self.hierarchy_key), "is_")(None)
                )
                .all()
            )
            if not isinstance(root_entries, list):
                root_entries = [root_entries]
            add_children_to_tree(root_entries, self.rootItem)

Upvotes: 2

Related Questions