Как объединять заголовки столбцов таблицы QTableWidget в PyQT5

Рейтинг: 1Ответов: 1Опубликовано: 27.04.2023

Как при создании таблицы PyQt5 объединять заголовки столбцов?
Мне нужно сделать структуру, в которой у нескольких столбцов один заголовок или у одного столбца их два.

Нужна таблица примерно как на картинке:

введите сюда описание изображения

Ответы

▲ 1Принят

Как вариант:

import sys
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.Qt import *


class Section(object):
    def __init__(self, label='', children=None, isRoot=False):
        self.label = label
        self._children = []
        if children:
            self._children = []
            for child in children:
                child.parent = self
                self._children.append(child)
        self._isRoot = isRoot
        self.parent = None

    def children(self):
        return self._children

    def isRoot(self):
        return self._isRoot

    def iterate(self):
        # итератор, который рекурсивно просматривает *все* элементы
        if not self._isRoot:
            yield self
        items = []
        for child in self._children:
            items.extend([i for i in child.iterate()])
        for item in items:
           yield item 

    def sectionForColumn(self, column):
        # получить первый (дочерний) элемент для данного столбца
        if not self._isRoot:
            return self.root().sectionForColumn(column)
        for child in self.iterate():
            if not child._children:
                if child.column() == column:
                    return child

    def root(self):
        if self._isRoot:
            return self
        return self.parent.root()

    def level(self):
        # мы используем уровни, начинающиеся с 0 (это root); 
        # это сделано для простоты и производительности
        if self._isRoot:
            return 0
        parent = self.parent
        level = 0
        while parent:
            level += 1
            parent = parent.parent
        return level

    def column(self):
        if self._isRoot:
            return 0
        parentColIndex = self.parent._children.index(self)
        column = self.parent.column()
        for c in self.parent._children[:parentColIndex]:
            column += c.columnCount()
        return column

    def columnCount(self):
        # вернуть количество столбцов (потомков) для этого раздела
        if not self._children:
            return 1
        columns = 0
        for child in self._children:
            columns += child.columnCount()
        return columns

    def subLevels(self):
        if not self._children:
            return 0
        levels = 0
        for child in self._children:
            levels = max(levels, child.subLevels())
        return 1 + levels
        

class Structure(Section):
    def __init__(self, label='', children=None):
        super().__init__(label, children, isRoot=True)
        
        
