将小部件旋转一定程度

2022-02-23 00:00:00 python pyqt

问题描述

我是pyqt新手,需要帮助旋转标签。我很困惑,无法理解如何以特定的角度旋转整个小部件。不是小部件的内容,而是小部件本身。我正在搜索解决方案,但什么也找不到。


解决方案

正如@eyllanesc正确解释的那样,Qt中不支持"小部件轮换"(与大多数标准框架一样)。

不过,您手上有几个小把戏。

"简单"标签(不使用QLabel)

这是"简单"的解决方案。由于您谈论的是"标签",因此可以使用一些数学运算来实现。

此方法最大的优点是大小提示"简单",即它只基于文本内容(如QFontMetrics.boundingRect()),只要更改了主要字体、文本或对齐方式,大小提示就会反映它们。
虽然它支持多行标签,但是如果您需要使用富文本,这种方法最大的问题就出现在这里;QTextDocument可以用来代替标准字符串,但这将需要更复杂的大小提示计算实现。

from math import radians, sin, cos
from random import randrange

from PyQt5 import QtCore, QtGui, QtWidgets

class AngledLabel(QtWidgets.QWidget):
    _alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop

    def __init__(self, text='', angle=0, parent=None):
        super(AngledLabel, self).__init__(parent)
        self._text = text
        self._angle = angle % 360
        # keep radians of the current angle *and* its opposite; we're using
        # rectangles to get the overall area of the text, and since they use
        # right angles, that opposite is angle + 90
        self._radians = radians(-angle)
        self._radiansOpposite = radians(-angle + 90)

    def alignment(self):
        return self._alignment

    def setAlignment(self, alignment):
        # text alignment might affect the text size!
        if alignment == self._alignment:
            return
        self._alignment = alignment
        self.setMinimumSize(self.sizeHint())

    def angle(self):
        return self._angle

    def setAngle(self, angle):
        # the angle clearly affects the overall size
        angle %= 360
        if angle == self._angle:
            return
        self._angle = angle
        # update the radians to improve optimization of sizeHint and paintEvent
        self._radians = radians(-angle)
        self._radiansOpposite = radians(-angle + 90)
        self.setMinimumSize(self.sizeHint())

    def text(self):
        return self._text

    def setText(self, text):
        if text == self._text:
            return
        self._text = text
        self.setMinimumSize(self.sizeHint())

    def sizeHint(self):
        # get the bounding rectangle of the text
        rect = self.fontMetrics().boundingRect(QtCore.QRect(), self._alignment, self._text)
        # use trigonometry to get the actual size of the rotated rectangle
        sinWidth = abs(sin(self._radians) * rect.width())
        cosWidth = abs(cos(self._radians) * rect.width())
        sinHeight = abs(sin(self._radiansOpposite) * rect.height())
        cosHeight = abs(cos(self._radiansOpposite) * rect.height())
        return QtCore.QSize(cosWidth + cosHeight, sinWidth + sinHeight)

    def minimumSizeHint(self):
        return self.sizeHint()

    def paintEvent(self, event):
        qp = QtGui.QPainter(self)
        textRect = self.fontMetrics().boundingRect(
            QtCore.QRect(), self._alignment, self._text)
        width = textRect.width()
        height = textRect.height()
        # we have to translate the painting rectangle, and that depends on which
        # "angle sector" the current angle is
        if self._angle <= 90:
            deltaX = 0
            deltaY = sin(self._radians) * width
        elif 90 < self._angle <= 180:
            deltaX = cos(self._radians) * width
            deltaY = sin(self._radians) * width + sin(self._radiansOpposite) * height
        elif 180 < self._angle <= 270:
            deltaX = cos(self._radians) * width + cos(self._radiansOpposite) * height
            deltaY = sin(self._radiansOpposite) * height
        else:
            deltaX = cos(self._radiansOpposite) * height
            deltaY = 0
        qp.translate(.5 - deltaX, .5 - deltaY)
        qp.rotate(-self._angle)
        qp.drawText(self.rect(), self._alignment, self._text)


class TestWindow(QtWidgets.QWidget):
    def __init__(self):
        super(TestWindow, self).__init__()
        layout = QtWidgets.QGridLayout()
        self.setLayout(layout)

        self.randomizeButton = QtWidgets.QPushButton('Randomize!')
        layout.addWidget(self.randomizeButton, 0, 0, 1, 3)
        self.randomizeButton.clicked.connect(self.randomize)

        layout.addWidget(QtWidgets.QLabel('Standard label'), 1, 0)
        text = 'Some text'
        layout.addWidget(QtWidgets.QLabel(text), 1, 2)
        self.labels = []
        for row, angle in enumerate([randrange(360) for _ in range(8)], 2):
            angleLabel = QtWidgets.QLabel(u'{}°'.format(angle))
            angleLabel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
            layout.addWidget(angleLabel, row, 0)
            label = AngledLabel(text, angle)
            layout.addWidget(label, row, 2)
            self.labels.append((angleLabel, label))

        separator = QtWidgets.QFrame()
        separator.setFrameShape(separator.VLine|separator.Sunken)
        layout.addWidget(separator, 1, 1, layout.rowCount() - 1, 1)

    def randomize(self):
        for angleLabel, label in self.labels:
            angle = randrange(360)
            angleLabel.setText(str(angle))
            label.setAngle(angle)
        self.adjustSize()


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = TestWindow()
    w.show()
    sys.exit(app.exec_())

