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.setMouseTracking(True)
        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
        painter.setPen(QtGui.QColor(255,255,255,127))
        
        for i in range(len(coords)):
            painter.setBrush(QtCore.Qt.NoBrush)
            if (self.mouse.position-coords[i]).mag() <= self.tolerance:
                c = QtGui.QColor(255,255,0,127)
                painter.setBrush(QtGui.QBrush(c))
                if i == len(coords)-1:
                    painter.setPen(QtGui.QColor(c))
            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.setPen(QtGui.QColor(255,255,255,127))
            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
        self.repaint()
        
        coords = self.rectangle.getCoords(self.rotator)
        for i in range(len(coords)):
            picking = v-coords[i]
            if picking.mag() <= self.tolerance:
                self.pick = i
                break
       
    def mouseReleaseEvent(self,e):
        self.mouse.leftState = 1
        self.repaint()
        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.mouse.position-self.rectangle.center()
                self.rectangle.a = math.atan2(motion[0],- motion[1])
                
            else:
                rotatedmouse = Vec(self.mouse.position[:])
                rotatedmouse -= self.rectangle.center()
                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 += self.rectangle.center()
                
                if self.pick%3 == 0:
                    self.rectangle.p1[0] = rotatedmouse[0]
                elif self.pick%3 == 1:
                    pass
                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:
                    pass
                elif self.pick < 9:
                    self.rectangle.p2[1] = rotatedmouse[1]
                
                if self.pick == 4:
                    self.rectangle.setCenter(self.mouse.position)
        
        if self.mouse.leftState%2: 
            self.mouse.leftState -= 1
        self.repaint()
        
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 = self.center()
        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 = vec-self.center()
        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())
        m.rotate(self.a)
        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()
w.setLayout(l)
win = EditableRectangleWidget(w)
l.addWidget(win, 1, 1)
l.setColumnStretch(0,True)
l.setColumnStretch(1,False)
l.setColumnStretch(2,True)
l.setColumnMinimumWidth(1, 128)
l.setRowStretch(0,True)
l.setRowStretch(1,False)
l.setRowStretch(2,True)
l.setRowMinimumHeight(1, 128)
w.show()

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 *