Skip to content Skip to sidebar Skip to footer

Rotate The Widget For Some Degree

I am a new to pyqt and need help with rotating the label. I am confused and cannot understand how to rotate the whole widget on a specific angle. Not the content of the widget, but

Solution 1:

A QWidget does not support rotation, but a workaround is to insert the widget into a QGraphicsProxyWidget and add it to a QGraphicsScene, and then rotate the QGraphicsProxyWidget that visually generates the same widget rotation effect.

from PyQt5 import QtCore, QtGui, QtWidgets


defmain():
    import sys

    app = QtWidgets.QApplication(sys.argv)

    label = QtWidgets.QLabel("Stack Overflow", alignment=QtCore.Qt.AlignCenter)

    graphicsview = QtWidgets.QGraphicsView()
    scene = QtWidgets.QGraphicsScene(graphicsview)
    graphicsview.setScene(scene)

    proxy = QtWidgets.QGraphicsProxyWidget()
    proxy.setWidget(label)
    proxy.setTransformOriginPoint(proxy.boundingRect().center())
    scene.addItem(proxy)

    slider = QtWidgets.QSlider(minimum=0, maximum=359, orientation=QtCore.Qt.Horizontal)
    slider.valueChanged.connect(proxy.setRotation)

    label_text = QtWidgets.QLabel(
        "{}°".format(slider.value()), alignment=QtCore.Qt.AlignCenter
    )
    slider.valueChanged.connect(
        lambda value: label_text.setText("{}°".format(slider.value()))
    )

    slider.setValue(45)

    w = QtWidgets.QWidget()
    lay = QtWidgets.QVBoxLayout(w)
    lay.addWidget(graphicsview)
    lay.addWidget(slider)
    lay.addWidget(label_text)
    w.resize(640, 480)
    w.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

enter image description here

Solution 2:

As @eyllanesc correctly explains, there's no "widget rotation" support in Qt (as in most standard frameworks).

There are a couple of tricks on your hand, though.

"Simple" label (not using a QLabel)

That's the "simple" solution. Since you're talking about a "label", that can be implemented using some math.

The biggest advantage in this approach is that the size hint is "simple", meaning that it's only based on the text contents (as in QFontMetrics.boundingRect()), and whenever the main font, text or alignment is changed, the size hint reflects them. While it supports multi-line labels, the biggest problem about this approach comes in place if you need to use rich text, though; a QTextDocument can be used instead of a standard string, but that would require a more complex implementation for size hint computing.

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

from PyQt5 import QtCore, QtGui, QtWidgets

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

    defalignment(self):
        return self._alignment

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

    defangle(self):
        return self._angle

    defsetAngle(self, angle):
        # the angle clearly affects the overall size
        angle %= 360if 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())

    deftext(self):
        return self._text

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

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

    defminimumSizeHint(self):
        return self.sizeHint()

    defpaintEvent(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 isif self._angle <= 90:
            deltaX = 0
            deltaY = sin(self._radians) * width
        elif90 < self._angle <= 180:
            deltaX = cos(self._radians) * width
            deltaY = sin(self._radians) * width + sin(self._radiansOpposite) * height
        elif180 < 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)


classTestWindow(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 inenumerate([randrange(360) for _ inrange(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)

    defrandomize(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 implementation

I would also like to expand the solution proposed by eyllanesc, as it is more modular and allows to use "any" widget; unfortunately, while his answer works as expected, I'm afraid that it's an answer that is just valid "for the sake of the argument". From the graphical point of view, the obvious issues are the QGraphicsView visual hints (borders and background). But, since we're talking about widgets that might have to be inserted in a graphical interface, the size (and its hint[s]) require some care. The main advantage of this approach is that almost any type of widget can be added to the interface, but due to the nature of per-widget size policy and QGraphicsView implementations, if the content of the "rotated" widget changes, perfect drawing will always be something hard to achieve.

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

classAngledObject(QtWidgets.QGraphicsView):
    _angle = 0def__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)

    defangle(self):
        return self._angle

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

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

    defsizeHint(self):
        return self.scene().itemsBoundingRect().size().toSize()

    defminimumSizeHint(self):
        return self.sizeHint()


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

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

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

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


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


classTestWindow(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 inenumerate([randrange(360) for _ inrange(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 inenumerate([randrange(360) for _ inrange(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)

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

As you can see, the "randomize" functions have very different results. While the second approach allows using more complex widgets, the first one better reacts to contents changes.

Post a Comment for "Rotate The Widget For Some Degree"