在 QLabel 内旋转 QPixmap 会导致像素图沿 x 轴移动,而不是停留在 QLabel 内

Rotate a QPixmap inside a QLabel causes the pixmap to move along the x-axis rather than staying inside the QLabel

我试图让一个球(QLabel 内的 QPixmap)在从屏幕边缘弹起的同时旋转。但是球,即使它确实旋转,似乎也沿着 QLabel 内的轴移动,所以在计时器移动几次后,它移动到 QLabel 的边界之外,因此不再出现。

请看下面我的代码。

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import random
x = 0
y = 00
velX = 2
velY = 1
randX = random.choice([1, 2, 3])
randY = random.choice([1, 2, 3])


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.setWindowTitle('ball move')
        self.setMinimumWidth(800)
        self.setMaximumWidth(800)
        self.setMinimumHeight(500)
        self.setMaximumHeight(500)
        self.setStyleSheet('background-color: black;border:none;')

        self.ballLabel = QLabel(self)
        self.ballPixmap = QPixmap('ball.png')
        self.resizedBallPixmap = self.ballPixmap.scaled(50, 50, Qt.KeepAspectRatio, Qt.FastTransformation)
        self.ballLabel.setFixedSize(50, 50)

        self.ballRotation = 10
        self.ballLabel.setPixmap(self.resizedBallPixmap)
        self.ballLabel.setStyleSheet('border:1px solid red;')
        self.ballLabel.show()

        self.ballLabel.move(0, 0)

    def rotateBall(self):
        self.resizedBallPixmap = self.resizedBallPixmap.transformed(
            QTransform().rotate(self.ballRotation), Qt.SmoothTransformation)

        # self.resizedBallPixmap = self.resizedBallPixmap.transformed(QTransform().translate(self.resizedBallPixmap.size().width()/2,                                    self.resizedBallPixmap.size().height()/2))

        self.ballLabel.setPixmap(self.resizedBallPixmap)


def ballMove():
    global x, y, velX, velY, randX, randY
    if (main_window.ballLabel.pos().x() + 50) > 800:
        velX = -1
        randX = random.choice([1, 2, 3])
        randY = random.choice([1, 2, 3])
    elif main_window.ballLabel.pos().x() < 0:
        velX = 1
        randX = random.choice([1, 2, 3])
        randY = random.choice([1, 2, 3])
    elif (main_window.ballLabel.pos().y() + 50) > 500:
        velY = -1
        randX = random.choice([1, 2, 3])
        randY = random.choice([1, 2, 3])
    elif main_window.ballLabel.pos().y() < 0:
        velY = 1
        randX = random.choice([1, 2, 3])
        randY = random.choice([1, 2, 3])

    x += velX*randX
    y += velY*randY
    main_window.rotateBall()
    main_window.ballLabel.move(x, y)


if __name__ == "__main__":
    app = QApplication([])
    main_window = MainWindow()
    main_window.show()
    timer = QTimer()
    timer.timeout.connect(ballMove)
    timer.start(1000)
    app.exec_()

解释:

要理解问题,必须使用以下代码分析 self.resizedBallPixmap 的大小:

def rotateBall(self):
    print(self.resizedBallPixmap.size())
    # ...

输出:

PyQt5.QtCore.QSize(50, 50)
PyQt5.QtCore.QSize(59, 58)
PyQt5.QtCore.QSize(70, 68)
PyQt5.QtCore.QSize(81, 80)
PyQt5.QtCore.QSize(94, 93)
PyQt5.QtCore.QSize(110, 108)
PyQt5.QtCore.QSize(128, 126)
PyQt5.QtCore.QSize(149, 147)
PyQt5.QtCore.QSize(173, 171)
PyQt5.QtCore.QSize(201, 199)
PyQt5.QtCore.QSize(233, 231)
PyQt5.QtCore.QSize(271, 268)
PyQt5.QtCore.QSize(314, 311)
...

可以看到,QPixmap的大小是变化的,为什么会变化?因为当旋转一个矩形时,ex内接的矩形必须更大,而导致矩形变大的原因是什么?好吧,QLabel 的大小不足以绘制 QPixmap,它只画了左边的部分,让用户观察到球在前进。

解决方案:

解决方法是QPixmap在旋转的时候被切掉,只保留需要的部分。另外,每次旋转变换都会产生失真,所以不建议对同一个QPixmap进行迭代,而是保持原来的QPixmap,增加旋转的角度。

class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        # ...

        self.ballLabel.move(0, 0)
        <b>self.angle = 0</b>

    def rotateBall(self):
        <b>self.angle += self.ballRotation
        pixmap = self.resizedBallPixmap.transformed(
            QTransform().rotate(self.angle), Qt.FastTransformation
        )
        r = QtCore.QRect(self.resizedBallPixmap.rect())
        r.moveCenter(pixmap.rect().center())
        pixmap = pixmap.copy(r)
        self.ballLabel.setPixmap(pixmap)</b>

更好的解决方案是使用 Qt 图形框架的元素,例如实现旋转和平移的 QGraphicsItems。

class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.setWindowTitle("ball move")
        self.setFixedSize(800, 500)

        scene = QGraphicsScene()
        scene.setSceneRect(QRectF(QPointF(), QSizeF(self.size())))
        view = QGraphicsView(scene)
        self.setCentralWidget(view)

        self.setStyleSheet("background-color: black;border:none;")

        pixmap = QPixmap("ball.png").scaled(
            50, 50, Qt.KeepAspectRatio, Qt.FastTransformation
        )
        self.ballLabel = scene.addPixmap(pixmap)
        self.ballLabel.setTransformOriginPoint(self.ballLabel.boundingRect().center())


class BallManager(QObject):
    positionChanged = pyqtSignal(QPointF)
    angleChanged = pyqtSignal(float)

    def __init__(self, parent=None):
        super(BallManager, self).__init__(parent)

        self.pos = QPointF(0, 0)
        self.angle = 0

        self.vel = QPointF(2, 1)
        self.rand = QPointF(*random.sample([1, 2, 3], 2))

        self.step_angle = 10

        self.timer = QTimer(interval=1000, timeout=self.ballMove)
        self.timer.start()

    def ballMove(self):
        if (self.pos.x() + 50) > 800:
            self.vel.setX(-1)
            self.randX = QPointF(*random.sample([1, 2, 3], 2))
        elif self.pos.x() < 0:
            self.vel.setX(1)
            self.rand = QPointF(*random.sample([1, 2, 3], 2))
        elif (self.pos.y() + 50) > 500:
            self.vel.setY(-1)
            self.rand = QPointF(*random.sample([1, 2, 3], 2))
        elif self.pos.y() < 0:
            self.vel.setY(1)
            self.rand = QPointF(*random.sample([1, 2, 3], 2))
        self.pos += QPointF(self.vel.x() * self.rand.x(), self.vel.y() * self.rand.y())
        self.angle += self.step_angle

        self.positionChanged.emit(self.pos)
        self.angleChanged.emit(self.angle)


if __name__ == "__main__":
    app = QApplication([])
    main_window = MainWindow()
    main_window.show()
    manager = BallManager()
    manager.positionChanged.connect(main_window.ballLabel.setPos)
    manager.angleChanged.connect(main_window.ballLabel.setRotation)
    app.exec_()