Создание указанной таблицы с использованием конструктора pyqt5 - PullRequest
1 голос
/ 12 июля 2020

Я хочу создать специальную таблицу c, как показано на рисунке ниже, с помощью конструктора pyqt, и я не смогу добиться хорошего результата. Я хочу сделать эту таблицу в окне и содержать такие же элементы и одинаковые размеры. Я пытался использовать макеты с помощью LineEdits и Qlabels, но у меня тоже не получалось. Спасибо.

1 Ответ

2 голосов
/ 26 июля 2020

Предпосылка: ваш вопрос не требовал большого количества исследовательских усилий, и из того, что было сказано, совершенно ясно, что вы все еще немного неопытны; это, вероятно, сделает этот ответ очень сложным, но это потому, что то, что вы спросили, не просто.

Хотя достижение того, о чем вас просят, не невозможно, это нелегко. Кроме того, вы не можете сделать это непосредственно в дизайнере.

Основная проблема заключается в том, что представления элементов Qt используют QHeaderView, который использует одномерную структуру ; добавление еще одного «размерного» слоя значительно усложняет задачу.

Итак, первый аспект, который вам необходимо учитывать, это то, что виджет таблицы должен иметь новый настраиваемый набор QHeaderView для горизонтального заголовка, поэтому вы очевидно, нужно создать подкласс QHeaderView; но для того, чтобы все работало, вам также необходимо создать подкласс QTableWidget.

Из-за «моноразмерности» заголовка (который использует только одну координату для своих данных) вам необходимо «сгладить» структуру и создать уровень абстракции, чтобы получить к ней доступ.

Для этого я создал Structure класс с функциями, которые позволяют получить доступ к нему как к некой древовидной модели:

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):
        # an iterator that cycles through *all* items recursively
        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):
        # get the first (child) item for the given 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):
        # while levels should start from -1 (root), we're using levels starting
        # from 0 (which is root); this is done for simplicity and performance
        if self._isRoot:
            return 0
        parent = self.parent
        level = 0
        while parent:
            level += 1
            parent = parent.parent
        return level

    def column(self):
        # root column should be -1; see comment on level()
        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):
        # return the column (child) count for this section
        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):
    # a "root" class created just for commodity
    def __init__(self, label='', children=None):
        super().__init__(label, children, isRoot=True)

С помощью этого класса вы можете создать свою собственную структуру заголовка, например:

structure = Structure('Root item', (
    Section('First parent, two sub levels', (
        Section('First child, no children'), 
        Section('Second child, two children', (
            Section('First subchild'), 
            Section('Second subchild')
            )
        )
    )), 
    # column index = 3
    Section('Second parent', (
        Section('First child'), 
        Section('Second child')
        )), 
    # column index = 5
    Section('Third parent, no children'), 
    # ...
))

А вот подклассы QHeaderView и QTableWidget с минимальным воспроизводимым кодом:

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):
        # ensure that the parent section is always updated
        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:
            # avoid recursion
            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

    # -------- base class reimplementations -------- #

    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:
            # if the clicked section has children, select all of its columns
            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
            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:
            # update sections when click/dragging (required if highlight is enabled)
            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
            # unset the cursor, in case it was set for a section handle
            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):
        # update the corner widget
        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):
        # ignore column count, as we're using setStructure() instead
        pass


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

    structure = Structure('UNITE', (
        Section('Hrs de marche', (
            Section('Expl'), 
            Section('Indi', (
                Section('Prev'), 
                Section('Accid')
            ))
        )), 
        Section('Dem', (
            Section('TST'), 
            Section('Epl')
        )), 
        Section('Decle'), 
        Section('a'), 
        Section('Consom'), 
        Section('Huile'), 
    ))

    tableWidget = CustomHeaderTableWidget()
    tableWidget.setStructure(structure)

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

    tableWidget.show()
    sys.exit(app.exec())

Некоторые соображения, так как приведенный выше пример не идеален:

  • разделы не перемещаемые (если вы попытаетесь установить setSectionsMovable и попробуйте перетащить раздел, возможно, в какой-то момент он сломается sh);
  • хотя я пытался избежать изменения размера "родительского" раздела (курсор изменения размера не отображается), все еще можно изменить размер дочернего элемента on от родительского прямоугольника;
  • изменение горизонтальной структуры модели может дать неожиданные результаты (я реализовал только базовые c операции);
  • Structure стандартное python object подкласс, и он полностью не связан с QTableWidget;
  • с учетом вышеизложенного, использование таких функций, как horizontalHeaderItem, setHorizontalHeaderItem или setHorizontalHeaderLabels, может работать не так, как ожидалось;

Теперь, как использовать это в дизайнере? Вам необходимо использовать продвигаемый виджет . Добавьте QTableWidget, щелкните его правой кнопкой мыши и выберите Promote to..., убедитесь, что «QTableWidget» выбран в поле «Имя базового класса», введите «CustomHeaderTableWidget» в поле «Имя продвигаемого класса», а затем имя файла, который содержит подкласс в поле «Заголовочный файл» (обратите внимание, что он обрабатывается как имя модуля python, поэтому он должен быть без расширения файла .py); нажмите «Добавить», нажмите «Продвинуть» и сохраните. Учтите, что оттуда вы все равно должны предоставить настраиваемый Structure, и если вы добавили какую-либо строку и столбец в Designer, они должны отражать количество столбцов структуры.

Наконец, поскольку это интересный вопрос, я мог бы вернуться к нему в будущем и, в конце концов, обновить код.

А пока я настоятельно рекомендую вам внимательно изучить код, изучить все повторные реализации QHeaderView (см. что ниже base class reimplementations комментарий) и что на самом деле делают оригинальные методы, прочитав документацию QHeaderView .

...