如何制作一个可以在 Qt 中将其行折叠成类别的表格?

2022-01-12 00:00:00 python pyqt pyqt5

问题描述

我想在 Qt 中制作一个表格,可以将其行折叠和展开成组(行按特定列的内容分组),例如:

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:

所有组已展开:

第一组折叠:

点击组标题行"时,所有子行"要么折叠到组标题行"中,要么显示在其下方.该表还应该能够取消组合"自身并成为普通表.

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.

我尝试使用带有 QTableWidget 的 QTreeView 作为子小部件,但是将表取消组合"到单个表中就成了问题.

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.

我还尝试使用 QTableView 并将组标题行"添加到表中.它有点工作,但正确实现它非常困难,因为它涉及移动行并插入这些假行",这些假行"的行为与其他行完全不同,从而弄乱了底层的 QStandardItemModel.这也使排序变得异常复杂.

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.

有没有更好的方法来实现这种小部件,或者可能已经存在实现这种功能的标准 Qt 小部件?我认为我最终可以通过假行"(也许)使它与我当前的 QTableView 一起工作,但到目前为止,它很容易被破坏并且难以实现,我真的想要一个更好的解决方案......

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...


解决方案

在这种情况下,应该使用 QTreeView,如下例所示:

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_())

相关文章