class AdvancedHeader(QtWidgets.QHeaderView):
    _resizing = False
    _resizeToColumnLock = False

    def __init__(self, view, structure=None):
        super().__init__(QtCore.Qt.Horizontal, view)
        self.structure = structure or Structure()
        self.sectionResized.connect(self.updateSections)
        self.sectionHandleDoubleClicked.connect(self.emitHandleDoubleClicked)

    def setStructure(self, structure):
        if structure == self.structure:
            return
        self.structure = structure
        self.updateGeometries()

    def updateSections(self, index=0):
        # убедитесь, что родительский раздел всегда обновляется
        if not self.structure.children():
            return
        section = self.structure.sectionForColumn(index)
        while not section.parent.isRoot():
            section = section.parent
        leftColumn = section.column()
        left = self.sectionPosition(leftColumn)
        width = sum(self.sectionSize(leftColumn + c) \
            for c in range(section.columnCount()))
        self.viewport().update(left - self.offset(), 0, width, self.height())

    def sectionRect(self, section):
        if not self.structure.children():
            return
        column = section.column()
        left = 0
        for c in range(column):
            left += self.sectionSize(c)

        bottom = self.height()
        rowHeight = bottom / self.structure.subLevels()
        if section.parent.isRoot():
            top = 0
        else:
            top = (section.level() - 1) * rowHeight

        width = sum(self.sectionSize(column + c) \
            for c in range(section.columnCount()))

        if section.children():
            height = rowHeight
        else:
            root = section.root()
            rowCount = root.subLevels()
            parent = section.parent
            while parent.parent:
                rowCount -= 1
                parent = parent.parent
            height = rowHeight * rowCount
        return QtCore.QRect(left, top, width, height)

    def paintSubSection(self, painter, section, level, rowHeight):
        sectionRect = self.sectionRect(section).adjusted(0, 0, -1, -1)
        painter.drawRect(sectionRect)

        painter.save()
        font = painter.font()
        selection = self.selectionModel()
        column = section.column()
        sectionColumns = set([column + c for c in range(section.columnCount())])
        selectedColumns = set([i.column() for i in selection.selectedColumns()])
        if ((section.children() and selectedColumns & sectionColumns == sectionColumns) or
            (not section.children() and column in selectedColumns)):
                font.setBold(True)
                painter.setFont(font)

        painter.drawText(sectionRect, QtCore.Qt.AlignCenter, section.label)
        painter.restore()

        for child in section.children():
            self.paintSubSection(painter, child, child.level(), rowHeight)

    def sectionHandleAt(self, pos):
        x = pos.x() + self.offset()
        visual = self.visualIndexAt(x)
        if visual < 0:
            return visual

        for section in self.structure.iterate():
            rect = self.sectionRect(section)
            if pos in rect:
                break
        else:
            return -1
        grip = self.style().pixelMetric(QtWidgets.QStyle.PM_HeaderGripMargin, 
            None, self)
        if x < rect.x() + grip:
            return section.column() - 1
        elif x > rect.x() + rect.width() - grip:
            return section.column() + section.columnCount() - 1
        return -1

        logical = self.logicalIndex(visual)
        position = self.sectionViewportPosition(logical)

        atLeft = x < (position + grip)
        atRight = x > (position + self.sectionSize(logical) - grip)
        if self.orientation() == QtCore.Qt.Horizontal and self.isRightToLeft():
            atLeft, atRight = atRight, atLeft

        if atLeft:
            while visual >= 0:
                visual -= 1
                logical = self.logicalIndex(visual)
                if not self.isSectionHidden(logical):
                    break
            else:
                logical = -1
        elif not atRight:
            logical = -1
        return logical

    def emitHandleDoubleClicked(self, index):
        if self._resizeToColumnLock:
            return
        pos = self.viewport().mapFromGlobal(QtGui.QCursor.pos())
        handle = self.sectionHandleAt(pos)
        if handle != index:
            return
        self._resizeToColumnLock = True
        for section in self.structure.iterate():
            if index in range(section.column(), section.column() + \
                section.columnCount()):
                rect = self.sectionRect(section)
                if rect.y() <= pos.y() <= rect.y() + rect.height():
                    sectCol = section.column()
                    for col in range(sectCol, sectCol + section.columnCount()):
                        if col == index:
                            continue
                        self.sectionHandleDoubleClicked.emit(col)
                    break
        self._resizeToColumnLock = False

    def sizeHint(self):
        hint = super().sizeHint()
        hint.setHeight(hint.height() * self.structure.subLevels())
        return hint

    def mousePressEvent(self, event):
        super().mousePressEvent(event)
        if event.button() != QtCore.Qt.LeftButton:
            return
        handle = self.sectionHandleAt(event.pos())
        if handle >= 0:
            self._resizing = True
        else:
            # если в выбранном разделе есть дочерние элементы, 
            # выберите все его столбцы
            cols = []
            for section in self.structure.iterate():
                sectionRect = self.sectionRect(section)
                if event.pos() in sectionRect:
                    firstColumn = section.column()
                    columnCount = section.columnCount()
                    for column in range(firstColumn, firstColumn + columnCount):
                        cols.append(column)
                    break
            if cols:                               
                self.sectionPressed.emit(cols[0])  
                for col in cols[1:]:               
                    self.sectionEntered.emit(col)  

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)
        handle = self.sectionHandleAt(event.pos())
        if not event.buttons():
            if handle < 0:
                self.unsetCursor()
        elif handle < 0 and not self._resizing:
            # обновлять разделы при нажатии/перетаскивании 
            # (требуется, если выделение включено)
            pos = event.pos()
            pos.setX(pos.x() + self.offset())
            for section in self.structure.iterate():
                if pos in self.sectionRect(section):
                    self.updateSections(section.column())
                    break
            # сбросить курсор, если он был установлен для дескриптора раздела
            self.unsetCursor()

    def mouseReleaseEvent(self, event):
        self._resizing = False
        super().mouseReleaseEvent(event)

    def paintEvent(self, event):
        qp = QtGui.QPainter(self.viewport())
        qp.setRenderHints(qp.Antialiasing)
        qp.translate(.5, .5)
        height = self.height()
        rowHeight = height / self.structure.subLevels()
        qp.translate(-self.horizontalOffset(), 0)
        column = 0
        for parent in self.structure.children():
            self.paintSubSection(qp, parent, 0, rowHeight)
            column += 1


