QGraphicsItem question: setting some limitations to the item transforms

Hi,

I have a question regarding QGraphicsItem. What I am trying to do is to recreate an “Osipa Style” face controls but in PyQt so that animators can control the face from the gui and not from a node in the viewport.

This is the code I currently wrote. As you can see I started from the example found in the PyQt folder and modified for my purposes, so I apologize if it’s still in a messy state.
If you save the code in a .pyw and run it, from the command prompt, you will see that this prototype is working, as it’s spitting out the values that I will need to plug into my node in Maya, however, I can’t find a way to have the Item go over the rectangle area when the mouse moves.

Do you have an idea on how I can fix it?


#!/usr/bin/env python

from PyQt4 import QtCore, QtGui

class Node(QtGui.QGraphicsItem):
    Type = QtGui.QGraphicsItem.UserType + 1

    def __init__(self, graphWidget):
        super(Node, self).__init__()

        self.graph = graphWidget
        self.newPos = QtCore.QPointF()

        self.setFlag(QtGui.QGraphicsItem.ItemIsMovable)
        self.setFlag(QtGui.QGraphicsItem.ItemSendsGeometryChanges)
        self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
        self.setZValue(1)

    def type(self):
        return Node.Type


    def calculateForces(self):
        # normalize the coordinates
#        print self.x()
#        print self.y()
        norX = self.x()/50
        norY = (self.y()/50) * (-1)
        
        if not self.scene() or self.scene().mouseGrabberItem() is self:
            #check where X is
            if (norX>1.0):
                self.newPos.setX(50)
            elif (norX<(-1.0)):
                self.newPos.setX(-50)
            else:
                self.newPos.setX(norX*50)
                        
            #check where Y is
            if (norY>1.0):
                self.newPos.setY(-50)
            elif (norY<(-1.0)):
                self.newPos.setY(50)
            else:
                self.newPos.setY(norY*50*-1)
            
            print self.newPos.x()/50
            print (self.newPos.y()/50) * -1
            
            #self.newPos = self.pos()
            return

    def advance(self):
        if self.newPos == self.pos():
            return False

        self.setPos(self.newPos)
        return True

    def boundingRect(self):
        adjust = 2.0
        return QtCore.QRectF(-10 - adjust, -10 - adjust, 23 + adjust,
                23 + adjust)

    def shape(self):
        path = QtGui.QPainterPath()
        path.addEllipse(-10, -10, 20, 20)
        return path

    def paint(self, painter, option, widget):
        painter.setPen(QtCore.Qt.NoPen)
        painter.setBrush(QtCore.Qt.darkGray)
        painter.drawEllipse(-7, -7, 20, 20)

        gradient = QtGui.QRadialGradient(-3, -3, 10)
        if option.state & QtGui.QStyle.State_Sunken:
            gradient.setCenter(3, 3)
            gradient.setFocalPoint(3, 3)
            gradient.setColorAt(1, QtGui.QColor(QtCore.Qt.yellow).light(120))
            gradient.setColorAt(0, QtGui.QColor(QtCore.Qt.darkYellow).light(120))
        else:
            gradient.setColorAt(0, QtCore.Qt.yellow)
            gradient.setColorAt(1, QtCore.Qt.darkYellow)

        painter.setBrush(QtGui.QBrush(gradient))
        painter.setPen(QtGui.QPen(QtCore.Qt.black, 0))
        painter.drawEllipse(-10, -10, 20, 20)

    def itemChange(self, change, value):
        if change == QtGui.QGraphicsItem.ItemPositionHasChanged:      
            self.graph.itemMoved()

        return super(Node, self).itemChange(change, value)

    def mousePressEvent(self, event):
        self.update()
        super(Node, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        self.update()
        super(Node, self).mouseReleaseEvent(event)


class GraphWidget(QtGui.QGraphicsView):
    def __init__(self):
        super(GraphWidget, self).__init__()

        self.timerId = 0
        
        scene = QtGui.QGraphicsScene(self)
        scene.setItemIndexMethod(QtGui.QGraphicsScene.NoIndex)
        scene.setSceneRect(-50, -50, 100, 100)
        self.setScene(scene)
        self.setCacheMode(QtGui.QGraphicsView.CacheBackground)
        self.setViewportUpdateMode(QtGui.QGraphicsView.BoundingRectViewportUpdate)
        self.setRenderHint(QtGui.QPainter.Antialiasing)
        self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter)
        self.message = "The coordinates are:"
        self.coordX = 0
        self.coordY = 0
        self.coord = ""


        self.centerNode = Node(self)
        scene.addItem(self.centerNode)

        self.centerNode.setPos(0, 0)

        self.scale(1, 1)
        self.setMinimumSize(100, 100)
        self.setWindowTitle("Face Controls")

    def itemMoved(self):
        if not self.timerId:
            self.timerId = self.startTimer(1000 / 25)
            
            
            

    def keyPressEvent(self, event):
        key = event.key()

        if key == QtCore.Qt.Key_Up:
            self.centerNode.moveBy(0, -20)
        elif key == QtCore.Qt.Key_Down:
            self.centerNode.moveBy(0, 20)
        elif key == QtCore.Qt.Key_Left:
            self.centerNode.moveBy(-20, 0)
        elif key == QtCore.Qt.Key_Right:
            self.centerNode.moveBy(20, 0)
        elif key == QtCore.Qt.Key_Plus:
            self.scaleView(1.2)
        elif key == QtCore.Qt.Key_Minus:
            self.scaleView(1 / 1.2)
        elif key == QtCore.Qt.Key_Space or key == QtCore.Qt.Key_Enter:
            for item in self.scene().items():
                if isinstance(item, Node):
                    item.setPos(-150 + QtCore.qrand() % 300, -150 + QtCore.qrand() % 300)
        else:
            super(GraphWidget, self).keyPressEvent(event)

    def timerEvent(self, event):
        nodes = [item for item in self.scene().items() if isinstance(item, Node)]

        for node in nodes:
            node.calculateForces()

        itemsMoved = False
        for node in nodes:
            if node.advance():
                itemsMoved = True

        if not itemsMoved:
            self.killTimer(self.timerId)
            self.timerId = 0

    

    def drawBackground(self, painter, rect):
        # Shadow.
        sceneRect = self.sceneRect()
        rightShadow = QtCore.QRectF(sceneRect.right(), sceneRect.top() + 5, 5,
                sceneRect.height())
        bottomShadow = QtCore.QRectF(sceneRect.left() + 5, sceneRect.bottom(),
                sceneRect.width(), 5)
        if rightShadow.intersects(rect) or rightShadow.contains(rect):
            painter.fillRect(rightShadow, QtCore.Qt.darkGray)
        if bottomShadow.intersects(rect) or bottomShadow.contains(rect):
            painter.fillRect(bottomShadow, QtCore.Qt.darkGray)

        # Fill.
        gradient = QtGui.QLinearGradient(sceneRect.topLeft(),
                sceneRect.bottomRight())
        gradient.setColorAt(0, QtCore.Qt.white)
        gradient.setColorAt(1, QtCore.Qt.lightGray)
        painter.fillRect(rect.intersect(sceneRect), QtGui.QBrush(gradient))
        painter.setBrush(QtCore.Qt.NoBrush)
        painter.drawRect(sceneRect)

        # Text.
        textRect = QtCore.QRectF(sceneRect.left() + 4, sceneRect.top() + 4,
                sceneRect.width() - 4, sceneRect.height() - 4)
        

        font = painter.font()
        font.setPointSize(8)
        painter.setFont(font)
        painter.setPen(QtCore.Qt.lightGray)
        painter.drawText(textRect.translated(2, 2), self.message)
        painter.setPen(QtCore.Qt.black)
        painter.drawText(textRect, self.message)



