Transforming a rectangle in PyQt

I was working on editing images (translate, scale, rotate) in a QGraphicsView. After fighting QRect’s setters sometimes changing width/height and sometimes not as well as Qt’s weird update issues, QGraphicsScene’s scrollbars & local offset & incorrect mouse positions & very very incorrect depth sorting I decided to start from scratch.

Here’s a widget that allows you to edit a rectangle,
convert it to a QMatrix (rect.asQMatrix) and apply that to a pixmap (QPixmap.transformed) and there’s your image editing.

import math
from PyQt4 import QtCore, QtGui
from Vmath.vec import Vec

class EditableRectangleWidget(QtGui.QFrame):
    def __init__(self, parent):
        QtGui.QFrame.__init__(self, parent)
        self.mouse = MouseData()
        self.rectangle = Rect(0,0,128,128)
        self.tolerance = 7
        self.pick = None
        self.rotator = self.tolerance+self.tolerance+24
    def drawPoly(self, painter, coords):
        ln = len(coords)
        for i in range(ln):
            painter.drawLine( QtCore.QLine(coords[i][0], coords[i][1], coords[(i+1)%ln][0], coords[(i+1)%ln][1]) )
    def paintEvent(self, e):
        QtGui.QFrame.paintEvent(self, e)
        painter = QtGui.QPainter(self)
        coords = self.rectangle.getCoords(self.rotator)
        self.drawPoly( painter, [ coords[0], coords[2]-Vec(1,0), coords[8]-Vec(1,1), coords[6]-Vec(0,1) ] )
        #draw rotator line
        for i in range(len(coords)):
            if (self.mouse.position-coords[i]).mag() <= self.tolerance:
                c = QtGui.QColor(255,255,0,127)
                if i == len(coords)-1:
            if i == len(coords)-1:
                painter.drawLine( QtCore.QLine(coords[4][0], coords[4][1]-self.tolerance, coords[-1][0], coords[-1][1]+self.tolerance) )
            painter.drawEllipse( QtCore.QRectF(coords[i][0]-self.tolerance+1, coords[i][1]-self.tolerance+1,
                                               self.tolerance+self.tolerance-2, self.tolerance+self.tolerance-2) )
    def mousePressEvent(self,e):
        self.mouse.leftState = 3
        v = Vec( e.pos().x(), e.pos().y() )
        self.mouse.clickedAt = v
        self.mouse.position = v
        coords = self.rectangle.getCoords(self.rotator)
        for i in range(len(coords)):
            picking = v-coords[i]
            if picking.mag() <= self.tolerance:
                self.pick = i
    def mouseReleaseEvent(self,e):
        self.mouse.leftState = 1
        self.pick = None
    def mouseMoveEvent(self,e):
        self.mouse.position = Vec( e.pos().x(), e.pos().y() )

        if self.mouse.leftState > 1 and self.pick != None:
            if self.pick == 9: #rotate
                motion =
                self.rectangle.a = math.atan2(motion[0],- motion[1])
                rotatedmouse = Vec(self.mouse.position[:])
                rotatedmouse -=
                rotatedmouse = Vec( rotatedmouse[0]*math.cos(-self.rectangle.a)+rotatedmouse[1]*-math.sin(-self.rectangle.a),
                     rotatedmouse[0]*math.sin(-self.rectangle.a)+rotatedmouse[1]*math.cos(-self.rectangle.a) )
                rotatedmouse +=
                if self.pick%3 == 0:
                    self.rectangle.p1[0] = rotatedmouse[0]
                elif self.pick%3 == 1:
                elif self.pick%3 == 2:
                    self.rectangle.p2[0] = rotatedmouse[0]
                if self.pick < 3:
                    self.rectangle.p1[1] = rotatedmouse[1]
                elif self.pick < 6:
                elif self.pick < 9:
                    self.rectangle.p2[1] = rotatedmouse[1]
                if self.pick == 4:
        if self.mouse.leftState%2: 
            self.mouse.leftState -= 1
class Rect():
    def __init__(self, x1, y1, x2, y2):
        self.p1 = Vec(x1,y1)
        self.p2 = Vec(x2,y2)
        self.a = 0
    def getCoords(self, rotator=None):
        c =
        coords = [self.p1, Vec(c[0], self.p1[1]), Vec(self.p2[0], self.p1[1]),
                Vec(self.p1[0], c[1]), c, Vec(self.p2[0], c[1]),
                Vec(self.p1[0], self.p2[1]), Vec(c[0], self.p2[1]), self.p2]
        if rotator:
            coords.append( Vec(coords[4][:]) )
            coords[-1][1] -= rotator
        for i in range(len(coords)):
            coords[i] -= c
            coords[i] = Vec( coords[i][0]*math.cos(self.a)+coords[i][1]*-math.sin(self.a),
                             coords[i][0]*math.sin(self.a)+coords[i][1]*math.cos(self.a) )
            coords[i] += c
        return coords

    def center(self):
        return (self.p2-self.p1)*0.5+self.p1
    def setCenter(self, vec):
        t =
        self.p1 += t
        self.p2 += t
    def width(self):
        return self.p2[0]-self.p1[0]
    def height(self):
        return self.p2[1]-self.p1[1]
    def asQRect(self):
        return QtCore.QRect(self.p1[0], self.p1[1], self.width(), self.height())
    def asQMatrix(self):
        m = QtGui.QMatrix(self.p1[0], self.p1[1], self.width(), self.height())
        return m
class MouseData():
    def __init__(self):
        self.position = Vec(0,0)
        self.leftState = 0
        self.clickedAt = Vec(0,0)

w = QtGui.QWidget()
l = QtGui.QGridLayout()
win = EditableRectangleWidget(w)
l.addWidget(win, 1, 1)
l.setColumnMinimumWidth(1, 128)
l.setRowMinimumHeight(1, 128)

Note: the vector class was posted before here

After spending hours not understanding why my maths and logics didn't work, I applied them manually and they do work so I hope I'm not doing something severly wrong and can conclude QT gets in its own way sometimes, as when using a QRect and doing the same in the mouse move event (using setRight, setLeft) it keeps scaling down the rectangle and whatnot.

2 thoughts on “Transforming a rectangle in PyQt

Leave a Reply

Your email address will not be published. Required fields are marked *