QGraphicsView实现

我还想扩展eyllanesc提出的解决方案,因为它更加模块化,并且允许使用"任何"小部件;不幸的是,尽管他的答案如预期的那样工作,但我担心这只是一个"为了论证"而有效的答案。
从图形的角度来看,最明显的问题是QGraphicsView视觉提示(边框和背景)。但是,由于我们讨论的是可能必须插入到图形界面中的小部件,因此需要注意大小(及其提示)。 这种方法的主要优点是,几乎任何类型的小部件都可以添加到界面,但是由于每个小部件大小策略和QGraphicsView实现的性质,如果"旋转"的小部件的内容发生变化,完美的绘制总是很难实现的。

from random import randrange
from PyQt5 import QtCore, QtGui, QtWidgets

class AngledObject(QtWidgets.QGraphicsView):
    _angle = 0

    def __init__(self, angle=0, parent=None):
        super(AngledObject, self).__init__(parent)
        # to prevent the graphics view to draw its borders or background, set the
        # FrameShape property to 0 and a transparent background
        self.setFrameShape(0)
        self.setStyleSheet('background: transparent')
        self.setScene(QtWidgets.QGraphicsScene())
        # ignore scroll bars!
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)

    def angle(self):
        return self._angle

    def setAngle(self, angle):
        angle %= 360
        if angle == self._angle:
            return
        self._angle = angle
        self._proxy.setTransform(QtGui.QTransform().rotate(-angle))
        self.adjustSize()

    def resizeEvent(self, event):
        super(AngledObject, self).resizeEvent(event)
        # ensure that the scene is fully visible after resizing
        QtCore.QTimer.singleShot(0, lambda: self.centerOn(self.sceneRect().center()))

    def sizeHint(self):
        return self.scene().itemsBoundingRect().size().toSize()

    def minimumSizeHint(self):
        return self.sizeHint()


class AngledLabel(AngledObject):
    def __init__(self, text='', angle=0, parent=None):
        super(AngledLabel, self).__init__(angle, parent)
        self._label = QtWidgets.QLabel(text)
        self._proxy = self.scene().addWidget(self._label)
        self._label.setStyleSheet('background: transparent')
        self.setAngle(angle)
        self.alignment = self._label.alignment

    def setAlignment(self, alignment):
        # text alignment might affect the text size!
        if alignment == self._label.alignment():
            return
        self._label.setAlignment(alignment)
        self.setMinimumSize(self.sizeHint())

    def text(self):
        return self._label.text()

    def setText(self, text):
        if text == self._label.text():
            return
        self._label.setText(text)
        self.setMinimumSize(self.sizeHint())


class AngledButton(AngledObject):
    def __init__(self, text='', angle=0, parent=None):
        super(AngledButton, self).__init__(angle, parent)
        self._button = QtWidgets.QPushButton(text)
        self._proxy = self.scene().addWidget(self._button)
        self.setAngle(angle)


class TestWindow(QtWidgets.QWidget):
    def __init__(self):
        super(TestWindow, self).__init__()
        layout = QtWidgets.QGridLayout()
        self.setLayout(layout)

        self.randomizeButton = QtWidgets.QPushButton('Randomize!')
        layout.addWidget(self.randomizeButton, 0, 0, 1, 3)
        self.randomizeButton.clicked.connect(self.randomize)

        layout.addWidget(QtWidgets.QLabel('Standard label'), 1, 0)
        text = 'Some text'
        layout.addWidget(QtWidgets.QLabel(text), 1, 2)
        self.labels = []
        for row, angle in enumerate([randrange(360) for _ in range(4)], 2):
            angleLabel = QtWidgets.QLabel(u'{}°'.format(angle))
            angleLabel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
            layout.addWidget(angleLabel, row, 0)
            label = AngledLabel(text, angle)
            layout.addWidget(label, row, 2)
            self.labels.append((angleLabel, label))

        for row, angle in enumerate([randrange(360) for _ in range(4)], row + 1):
            angleLabel = QtWidgets.QLabel(u'{}°'.format(angle))
            angleLabel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
            layout.addWidget(angleLabel, row, 0)
            label = AngledButton('Button!', angle)
            layout.addWidget(label, row, 2)
            self.labels.append((angleLabel, label))

        separator = QtWidgets.QFrame()
        separator.setFrameShape(separator.VLine|separator.Sunken)
        layout.addWidget(separator, 1, 1, layout.rowCount() - 1, 1)

    def randomize(self):
        for angleLabel, label in self.labels:
            angle = randrange(360)
            angleLabel.setText(str(angle))
            label.setAngle(angle)
        self.adjustSize()


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = TestWindow()
    w.show()
    sys.exit(app.exec_())

如您所见,"随机化"函数有非常不同的结果。虽然第二种方法允许使用更复杂的小部件,但第一种方法可以更好地响应内容更改。

相关文章