Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I make a table that can collapse its rows into categories in Qt?

Tags:

python

pyqt

pyqt5

I want to make a table in Qt that can collapse and expand its rows into groups (the rows are grouped by the content of a specific column), such as this:

All groups expanded:

Rows expanded

First group collapsed:

Rows collapsed

When clicking on the "group header rows", all "child rows" are either collapsed into the "group header row" or shown underneath it. The table should also be able to "un-group" itself and become a normal table.

I've tried using a QTreeView with QTableWidget as child widgets, but then it becomes a problem to "un-group" the tables into a single table.

I also tried using QTableView and adding the "group header rows" to the table. It sort of works, but it has been very tough to implement it correctly, since it involves moving rows around and inserting these "fake rows" that behave very differently the rest, thus messing up the underlying QStandardItemModel. It also makes sorting unreasonably complicated.

Is there any better way to implement this kind of widget, or maybe there already exists a standard Qt widget that implements this functionality? I reckon I can eventually make it work with my current QTableView with "fake rows" (maybe), but so far it has been so prone to breaking and hard to implement that I really want a better solution...

like image 380
drakenation Avatar asked Oct 27 '25 17:10

drakenation


1 Answers

In this case, a QTreeView should be used as shown in the following example:

from PyQt5 import QtCore, QtGui, QtWidgets

datas = {
    "Category 1": [
        ("New Game 2", "Playnite", "", "", "Never", "Not Played", ""),
        ("New Game 3", "Playnite", "", "", "Never", "Not Played", ""),
    ],
    "No Category": [
        ("New Game", "Playnite", "", "", "Never", "Not Plated", ""),
    ]
}

class GroupDelegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, parent=None):
        super(GroupDelegate, self).__init__(parent)
        self._plus_icon = QtGui.QIcon("plus.png")
        self._minus_icon = QtGui.QIcon("minus.png")

    def initStyleOption(self, option, index):
        super(GroupDelegate, self).initStyleOption(option, index)
        if not index.parent().isValid():
            is_open = bool(option.state & QtWidgets.QStyle.State_Open)
            option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
            option.icon = self._minus_icon if is_open else self._plus_icon

class GroupView(QtWidgets.QTreeView):
    def __init__(self, model, parent=None):
        super(GroupView, self).__init__(parent)
        self.setIndentation(0)
        self.setExpandsOnDoubleClick(False)
        self.clicked.connect(self.on_clicked)
        delegate = GroupDelegate(self)
        self.setItemDelegateForColumn(0, delegate)
        self.setModel(model)
        self.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
        self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        self.setStyleSheet("background-color: #0D1225;")

    @QtCore.pyqtSlot(QtCore.QModelIndex)
    def on_clicked(self, index):
        if not index.parent().isValid() and index.column() == 0:
            self.setExpanded(index, not self.isExpanded(index))


class GroupModel(QtGui.QStandardItemModel):
    def __init__(self, parent=None):
        super(GroupModel, self).__init__(parent)
        self.setColumnCount(8)
        self.setHorizontalHeaderLabels(["", "Name", "Library", "Release Date", "Genre(s)", "Last Played", "Time Played", ""])
        for i in range(self.columnCount()):
            it = self.horizontalHeaderItem(i)
            it.setForeground(QtGui.QColor("#F2F2F2"))

    def add_group(self, group_name):
        item_root = QtGui.QStandardItem()
        item_root.setEditable(False)
        item = QtGui.QStandardItem(group_name)
        item.setEditable(False)
        ii = self.invisibleRootItem()
        i = ii.rowCount()
        for j, it in enumerate((item_root, item)):
            ii.setChild(i, j, it)
            ii.setEditable(False)
        for j in range(self.columnCount()):
            it = ii.child(i, j)
            if it is None:
                it = QtGui.QStandardItem()
                ii.setChild(i, j, it)
            it.setBackground(QtGui.QColor("#002842"))
            it.setForeground(QtGui.QColor("#F2F2F2"))
        return item_root

    def append_element_to_group(self, group_item, texts):
        j = group_item.rowCount()
        item_icon = QtGui.QStandardItem()
        item_icon.setEditable(False)
        item_icon.setIcon(QtGui.QIcon("game.png"))
        item_icon.setBackground(QtGui.QColor("#0D1225"))
        group_item.setChild(j, 0, item_icon)
        for i, text in enumerate(texts):
            item = QtGui.QStandardItem(text)
            item.setEditable(False)
            item.setBackground(QtGui.QColor("#0D1225"))
            item.setForeground(QtGui.QColor("#F2F2F2"))
            group_item.setChild(j, i+1, item)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        model = GroupModel(self)
        tree_view = GroupView(model)
        self.setCentralWidget(tree_view)

        for group, childrens in datas.items():
            group_item = model.add_group(group)
            for children in childrens:
                model.append_element_to_group(group_item, children)

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.resize(720, 240)
    w.show()
    sys.exit(app.exec_())

enter image description here

enter image description here

like image 89
eyllanesc Avatar answered Oct 30 '25 06:10

eyllanesc



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!