Making a specified table using pyqt5 designer

半腔热情 提交于 2021-01-29 03:57:34

问题


I want to make a specific table like shown in the picture below using pyqt designer and I coulnd't make a good result. I want to make this table in a window and contains the same elements and same dimensions. I tried to use layouts using LineEdits and Qlabels but I couldnt make it too . Thank you.


回答1:


Premise: your question didn't show lots of research efforts, and from what was said it's quite clear that you're still a bit inexperienced; this will probably make this answer very complicated, but that's because what you asked is not simple.

While achieving what it is asked is not impossible, it is not easy.
Also, you cannot do it directly in designer.

The main problem is that Qt's item views use QHeaderView, which uses a monodimensional structure; adding another "dimension" layer makes things much more difficult.

So, the first aspect you need to consider is that the table widget needs to have a new, custom QHeaderView set for the horizontal header, so you'll obviously need to subclass QHeaderView; but in order to make things work you'll also need to subclass QTableWidget too.

Due to the "monodimensionality" of the header (which only uses a single coordinate for its data), you need to "flatten" the structure and create an abstraction layer in order to access it.

In order to achieve that, I created a Structure class, with functions that allow access to it as some sort of tree-model:

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)

With this class, you can create your own header structure like this:

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'), 
    # ...
))

And here are the QHeaderView and QTableWidget subclasses, with a minimal reproducible code:

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

Some considerations, since the above example is not perfect:

  • sections are not movable (if you try to set setSectionsMovable and try to drag a section, it would probably crash at some point);
  • while I tried to avoid resizing of a "parent" section (the resize cursor is not shown), it is still possible to resize a child section from the parent rectangle;
  • changing the horizontal structure of the model might give unexpected results (I only implemented basic operations);
  • Structure is a standard python object subclass, and it's completely unlinked from the QTableWidget;
  • considering the above, using functions like horizontalHeaderItem, setHorizontalHeaderItem or setHorizontalHeaderLabels might not work as expected;

Now, how to use it in designer? You need to use a promoted widget.
Add the QTableWidget, right click on it and select Promote to..., ensure that "QTableWidget" is selected in the "Base class name" combo, type "CustomHeaderTableWidget" in the "Promoted class name" field and then the file name that contains the subclass in the "Header file" field (note that it's treated like a python module name, so it has to be without the .py file extension); click "Add", click "Promote" and save it. Consider that, from there, you must still provide the custom Structure, and if you added any row and column in Designer it must reflect the structure column count.


Finally, since the matter is interesting, I might return on it in the future and update the code, eventually.

In the meantime, I strongly suggest you to carefully study the code, explore all the reimplementations of QHeaderView (see what's below base class reimplementations comment) and what the original methods actually do by reading the QHeaderView documentation.



来源:https://stackoverflow.com/questions/62859541/making-a-specified-table-using-pyqt5-designer

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!