问题
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 pythonobject
subclass, and it's completely unlinked from the QTableWidget;- considering the above, using functions like
horizontalHeaderItem
,setHorizontalHeaderItem
orsetHorizontalHeaderLabels
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