class CustomHeaderTableWidget(QtWidgets.QTableWidget):
    structure = None
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        customHeader = AdvancedHeader(self)
        self.setHorizontalHeader(customHeader)
        customHeader.setSectionsClickable(True)
        customHeader.setHighlightSections(True)

        self.cornerHeader = QtWidgets.QLabel(self)
        self.cornerHeader.setAlignment(QtCore.Qt.AlignCenter)
        self.cornerHeader.setStyleSheet('border: 1px solid black;')
        self.cornerHeader.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
        self.verticalHeader().setMinimumWidth(
            self.cornerHeader.minimumSizeHint().width() + \
            self.fontMetrics().width(' '))
        self._cornerButton = self.findChild(QtWidgets.QAbstractButton)

        self.setStructure(kwargs.get('structure') or Section('ROOT', isRoot=True))

        self.selectionModel().selectionChanged.connect(
            self.selectionModelSelChanged)

    def setStructure(self, structure):
        if structure == self.structure:
            return
        self.structure = structure
        if not structure:
            super().setColumnCount(0)
            self.cornerHeader.setText('')
        else:
            super().setColumnCount(structure.columnCount())
            self.cornerHeader.setText(structure.label)
        self.horizontalHeader().setStructure(structure)
        self.updateGeometries()

    def selectionModelSelChanged(self):
        # обновить угловой виджет
        selected = len(self.selectionModel().selectedIndexes())
        count = self.model().rowCount() * self.model().columnCount()
        font = self.cornerHeader.font()
        font.setBold(selected == count)
        self.cornerHeader.setFont(font)

    def updateGeometries(self):
        super().updateGeometries()
        vHeader = self.verticalHeader()
        if not vHeader.isVisible():
            return
        style = self.verticalHeader().style()
        opt = QtWidgets.QStyleOptionHeader()
        opt.initFrom(vHeader)
        margin = style.pixelMetric(style.PM_HeaderMargin, opt, vHeader)
        width = self.cornerHeader.minimumSizeHint().width() + margin * 2
        
        vHeader.setMinimumWidth(width)
        self.cornerHeader.setGeometry(self._cornerButton.geometry())

    def setColumnCount(self, count):
        # игнорировать количество столбцов, 
        # так как вместо этого мы используем setStructure()
        pass


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)

    structure = Structure('UNITE', (
        Section('A'), 
        Section('B', (
            Section('C'), 
        )), 
        Section('D', (
            Section('E'), 
            Section('F'),
            Section('G')
        )), 

    ))

    tableWidget = CustomHeaderTableWidget()
    tableWidget.setStructure(structure)

    tableWidget.setRowCount(6)
    tableWidget.setVerticalHeaderLabels(
        ['Row {}'.format(r + 1) for r in range(tableWidget.rowCount())])

    tableWidget.resize(600, 300)
    tableWidget.show()
    sys.exit(app.exec())

введите сюда описание изображения