Here is the code. It runs. To exhibit this bug. Right-click the ellipse, scale it by click-dragging on the ellipse. Right-click it > "Done editing". Then do the same thing with "Rotate."
I've tried over 10 different permutations of using self.setRotation
, self.setTransform
, painter.rotate
, and so on... The only time it did rotate was when I did self.setTransform(self.transform().rotate(r))
but the result was wrong: scale & rotate in the wrong order or something.
from PyQt5.QtWidgets import QGraphicsItem, QMenu
from PyQt5.QtGui import QTransform, QPen, QPainter, QColor, QBrush
from PyQt5.QtCore import Qt, QPointF, QRectF, QEvent
from math import sqrt
def scaleRect(rect, scale_x, scale_y):
T = QTransform.fromScale(scale_x, scale_y)
return T.mapRect(rect)
def debugPrintTransformMatrix(T):
print(str(T.m11()) + ' ' + str(T.m12()) + ' ' + str(T.m13()))
print(str(T.m21()) + ' ' + str(T.m22()) + ' ' + str(T.m23()))
print(str(T.m31()) + ' ' + str(T.m32()) + ' ' + str(T.m33()))
# Assumes no shearing or stretching.
# Only Rotation, Translation, and Scaling.
def extractTransformScale(T):
# This is math matrix notation transposed (debug print self.sceneTransform() to see)
Sx = sqrt(T.m11()**2 + T.m12()**2)
Sy = sqrt(T.m21()**2 + T.m22()**2)
return Sx, Sy
def extractTransformTranslate(T):
return T.m31(), T.m32()
class Object(QGraphicsItem):
def sceneEvent(self, event):
if event.type() == QEvent.GraphicsSceneMouseMove:
# move, scale, or rotate
if self._mode in ['scale', 'rotate']:
mouse_pos = event.scenePos()
last_pos = event.lastScenePos()
if self._mode == 'scale':
s = self.mouseScalingFactors(mouse_pos, last_pos)
self.setTransform(self.transform().scale(*s))
if self._mode == 'rotate':
r = self.mouseRotationAngle(mouse_pos, last_pos)
self.setRotation(self.rotation() + r)
return True
return super().sceneEvent(event)
def __init__(self):
super().__init__()
self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsSelectable)
self._selectionPen = QPen(Qt.black, 1.0, style=Qt.DotLine, cap=Qt.FlatCap)
self._lastPos = QPointF(0, 0)
self.setPos(self._lastPos)
self._mode = 'neutral'
def setRenderHints(self, painter):
painter.setRenderHints(QPainter.SmoothPixmapTransform | QPainter.HighQualityAntialiasing | QPainter.TextAntialiasing)
def boundingRectExtraScale(self):
return (1.2 , 1.2)
def mouseScalingFactors(self, pos, last_pos):
delta = pos - last_pos
return (1.01 ** delta.x(), 1.01 ** delta.y())
def mouseRotationAngle(self, pos, last_pos):
return 1 #TODO
def createDefaultContextMenu(self):
menu = QMenu()
if self._mode == 'neutral':
menu.addAction('Scale').triggered.connect(lambda: self.setMode('scale'))
menu.addAction('Rotate').triggered.connect(lambda: self.setMode('rotate'))
else:
menu.addAction('Done Editing').triggered.connect(lambda: self.setMode('neutral'))
return menu
def contextMenuEvent(self, event):
menu = self.createDefaultContextMenu()
menu.exec(event.screenPos())
def setMode(self, mode):
self._mode = mode
def setSelected(self, selected):
super().setSelected(selected)
self.update()
class ShapedObject(Object):
def __init__(self):
super().__init__()
self._shape = {
'name' : 'ellipse',
'radius': 35
}
self._brush = QBrush(Qt.darkGreen)
self._pen = QPen(Qt.yellow, 3)
def shapeDef(self):
return self._shape
def boundingRect(self):
rect = self.shapeRect()
s = self.boundingRectExtraScale()
return scaleRect(rect, *s)
def shape(self): #TODO QPainterPath shape for collision detection
# Should call self.boundingRectExtraScale()
return super().shape()
def paint(self, painter, option, widget):
self.setRenderHints(painter)
#super().paint(painter, option, widget)
shape = self.shapeDef()
name = shape['name']
painter.setBrush(self._brush)
painter.setPen(self._pen)
painter.save()
# ********** HERE IS THE PROBLEM *************
debugPrintTransformMatrix(painter.transform())
painter.rotate(5)
debugPrintTransformMatrix(painter.transform())
rect = self.shapeRect()
if name == 'ellipse':
painter.drawEllipse(rect)
painter.restore()
def shapeRect(self):
shape = self.shapeDef()
name = shape['name']
if name == 'ellipse':
r = shape['radius']
rect = QRectF(-r, -r, 2*r, 2*r)
return rect
####
import sys
from PyQt5.QtWidgets import QMainWindow, QGraphicsScene, QGraphicsView, QApplication
if __name__ == '__main__':
app = QApplication(sys.argv)
window = QMainWindow()
view = QGraphicsView()
scene = QGraphicsScene()
view.setScene(scene)
window.setCentralWidget(view)
ellipse = ShapedObject()
scene.addItem(ellipse)
window.show()
sys.exit(app.exec_())
Got it to work. It was the order of the operations rotate / scale. Never use an circle to test your rotation code, lol!