if __name__ == '__main__':

    import sys

    app = QtGui.QApplication(sys.argv)
    QtCore.qsrand(QtCore.QTime(0,0,0).secsTo(QtCore.QTime.currentTime()))

    widget = GraphWidget()
    widget.show()

    sys.exit(app.exec_())


Thanks

Carlo

I suggest you come up with something different than using a timer to force the object back into its position, timers were never ment for such things.

Instead of using a QGraphicsItem for the little ball, just store the cursor x, y inside GraphicsWidget and inside drawbackground do a painter.drawEllipse(…), you don’t need to have each thing inside your graphics scene as a separate node :slight_smile:

So basically when you have a mousePressEvent set a state to true and in mouseMoveEvent keep updating it untill a mouseReleaseEvent is raised. Inside mouseMoveEvent you would do:
x = max(boundingLeft, min( boundingRight, x ))
y = max(boundingTop, min( boundingBottom, y ))

where boundingLeft, boundingTop, boundingRight and boundingBottom are the four corners of the rectangle you want it to be inside.
This will keep the value within your bounding rectangle, then you can just convert it to normalized value by division for maya like rig controller setup.

I don’t remember but you might need to flip y depending on where coordinates start from.

So done, alot simpler and cleaner approach with less code and more readable.

Awesome. I will try that approach. Yes, I can see why this approach does not make sense for what I am trying to do.

Thanks!

Carlo

Hehe no problems, let us know how it went :slight_smile: