How to correctly rotate a QGraphicsItem around different anchors in Qt C++

416 Views Asked by At

I am working on a custom QGraphicsItem that has two anchor points, and I want to be able to rotate the item around these anchors when the user interacts with them. I have implemented a mousePressEvent and mouseMoveEvent to detect which anchor the user clicked on, set the rotation anchor point, and compute the angle of rotation.

Here is a simplified version of my code:

MyView.h

static constexpr float ANCHOR_RADIUS = 10;

class MyView : public QGraphicsItem {
public:
    MyView(float xPos, float yPos, float width, float height, QGraphicsItem *parent = nullptr)
        : _width(width), _height(height), _viewState(VIEW) {
        setPos(xPos, yPos);

        setFlag(ItemIsMovable);

        auto diameter = 2 * ANCHOR_RADIUS;
        _anchor1.setRect(0, 0, diameter, diameter);
        _anchor2.setRect(width - diameter, 0, diameter, diameter);
    }

    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override {
        auto diameter = 2 * ANCHOR_RADIUS;
        _anchor1.setRect(0, 0, diameter, diameter);
        _anchor2.setRect(_width - diameter, 0, diameter, diameter);

        // Pin 1 and 2 coordinate
        auto c1 = _anchor1.center();
        auto c2 = _anchor2.center();

        painter->drawLine(static_cast<int> (c1.x()), static_cast<int>(c1.y()),
                          static_cast<int>(c2.x()), static_cast<int>(c2.y()));

        painter->drawRect(boundingRect());

        painter->drawEllipse(_anchor1);
        painter->drawEllipse(_anchor2);
    }

    [[nodiscard]] QRectF boundingRect() const override {
        return {0, 0, static_cast<qreal>(_width), static_cast<qreal>(_height)};
    }

    enum ViewState {
        ANCHOR1, ANCHOR2, VIEW
    };

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
        _tapPoint = event->pos();

        auto cp1 = _anchor1.center(); // get center point of anchor 1
        auto cp2 = _anchor2.center(); // get center point of anchor 2

        // Anchor 1 clicked
        if (_anchor1.contains(_tapPoint)) {
            setTransformOriginPoint(cp2.x(), cp2.y()); // set rotation anchor to anchor 2
            _viewState = ANCHOR1;
        }
            // Anchor 2 clicked
        else if (_anchor2.contains(_tapPoint)) {
            setTransformOriginPoint(cp1.x(), cp1.y()); // set rotation anchor to anchor 1
            _viewState = ANCHOR2;
        }
            // View clicked
        else {
            QGraphicsItem::mousePressEvent(event);
            _viewState = VIEW;
        }
    }
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override {
        auto p = event->pos();

        switch (_viewState) {
            case ANCHOR1: {

                // calculate the angle of the rotation based on the mouse touch
                auto angle = qRadiansToDegrees(qAtan2(p.y() - _anchor2.y(), _width));
                setRotation(rotation() - angle); // rotate the item around anchor 2

                break;
            }
            case ANCHOR2: {
                // calculate the angle of the rotation based on the mouse touch
                auto angle = qRadiansToDegrees(qAtan2(p.y() - _anchor1.y(), _width));
                setRotation(rotation() + angle); // rotate the item around anchor 1
                break;
            }
            default:
                QGraphicsItem::mouseMoveEvent(event); // move the item normally
        }
    }

private:
    float _width, _height;
    QRectF _anchor1, _anchor2;
    QPointF _tapPoint;
    ViewState _viewState;
};

main.cpp

static constexpr int WIDTH = 500;
static constexpr int HEIGHT = 500;

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    QGraphicsScene scene;
    scene.setSceneRect(QRectF(0, 0, WIDTH, HEIGHT));

    QGraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);

    QVBoxLayout layout;
    layout.addWidget(&view);

    QWidget widget;
    widget.setLayout(&layout);

    MyView myView(100, 100, 200, 20);
    scene.addItem(&myView);

    widget.show();

    return QApplication::exec();
}

However, when I try to rotate the item from one anchor point (around the other) and then rotate it again from the other anchor point, it jumps back to the initial position! I am not sure why this is happening.

As you can see in this video, when I first rotate the view it works, but when I try to rotate it from the other anchor, its position jumps to another position!

enter image description here

This is what I am trying to achieve (created with the GeoGebra tool): enter image description here

The solution needs to be applicable to any shape drawn within the MyView::paint() function, rather than being limited to just a line. Although there is an online solution available here, it only works for a line, and similarly, @kenash0625's solution also only works for a line.

Question: What could be causing this issue, and how can I modify my code to achieve the desired behavior of smoothly rotating around different anchor points?

2

There are 2 best solutions below

5
kenash0625 On BEST ANSWER

this one should be applicable to any shape drawn within the MyView::paint() function

I made 2 change to your code

  1. add call to QGraphicsItem::setTransformations(const QList<QGraphicsTransform *> &transformations)

  2. change from

auto angle = qRadiansToDegrees(qAtan2(p.y()- _anchor2.y() , _width));

to

auto angle = qRadiansToDegrees(qAtan2( _anchor2.y() -p.y(), _width));

here is edited code:


#include<QGraphicsItem>
#include<QPainter>
#include<QGraphicsSceneMouseEvent>
#include<QGraphicsScene>
#include<QGraphicsView>
#include <QGraphicsRotation>
#include<qmath.h>
static constexpr float ANCHOR_RADIUS = 10;

class MyView : public QGraphicsItem {
public:
    MyView(float xPos, float yPos, float width, float height, QGraphicsItem* parent = nullptr)
        : _width(width), _height(height), _viewState(VIEW) {
        setPos(xPos, yPos);

        setFlag(ItemIsMovable);

        auto diameter = 2 * ANCHOR_RADIUS;
        _anchor1.setRect(0, 0, diameter, diameter);
        _anchor2.setRect(width - diameter, 0, diameter, diameter);
    }

    void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override {
        auto diameter = 2 * ANCHOR_RADIUS;
        _anchor1.setRect(0, 0, diameter, diameter);
        _anchor2.setRect(_width - diameter, 0, diameter, diameter);

        // Pin 1 and 2 coordinate
        auto c1 = _anchor1.center();
        auto c2 = _anchor2.center();

        painter->drawLine(static_cast<int> (c1.x()), static_cast<int>(c1.y()),
            static_cast<int>(c2.x()), static_cast<int>(c2.y()));

        painter->drawRect(boundingRect());

        painter->drawEllipse(_anchor1);
        painter->drawEllipse(_anchor2);
    }

    [[nodiscard]] QRectF boundingRect() const override {
        return { 0, 0, static_cast<qreal>(_width), static_cast<qreal>(_height) };
    }

    enum ViewState {
        ANCHOR1, ANCHOR2, VIEW
    };

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent* event) override {
        _tapPoint = event->pos();

        // Anchor 1 clicked
        if (_anchor1.contains(_tapPoint)) {
            _viewState = ANCHOR1;
        }
        // Anchor 2 clicked
        else if (_anchor2.contains(_tapPoint)) {
            _viewState = ANCHOR2;
        }
        // View clicked
        else {
            QGraphicsItem::mousePressEvent(event);
            _viewState = VIEW;
        }
    }
    void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override {
        auto p = event->pos();

        auto cp1 = _anchor1.center(); // get center point of anchor 1
        auto cp2 = _anchor2.center(); // get center point of anchor 2

        switch (_viewState) {
        case ANCHOR1: {
            // calculate the angle of the rotation based on the mouse touch
            auto angle = qRadiansToDegrees(qAtan2( _anchor2.y() -p.y(), _width));
            QGraphicsRotation* rot = new QGraphicsRotation;
            rot->setOrigin(QVector3D(cp2.x(), cp2.y(), 0));
            rot->setAxis(Qt::ZAxis);
            rot->setAngle(angle);
            _trans.push_back(rot);
            setTransformations(_trans);
            break;
        }
        case ANCHOR2: {
            // calculate the angle of the rotation based on the mouse touch
            auto angle = qRadiansToDegrees(qAtan2(p.y() - _anchor1.y(), _width));
            QGraphicsRotation* rot = new QGraphicsRotation;
            rot->setOrigin(QVector3D(cp1.x(), cp1.y(), 0));
            rot->setAxis(Qt::ZAxis);
            rot->setAngle(angle);
            _trans.push_back(rot);
            setTransformations(_trans);
            break;
        }
        default:
            QGraphicsItem::mouseMoveEvent(event); // move the item normally
        }
    }

private:
    float _width, _height;
    QRectF _anchor1, _anchor2;
    QPointF _tapPoint;
    ViewState _viewState;
    QList<QGraphicsTransform*> _trans;
};

static constexpr int WIDTH = 500;
static constexpr int HEIGHT = 500;

int main7(int argc, char* argv[]) {
    QApplication a(argc, argv);

    QGraphicsScene scene;
    scene.setSceneRect(QRectF(0, 0, WIDTH, HEIGHT));

    QGraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);

    QVBoxLayout layout;
    layout.addWidget(&view);

    QWidget widget;
    widget.setLayout(&layout);

    MyView myView(100, 100, 200, 20);
    scene.addItem(&myView);

    widget.show();

    return QApplication::exec();
}
#include"FileName.moc"
5
kaan kaya On

Cannot add a comment so I'm posting this as an answer, it may help

Looks like the position of these items aren't correctly set when moving around is done, so you can try setting it manually;

Use mouseReleaseEvent() to know when the positioning is done.

Get the locations of items using scenePos().

And to make sure they stay in the position that is set by the user, use setPos() function.


edit:

Example usage(note that this is only pseudocode):

//We will be here when user releases the mouse
void MyView::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
    
    //Lets call first item _anchor1.
    //Set position of _anchor1 with setPos(),
    //giving the argument of _anchor1's current position using scenePos().
    _anchor1.setPos(_anchor1.scenePos());

    //lets call second item _anchor2
    //Set position of _anchor1 with setPos(),
    //giving the argument of _anchor1's current position using scenePos().
    _anchor2.setPos(_anchor2.scenePos());

}

this may or may not work.