使用 PySide2 或 PyQt5,我想制作一个带有 45 度角标题标签的表格小部件,如下图所示。
我在 QTable 小部件的 QtCreator(设计器)中没有看到类似的内容。我可以使用如下方式旋转标签:
class MyLabel(QtGui.QWidget):
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setPen(QtCore.Qt.black)
painter.translate(20, 100)
painter.rotate(-45)
painter.drawText(0, 0, "hellos")
painter.end()
Run Code Online (Sandbox Code Playgroud)
但是,有几个小问题。理想情况下,这将是一个 QLineEdit 小部件,我需要这些小部件“发挥得很好”,以免与其他任何内容重叠,并且我希望它们从标题填充到表格上方。我正在寻找建议。
这是一个非常有趣的话题,因为Qt没有提供这样的功能,但是它是可以实现的。
\n下面的例子远非完美,我将列出它的主要优点/缺点。
我认为应该可以支持所有标题功能(自定义/可拉伸部分大小、可移动部分、项目滚动等),但需要对 QTableView 和 QHeaderView 方法进行非常深入的重新实现过程。
\n\n无论如何,这就是我到目前为止得到的结果,它支持滚动、绘画和基本的鼠标交互(单击时突出显示部分)。
\n\n\n\n\n\n\n\nimport sys\nfrom math import sqrt, sin, acos, hypot, degrees, radians\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\nclass AngledHeader(QtWidgets.QHeaderView):\n borderPen = QtGui.QColor(0, 190, 255)\n labelBrush = QtGui.QColor(255, 212, 0)\n def __init__(self, parent=None):\n QtWidgets.QHeaderView.__init__(self, QtCore.Qt.Horizontal, parent)\n self.setSectionResizeMode(self.Fixed)\n self.setDefaultSectionSize(sqrt((self.fontMetrics().height() + 4)** 2 *2))\n self.setSectionsClickable(True)\n self.setDefaultSectionSize(int(sqrt((self.fontMetrics().height() + 4)** 2 *2)))\n self.setMaximumHeight(100)\n # compute the ellipsis size according to the angle; remember that:\n # 1. if the angle is not 45 degrees, you\'ll need to compute this value \n # using trigonometric functions according to the angle;\n # 2. we assume ellipsis is done with three period characters, so we can \n # "half" its size as (usually) they\'re painted on the bottom line and \n # they are large enough, allowing us to show as much as text is possible\n self.fontEllipsisSize = int(hypot(*[self.fontMetrics().height()] * 2) * .5)\n self.setSectionsClickable(True)\n\n def sizeHint(self):\n # compute the minimum height using the maximum header label "hypotenuse"\'s\n hint = QtWidgets.QHeaderView.sizeHint(self)\n count = self.count()\n if not count:\n return hint\n fm = self.fontMetrics()\n width = minSize = self.defaultSectionSize()\n # set the minimum width to ("hypotenuse" * sectionCount) + minimumHeight\n # at least, ensuring minimal horizontal scroll bar interaction\n hint.setWidth(width * count + self.minimumHeight())\n maxDiag = maxWidth = maxHeight = 1\n for s in range(count):\n if self.isSectionHidden(s):\n continue\n # compute the diagonal of the text\'s bounding rect, \n # shift its angle by 45\xc2\xb0 to get the minimum required \n # height\n rect = fm.boundingRect(\n str(self.model().headerData(s, QtCore.Qt.Horizontal)) + \' \')\n # avoid math domain errors for empty header labels\n diag = max(1, hypot(rect.width(), rect.height()))\n if diag > maxDiag:\n maxDiag = diag\n maxWidth = max(1, rect.width())\n maxHeight = max(1, rect.height())\n # get the angle of the largest boundingRect using the "Law of cosines":\n # https://en.wikipedia.org/wiki/Law_of_cosines\n angle = degrees(acos(\n (maxDiag ** 2 + maxWidth ** 2 - maxHeight ** 2) / \n (2. * maxDiag * maxWidth)\n ))\n # compute the minimum required height using the angle found above\n minSize = max(minSize, sin(radians(angle + 45)) * maxDiag)\n hint.setHeight(min(self.maximumHeight(), minSize))\n return hint\n\n def mousePressEvent(self, event):\n width = self.defaultSectionSize()\n start = self.sectionViewportPosition(0)\n rect = QtCore.QRect(0, 0, width, -self.height())\n transform = QtGui.QTransform().translate(0, self.height()).shear(-1, 0)\n for s in range(self.count()):\n if self.isSectionHidden(s):\n continue\n if transform.mapToPolygon(\n rect.translated(s * width + start, 0)).containsPoint(\n event.pos(), QtCore.Qt.WindingFill):\n self.sectionPressed.emit(s)\n return\n\n def paintEvent(self, event):\n qp = QtGui.QPainter(self.viewport())\n qp.setRenderHints(qp.Antialiasing)\n width = self.defaultSectionSize()\n delta = self.height()\n # add offset if the view is horizontally scrolled\n qp.translate(self.sectionViewportPosition(0) - .5, -.5)\n fmDelta = (self.fontMetrics().height() - self.fontMetrics().descent()) * .5\n # create a reference rectangle (note that the negative height)\n rect = QtCore.QRectF(0, 0, width, -delta)\n diagonal = hypot(delta, delta)\n for s in range(self.count()):\n if self.isSectionHidden(s):\n continue\n qp.save()\n qp.save()\n qp.setPen(self.borderPen)\n # apply a "shear" transform making the rectangle a parallelogram;\n # since the transformation is applied top to bottom\n # we translate vertically to the bottom of the view\n # and draw the "negative height" rectangle\n qp.setTransform(qp.transform().translate(s * width, delta).shear(-1, 0))\n qp.drawRect(rect)\n qp.setPen(QtCore.Qt.NoPen)\n qp.setBrush(self.labelBrush)\n qp.drawRect(rect.adjusted(2, -2, -2, 2))\n qp.restore()\n\n qp.translate(s * width + width, delta)\n qp.rotate(-45)\n label = str(self.model().headerData(s, QtCore.Qt.Horizontal))\n elidedLabel = self.fontMetrics().elidedText(\n label, QtCore.Qt.ElideRight, diagonal - self.fontEllipsisSize)\n qp.drawText(0, -fmDelta, elidedLabel)\n qp.restore()\n\n\nclass AngledTable(QtWidgets.QTableView):\n def __init__(self, *args, **kwargs):\n QtWidgets.QTableView.__init__(self, *args, **kwargs)\n self.setHorizontalHeader(AngledHeader(self))\n self.verticalScrollBarSpacer = QtWidgets.QWidget()\n self.addScrollBarWidget(self.verticalScrollBarSpacer, QtCore.Qt.AlignTop)\n self.fixLock = False\n\n def setModel(self, model):\n if self.model():\n self.model().headerDataChanged.disconnect(self.fixViewport)\n QtWidgets.QTableView.setModel(self, model)\n model.headerDataChanged.connect(self.fixViewport)\n\n def fixViewport(self):\n if self.fixLock:\n return\n self.fixLock = True\n # delay the viewport/scrollbar states since the view has to process its \n # new header data first\n QtCore.QTimer.singleShot(0, self.delayedFixViewport)\n\n def delayedFixViewport(self):\n # add a right margin through the horizontal scrollbar range\n QtWidgets.QApplication.processEvents()\n header = self.horizontalHeader()\n if not header.isVisible():\n self.verticalScrollBarSpacer.setFixedHeight(0)\n self.updateGeometries()\n return\n self.verticalScrollBarSpacer.setFixedHeight(header.sizeHint().height())\n bar = self.horizontalScrollBar()\n bar.blockSignals(True)\n step = bar.singleStep() * (header.height() / header.defaultSectionSize())\n bar.setMaximum(bar.maximum() + step)\n bar.blockSignals(False)\n self.fixLock = False\n\n def resizeEvent(self, event):\n # ensure that the viewport and scrollbars are updated whenever \n # the table size change\n QtWidgets.QTableView.resizeEvent(self, event)\n self.fixViewport()\n\n\nclass TestWidget(QtWidgets.QWidget):\n def __init__(self):\n QtWidgets.QWidget.__init__(self)\n l = QtWidgets.QGridLayout()\n self.setLayout(l)\n self.table = AngledTable()\n l.addWidget(self.table)\n model = QtGui.QStandardItemModel(4, 5)\n self.table.setModel(model)\n self.table.setHorizontalScrollMode(self.table.ScrollPerPixel)\n model.setVerticalHeaderLabels([\'Location {}\'.format(l + 1) for l in range(8)])\n columns = [\'Column {}\'.format(c + 1) for c in range(8)]\n columns[3] += \' very, very, very, very, very, very, long\'\n model.setHorizontalHeaderLabels(columns)\n\n\nif __name__ == \'__main__\':\n app = QtWidgets.QApplication(sys.argv)\n w = TestWidget()\n w.show()\n sys.exit(app.exec_())\nRun Code Online (Sandbox Code Playgroud)\n\n请注意,我使用 QTransforms 而不是 QPolygons 编辑了绘画和单击检测代码:虽然理解其机制有点复杂,但它比创建多边形并在每次必须添加列标题时计算其点要快。画。
\n此外,我还添加了对最大标题高度的支持(以防任何标题标签变得太长),以及一个“间隔”小部件,可将垂直滚动条移动到表格内容的实际“开始”位置。
| 归档时间: |
|
| 查看次数: |
1265 次 |
| 最近记录: |