0

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

  • 201: Kitchen
    • 101: Apple
    • 102: Banana
  • 301: Cellar
    • 302: Potatoes
      • 202: Nail

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?

Luke
  • 75
  • 1
  • 6
  • 1
    "I lose information about the parent in the QTableView" That doesn't make much sense. The view isn't supposed to provide you with information. That information comes from the source model you're proxying over. When making a proxy, forget about the view - models don't care about the views, they have to work with no views attached! I'm a bit at loss at what exactly your problem is: this is not hard to do at all. Post some code please, otherwise this won't be resolved without someone just doing it all for you. – Kuba hasn't forgotten Monica Feb 24 '21 at 17:53
  • In any case, you don't need to do anything special to show a tree "as if" it was a table. Just tell the tree view to flatten itself (look it up). Takes literally a line of code, although the table format won't be exactly as you show it - it'll just be a flattened tree. Put all three columns in the tree and then hide one when showing it as a tree, or show all when showing it as pseudo-table. – Kuba hasn't forgotten Monica Feb 24 '21 at 17:55
  • 1
    See if [this answer](https://stackoverflow.com/a/21569029/1329652) is of use: it does at least a part of what you need. – Kuba hasn't forgotten Monica Feb 24 '21 at 18:02
  • I worded that first part wrong - I meant I lose information if I use a proxy model to flatten my standard model. But I like the idea of flattening the tree - didn't know that was possible! I unfortunately can't find any examples in Python of a tree with two-dimensional children. – Luke Feb 24 '21 at 22:15

1 Answers1

0

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)
Luke
  • 75
  • 1
  • 6