pythonqtpyside2qtableviewqtreeview

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?


Solution

  • 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)